From 3b8182ecc40189e9b34d0dcba2e6ba6b5086a4cd Mon Sep 17 00:00:00 2001 From: Steve Pieper Date: Wed, 25 Oct 2023 10:59:14 -0400 Subject: [PATCH 1/9] ENH: allow URL "filenames" in database Allow the ctkDICOMDatabase to hold URLs for retrieving instances in addition to traditional filenames (really filepaths). Ensure that any code that tries to calculate absolute file paths from relative paths does not mangle the URLs. Do this by detecting the URL sceme (e.g. http in http://) and ignore these when calculating abolute paths. --- Libs/DICOM/Core/ctkDICOMDatabase.cpp | 35 ++++++++++++++++++++-------- Libs/DICOM/Core/ctkDICOMDatabase_p.h | 3 +++ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/Libs/DICOM/Core/ctkDICOMDatabase.cpp b/Libs/DICOM/Core/ctkDICOMDatabase.cpp index ee0300b80b..2a90be8c02 100644 --- a/Libs/DICOM/Core/ctkDICOMDatabase.cpp +++ b/Libs/DICOM/Core/ctkDICOMDatabase.cpp @@ -206,7 +206,7 @@ QStringList ctkDICOMDatabasePrivate::allFilesInDatabase() while (allFilesQuery.next()) { - allFileNames << this->absolutePathFromInternal(allFilesQuery.value(0).toString()); + allFileNames << this->absolutePathFromInternalIfFile(allFilesQuery.value(0).toString()); } return allFileNames; } @@ -311,7 +311,7 @@ QStringList ctkDICOMDatabasePrivate::filenames(QString table) while (allFilesQuery.next()) { - allFileNames << this->absolutePathFromInternal(allFilesQuery.value(0).toString()); + allFileNames << this->absolutePathFromInternalIfFile(allFilesQuery.value(0).toString()); } return allFileNames; } @@ -1444,6 +1444,19 @@ QString ctkDICOMDatabasePrivate::internalPathFromAbsolute(const QString& filenam return ctk::internalPathFromAbsolute(filename, q->databaseDirectory()); } +//------------------------------------------------------------------------------ +QString ctkDICOMDatabasePrivate::absolutePathFromInternalIfFile(const QString& filename) +{ + Q_Q(ctkDICOMDatabase); + QString returnFileName = filename; + if (QUrl(filename).scheme().isEmpty()) // local path, not url + { + returnFileName = this->absolutePathFromInternal(filename); + } + return returnFileName; +} + + //------------------------------------------------------------------------------ CTK_GET_CPP(ctkDICOMDatabase, bool, isDisplayedFieldsTableAvailable, DisplayedFieldsTableAvailable); CTK_GET_CPP(ctkDICOMDatabase, bool, useShortStoragePath, UseShortStoragePath); @@ -2094,7 +2107,9 @@ QStringList ctkDICOMDatabase::filesForSeries(QString seriesUID, int hits/*=-1*/) QStringList allFileNames; while (query.next()) { - allFileNames << d->absolutePathFromInternal(query.value(0).toString()); + QString fileName = query.value(0).toString(); + fileName = d->absolutePathFromInternalIfFile(fileName); + allFileNames << fileName; if (hits > 0 && allFileNames.size() >= hits) { // reached the number of requested files @@ -2115,7 +2130,7 @@ QString ctkDICOMDatabase::fileForInstance(QString sopInstanceUID) QString result; if (query.next()) { - result = d->absolutePathFromInternal(query.value(0).toString()); + result = d->absolutePathFromInternalIfFile(query.value(0).toString()); } return result; } @@ -2550,7 +2565,7 @@ bool ctkDICOMDatabase::allFilesModifiedTimes(QMap& modifiedT bool success = d->loggedExec(allFilesModifiedQuery); while (allFilesModifiedQuery.next()) { - QString filename = d->absolutePathFromInternal(allFilesModifiedQuery.value(0).toString()); + QString filename = d->absolutePathFromInternalIfFile(allFilesModifiedQuery.value(0).toString()); QDateTime modifiedTime = QDateTime::fromString(allFilesModifiedQuery.value(1).toString(), Qt::ISODate); if (modifiedTimeForFilepath.contains(filename) && modifiedTimeForFilepath[filename] <= modifiedTime) { @@ -2639,7 +2654,7 @@ bool ctkDICOMDatabase::removeSeries(const QString& seriesInstanceUID, bool clear // check that the file is below our internal storage if (QFileInfo(dbFilePath).isRelative()) { - QString absPath = d->absolutePathFromInternal(dbFilePath); + QString absPath = d->absolutePathFromInternalIfFile(dbFilePath); if (QFile(absPath).remove()) { if (d->LoggedExecVerbose) @@ -2658,7 +2673,7 @@ bool ctkDICOMDatabase::removeSeries(const QString& seriesInstanceUID, bool clear } } // Remove thumbnail (if exists) - QFile thumbnailFile(d->absolutePathFromInternal(thumbnailPath)); + QFile thumbnailFile(d->absolutePathFromInternalIfFile(thumbnailPath)); if (thumbnailFile.exists()) { if (!thumbnailFile.remove()) @@ -2839,7 +2854,7 @@ QString ctkDICOMDatabase::cachedTag(const QString sopInstanceUID, const QString QSqlQuery selectValue( d->TagCacheDatabase ); selectValue.prepare( "SELECT Value FROM TagCache WHERE SOPInstanceUID = :sopInstanceUID AND Tag = :tag" ); selectValue.bindValue(":sopInstanceUID",sopInstanceUID); - selectValue.bindValue(":tag",tag); + selectValue.bindValue(":tag",tag.toUpper()); d->loggedExec(selectValue); QString result(""); if (selectValue.next()) @@ -2874,7 +2889,7 @@ void ctkDICOMDatabase::getCachedTags(const QString sopInstanceUID, QMapisEmpty()) { // replace empty strings with special flag string diff --git a/Libs/DICOM/Core/ctkDICOMDatabase_p.h b/Libs/DICOM/Core/ctkDICOMDatabase_p.h index e76b94480a..0318b06ba6 100644 --- a/Libs/DICOM/Core/ctkDICOMDatabase_p.h +++ b/Libs/DICOM/Core/ctkDICOMDatabase_p.h @@ -121,6 +121,9 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabasePrivate /// Convert an absolute path to an internal path (absolute if outside database folder, relative if inside database folder). QString internalPathFromAbsolute(const QString& filename); + /// If not a URL, convert to abolute path + QString absolutePathFromInternalIfFile(const QString& filename); + /// Name of the database file (i.e. for SQLITE the sqlite file) QString DatabaseFileName; From fbae32fd7ce21f8b79887df1da35754f4f28408e Mon Sep 17 00:00:00 2001 From: Steve Pieper Date: Sun, 5 Nov 2023 12:27:04 -0500 Subject: [PATCH 2/9] ENH: Always use upper case hex for tags Previously, the TagCache database would store whatever hex string was passed in, so depending on how the tags were specified duplicate entries could end up being cached, such as "0010,000d" and '0010,000D". This wasted space, but also caused inefficiencies. Although usage varies, the DICOM standard uses upper case hex both in the body of the standard text, and when defining the DICOM JSON and XML models. (https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_f.2.html) This change always converts tags to upper case hex when storing and query of the tag cache. Users of the library can still pass lower case hex tags to methods, but they will be converted to upper case internally, so most use cases will be unchanged. The only externally visible method where the behavior will change is: `Q_INVOKABLE void getCachedTags(const QString sopInstanceUID, QMap &cachedTags);` Previously tags could have been of mixed case depending on how they were specified for caching. Now they will be all upper case hex. Currently, the only known use of `getCachedTags` is internnally to `ctkDICOMDatabase` so this change of behavior is unlikely to cause problems in practice. Note also that while the DICOM standard models for JSON and XML omit the comma between group and element, but `ctkDICOMTagCache` implementation retains the comma in the API and the database. --- Libs/DICOM/Core/ctkDICOMDatabase.cpp | 50 ++++++++++++++++++---------- Libs/DICOM/Core/ctkDICOMItem.cpp | 6 ++-- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/Libs/DICOM/Core/ctkDICOMDatabase.cpp b/Libs/DICOM/Core/ctkDICOMDatabase.cpp index 2a90be8c02..8752b0d422 100644 --- a/Libs/DICOM/Core/ctkDICOMDatabase.cpp +++ b/Libs/DICOM/Core/ctkDICOMDatabase.cpp @@ -235,11 +235,12 @@ QString ctkDICOMDatabasePrivate::readValueFromFile(const QString& fileName, cons return ""; } + QString upperTag = tag.toUpper(); QString value; unsigned short group, element; - q->tagToGroupElement(tag, group, element); + q->tagToGroupElement(upperTag, group, element); DcmTagKey tagKey(group, element); - if (this->TagsToExcludeFromStorage.contains(tag)) + if (this->TagsToExcludeFromStorage.contains(upperTag)) { if (dataset.TagExists(tagKey)) { @@ -256,7 +257,7 @@ QString ctkDICOMDatabasePrivate::readValueFromFile(const QString& fileName, cons } // Store result in cache - q->cacheTag(sopInstanceUID, tag, value); + q->cacheTag(sopInstanceUID, upperTag, value); return value; } @@ -562,11 +563,12 @@ void ctkDICOMDatabasePrivate::precacheTags(const ctkDICOMItem& dataset, const QS QStringList sopInstanceUIDs, tags, values; foreach (const QString &tag, this->TagsToPrecache) { + QString upperTag = tag.toUpper(); unsigned short group, element; - q->tagToGroupElement(tag, group, element); + q->tagToGroupElement(upperTag, group, element); DcmTagKey tagKey(group, element); QString value; - if (this->TagsToExcludeFromStorage.contains(tag)) + if (this->TagsToExcludeFromStorage.contains(upperTag)) { if (dataset.TagExists(tagKey)) { @@ -582,7 +584,7 @@ void ctkDICOMDatabasePrivate::precacheTags(const ctkDICOMItem& dataset, const QS value = dataset.GetAllElementValuesAsString(tagKey); } sopInstanceUIDs << sopInstanceUID; - tags << tag; + tags << upperTag; values << value; } @@ -1076,11 +1078,12 @@ void ctkDICOMDatabase::insert(const QList& ind insertTags.bindValue(0, sopInstanceUID); foreach(const QString & tag, d->TagsToPrecache) { + QString upperTag = tag.toUpper(); unsigned short group, element; - this->tagToGroupElement(tag, group, element); + this->tagToGroupElement(upperTag, group, element); DcmTagKey tagKey(group, element); QString value; - if (d->TagsToExcludeFromStorage.contains(tag)) + if (d->TagsToExcludeFromStorage.contains(upperTag)) { if (dataset.TagExists(tagKey)) { @@ -1095,7 +1098,7 @@ void ctkDICOMDatabase::insert(const QList& ind { value = dataset.GetAllElementValuesAsString(tagKey); } - insertTags.bindValue(1, tag); + insertTags.bindValue(1, upperTag); if (value.isEmpty()) { insertTags.bindValue(2, TagNotInInstance); @@ -1259,7 +1262,7 @@ QString ctkDICOMDatabasePrivate::getDisplaySeriesFieldsKey(QString seriesInstanc return seriesInstanceUID; } - logger.error("Failed to find series with SeriesInstanceUID=" + seriesInstanceUID); + logger.error("in getDisplaySeriesFieldsKey: Failed to find series with SeriesInstanceUID=" + seriesInstanceUID); return QString(); } @@ -1472,7 +1475,7 @@ ctkDICOMDatabase::ctkDICOMDatabase(QString databaseFile) : d_ptr(new ctkDICOMDatabasePrivate(*this)) { Q_D(ctkDICOMDatabase); - d->TagsToExcludeFromStorage << groupElementToTag(0x7fe0,0x0010); // pixel data + d->TagsToExcludeFromStorage << groupElementToTag(0x7FE0,0x0010); // pixel data d->registerCompressionLibraries(); d->init(databaseFile); } @@ -1482,7 +1485,7 @@ ctkDICOMDatabase::ctkDICOMDatabase(QObject* parent) { Q_UNUSED(parent); Q_D(ctkDICOMDatabase); - d->TagsToExcludeFromStorage << groupElementToTag(0x7fe0, 0x0010); // pixel data + d->TagsToExcludeFromStorage << groupElementToTag(0x7FE0, 0x0010); // pixel data d->registerCompressionLibraries(); } @@ -2254,6 +2257,7 @@ void ctkDICOMDatabase::loadFileHeader(QString fileName) if (dO) { QString tag = QString("%1,%2").arg(dO->getGTag(),4,16,QLatin1Char('0')).arg(dO->getETag(),4,16,QLatin1Char('0')); + tag = tag.toUpper(); std::ostringstream s; dO->print(s); d->LoadedHeader[tag] = QString(s.str().c_str()); @@ -2285,6 +2289,7 @@ QString ctkDICOMDatabase::headerValue (QString key) QString ctkDICOMDatabase::instanceValue(QString sopInstanceUID, QString tag) { Q_D(ctkDICOMDatabase); + tag = tag.toUpper(); // Read from cache, if available QString value = this->cachedTag(sopInstanceUID, tag); if (value == TagNotInInstance || value == ValueIsEmptyString || value == ValueIsNotStored) @@ -2319,6 +2324,7 @@ QString ctkDICOMDatabase::fileValue(const QString fileName, QString tag) Q_D(ctkDICOMDatabase); // Read from cache, if available + tag = tag.toUpper(); QString sopInstanceUID = this->instanceForFile(fileName); QString value = this->cachedTag(sopInstanceUID, tag); if (value == TagNotInInstance || value == ValueIsEmptyString || value == ValueIsNotStored) @@ -2347,7 +2353,8 @@ QString ctkDICOMDatabase::fileValue(const QString fileName, const unsigned short bool ctkDICOMDatabase::instanceValueExists(const QString sopInstanceUID, const QString tag) { Q_D(ctkDICOMDatabase); - QString value = this->cachedTag(sopInstanceUID, tag); + QString upperTag = tag.toUpper(); + QString value = this->cachedTag(sopInstanceUID, upperTag); if (value == TagNotInInstance || value == ValueIsEmptyString) { return false; @@ -2363,7 +2370,7 @@ bool ctkDICOMDatabase::instanceValueExists(const QString sopInstanceUID, const Q { return false; } - value = d->readValueFromFile(filePath, sopInstanceUID, tag); + value = d->readValueFromFile(filePath, sopInstanceUID, upperTag); return (value != TagNotInInstance && value != ValueIsEmptyString); } @@ -2378,6 +2385,7 @@ bool ctkDICOMDatabase::instanceValueExists(const QString sopInstanceUID, const u bool ctkDICOMDatabase::fileValueExists(const QString fileName, QString tag) { Q_D(ctkDICOMDatabase); + tag = tag.toUpper(); QString sopInstanceUID = this->instanceForFile(fileName); QString value = this->cachedTag(sopInstanceUID, tag); if (value == TagNotInInstance || value == ValueIsEmptyString) @@ -2420,7 +2428,8 @@ bool ctkDICOMDatabase::tagToGroupElement(const QString tag, unsigned short& grou //------------------------------------------------------------------------------ QString ctkDICOMDatabase::groupElementToTag(const unsigned short& group, const unsigned short& element) { - return QString("%1,%2").arg(group,4,16,QLatin1Char('0')).arg(element,4,16,QLatin1Char('0')); + QString groupElement = QString("%1,%2").arg(group,4,16,QLatin1Char('0')).arg(element,4,16,QLatin1Char('0')); + return groupElement.toUpper(); } // @@ -2522,11 +2531,16 @@ const QStringList ctkDICOMDatabase::tagsToPrecache() void ctkDICOMDatabase::setTagsToExcludeFromStorage(const QStringList tags) { Q_D(ctkDICOMDatabase); - if (d->TagsToExcludeFromStorage == tags) + QStringList upperTags; + foreach (const QString &tag, tags) + { + upperTags << tag.toUpper(); + } + if (d->TagsToExcludeFromStorage == upperTags) { return; } - d->TagsToExcludeFromStorage = tags; + d->TagsToExcludeFromStorage = upperTags; emit tagsToExcludeFromStorageChanged(); } @@ -2904,7 +2918,7 @@ bool ctkDICOMDatabase::cacheTag(const QString sopInstanceUID, const QString tag, { QStringList sopInstanceUIDs, tags, values; sopInstanceUIDs << sopInstanceUID; - tags << tag; + tags << tag.toUpper(); values << value; return this->cacheTags(sopInstanceUIDs, tags, values); } diff --git a/Libs/DICOM/Core/ctkDICOMItem.cpp b/Libs/DICOM/Core/ctkDICOMItem.cpp index 75744ff3b7..9861037f88 100644 --- a/Libs/DICOM/Core/ctkDICOMItem.cpp +++ b/Libs/DICOM/Core/ctkDICOMItem.cpp @@ -958,12 +958,14 @@ QString ctkDICOMItem::TranslateDefinedTermModality( const QString& dt ) QString ctkDICOMItem::TagKey( const DcmTag& tag ) { - return QString("(%1,%2)").arg( tag.getGroup(), 4, 16, QLatin1Char('0')).arg( tag.getElement(), 4, 16, QLatin1Char('0') ); + QString groupElement = QString("(%1,%2)").arg( tag.getGroup(), 4, 16, QLatin1Char('0')).arg( tag.getElement(), 4, 16, QLatin1Char('0') ); + return groupElement.toUpper(); } QString ctkDICOMItem::TagKeyStripped( const DcmTag& tag ) { - return QString("%1,%2").arg( tag.getGroup(), 4, 16, QLatin1Char('0')).arg( tag.getElement(), 4, 16, QLatin1Char('0') ); + QString groupElement = QString("%1,%2").arg( tag.getGroup(), 4, 16, QLatin1Char('0')).arg( tag.getElement(), 4, 16, QLatin1Char('0') ); + return groupElement.toUpper(); } QString ctkDICOMItem::TagDescription( const DcmTag& tag ) From 03e6eccf4a142e775a187bf8d5338abab3d661c6 Mon Sep 17 00:00:00 2001 From: Steve Pieper Date: Sun, 5 Nov 2023 12:33:24 -0500 Subject: [PATCH 3/9] ENH: applying DICOM display updates even if some fail Previously, if some data imported into the `ctkDICOMDatabase` had incomplete information then the display field updates would abort. With this change, a more informative message is generated to support troubleshooting of any failed updates, but the rest of the process completes so that any valid display field updates can be shown to the user. --- Libs/DICOM/Core/ctkDICOMDatabase.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Libs/DICOM/Core/ctkDICOMDatabase.cpp b/Libs/DICOM/Core/ctkDICOMDatabase.cpp index 8752b0d422..e9c4be9643 100644 --- a/Libs/DICOM/Core/ctkDICOMDatabase.cpp +++ b/Libs/DICOM/Core/ctkDICOMDatabase.cpp @@ -1378,8 +1378,8 @@ bool ctkDICOMDatabasePrivate::applyDisplayedFieldsChanges( QMapresetLastInsertedValues(); this->DisplayedFieldGenerator = new ctkDICOMDisplayedFieldGenerator(q_ptr); @@ -965,7 +965,7 @@ void ctkDICOMDatabasePrivate::insert(const ctkDICOMItem& dataset, const QString& } QSqlQuery insertImageStatement(Database); - insertImageStatement.prepare("INSERT INTO Images ( 'SOPInstanceUID', 'Filename', 'SeriesInstanceUID', 'InsertTimestamp' ) VALUES ( ?, ?, ?, ? )"); + insertImageStatement.prepare("INSERT INTO Images ( 'SOPInstanceUID', 'Filename', 'URL', 'SeriesInstanceUID', 'InsertTimestamp' ) VALUES ( ?, ?, NULL, ?, ? )"); insertImageStatement.addBindValue(sopInstanceUID); insertImageStatement.addBindValue(storedFilePathInDatabase); insertImageStatement.addBindValue(seriesInstanceUID); @@ -1112,7 +1112,7 @@ void ctkDICOMDatabase::insert(const QList& ind // Insert image files QSqlQuery insertImageStatement(d->Database); - insertImageStatement.prepare("INSERT INTO Images ( 'SOPInstanceUID', 'Filename', 'SeriesInstanceUID', 'InsertTimestamp' ) VALUES ( ?, ?, ?, ? )"); + insertImageStatement.prepare("INSERT INTO Images ( 'SOPInstanceUID', 'Filename', 'URL', 'SeriesInstanceUID', 'InsertTimestamp' ) VALUES ( ?, ?, NULL, ?, ? )"); insertImageStatement.addBindValue(sopInstanceUID); insertImageStatement.addBindValue(d->internalPathFromAbsolute(storedFilePath)); insertImageStatement.addBindValue(seriesInstanceUID); @@ -2125,6 +2125,22 @@ QString ctkDICOMDatabase::fileForInstance(QString sopInstanceUID) return result; } +//------------------------------------------------------------------------------ +QString ctkDICOMDatabase::urlForInstance(QString sopInstanceUID) +{ + Q_D(ctkDICOMDatabase); + QSqlQuery query(d->Database); + query.prepare("SELECT URL FROM Images WHERE SOPInstanceUID=?"); + query.addBindValue(sopInstanceUID); + query.exec(); + QString result; + if (query.next()) + { + result = query.value(0).toString(); + } + return result; +} + //------------------------------------------------------------------------------ QString ctkDICOMDatabase::seriesForFile(QString fileName) { @@ -2157,6 +2173,22 @@ QString ctkDICOMDatabase::instanceForFile(QString fileName) return result; } +//------------------------------------------------------------------------------ +QString ctkDICOMDatabase::instanceForURL(QString url) +{ + Q_D(ctkDICOMDatabase); + QSqlQuery query(d->Database); + query.prepare( "SELECT SOPInstanceUID FROM Images WHERE URL=?"); + query.addBindValue(url); + query.exec(); + QString result; + if (query.next()) + { + result = query.value(0).toString(); + } + return result; +} + //------------------------------------------------------------------------------ QDateTime ctkDICOMDatabase::insertDateTimeForInstance(QString sopInstanceUID) { diff --git a/Libs/DICOM/Core/ctkDICOMDatabase.h b/Libs/DICOM/Core/ctkDICOMDatabase.h index 98ce3aa369..9fa6597b47 100644 --- a/Libs/DICOM/Core/ctkDICOMDatabase.h +++ b/Libs/DICOM/Core/ctkDICOMDatabase.h @@ -190,6 +190,8 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject QStringList seriesFieldNames() const; Q_INVOKABLE QString fileForInstance(const QString sopInstanceUID); + Q_INVOKABLE QString urlForInstance(const QString sopInstanceUID); + Q_INVOKABLE QString instanceForURL(const QString url); Q_INVOKABLE QString seriesForFile(QString fileName); Q_INVOKABLE QString instanceForFile(const QString fileName); Q_INVOKABLE QDateTime insertDateTimeForInstance(const QString fileName); From 8f9f14a88188655f5ca982d23b3087fb27b56bac Mon Sep 17 00:00:00 2001 From: Steve Pieper Date: Mon, 13 Nov 2023 19:31:23 -0500 Subject: [PATCH 6/9] BUG: Fix dicom db insert with URL field We should be checking the return value of QSqlQuery::exec to catch problems. Filed an issue to track this: https://github.com/commontk/CTK/issues/1155 --- Libs/DICOM/Core/ctkDICOMDatabase.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Libs/DICOM/Core/ctkDICOMDatabase.cpp b/Libs/DICOM/Core/ctkDICOMDatabase.cpp index 0189099f27..799b7862df 100644 --- a/Libs/DICOM/Core/ctkDICOMDatabase.cpp +++ b/Libs/DICOM/Core/ctkDICOMDatabase.cpp @@ -965,9 +965,10 @@ void ctkDICOMDatabasePrivate::insert(const ctkDICOMItem& dataset, const QString& } QSqlQuery insertImageStatement(Database); - insertImageStatement.prepare("INSERT INTO Images ( 'SOPInstanceUID', 'Filename', 'URL', 'SeriesInstanceUID', 'InsertTimestamp' ) VALUES ( ?, ?, NULL, ?, ? )"); + insertImageStatement.prepare("INSERT INTO Images ( 'SOPInstanceUID', 'Filename', 'URL', 'SeriesInstanceUID', 'InsertTimestamp' ) VALUES ( ?, ?, ?, ?, ? )"); insertImageStatement.addBindValue(sopInstanceUID); insertImageStatement.addBindValue(storedFilePathInDatabase); + insertImageStatement.addBindValue(QString("")); insertImageStatement.addBindValue(seriesInstanceUID); insertImageStatement.addBindValue(QDateTime::currentDateTime()); insertImageStatement.exec(); @@ -1112,9 +1113,10 @@ void ctkDICOMDatabase::insert(const QList& ind // Insert image files QSqlQuery insertImageStatement(d->Database); - insertImageStatement.prepare("INSERT INTO Images ( 'SOPInstanceUID', 'Filename', 'URL', 'SeriesInstanceUID', 'InsertTimestamp' ) VALUES ( ?, ?, NULL, ?, ? )"); + insertImageStatement.prepare("INSERT INTO Images ( 'SOPInstanceUID', 'Filename', 'URL', 'SeriesInstanceUID', 'InsertTimestamp' ) VALUES ( ?, ?, ?, ?, ? )"); insertImageStatement.addBindValue(sopInstanceUID); insertImageStatement.addBindValue(d->internalPathFromAbsolute(storedFilePath)); + insertImageStatement.addBindValue(QString("")); insertImageStatement.addBindValue(seriesInstanceUID); insertImageStatement.addBindValue(QDateTime::currentDateTime()); insertImageStatement.exec(); From 0c47c6a9d58aab9543b06b42e97e8dcdb6798e70 Mon Sep 17 00:00:00 2001 From: Steve Pieper Date: Tue, 14 Nov 2023 14:06:17 -0500 Subject: [PATCH 7/9] ENH: Allow file or url to be null in dicom-schema.sql Since neither of these are required fields, the NOT NULL constraint is not appropriate. --- Libs/DICOM/Core/Resources/dicom-schema.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Libs/DICOM/Core/Resources/dicom-schema.sql b/Libs/DICOM/Core/Resources/dicom-schema.sql index 44bcf7b9b0..4ba74ea6a6 100644 --- a/Libs/DICOM/Core/Resources/dicom-schema.sql +++ b/Libs/DICOM/Core/Resources/dicom-schema.sql @@ -27,8 +27,8 @@ INSERT INTO 'SchemaInfo' VALUES('0.8.0'); CREATE TABLE 'Images' ( 'SOPInstanceUID' VARCHAR(64) NOT NULL, - 'Filename' VARCHAR(1024) NOT NULL , - 'URL' VARCHAR(2048) NOT NULL , + 'Filename' VARCHAR(1024), + 'URL' VARCHAR(2048), 'SeriesInstanceUID' VARCHAR(64) NOT NULL , 'InsertTimestamp' VARCHAR(20) NOT NULL , 'DisplayedFieldsUpdatedTimestamp' DATETIME NULL , From d6be8872d72cbdc574c7ef731681af45eb7ba72e Mon Sep 17 00:00:00 2001 From: Steve Pieper Date: Tue, 14 Nov 2023 18:37:41 -0500 Subject: [PATCH 8/9] ENH: update dicom db schema and api for files and urls Schema now has URL column of size 2048 (which based on research is suggested as the max size for URLs). The schema now allows either field to be NULL and does not require the Filename column to be unique. The api now icnludes a urlsForSeries option to get the list of URLs for a given series instance uid. The fileValue method now accepts either filePaths or URLs with filePaths being given priority if both exist. --- Libs/DICOM/Core/Resources/dicom-schema.sql | 6 ++-- Libs/DICOM/Core/ctkDICOMDatabase.cpp | 36 ++++++++++++++++++++-- Libs/DICOM/Core/ctkDICOMDatabase.h | 12 ++++++-- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/Libs/DICOM/Core/Resources/dicom-schema.sql b/Libs/DICOM/Core/Resources/dicom-schema.sql index 4ba74ea6a6..0da2e0d060 100644 --- a/Libs/DICOM/Core/Resources/dicom-schema.sql +++ b/Libs/DICOM/Core/Resources/dicom-schema.sql @@ -27,8 +27,8 @@ INSERT INTO 'SchemaInfo' VALUES('0.8.0'); CREATE TABLE 'Images' ( 'SOPInstanceUID' VARCHAR(64) NOT NULL, - 'Filename' VARCHAR(1024), - 'URL' VARCHAR(2048), + 'Filename' VARCHAR(1024) NULL, + 'URL' VARCHAR(2048) NULL, 'SeriesInstanceUID' VARCHAR(64) NOT NULL , 'InsertTimestamp' VARCHAR(20) NOT NULL , 'DisplayedFieldsUpdatedTimestamp' DATETIME NULL , @@ -85,7 +85,7 @@ CREATE TABLE 'Series' ( 'DisplayedFieldsUpdatedTimestamp' DATETIME NULL , PRIMARY KEY ('SeriesInstanceUID') ); -CREATE UNIQUE INDEX IF NOT EXISTS 'ImagesFilenameIndex' ON 'Images' ('Filename'); +CREATE INDEX IF NOT EXISTS 'ImagesFilenameIndex' ON 'Images' ('Filename'); CREATE INDEX IF NOT EXISTS 'ImagesSeriesIndex' ON 'Images' ('SeriesInstanceUID'); CREATE INDEX IF NOT EXISTS 'SeriesStudyIndex' ON 'Series' ('StudyInstanceUID'); CREATE INDEX IF NOT EXISTS 'StudiesPatientIndex' ON 'Studies' ('PatientsUID'); diff --git a/Libs/DICOM/Core/ctkDICOMDatabase.cpp b/Libs/DICOM/Core/ctkDICOMDatabase.cpp index 799b7862df..eecc3e962e 100644 --- a/Libs/DICOM/Core/ctkDICOMDatabase.cpp +++ b/Libs/DICOM/Core/ctkDICOMDatabase.cpp @@ -2111,6 +2111,28 @@ QStringList ctkDICOMDatabase::filesForSeries(QString seriesUID, int hits/*=-1*/) return allFileNames; } +//------------------------------------------------------------------------------ +QStringList ctkDICOMDatabase::urlsForSeries(QString seriesUID, int hits/*=-1*/) +{ + Q_D(ctkDICOMDatabase); + QSqlQuery query(d->Database); + query.prepare("SELECT URL FROM Images WHERE SeriesInstanceUID=?"); + query.addBindValue(seriesUID); + query.exec(); + QStringList allURLs; + while (query.next()) + { + QString url = query.value(0).toString(); + allURLs << url; + if (hits > 0 && allURLs.size() >= hits) + { + // reached the number of requested files + break; + } + } + return allURLs; +} + //------------------------------------------------------------------------------ QString ctkDICOMDatabase::fileForInstance(QString sopInstanceUID) { @@ -2345,8 +2367,18 @@ QString ctkDICOMDatabase::fileValue(const QString fileName, QString tag) Q_D(ctkDICOMDatabase); // Read from cache, if available - tag = tag.toUpper(); + + // first, try treating argument as filePath QString sopInstanceUID = this->instanceForFile(fileName); + + // second, try treating argument as a url + if (sopInstanceUID.isEmpty()) + { + sopInstanceUID = this->instanceForURL(fileName); + } + + // third, look for the value + tag = tag.toUpper(); QString value = this->cachedTag(sopInstanceUID, tag); if (value == TagNotInInstance || value == ValueIsEmptyString || value == ValueIsNotStored) { @@ -2357,7 +2389,7 @@ QString ctkDICOMDatabase::fileValue(const QString fileName, QString tag) return value; } - // Read value from file + // Read value from file as a fallback (won't work if fileName is a URL) value = d->readValueFromFile(fileName, sopInstanceUID, tag); return value; } diff --git a/Libs/DICOM/Core/ctkDICOMDatabase.h b/Libs/DICOM/Core/ctkDICOMDatabase.h index 9fa6597b47..e9669f0f15 100644 --- a/Libs/DICOM/Core/ctkDICOMDatabase.h +++ b/Libs/DICOM/Core/ctkDICOMDatabase.h @@ -175,6 +175,7 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject /// This is useful for retrieving first file, for example for getting access to fields within that file /// using fileValue() method. Q_INVOKABLE QStringList filesForSeries(const QString seriesUID, int hits=-1); + Q_INVOKABLE QStringList urlsForSeries(const QString seriesUID, int hits=-1); Q_INVOKABLE QHash descriptionsForFile(QString fileName); Q_INVOKABLE QString descriptionForSeries(const QString seriesUID); @@ -267,7 +268,7 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject /// When a DICOM file is stored in the database (insert is called with storeFile=true) then /// path is constructed from study, series, and SOP instance UID. /// If useShortStoragePath is false then the full UIDs are used as subfolder and file name. - /// If useShortStoragePath is true (this is the default) then the path is shortened + /// If useShortStoragePath is true (this is the default) then the path is shortened /// to approximately 40 characters, by replacing UIDs with hashes generated from them. /// UIDs can be 40-60 characters long each, therefore the the total path (including database folder base path) /// can exceed maximum path length on some file systems. It is recommended to enable useShortStoragePath @@ -320,7 +321,14 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject /// \brief Access element values for given instance /// @param sopInstanceUID A string with the uid for a given instance /// (corresponding file will be found via the database) - /// @param fileName Full path to a dicom file to load. + /// @param fileName Full path to a dicom file to load. Note that this string + /// can either be a file path or a URL since the database can have one or + /// both of these fields as of schema version 0.8.0. Since the fileValue + /// api is widely used, this overload allows either value to be used to retrieve + /// values from the tag cache. If a value is found for the fileName the + /// fileName it is returned; if not, the fileName is used as a URL. If no + /// value is found for the URL, the fileName is used as a path to a dicom file + /// where the value is looked up. /// @param group The group portion of the tag as an integer /// @param element The element portion of the tag as an integer /// @Returns empty string if element is missing or excluded from storage. From df632748cc8e02a427cbc857d179c75844009590 Mon Sep 17 00:00:00 2001 From: Steve Pieper Date: Wed, 13 Dec 2023 10:16:14 -0500 Subject: [PATCH 9/9] ENH: Add index for URL to dicom database schema Based on suggestion from @jcfr https://github.com/commontk/CTK/pull/1154#discussion_r1424301591 --- Libs/DICOM/Core/Resources/dicom-schema.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/Libs/DICOM/Core/Resources/dicom-schema.sql b/Libs/DICOM/Core/Resources/dicom-schema.sql index 0da2e0d060..06b352f9f4 100644 --- a/Libs/DICOM/Core/Resources/dicom-schema.sql +++ b/Libs/DICOM/Core/Resources/dicom-schema.sql @@ -86,6 +86,7 @@ CREATE TABLE 'Series' ( PRIMARY KEY ('SeriesInstanceUID') ); CREATE INDEX IF NOT EXISTS 'ImagesFilenameIndex' ON 'Images' ('Filename'); +CREATE INDEX IF NOT EXISTS 'ImagesFilenameIndex' ON 'Images' ('URL'); CREATE INDEX IF NOT EXISTS 'ImagesSeriesIndex' ON 'Images' ('SeriesInstanceUID'); CREATE INDEX IF NOT EXISTS 'SeriesStudyIndex' ON 'Series' ('StudyInstanceUID'); CREATE INDEX IF NOT EXISTS 'StudiesPatientIndex' ON 'Studies' ('PatientsUID');