# HG changeset patch # User Sebastien Jodogne # Date 1613474321 -3600 # Node ID 5b929e6b3c36733e9ba17b0437b4e764569b1aec # Parent 1f455b86b054db6ccd1defd76505da2e62ad6804 removal of "dicom-as-json" attachments diff -r 1f455b86b054 -r 5b929e6b3c36 NEWS --- a/NEWS Fri Feb 12 12:13:19 2021 +0100 +++ b/NEWS Tue Feb 16 12:18:41 2021 +0100 @@ -1,7 +1,28 @@ Pending changes in the mainline =============================== +General +------- + +* BREAKING CHANGE: The "dicom-as-json" attachments are not explicitly stored anymore. + If the storage area doesn't support range reading, or if "StorageCompression" + is enabled, a new attachment "dicom-until-pixel-data" is generated. * New metadata automatically computed at the instance level: "PixelDataOffset" + +REST API +-------- + +* BREAKING CHANGE: The "/instances/.../tags" route does not report the tags + after "Pixel Data" (7fe0,0010) anymore + +Plugins +------- + +* New value in enumeration: OrthancPluginDicomToJsonFlags_StopAfterPixelData + +Maintenance +----------- + * Optimization in C-STORE SCP by avoiding an unnecessary DICOM parsing * Fix build on big-endian architectures * Possibility to generate a static library containing the Orthanc Framework diff -r 1f455b86b054 -r 5b929e6b3c36 OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp --- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Fri Feb 12 12:13:19 2021 +0100 +++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Tue Feb 16 12:18:41 2021 +0100 @@ -886,7 +886,8 @@ unsigned int maxStringLength, Encoding encoding, bool hasCodeExtensions, - const std::set& ignoreTagLength) + const std::set& ignoreTagLength, + unsigned int depth) { if (parent.type() == Json::nullValue) { @@ -925,7 +926,8 @@ { DcmItem* child = sequence.getItem(i); Json::Value& v = target.append(Json::objectValue); - DatasetToJson(v, *child, format, flags, maxStringLength, encoding, hasCodeExtensions, ignoreTagLength); + DatasetToJson(v, *child, format, flags, maxStringLength, encoding, hasCodeExtensions, + ignoreTagLength, depth + 1); } } } @@ -938,7 +940,8 @@ unsigned int maxStringLength, Encoding encoding, bool hasCodeExtensions, - const std::set& ignoreTagLength) + const std::set& ignoreTagLength, + unsigned int depth) { assert(parent.type() == Json::objectValue); @@ -952,6 +955,14 @@ DicomTag tag(FromDcmtkBridge::Convert(element->getTag())); + // New flag in Orthanc 1.9.1 + if (depth == 0 && + (flags & DicomToJsonFlags_StopAfterPixelData) && + tag > DICOM_TAG_PIXEL_DATA) + { + continue; + } + /*element->getTag().isPrivate()*/ if (tag.IsPrivate() && !(flags & DicomToJsonFlags_IncludePrivateTags)) @@ -978,8 +989,8 @@ } } - FromDcmtkBridge::ElementToJson(parent, *element, format, flags, - maxStringLength, encoding, hasCodeExtensions, ignoreTagLength); + FromDcmtkBridge::ElementToJson(parent, *element, format, flags, maxStringLength, encoding, + hasCodeExtensions, ignoreTagLength, depth); } } @@ -997,7 +1008,7 @@ Encoding encoding = DetectEncoding(hasCodeExtensions, dataset, defaultEncoding); target = Json::objectValue; - DatasetToJson(target, dataset, format, flags, maxStringLength, encoding, hasCodeExtensions, ignoreTagLength); + DatasetToJson(target, dataset, format, flags, maxStringLength, encoding, hasCodeExtensions, ignoreTagLength, 0); } @@ -1009,7 +1020,7 @@ { std::set ignoreTagLength; target = Json::objectValue; - DatasetToJson(target, dataset, format, flags, maxStringLength, Encoding_Ascii, false, ignoreTagLength); + DatasetToJson(target, dataset, format, flags, maxStringLength, Encoding_Ascii, false, ignoreTagLength, 0); } diff -r 1f455b86b054 -r 5b929e6b3c36 OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h --- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h Fri Feb 12 12:13:19 2021 +0100 +++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h Tue Feb 16 12:18:41 2021 +0100 @@ -69,7 +69,8 @@ unsigned int maxStringLength, Encoding encoding, bool hasCodeExtensions, - const std::set& ignoreTagLength); + const std::set& ignoreTagLength, + unsigned int depth); static void ElementToJson(Json::Value& parent, DcmElement& element, @@ -78,7 +79,8 @@ unsigned int maxStringLength, Encoding dicomEncoding, bool hasCodeExtensions, - const std::set& ignoreTagLength); + const std::set& ignoreTagLength, + unsigned int depth); static void ChangeStringEncoding(DcmItem& dataset, Encoding source, diff -r 1f455b86b054 -r 5b929e6b3c36 OrthancFramework/Sources/Enumerations.h --- a/OrthancFramework/Sources/Enumerations.h Fri Feb 12 12:13:19 2021 +0100 +++ b/OrthancFramework/Sources/Enumerations.h Tue Feb 16 12:18:41 2021 +0100 @@ -580,6 +580,7 @@ DicomToJsonFlags_IncludePixelData = (1 << 3), DicomToJsonFlags_ConvertBinaryToAscii = (1 << 4), DicomToJsonFlags_ConvertBinaryToNull = (1 << 5), + DicomToJsonFlags_StopAfterPixelData = (1 << 6), // New in Orthanc 1.9.1 // Some predefined combinations DicomToJsonFlags_None = 0, @@ -587,7 +588,8 @@ DicomToJsonFlags_IncludePixelData | DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_IncludeUnknownTags | - DicomToJsonFlags_ConvertBinaryToNull) + DicomToJsonFlags_ConvertBinaryToNull | + DicomToJsonFlags_StopAfterPixelData /* added in 1.9.1 */) }; enum DicomFromJsonFlags diff -r 1f455b86b054 -r 5b929e6b3c36 OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp --- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Fri Feb 12 12:13:19 2021 +0100 +++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Tue Feb 16 12:18:41 2021 +0100 @@ -628,6 +628,40 @@ } +TEST(ParsedDicomFile, ToJsonFlags3) +{ + ParsedDicomFile f(false); + + { + Uint8 v[2] = { 0, 0 }; + ASSERT_TRUE(f.GetDcmtkObject().getDataset()->putAndInsertString(DCM_PatientName, "HELLO^").good()); + ASSERT_TRUE(f.GetDcmtkObject().getDataset()->putAndInsertUint8Array(DCM_PixelData, v, 2).good()); + ASSERT_TRUE(f.GetDcmtkObject().getDataset()->putAndInsertString(DcmTag(0x07fe1, 0x0010), "WORLD^").good()); + } + + std::string s; + Toolbox::EncodeDataUriScheme(s, "application/octet-stream", std::string(2, '\0')); + + { + Json::Value v; + f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast(DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_IncludePixelData | DicomToJsonFlags_StopAfterPixelData), 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(2u, v.size()); + ASSERT_EQ("HELLO^", v["0010,0010"].asString()); + ASSERT_EQ(s, v["7fe0,0010"].asString()); + } + + { + Json::Value v; + f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_IncludePrivateTags, 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(2u, v.size()); + ASSERT_EQ("HELLO^", v["0010,0010"].asString()); + ASSERT_EQ("WORLD^", v["7fe1,0010"].asString()); + } +} + + TEST(DicomFindAnswers, Basic) { DicomFindAnswers a(false); diff -r 1f455b86b054 -r 5b929e6b3c36 OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp --- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Fri Feb 12 12:13:19 2021 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Tue Feb 16 12:18:41 2021 +0100 @@ -670,7 +670,7 @@ } - void OrthancPluginDatabase::ListAvailableAttachments(std::list& target, + void OrthancPluginDatabase::ListAvailableAttachments(std::set& target, int64_t id) { ResetAnswers(); @@ -690,7 +690,7 @@ for (std::list::const_iterator it = answerInt32_.begin(); it != answerInt32_.end(); ++it) { - target.push_back(static_cast(*it)); + target.insert(static_cast(*it)); } } } diff -r 1f455b86b054 -r 5b929e6b3c36 OrthancServer/Plugins/Engine/OrthancPluginDatabase.h --- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h Fri Feb 12 12:13:19 2021 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h Tue Feb 16 12:18:41 2021 +0100 @@ -230,7 +230,7 @@ virtual bool IsProtectedPatient(int64_t internalId) ORTHANC_OVERRIDE; - virtual void ListAvailableAttachments(std::list& target, + virtual void ListAvailableAttachments(std::set& target, int64_t id) ORTHANC_OVERRIDE; diff -r 1f455b86b054 -r 5b929e6b3c36 OrthancServer/Plugins/Engine/OrthancPlugins.cpp --- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Fri Feb 12 12:13:19 2021 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Tue Feb 16 12:18:41 2021 +0100 @@ -1702,6 +1702,7 @@ static_cast(OrthancPluginDicomToJsonFlags_IncludePixelData) != static_cast(DicomToJsonFlags_IncludePixelData) || static_cast(OrthancPluginDicomToJsonFlags_ConvertBinaryToNull) != static_cast(DicomToJsonFlags_ConvertBinaryToNull) || static_cast(OrthancPluginDicomToJsonFlags_ConvertBinaryToAscii) != static_cast(DicomToJsonFlags_ConvertBinaryToAscii) || + static_cast(OrthancPluginDicomToJsonFlags_StopAfterPixelData) != static_cast(DicomToJsonFlags_StopAfterPixelData) || static_cast(OrthancPluginCreateDicomFlags_DecodeDataUriScheme) != static_cast(DicomFromJsonFlags_DecodeDataUriScheme) || static_cast(OrthancPluginCreateDicomFlags_GenerateIdentifiers) != static_cast(DicomFromJsonFlags_GenerateIdentifiers)) diff -r 1f455b86b054 -r 5b929e6b3c36 OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h --- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Fri Feb 12 12:13:19 2021 +0100 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Tue Feb 16 12:18:41 2021 +0100 @@ -835,6 +835,7 @@ OrthancPluginDicomToJsonFlags_IncludePixelData = (1 << 3), /*!< Include the pixel data */ OrthancPluginDicomToJsonFlags_ConvertBinaryToAscii = (1 << 4), /*!< Output binary tags as-is, dropping non-ASCII */ OrthancPluginDicomToJsonFlags_ConvertBinaryToNull = (1 << 5), /*!< Signal binary tags as null values */ + OrthancPluginDicomToJsonFlags_StopAfterPixelData = (1 << 6), /*!< Stop processing after pixel data (new in 1.9.1) */ _OrthancPluginDicomToJsonFlags_INTERNAL = 0x7fffffff } OrthancPluginDicomToJsonFlags; diff -r 1f455b86b054 -r 5b929e6b3c36 OrthancServer/Plugins/Samples/StorageArea/CMakeLists.txt --- a/OrthancServer/Plugins/Samples/StorageArea/CMakeLists.txt Fri Feb 12 12:13:19 2021 +0100 +++ b/OrthancServer/Plugins/Samples/StorageArea/CMakeLists.txt Tue Feb 16 12:18:41 2021 +0100 @@ -23,4 +23,12 @@ include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake) +set(USE_LEGACY_API OFF CACHE BOOL "Whether to enable support for read-range") + +if (USE_LEGACY_API) + add_definitions(-DUSE_LEGACY_API=1) +else() + add_definitions(-DUSE_LEGACY_API=0) +endif() + add_library(PluginTest SHARED Plugin.cpp) diff -r 1f455b86b054 -r 5b929e6b3c36 OrthancServer/Plugins/Samples/StorageArea/Plugin.cpp --- a/OrthancServer/Plugins/Samples/StorageArea/Plugin.cpp Fri Feb 12 12:13:19 2021 +0100 +++ b/OrthancServer/Plugins/Samples/StorageArea/Plugin.cpp Tue Feb 16 12:18:41 2021 +0100 @@ -26,9 +26,6 @@ #include -#define USE_LEGACY_API 0 - - static OrthancPluginContext* context = NULL; diff -r 1f455b86b054 -r 5b929e6b3c36 OrthancServer/Sources/Database/IDatabaseWrapper.h --- a/OrthancServer/Sources/Database/IDatabaseWrapper.h Fri Feb 12 12:13:19 2021 +0100 +++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h Tue Feb 16 12:18:41 2021 +0100 @@ -43,6 +43,7 @@ #include #include +#include namespace Orthanc { @@ -153,7 +154,7 @@ virtual bool IsProtectedPatient(int64_t internalId) = 0; - virtual void ListAvailableAttachments(std::list& target, + virtual void ListAvailableAttachments(std::set& target, int64_t id) = 0; virtual void LogChange(int64_t internalId, diff -r 1f455b86b054 -r 5b929e6b3c36 OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp --- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Fri Feb 12 12:13:19 2021 +0100 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Tue Feb 16 12:18:41 2021 +0100 @@ -803,7 +803,7 @@ } - void SQLiteDatabaseWrapper::ListAvailableAttachments(std::list& target, + void SQLiteDatabaseWrapper::ListAvailableAttachments(std::set& target, int64_t id) { target.clear(); @@ -814,7 +814,7 @@ while (s.Step()) { - target.push_back(static_cast(s.ColumnInt(0))); + target.insert(static_cast(s.ColumnInt(0))); } } diff -r 1f455b86b054 -r 5b929e6b3c36 OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h --- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h Fri Feb 12 12:13:19 2021 +0100 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h Tue Feb 16 12:18:41 2021 +0100 @@ -234,7 +234,7 @@ FileContentType attachment) ORTHANC_OVERRIDE; - virtual void ListAvailableAttachments(std::list& target, + virtual void ListAvailableAttachments(std::set& target, int64_t id) ORTHANC_OVERRIDE; diff -r 1f455b86b054 -r 5b929e6b3c36 OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Fri Feb 12 12:13:19 2021 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Tue Feb 16 12:18:41 2021 +0100 @@ -1576,12 +1576,12 @@ const std::string resourceType = call.GetFullUri() [0]; const std::string publicId = call.GetUriComponent("id", ""); - std::list attachments; + std::set attachments; OrthancRestApi::GetIndex(call).ListAvailableAttachments(attachments, publicId, StringToResourceType(resourceType.c_str())); Json::Value result = Json::arrayValue; - for (std::list::const_iterator + for (std::set::const_iterator it = attachments.begin(); it != attachments.end(); ++it) { result.append(EnumerationToString(*it)); diff -r 1f455b86b054 -r 5b929e6b3c36 OrthancServer/Sources/ServerContext.cpp --- a/OrthancServer/Sources/ServerContext.cpp Fri Feb 12 12:13:19 2021 +0100 +++ b/OrthancServer/Sources/ServerContext.cpp Tue Feb 16 12:18:41 2021 +0100 @@ -583,12 +583,14 @@ ServerIndex::Attachments attachments; attachments.push_back(dicomInfo); - FileInfo jsonInfo; - if (true /* TODO - !area_.HasReadRange() || !hasPixelDataOffset || compression != CompressionType_DicomAsJson */) + FileInfo dicomUntilPixelData; + if (hasPixelDataOffset && + (!area_.HasReadRange() || + compression != CompressionType_None)) { - jsonInfo = accessor.Write(dicomAsJson.toStyledString(), - FileContentType_DicomAsJson, compression, storeMD5_); - attachments.push_back(jsonInfo); + dicomUntilPixelData = accessor.Write(dicom.GetBufferData(), pixelDataOffset, + FileContentType_DicomUntilPixelData, compression, storeMD5_); + attachments.push_back(dicomUntilPixelData); } typedef std::map InstanceMetadata; @@ -610,9 +612,9 @@ { accessor.Remove(dicomInfo); - if (jsonInfo.IsValid()) + if (dicomUntilPixelData.IsValid()) { - accessor.Remove(jsonInfo); + accessor.Remove(dicomUntilPixelData); } } @@ -807,61 +809,178 @@ } + static void InjectEmptyPixelData(Json::Value& dicomAsJson) + { + // This is for backward compatibility with Orthanc <= 1.9.0 + Json::Value pixelData = Json::objectValue; + pixelData["Name"] = "PixelData"; + pixelData["Type"] = "Null"; + pixelData["Value"] = Json::nullValue; + + dicomAsJson["7fe0,0010"] = pixelData; + } + + void ServerContext::ReadDicomAsJson(Json::Value& result, const std::string& instancePublicId, const std::set& ignoreTagLength) { - if (ignoreTagLength.empty()) + /** + * CASE 1: The DICOM file, truncated at pixel data, is available + * as an attachment (it was created either because the storage + * area does not support range reads, or it "StorageCompression" + * is enabled). Simply return this attachment. + **/ + + FileInfo attachment; + + if (index_.LookupAttachment(attachment, instancePublicId, FileContentType_DicomUntilPixelData)) { - std::string tmp; + std::string dicom; { - FileInfo attachment; - if (index_.LookupAttachment(attachment, instancePublicId, FileContentType_DicomAsJson)) + StorageAccessor accessor(area_, GetMetricsRegistry()); + accessor.Read(dicom, attachment); + } + + ParsedDicomFile parsed(dicom); + OrthancConfiguration::DefaultDicomDatasetToJson(result, parsed, ignoreTagLength); + InjectEmptyPixelData(result); + } + else + { + /** + * The truncated DICOM file is not stored as a standalone + * attachment. Lookup whether the pixel data offset has already + * been computed for this instance. + **/ + + bool hasPixelDataOffset; + uint64_t pixelDataOffset; + + { + std::string s; + if (index_.LookupMetadata(s, instancePublicId, ResourceType_Instance, + MetadataType_Instance_PixelDataOffset)) { - StorageAccessor accessor(area_, GetMetricsRegistry()); - accessor.Read(tmp, attachment); + hasPixelDataOffset = false; + + if (!s.empty()) + { + try + { + pixelDataOffset = boost::lexical_cast(s); + hasPixelDataOffset = true; + } + catch (boost::bad_lexical_cast&) + { + } + } + + if (!hasPixelDataOffset) + { + LOG(ERROR) << "Metadata \"PixelDataOffset\" is corrupted for instance: " << instancePublicId; + } } else { - // The "DICOM as JSON" summary is not available from the Orthanc - // store (most probably deleted), reconstruct it from the DICOM file - std::string dicom; - ReadDicom(dicom, instancePublicId); + // This instance was created by a version of Orthanc <= 1.9.0 + hasPixelDataOffset = false; + } + } + + + if (hasPixelDataOffset && + area_.HasReadRange() && + index_.LookupAttachment(attachment, instancePublicId, FileContentType_Dicom) && + attachment.GetCompressionType() == CompressionType_None) + { + /** + * CASE 2: The pixel data offset is known, AND that a range read + * can be used to retrieve the truncated DICOM file. Note that + * this case cannot be used if "StorageCompression" option is + * "true". + **/ + + StorageAccessor accessor(area_, GetMetricsRegistry()); + std::unique_ptr dicom( + area_.ReadRange(attachment.GetUuid(), FileContentType_Dicom, 0, pixelDataOffset)); - LOG(INFO) << "Reconstructing the missing DICOM-as-JSON summary for instance: " - << instancePublicId; - - ParsedDicomFile parsed(dicom); + if (dicom.get() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + assert(dicom->GetSize() == pixelDataOffset); + ParsedDicomFile parsed(dicom->GetData(), dicom->GetSize()); + OrthancConfiguration::DefaultDicomDatasetToJson(result, parsed, ignoreTagLength); + InjectEmptyPixelData(result); + } + } + else if (ignoreTagLength.empty() && + index_.LookupAttachment(attachment, instancePublicId, FileContentType_DicomAsJson)) + { + /** + * CASE 3: This instance was created using Orthanc <= + * 1.9.0. Reuse the old "DICOM-as-JSON" attachment if available. + * This is for backward compatibility: A call to + * "/tools/invalidate-tags" or to one flavors of + * "/.../.../reconstruct" will disable this case. + **/ + + std::string dicomAsJson; - Json::Value summary; - OrthancConfiguration::DefaultDicomDatasetToJson(summary, parsed); + { + StorageAccessor accessor(area_, GetMetricsRegistry()); + accessor.Read(dicomAsJson, attachment); + } - tmp = summary.toStyledString(); + if (!Toolbox::ReadJson(result, dicomAsJson)) + { + throw OrthancException(ErrorCode_CorruptedFile, + "Corrupted DICOM-as-JSON attachment of instance: " + instancePublicId); + } + } + else + { + /** + * CASE 4: Neither the truncated DICOM file is accessible, nor + * the DICOM-as-JSON summary. We have to retrieve the full DICOM + * file from the storage area. + **/ + + std::string dicom; + ReadDicom(dicom, instancePublicId); - if (!AddAttachment(instancePublicId, FileContentType_DicomAsJson, - tmp.c_str(), tmp.size())) + ParsedDicomFile parsed(dicom); + OrthancConfiguration::DefaultDicomDatasetToJson(result, parsed, ignoreTagLength); + + if (!hasPixelDataOffset) + { + /** + * The pixel data offset was never computed for this + * instance, which indicates that it was created using + * Orthanc <= 1.9.0, or that calls to + * "LookupPixelDataOffset()" from earlier versions of + * Orthanc have failed. Try again this precomputation now + * for future calls. + **/ + if (DicomStreamReader::LookupPixelDataOffset(pixelDataOffset, dicom) && + pixelDataOffset < dicom.size()) { - throw OrthancException(ErrorCode_InternalError, - "Cannot associate the DICOM-as-JSON summary to instance: " + instancePublicId); + index_.SetMetadata(instancePublicId, MetadataType_Instance_PixelDataOffset, + boost::lexical_cast(pixelDataOffset)); + + if (!area_.HasReadRange() || + compressionEnabled_ != CompressionType_None) + { + AddAttachment(instancePublicId, FileContentType_DicomUntilPixelData, + dicom.empty() ? NULL: dicom.c_str(), pixelDataOffset); + } } } } - - if (!Toolbox::ReadJson(result, tmp)) - { - throw OrthancException(ErrorCode_CorruptedFile); - } - } - else - { - // The "DicomAsJson" attachment might have stored some tags as - // "too long". We are forced to re-parse the DICOM file. - std::string dicom; - ReadDicom(dicom, instancePublicId); - - ParsedDicomFile parsed(dicom); - OrthancConfiguration::DefaultDicomDatasetToJson(result, parsed, ignoreTagLength); } } diff -r 1f455b86b054 -r 5b929e6b3c36 OrthancServer/Sources/ServerEnumerations.cpp --- a/OrthancServer/Sources/ServerEnumerations.cpp Fri Feb 12 12:13:19 2021 +0100 +++ b/OrthancServer/Sources/ServerEnumerations.cpp Tue Feb 16 12:18:41 2021 +0100 @@ -74,6 +74,7 @@ dictContentType_.Add(FileContentType_Dicom, "dicom"); dictContentType_.Add(FileContentType_DicomAsJson, "dicom-as-json"); + dictContentType_.Add(FileContentType_DicomUntilPixelData, "dicom-until-pixel-data"); } void RegisterUserMetadata(int metadata, diff -r 1f455b86b054 -r 5b929e6b3c36 OrthancServer/Sources/ServerIndex.cpp --- a/OrthancServer/Sources/ServerIndex.cpp Fri Feb 12 12:13:19 2021 +0100 +++ b/OrthancServer/Sources/ServerIndex.cpp Tue Feb 16 12:18:41 2021 +0100 @@ -975,11 +975,13 @@ } } - // New in Orthanc 1.9.1 - SetInstanceMetadata(content, instanceMetadata, instanceId, - MetadataType_Instance_PixelDataOffset, - (hasPixelDataOffset ? - boost::lexical_cast(pixelDataOffset) : "")); + if (hasPixelDataOffset) + { + // New in Orthanc 1.9.1 + SetInstanceMetadata(content, instanceMetadata, instanceId, + MetadataType_Instance_PixelDataOffset, + boost::lexical_cast(pixelDataOffset)); + } const DicomValue* value; if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL && @@ -1900,7 +1902,7 @@ } - void ServerIndex::ListAvailableAttachments(std::list& target, + void ServerIndex::ListAvailableAttachments(std::set& target, const std::string& publicId, ResourceType expectedType) { @@ -2030,10 +2032,10 @@ ResourceType thisType = db_.GetResourceType(resource); - std::list f; + std::set f; db_.ListAvailableAttachments(f, resource); - for (std::list::const_iterator + for (std::set::const_iterator it = f.begin(); it != f.end(); ++it) { FileInfo attachment; diff -r 1f455b86b054 -r 5b929e6b3c36 OrthancServer/Sources/ServerIndex.h --- a/OrthancServer/Sources/ServerIndex.h Fri Feb 12 12:13:19 2021 +0100 +++ b/OrthancServer/Sources/ServerIndex.h Tue Feb 16 12:18:41 2021 +0100 @@ -218,7 +218,7 @@ ResourceType expectedType, MetadataType type); - void ListAvailableAttachments(std::list& target, + void ListAvailableAttachments(std::set& target, const std::string& publicId, ResourceType expectedType); diff -r 1f455b86b054 -r 5b929e6b3c36 OrthancServer/UnitTestsSources/ServerIndexTests.cpp --- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Fri Feb 12 12:13:19 2021 +0100 +++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Tue Feb 16 12:18:41 2021 +0100 @@ -37,6 +37,7 @@ #include "../../OrthancFramework/Sources/Compatibility.h" #include "../../OrthancFramework/Sources/FileStorage/FilesystemStorage.h" #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h" +#include "../../OrthancFramework/Sources/Images/Image.h" #include "../../OrthancFramework/Sources/Logging.h" #include "../Sources/Database/SQLiteDatabaseWrapper.h" @@ -734,6 +735,7 @@ { DicomMap summary; OrthancConfiguration::DefaultExtractDicomSummary(summary, toStore->GetParsedDicomFile()); + toStore->SetOrigin(DicomInstanceOrigin::FromPlugins()); DicomTransferSyntax transferSyntax; bool hasTransferSyntax = dicom.LookupTransferSyntax(transferSyntax); @@ -798,6 +800,10 @@ TEST(ServerIndex, Overwrite) { + // Create a dummy 1x1 image + Image image(PixelFormat_Grayscale8, 1, 1, false); + reinterpret_cast(image.GetBuffer()) [0] = 128; + for (unsigned int i = 0; i < 2; i++) { bool overwrite = (i == 0); @@ -831,6 +837,10 @@ { ParsedDicomFile dicom(instance, GetDefaultDicomEncoding(), false /* be strict */); + // Add a pixel data so as to have one "FileContentType_DicomUntilPixelData" + // (because of "context.SetCompressionEnabled(true)") + dicom.EmbedImage(image); + DicomInstanceHasher hasher(instance); std::unique_ptr toStore(DicomInstanceToStore::CreateFromParsedDicomFile(dicom)); @@ -842,15 +852,20 @@ ASSERT_EQ(id, id2); } - FileInfo dicom1, json1; + { + FileInfo nope; + ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, id, FileContentType_DicomAsJson)); + } + + FileInfo dicom1, pixelData1; ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom1, id, FileContentType_Dicom)); - ASSERT_TRUE(context.GetIndex().LookupAttachment(json1, id, FileContentType_DicomAsJson)); + ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData1, id, FileContentType_DicomUntilPixelData)); context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances); ASSERT_EQ(1u, countInstances); - ASSERT_EQ(dicom1.GetCompressedSize() + json1.GetCompressedSize(), diskSize); - ASSERT_EQ(dicom1.GetUncompressedSize() + json1.GetUncompressedSize(), uncompressedSize); + ASSERT_EQ(dicom1.GetCompressedSize() + pixelData1.GetCompressedSize(), diskSize); + ASSERT_EQ(dicom1.GetUncompressedSize() + pixelData1.GetUncompressedSize(), uncompressedSize); Json::Value tmp; context.ReadDicomAsJson(tmp, id); @@ -870,6 +885,9 @@ ParsedDicomFile dicom(instance2, GetDefaultDicomEncoding(), false /* be strict */); + // Add a pixel data so as to have one "FileContentType_DicomUntilPixelData" + dicom.EmbedImage(image); + std::unique_ptr toStore(DicomInstanceToStore::CreateFromParsedDicomFile(dicom)); toStore->SetOrigin(DicomInstanceOrigin::FromPlugins()); @@ -879,22 +897,27 @@ ASSERT_EQ(id, id2); } - FileInfo dicom2, json2; + { + FileInfo nope; + ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, id, FileContentType_DicomAsJson)); + } + + FileInfo dicom2, pixelData2; ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom2, id, FileContentType_Dicom)); - ASSERT_TRUE(context.GetIndex().LookupAttachment(json2, id, FileContentType_DicomAsJson)); + ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData2, id, FileContentType_DicomUntilPixelData)); context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances); ASSERT_EQ(1u, countInstances); - ASSERT_EQ(dicom2.GetCompressedSize() + json2.GetCompressedSize(), diskSize); - ASSERT_EQ(dicom2.GetUncompressedSize() + json2.GetUncompressedSize(), uncompressedSize); + ASSERT_EQ(dicom2.GetCompressedSize() + pixelData2.GetCompressedSize(), diskSize); + ASSERT_EQ(dicom2.GetUncompressedSize() + pixelData2.GetUncompressedSize(), uncompressedSize); if (overwrite) { ASSERT_NE(dicom1.GetUuid(), dicom2.GetUuid()); - ASSERT_NE(json1.GetUuid(), json2.GetUuid()); + ASSERT_NE(pixelData1.GetUuid(), pixelData2.GetUuid()); ASSERT_NE(dicom1.GetUncompressedSize(), dicom2.GetUncompressedSize()); - ASSERT_NE(json1.GetUncompressedSize(), json2.GetUncompressedSize()); + ASSERT_NE(pixelData1.GetUncompressedSize(), pixelData2.GetUncompressedSize()); context.ReadDicomAsJson(tmp, id); ASSERT_EQ("overwritten", tmp["0010,0010"]["Value"].asString()); @@ -909,9 +932,9 @@ else { ASSERT_EQ(dicom1.GetUuid(), dicom2.GetUuid()); - ASSERT_EQ(json1.GetUuid(), json2.GetUuid()); + ASSERT_EQ(pixelData1.GetUuid(), pixelData2.GetUuid()); ASSERT_EQ(dicom1.GetUncompressedSize(), dicom2.GetUncompressedSize()); - ASSERT_EQ(json1.GetUncompressedSize(), json2.GetUncompressedSize()); + ASSERT_EQ(pixelData1.GetUncompressedSize(), pixelData2.GetUncompressedSize()); context.ReadDicomAsJson(tmp, id); ASSERT_EQ("name", tmp["0010,0010"]["Value"].asString()); @@ -930,3 +953,74 @@ } +TEST(ServerIndex, DicomUntilPixelData) +{ + // Create a dummy 1x1 image + Image image(PixelFormat_Grayscale8, 1, 1, false); + reinterpret_cast(image.GetBuffer()) [0] = 128; + + for (unsigned int i = 0; i < 2; i++) + { + const bool compression = (i == 0); + + MemoryStorageArea storage; + SQLiteDatabaseWrapper db; // The SQLite DB is in memory + db.Open(); + ServerContext context(db, storage, true /* running unit tests */, 10); + context.SetupJobsEngine(true, false); + context.SetCompressionEnabled(compression); + + for (unsigned int j = 0; j < 2; j++) + { + const bool withPixelData = (j == 0); + + ParsedDicomFile dicom(true); + + if (withPixelData) + { + dicom.EmbedImage(image); + } + + std::string id; + size_t dicomSize; + + { + std::unique_ptr toStore(DicomInstanceToStore::CreateFromParsedDicomFile(dicom)); + dicomSize = toStore->GetBufferSize(); + toStore->SetOrigin(DicomInstanceOrigin::FromPlugins()); + ASSERT_EQ(StoreStatus_Success, context.Store(id, *toStore, StoreInstanceMode_Default)); + } + + std::set attachments; + context.GetIndex().ListAvailableAttachments(attachments, id, ResourceType_Instance); + + ASSERT_TRUE(attachments.find(FileContentType_Dicom) != attachments.end()); + + if (compression && + withPixelData) + { + ASSERT_EQ(2u, attachments.size()); + ASSERT_TRUE(attachments.find(FileContentType_DicomUntilPixelData) != attachments.end()); + } + else + { + ASSERT_EQ(1u, attachments.size()); + } + + std::string s; + bool found = context.GetIndex().LookupMetadata(s, id, ResourceType_Instance, + MetadataType_Instance_PixelDataOffset); + + if (withPixelData) + { + ASSERT_TRUE(found); + ASSERT_GT(boost::lexical_cast(s), 128 /* length of the DICOM preamble */); + ASSERT_LT(boost::lexical_cast(s), dicomSize); + } + else + { + ASSERT_FALSE(found); + } + } + } +} diff -r 1f455b86b054 -r 5b929e6b3c36 OrthancServer/UnitTestsSources/UnitTestsMain.cpp --- a/OrthancServer/UnitTestsSources/UnitTestsMain.cpp Fri Feb 12 12:13:19 2021 +0100 +++ b/OrthancServer/UnitTestsSources/UnitTestsMain.cpp Tue Feb 16 12:18:41 2021 +0100 @@ -332,16 +332,16 @@ ignoreTagLength.insert(DICOM_TAG_PATIENT_ID); FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short, - DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength); + DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength, 0); ASSERT_TRUE(b.isMember("0010,0010")); ASSERT_EQ("Hello", b["0010,0010"].asString()); FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short, - DicomToJsonFlags_Default, 3, Encoding_Ascii, false, ignoreTagLength); + DicomToJsonFlags_Default, 3, Encoding_Ascii, false, ignoreTagLength, 0); ASSERT_TRUE(b["0010,0010"].isNull()); // "Hello" has more than 3 characters FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Full, - DicomToJsonFlags_Default, 3, Encoding_Ascii, false, ignoreTagLength); + DicomToJsonFlags_Default, 3, Encoding_Ascii, false, ignoreTagLength, 0); ASSERT_TRUE(b["0010,0010"].isObject()); ASSERT_EQ("PatientName", b["0010,0010"]["Name"].asString()); ASSERT_EQ("TooLong", b["0010,0010"]["Type"].asString()); @@ -349,7 +349,7 @@ ignoreTagLength.insert(DICOM_TAG_PATIENT_NAME); FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short, - DicomToJsonFlags_Default, 3, Encoding_Ascii, false, ignoreTagLength); + DicomToJsonFlags_Default, 3, Encoding_Ascii, false, ignoreTagLength, 0); ASSERT_EQ("Hello", b["0010,0010"].asString()); } @@ -375,7 +375,7 @@ Json::Value b; std::set ignoreTagLength; FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short, - DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength); + DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength, 0); ASSERT_EQ("Hello", b["0010,0010"].asString()); } @@ -388,7 +388,7 @@ Json::Value b; std::set ignoreTagLength; FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short, - DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength); + DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength, 0); ASSERT_EQ(Json::arrayValue, b["0008,1110"].type()); ASSERT_EQ(2u, b["0008,1110"].size()); @@ -407,7 +407,7 @@ Json::Value b; std::set ignoreTagLength; FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Full, - DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength); + DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength, 0); Json::Value c; Toolbox::SimplifyDicomAsJson(c, b, DicomToJsonFormat_Human);