# HG changeset patch # User Sebastien Jodogne # Date 1625553643 -7200 # Node ID c1d6ce00be3f2b2a8b53bff0fea21b9ac7ffe359 # Parent 8cc9137b5c2e2d17ae165ee2e0e49e5c3e826fcc# Parent 9ce946d28e41047a5e2674190071512e29d2649c integration mainline->openssl-3.x diff -r 8cc9137b5c2e -r c1d6ce00be3f NEWS --- a/NEWS Tue Jul 06 08:36:54 2021 +0200 +++ b/NEWS Tue Jul 06 08:40:43 2021 +0200 @@ -1,6 +1,40 @@ Pending changes in the mainline =============================== +OpenSSL 3.x branch +------------------ + +* General information: + https://www.openssl.org/blog/blog/2021/06/17/OpenSSL3.0ReleaseCandidate/ +* Dropped support for static compilation of OpenSSL 1.0.2 and 1.1.1 +* Removed the OpenSSL license exception, as binary versions of Orthanc are now + designed to use OpenSSL 3.x, that was re-licensed under Apache 2.0, making + it compatible with the GPL/AGPL licenses used by the Orthanc project: + https://en.wikipedia.org/wiki/OpenSSL#Licensing + https://people.gnome.org/~markmc/openssl-and-the-gpl.html +* Upgraded dependencies for static builds (notably on Windows and LSB): + - openssl 3.0.0-beta1 + +General +------- + +* Anonymization is now also applied recursively to nested tags + +REST API +-------- + +* API version upgraded to 14 +* Added "Short", "Simplify" and/or "Full" options to control the format of DICOM tags in: + - POST /modalities/{id}/find-worklist + - POST /queries/{id}/answers/{index}/retrieve + - POST /queries/{id}/retrieve + +Maintenance +----------- + +* Fix broken "Do lookup" button in Orthanc Explorer +* Error code and description of jobs are now saved into the Orthanc database + Version 1.9.4 (2021-06-24) ========================== @@ -44,6 +78,7 @@ - GET /series/{id}/instances-tags, GET /studies/{id}/shared-tags - GET /patients/{id}/module, GET /patients/{id}/patient-module - GET /series/{id}/module, GET /studies/{id}/module, GET /instances/{id}/module + - GET /queries/{id}/answers&expand, GET /queries/{id}/answers/{index}/content - POST /tools/find * "/studies/{id}/split" accepts "Instances" parameter to split instances instead of series * "/studies/{id}/merge" accepts instances inside its "Resources" parameter @@ -57,20 +92,6 @@ * Upgraded dependencies for static builds (notably on Windows): - curl 7.77.0 -OpenSSL 3.x branch ------------------- - -* General information: - https://www.openssl.org/blog/blog/2021/06/17/OpenSSL3.0ReleaseCandidate/ -* Dropped support for static compilation of OpenSSL 1.0.2 and 1.1.1 -* Removed the OpenSSL license exception, as binary versions of Orthanc are now - designed to use OpenSSL 3.x, that was re-licensed under Apache 2.0, making - it compatible with the GPL/AGPL licenses used by the Orthanc project: - https://en.wikipedia.org/wiki/OpenSSL#Licensing - https://people.gnome.org/~markmc/openssl-and-the-gpl.html -* Upgraded dependencies for static builds (notably on Windows and LSB): - - openssl 3.0.0-beta1 - Version 1.9.3 (2021-05-07) ========================== diff -r 8cc9137b5c2e -r c1d6ce00be3f OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake --- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake Tue Jul 06 08:36:54 2021 +0200 +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake Tue Jul 06 08:40:43 2021 +0200 @@ -37,7 +37,7 @@ # Version of the Orthanc API, can be retrieved from "/system" URI in # order to check whether new URI endpoints are available even if using # the mainline version of Orthanc -set(ORTHANC_API_VERSION "13") +set(ORTHANC_API_VERSION "14") ##################################################################### diff -r 8cc9137b5c2e -r c1d6ce00be3f OrthancFramework/Sources/DicomFormat/DicomPath.cpp --- a/OrthancFramework/Sources/DicomFormat/DicomPath.cpp Tue Jul 06 08:36:54 2021 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomPath.cpp Tue Jul 06 08:40:43 2021 +0200 @@ -372,4 +372,42 @@ } } } + + + bool DicomPath::IsMatch(const DicomPath& pattern, + const std::vector& prefixTags, + const std::vector& prefixIndexes, + const DicomTag& finalTag) + { + if (prefixTags.size() != prefixIndexes.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (prefixTags.size() < pattern.GetPrefixLength()) + { + return false; + } + else + { + for (size_t i = 0; i < pattern.GetPrefixLength(); i++) + { + if (prefixTags[i] != pattern.GetPrefixTag(i) || + (!pattern.IsPrefixUniversal(i) && + prefixIndexes[i] != pattern.GetPrefixIndex(i))) + { + return false; + } + } + + if (prefixTags.size() == pattern.GetPrefixLength()) + { + return (finalTag == pattern.GetFinalTag()); + } + else + { + return (prefixTags[pattern.GetPrefixLength()] == pattern.GetFinalTag()); + } + } + } } diff -r 8cc9137b5c2e -r c1d6ce00be3f OrthancFramework/Sources/DicomFormat/DicomPath.h --- a/OrthancFramework/Sources/DicomFormat/DicomPath.h Tue Jul 06 08:36:54 2021 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomPath.h Tue Jul 06 08:40:43 2021 +0200 @@ -130,5 +130,10 @@ static bool IsMatch(const DicomPath& pattern, const DicomPath& path); + + static bool IsMatch(const DicomPath& pattern, + const std::vector& prefixTags, + const std::vector& prefixIndexes, + const DicomTag& finalTag); }; } diff -r 8cc9137b5c2e -r c1d6ce00be3f OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.cpp --- a/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.cpp Tue Jul 06 08:36:54 2021 +0200 +++ b/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.cpp Tue Jul 06 08:40:43 2021 +0200 @@ -197,24 +197,22 @@ void DicomFindAnswers::ToJson(Json::Value& target, size_t index, - bool simplify) const + DicomToJsonFormat format) const { - DicomToJsonFormat format = (simplify ? DicomToJsonFormat_Human : DicomToJsonFormat_Full); - const ParsedDicomFile& answer = GetAnswer(index); answer.DatasetToJson(target, format, DicomToJsonFlags_None, 0); } void DicomFindAnswers::ToJson(Json::Value& target, - bool simplify) const + DicomToJsonFormat format) const { target = Json::arrayValue; for (size_t i = 0; i < GetSize(); i++) { Json::Value answer; - ToJson(answer, i, simplify); + ToJson(answer, i, format); target.append(answer); } } @@ -236,5 +234,21 @@ { return Add(const_cast(dicom)); } + + void DicomFindAnswers::ToJson(Json::Value& target, + size_t index, + bool simplify) const + { + DicomToJsonFormat format = (simplify ? DicomToJsonFormat_Human : DicomToJsonFormat_Full); + ToJson(target, index, format); + } + + + void DicomFindAnswers::ToJson(Json::Value& target, + bool simplify) const + { + DicomToJsonFormat format = (simplify ? DicomToJsonFormat_Human : DicomToJsonFormat_Full); + ToJson(target, format); + } #endif } diff -r 8cc9137b5c2e -r c1d6ce00be3f OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.h --- a/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.h Tue Jul 06 08:36:54 2021 +0200 +++ b/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.h Tue Jul 06 08:40:43 2021 +0200 @@ -39,6 +39,13 @@ #if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1 // Alias for binary compatibility with Orthanc Framework 1.7.2 => don't use it anymore void Add(ParsedDicomFile& dicom); + + void ToJson(Json::Value& target, + bool simplify) const; + + void ToJson(Json::Value& target, + size_t index, + bool simplify) const; #endif public: @@ -72,11 +79,11 @@ DcmDataset* ExtractDcmDataset(size_t index) const; void ToJson(Json::Value& target, - bool simplify) const; + DicomToJsonFormat format) const; void ToJson(Json::Value& target, size_t index, - bool simplify) const; + DicomToJsonFormat format) const; bool IsComplete() const; diff -r 8cc9137b5c2e -r c1d6ce00be3f OrthancFramework/Sources/DicomParsing/DicomModification.cpp --- a/OrthancFramework/Sources/DicomParsing/DicomModification.cpp Tue Jul 06 08:36:54 2021 +0200 +++ b/OrthancFramework/Sources/DicomParsing/DicomModification.cpp Tue Jul 06 08:40:43 2021 +0200 @@ -44,6 +44,16 @@ namespace Orthanc { + namespace + { + enum TagOperation + { + TagOperation_Keep, + TagOperation_Remove + }; + } + + DicomModification::DicomTagRange::DicomTagRange(uint16_t groupFrom, uint16_t groupTo, uint16_t elementFrom, @@ -76,7 +86,57 @@ return (that_.IsCleared(tag) || that_.IsRemoved(tag) || that_.IsReplaced(tag)); - } + } + + bool IsKeptSequence(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag) + { + for (DicomModification::ListOfPaths::const_iterator + it = that_.keepSequences_.begin(); it != that_.keepSequences_.end(); ++it) + { + if (DicomPath::IsMatch(*it, parentTags, parentIndexes, tag)) + { + return true; + } + } + + return false; + } + + Action GetDefaultAction(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag) + { + if (parentTags.empty() || + !that_.isAnonymization_) + { + // Don't interfere with first-level tags or with modification + return Action_None; + } + else if (IsKeptSequence(parentTags, parentIndexes, tag)) + { + return Action_None; + } + else if (that_.ArePrivateTagsRemoved() && + tag.IsPrivate()) + { + // New in Orthanc 1.9.5 + // https://groups.google.com/g/orthanc-users/c/l1mcYCC2u-k/m/jOdGYuagAgAJ + return Action_Remove; + } + else if (that_.IsCleared(tag) || + that_.IsRemoved(tag)) + { + // New in Orthanc 1.9.5 + // https://groups.google.com/g/orthanc-users/c/l1mcYCC2u-k/m/jOdGYuagAgAJ + return Action_Remove; + } + else + { + return Action_None; + } + } public: explicit RelationshipsVisitor(DicomModification& that) : @@ -84,49 +144,56 @@ { } - virtual void VisitNotSupported(const std::vector& parentTags, - const std::vector& parentIndexes, - const DicomTag& tag, - ValueRepresentation vr) + virtual Action VisitNotSupported(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr) ORTHANC_OVERRIDE { + return GetDefaultAction(parentTags, parentIndexes, tag); } - virtual void VisitEmptySequence(const std::vector& parentTags, - const std::vector& parentIndexes, - const DicomTag& tag) + virtual Action VisitSequence(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + size_t countItems) ORTHANC_OVERRIDE { + return GetDefaultAction(parentTags, parentIndexes, tag); } - virtual void VisitBinary(const std::vector& parentTags, - const std::vector& parentIndexes, - const DicomTag& tag, - ValueRepresentation vr, - const void* data, - size_t size) - { - } - - virtual void VisitIntegers(const std::vector& parentTags, + virtual Action VisitBinary(const std::vector& parentTags, const std::vector& parentIndexes, const DicomTag& tag, ValueRepresentation vr, - const std::vector& values) + const void* data, + size_t size) ORTHANC_OVERRIDE { + return GetDefaultAction(parentTags, parentIndexes, tag); + } + + virtual Action VisitIntegers(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + const std::vector& values) ORTHANC_OVERRIDE + { + return GetDefaultAction(parentTags, parentIndexes, tag); } - virtual void VisitDoubles(const std::vector& parentTags, - const std::vector& parentIndexes, - const DicomTag& tag, - ValueRepresentation vr, - const std::vector& value) + virtual Action VisitDoubles(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + const std::vector& value) ORTHANC_OVERRIDE { + return GetDefaultAction(parentTags, parentIndexes, tag); } - virtual void VisitAttributes(const std::vector& parentTags, - const std::vector& parentIndexes, - const DicomTag& tag, - const std::vector& value) + virtual Action VisitAttributes(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + const std::vector& value) ORTHANC_OVERRIDE { + return GetDefaultAction(parentTags, parentIndexes, tag); } virtual Action VisitString(std::string& newValue, @@ -134,7 +201,7 @@ const std::vector& parentIndexes, const DicomTag& tag, ValueRepresentation vr, - const std::string& value) + const std::string& value) ORTHANC_OVERRIDE { /** * Note that all the tags in "uids_" have the VR UI (unique @@ -181,18 +248,30 @@ { // We are within a sequence - if (!that_.keepSequences_.empty()) + if (IsKeptSequence(parentTags, parentIndexes, tag)) { // New in Orthanc 1.9.4 - Solves issue LSD-629 - DicomPath path(parentTags, parentIndexes, tag); - - for (ListOfPaths::const_iterator it = that_.keepSequences_.begin(); - it != that_.keepSequences_.end(); ++it) + return Action_None; + } + + if (that_.isAnonymization_) + { + // New in Orthanc 1.9.5, similar to "GetDefaultAction()" + // https://groups.google.com/g/orthanc-users/c/l1mcYCC2u-k/m/jOdGYuagAgAJ + if (that_.ArePrivateTagsRemoved() && + tag.IsPrivate()) { - if (DicomPath::IsMatch(*it, path)) - { - return Action_None; - } + return Action_Remove; + } + else if (that_.IsRemoved(tag)) + { + return Action_Remove; + } + else if (that_.IsCleared(tag)) + { + // This is different from "GetDefaultAction()", because we know how to clear string tags + newValue.clear(); + return Action_Replace; } } @@ -1100,7 +1179,7 @@ static void ParseListOfTags(DicomModification& target, const Json::Value& query, - DicomModification::TagOperation operation, + TagOperation operation, bool force) { if (!query.isArray()) @@ -1125,18 +1204,18 @@ { throw OrthancException(ErrorCode_BadRequest, "Marking tag \"" + name + "\" as to be " + - (operation == DicomModification::TagOperation_Keep ? "kept" : "removed") + + (operation == TagOperation_Keep ? "kept" : "removed") + " requires the \"Force\" option to be set to true"); } switch (operation) { - case DicomModification::TagOperation_Keep: + case TagOperation_Keep: target.Keep(path); LOG(TRACE) << "Keep: " << name << " = " << path.Format(); break; - case DicomModification::TagOperation_Remove: + case TagOperation_Remove: target.Remove(path); LOG(TRACE) << "Remove: " << name << " = " << path.Format(); break; diff -r 8cc9137b5c2e -r c1d6ce00be3f OrthancFramework/Sources/DicomParsing/DicomModification.h --- a/OrthancFramework/Sources/DicomParsing/DicomModification.h Tue Jul 06 08:36:54 2021 +0200 +++ b/OrthancFramework/Sources/DicomParsing/DicomModification.h Tue Jul 06 08:40:43 2021 +0200 @@ -39,12 +39,6 @@ **/ public: - enum TagOperation - { - TagOperation_Keep, - TagOperation_Remove - }; - class IDicomIdentifierGenerator : public boost::noncopyable { public: diff -r 8cc9137b5c2e -r c1d6ce00be3f OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.cpp --- a/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.cpp Tue Jul 06 08:36:54 2021 +0200 +++ b/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.cpp Tue Jul 06 08:40:43 2021 +0200 @@ -371,32 +371,40 @@ #endif - void DicomWebJsonVisitor::VisitNotSupported(const std::vector &parentTags, - const std::vector &parentIndexes, - const DicomTag &tag, - ValueRepresentation vr) + ITagVisitor::Action + DicomWebJsonVisitor::VisitNotSupported(const std::vector &parentTags, + const std::vector &parentIndexes, + const DicomTag &tag, + ValueRepresentation vr) { + return Action_None; } - void DicomWebJsonVisitor::VisitEmptySequence(const std::vector& parentTags, - const std::vector& parentIndexes, - const DicomTag& tag) + ITagVisitor::Action + DicomWebJsonVisitor::VisitSequence(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + size_t countItems) { - if (tag.GetElement() != 0x0000) + if (countItems == 0 && + tag.GetElement() != 0x0000) { Json::Value& node = CreateNode(parentTags, parentIndexes, tag); node[KEY_VR] = EnumerationToString(ValueRepresentation_Sequence); } + + return Action_None; } - void DicomWebJsonVisitor::VisitBinary(const std::vector& parentTags, - const std::vector& parentIndexes, - const DicomTag& tag, - ValueRepresentation vr, - const void* data, - size_t size) + ITagVisitor::Action + DicomWebJsonVisitor::VisitBinary(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + const void* data, + size_t size) { assert(vr == ValueRepresentation_OtherByte || vr == ValueRepresentation_OtherDouble || @@ -456,14 +464,17 @@ } } } + + return Action_None; } - void DicomWebJsonVisitor::VisitIntegers(const std::vector& parentTags, - const std::vector& parentIndexes, - const DicomTag& tag, - ValueRepresentation vr, - const std::vector& values) + ITagVisitor::Action + DicomWebJsonVisitor::VisitIntegers(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + const std::vector& values) { if (tag.GetElement() != 0x0000 && vr != ValueRepresentation_NotSupported) @@ -482,13 +493,16 @@ node[KEY_VALUE] = content; } } + + return Action_None; } - void DicomWebJsonVisitor::VisitDoubles(const std::vector& parentTags, - const std::vector& parentIndexes, - const DicomTag& tag, - ValueRepresentation vr, - const std::vector& values) + ITagVisitor::Action + DicomWebJsonVisitor::VisitDoubles(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + const std::vector& values) { if (tag.GetElement() != 0x0000 && vr != ValueRepresentation_NotSupported) @@ -507,13 +521,16 @@ node[KEY_VALUE] = content; } } + + return Action_None; } - void DicomWebJsonVisitor::VisitAttributes(const std::vector& parentTags, - const std::vector& parentIndexes, - const DicomTag& tag, - const std::vector& values) + ITagVisitor::Action + DicomWebJsonVisitor::VisitAttributes(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + const std::vector& values) { if (tag.GetElement() != 0x0000) { @@ -531,6 +548,8 @@ node[KEY_VALUE] = content; } } + + return Action_None; } diff -r 8cc9137b5c2e -r c1d6ce00be3f OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h --- a/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h Tue Jul 06 08:36:54 2021 +0200 +++ b/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h Tue Jul 06 08:40:43 2021 +0200 @@ -85,43 +85,44 @@ void FormatXml(std::string& target) const; #endif - virtual void VisitNotSupported(const std::vector& parentTags, - const std::vector& parentIndexes, - const DicomTag& tag, - ValueRepresentation vr) + virtual Action VisitNotSupported(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr) ORTHANC_OVERRIDE; - virtual void VisitEmptySequence(const std::vector& parentTags, - const std::vector& parentIndexes, - const DicomTag& tag) + virtual Action VisitSequence(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + size_t countItems) ORTHANC_OVERRIDE; - virtual void VisitBinary(const std::vector& parentTags, - const std::vector& parentIndexes, - const DicomTag& tag, - ValueRepresentation vr, - const void* data, - size_t size) - ORTHANC_OVERRIDE; - - virtual void VisitIntegers(const std::vector& parentTags, + virtual Action VisitBinary(const std::vector& parentTags, const std::vector& parentIndexes, const DicomTag& tag, ValueRepresentation vr, - const std::vector& values) + const void* data, + size_t size) + ORTHANC_OVERRIDE; + + virtual Action VisitIntegers(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + const std::vector& values) ORTHANC_OVERRIDE; - virtual void VisitDoubles(const std::vector& parentTags, - const std::vector& parentIndexes, - const DicomTag& tag, - ValueRepresentation vr, - const std::vector& values) + virtual Action VisitDoubles(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + const std::vector& values) ORTHANC_OVERRIDE; - virtual void VisitAttributes(const std::vector& parentTags, - const std::vector& parentIndexes, - const DicomTag& tag, - const std::vector& values) + virtual Action VisitAttributes(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + const std::vector& values) ORTHANC_OVERRIDE; virtual Action VisitString(std::string& newValue, diff -r 8cc9137b5c2e -r c1d6ce00be3f OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp --- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Tue Jul 06 08:36:54 2021 +0200 +++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Tue Jul 06 08:40:43 2021 +0200 @@ -2340,7 +2340,7 @@ // Forward declaration - static void ApplyVisitorToElement(DcmElement& element, + static bool ApplyVisitorToElement(DcmElement& element, ITagVisitor& visitor, const std::vector& parentTags, const std::vector& parentIndexes, @@ -2356,6 +2356,8 @@ { assert(parentTags.size() == parentIndexes.size()); + std::set toRemove; + for (unsigned long i = 0; i < dataset.card(); i++) { DcmElement* element = dataset.getElement(i); @@ -2365,13 +2367,25 @@ } else { - ApplyVisitorToElement(*element, visitor, parentTags, parentIndexes, encoding, hasCodeExtensions); + if (!ApplyVisitorToElement(*element, visitor, parentTags, parentIndexes, encoding, hasCodeExtensions)) + { + toRemove.insert(element->getTag()); + } } } + + // Remove all the tags that were planned for removal (cf. ITagVisitor::Action_Remove) + for (std::set::const_iterator + it = toRemove.begin(); it != toRemove.end(); ++it) + { + std::unique_ptr tmp(dataset.remove(*it)); + } } - static void ApplyVisitorToLeaf(DcmElement& element, + // Returns "true" iff the element must be kept. If "false" is + // returned, the element will be removed. + static bool ApplyVisitorToLeaf(DcmElement& element, ITagVisitor& visitor, const std::vector& parentTags, const std::vector& parentIndexes, @@ -2401,6 +2415,20 @@ evr = EVR_UN; } + if (evr == EVR_UN) + { + // New in Orthanc 1.9.5 + DictionaryLocker locker; + + const DcmDictEntry* entry = locker->findEntry(element.getTag().getXTag(), + element.getTag().getPrivateCreator()); + + if (entry != NULL) + { + evr = entry->getEVR(); + } + } + const ValueRepresentation vr = FromDcmtkBridge::Convert(evr); @@ -2415,11 +2443,13 @@ Uint16* data16 = NULL; Uint8* data = NULL; + ITagVisitor::Action action; + if ((element.getTag() == DCM_PixelData || // (*) New in Orthanc 1.9.1 evr == EVR_OW) && element.getUint16Array(data16) == EC_Normal) { - visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data16, element.getLength()); + action = visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data16, element.getLength()); } else if (evr != EVR_OW && element.getUint8Array(data) == EC_Normal) @@ -2432,14 +2462,27 @@ * reimplemented in derived class "DcmPixelData"). However, * "getUint16Array()" works correctly, hence (*). **/ - visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data, element.getLength()); + action = visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data, element.getLength()); } else { - visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr); + action = visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr); } - return; // We're done + switch (action) + { + case ITagVisitor::Action_None: + return true; // We're done + + case ITagVisitor::Action_Remove: + return false; + + case ITagVisitor::Action_Replace: + throw OrthancException(ErrorCode_NotImplemented, "Iterator cannot replace binary data"); + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } } @@ -2473,7 +2516,10 @@ switch (action) { case ITagVisitor::Action_None: - break; + return true; + + case ITagVisitor::Action_Remove: + return false; case ITagVisitor::Action_Replace: { @@ -2481,20 +2527,20 @@ if (element.putString(s.c_str()) != EC_Normal) { throw OrthancException(ErrorCode_InternalError, - "Cannot replace value of tag: " + tag.Format()); + "Iterator cannot replace value of tag: " + tag.Format()); } - break; + return true; } default: throw OrthancException(ErrorCode_InternalError); } - - return; // We're done } + ITagVisitor::Action action; + try { // http://support.dcmtk.org/docs/dcvr_8h-source.html @@ -2522,7 +2568,7 @@ case EVR_UI: // unique identifier { Uint8* data = NULL; - + if (element.getUint8Array(data) == EC_Normal) { const Uint32 length = element.getLength(); @@ -2536,30 +2582,30 @@ if (l == length) { // Not a null-terminated plain string - visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr); + action = visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr); } else { std::string ignored; std::string s(reinterpret_cast(data), l); - ITagVisitor::Action action = visitor.VisitString - (ignored, parentTags, parentIndexes, tag, vr, - Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions)); - - if (action != ITagVisitor::Action_None) - { - LOG(WARNING) << "Cannot replace this string tag: " - << FromDcmtkBridge::GetTagName(element) - << " (" << tag.Format() << ")"; - } + action = visitor.VisitString(ignored, parentTags, parentIndexes, tag, vr, + Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions)); } } else { - visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr); + action = visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr); } - return; + if (action == ITagVisitor::Action_Replace) + { + LOG(WARNING) << "Iterator cannot replace this string tag: " + << FromDcmtkBridge::GetTagName(element) + << " (" << tag.Format() << ")"; + return true; + } + + break; } /** @@ -2582,7 +2628,7 @@ } } - visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values); + action = visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values); break; } @@ -2602,7 +2648,7 @@ } } - visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values); + action = visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values); break; } @@ -2625,7 +2671,7 @@ } } - visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values); + action = visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values); break; } @@ -2645,7 +2691,7 @@ } } - visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values); + action = visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values); break; } @@ -2666,7 +2712,7 @@ } } - visitor.VisitDoubles(parentTags, parentIndexes, tag, vr, values); + action = visitor.VisitDoubles(parentTags, parentIndexes, tag, vr, values); break; } @@ -2689,7 +2735,7 @@ } } - visitor.VisitDoubles(parentTags, parentIndexes, tag, vr, values); + action = visitor.VisitDoubles(parentTags, parentIndexes, tag, vr, values); break; } @@ -2716,7 +2762,7 @@ } assert(vr == ValueRepresentation_AttributeTag); - visitor.VisitAttributes(parentTags, parentIndexes, tag, values); + action = visitor.VisitAttributes(parentTags, parentIndexes, tag, values); break; } @@ -2728,7 +2774,7 @@ case EVR_SQ: // sequence of items { - return; + return true; } @@ -2751,8 +2797,8 @@ case EVR_PixelData: // used internally for uncompressed pixeld data case EVR_OverlayData: // used internally for overlay data { - visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr); - return; + action = visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr); + break; } @@ -2761,21 +2807,38 @@ **/ default: - return; + return true; + } + + switch (action) + { + case ITagVisitor::Action_None: + return true; // We're done + + case ITagVisitor::Action_Remove: + return false; + + case ITagVisitor::Action_Replace: + throw OrthancException(ErrorCode_NotImplemented, "Iterator cannot replace non-string-like data"); + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); } } catch (boost::bad_lexical_cast&) { - return; + return true; } catch (std::bad_cast&) { - return; + return true; } } - static void ApplyVisitorToElement(DcmElement& element, + // Returns "true" iff the element must be kept. If "false" is + // returned, the element will be removed. + static bool ApplyVisitorToElement(DcmElement& element, ITagVisitor& visitor, const std::vector& parentTags, const std::vector& parentIndexes, @@ -2788,7 +2851,7 @@ if (element.isLeaf()) { - ApplyVisitorToLeaf(element, visitor, parentTags, parentIndexes, tag, encoding, hasCodeExtensions); + return ApplyVisitorToLeaf(element, visitor, parentTags, parentIndexes, tag, encoding, hasCodeExtensions); } else { @@ -2797,24 +2860,38 @@ // etc. are not." The following dynamic_cast is thus OK. DcmSequenceOfItems& sequence = dynamic_cast(element); - if (sequence.card() == 0) - { - visitor.VisitEmptySequence(parentTags, parentIndexes, tag); - } - else + ITagVisitor::Action action = visitor.VisitSequence(parentTags, parentIndexes, tag, sequence.card()); + + switch (action) { - std::vector tags = parentTags; - std::vector indexes = parentIndexes; - tags.push_back(tag); - indexes.push_back(0); - - for (unsigned long i = 0; i < sequence.card(); i++) - { - indexes.back() = static_cast(i); - DcmItem* child = sequence.getItem(i); - ApplyVisitorToDataset(*child, visitor, tags, indexes, encoding, hasCodeExtensions); - } + case ITagVisitor::Action_None: + if (sequence.card() != 0) // Minor optimization to avoid creating "tags" and "indexes" if not needed + { + std::vector tags = parentTags; + std::vector indexes = parentIndexes; + tags.push_back(tag); + indexes.push_back(0); + + for (unsigned long i = 0; i < sequence.card(); i++) + { + indexes.back() = static_cast(i); + DcmItem* child = sequence.getItem(i); + ApplyVisitorToDataset(*child, visitor, tags, indexes, encoding, hasCodeExtensions); + } + } + + return true; // Keep + + case ITagVisitor::Action_Remove: + return false; + + case ITagVisitor::Action_Replace: + throw OrthancException(ErrorCode_NotImplemented, "Iterator cannot replace sequences"); + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); } + } } diff -r 8cc9137b5c2e -r c1d6ce00be3f OrthancFramework/Sources/DicomParsing/ITagVisitor.h --- a/OrthancFramework/Sources/DicomParsing/ITagVisitor.h Tue Jul 06 08:36:54 2021 +0200 +++ b/OrthancFramework/Sources/DicomParsing/ITagVisitor.h Tue Jul 06 08:40:43 2021 +0200 @@ -35,6 +35,7 @@ enum Action { Action_Replace, + Action_Remove, // New in Orthanc 1.9.5 Action_None }; @@ -42,46 +43,48 @@ { } - // Visiting a DICOM element that is internal to DCMTK - virtual void VisitNotSupported(const std::vector& parentTags, + // Visiting a DICOM element that is internal to DCMTK. Can return + // "Remove" or "None". + virtual Action VisitNotSupported(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr) = 0; + + // SQ - can return "Remove" or "None" + virtual Action VisitSequence(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + size_t countItems) = 0; + + // SL, SS, UL, US - can return "Remove" or "None" + virtual Action VisitIntegers(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + const std::vector& values) = 0; + + // FL, FD, OD, OF - can return "Remove" or "None" + virtual Action VisitDoubles(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + const std::vector& values) = 0; + + // AT - can return "Remove" or "None" + virtual Action VisitAttributes(const std::vector& parentTags, const std::vector& parentIndexes, const DicomTag& tag, - ValueRepresentation vr) = 0; + const std::vector& values) = 0; - // SQ - virtual void VisitEmptySequence(const std::vector& parentTags, - const std::vector& parentIndexes, - const DicomTag& tag) = 0; - - // SL, SS, UL, US - virtual void VisitIntegers(const std::vector& parentTags, + // OB, OL, OW, UN - can return "Remove" or "None" + virtual Action VisitBinary(const std::vector& parentTags, const std::vector& parentIndexes, const DicomTag& tag, ValueRepresentation vr, - const std::vector& values) = 0; - - // FL, FD, OD, OF - virtual void VisitDoubles(const std::vector& parentTags, - const std::vector& parentIndexes, - const DicomTag& tag, - ValueRepresentation vr, - const std::vector& values) = 0; + const void* data, + size_t size) = 0; - // AT - virtual void VisitAttributes(const std::vector& parentTags, - const std::vector& parentIndexes, - const DicomTag& tag, - const std::vector& values) = 0; - - // OB, OL, OW, UN - virtual void VisitBinary(const std::vector& parentTags, - const std::vector& parentIndexes, - const DicomTag& tag, - ValueRepresentation vr, - const void* data, - size_t size) = 0; - - // Visiting an UTF-8 string + // Visiting an UTF-8 string - can return "Replace", "Remove" or "None" virtual Action VisitString(std::string& newValue, const std::vector& parentTags, const std::vector& parentIndexes, diff -r 8cc9137b5c2e -r c1d6ce00be3f OrthancFramework/Sources/Enumerations.cpp --- a/OrthancFramework/Sources/Enumerations.cpp Tue Jul 06 08:36:54 2021 +0200 +++ b/OrthancFramework/Sources/Enumerations.cpp Tue Jul 06 08:40:43 2021 +0200 @@ -1158,6 +1158,25 @@ } + const char* EnumerationToString(DicomToJsonFormat format) + { + switch (format) + { + case DicomToJsonFormat_Full: + return "Full"; + + case DicomToJsonFormat_Human: + return "Simplify"; + + case DicomToJsonFormat_Short: + return "Short"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + Encoding StringToEncoding(const char* encoding) { std::string s(encoding); @@ -1809,6 +1828,27 @@ } + DicomToJsonFormat StringToDicomToJsonFormat(const std::string& format) + { + if (format == "Full") + { + return DicomToJsonFormat_Full; + } + else if (format == "Short") + { + return DicomToJsonFormat_Short; + } + else if (format == "Simplify") + { + return DicomToJsonFormat_Human; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + unsigned int GetBytesPerPixel(PixelFormat format) { switch (format) diff -r 8cc9137b5c2e -r c1d6ce00be3f OrthancFramework/Sources/Enumerations.h --- a/OrthancFramework/Sources/Enumerations.h Tue Jul 06 08:36:54 2021 +0200 +++ b/OrthancFramework/Sources/Enumerations.h Tue Jul 06 08:40:43 2021 +0200 @@ -801,6 +801,9 @@ const char* EnumerationToString(StorageCommitmentFailureReason reason); ORTHANC_PUBLIC + const char* EnumerationToString(DicomToJsonFormat format); + + ORTHANC_PUBLIC Encoding StringToEncoding(const char* encoding); ORTHANC_PUBLIC @@ -832,6 +835,9 @@ MimeType StringToMimeType(const std::string& mime); ORTHANC_PUBLIC + DicomToJsonFormat StringToDicomToJsonFormat(const std::string& format); + + ORTHANC_PUBLIC bool LookupMimeType(MimeType& target, const std::string& source); diff -r 8cc9137b5c2e -r c1d6ce00be3f OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp --- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp Tue Jul 06 08:36:54 2021 +0200 +++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp Tue Jul 06 08:40:43 2021 +0200 @@ -39,6 +39,8 @@ static const char* CREATION_TIME = "CreationTime"; static const char* LAST_CHANGE_TIME = "LastChangeTime"; static const char* RUNTIME = "Runtime"; + static const char* ERROR_CODE = "ErrorCode"; + static const char* ERROR_DETAILS = "ErrorDetails"; class JobsRegistry::JobHandler : public boost::noncopyable @@ -276,6 +278,11 @@ target[CREATION_TIME] = boost::posix_time::to_iso_string(creationTime_); target[LAST_CHANGE_TIME] = boost::posix_time::to_iso_string(lastStateChangeTime_); target[RUNTIME] = static_cast(runtime_.total_milliseconds()); + + // New in Orthanc 1.9.5 + target[ERROR_CODE] = static_cast(lastStatus_.GetErrorCode()); + target[ERROR_DETAILS] = lastStatus_.GetDetails(); + return true; } else @@ -307,7 +314,23 @@ job_->GetJobType(jobType_); job_->Start(); - lastStatus_ = JobStatus(ErrorCode_Success, "", *job_); + ErrorCode errorCode; + if (serialized.isMember(ERROR_CODE)) + { + errorCode = static_cast(SerializationToolbox::ReadInteger(serialized, ERROR_CODE)); + } + else + { + errorCode = ErrorCode_Success; // Backward compatibility with Orthanc <= 1.9.4 + } + + std::string details; + if (serialized.isMember(ERROR_DETAILS)) // Backward compatibility with Orthanc <= 1.9.4 + { + details = SerializationToolbox::ReadString(serialized, ERROR_DETAILS); + } + + lastStatus_ = JobStatus(errorCode, details, *job_); } }; diff -r 8cc9137b5c2e -r c1d6ce00be3f OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp --- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Tue Jul 06 08:36:54 2021 +0200 +++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Tue Jul 06 08:40:43 2021 +0200 @@ -61,6 +61,9 @@ #include #include #include +#include +#include +#include #include #include @@ -689,7 +692,7 @@ } Json::Value j; - a.ToJson(j, true); + a.ToJson(j, DicomToJsonFormat_Human); ASSERT_EQ(3u, j.size()); //std::cout << j; @@ -2258,6 +2261,31 @@ } +static bool MyIsMatch(const DicomPath& a, + const DicomPath& b) +{ + bool expected = DicomPath::IsMatch(a, b); + + std::vector prefixTags; + std::vector prefixIndexes; + + for (size_t i = 0; i < b.GetPrefixLength(); i++) + { + prefixTags.push_back(b.GetPrefixTag(i)); + prefixIndexes.push_back(b.GetPrefixIndex(i)); + } + + if (expected == DicomPath::IsMatch(a, prefixTags, prefixIndexes, b.GetFinalTag())) + { + return expected; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } +} + + TEST(DicomModification, DicomPath) { // Those are samples inspired by those from "man dcmodify" @@ -2363,30 +2391,30 @@ ASSERT_THROW(DicomPath::Parse("(0010,0010)0].PatientID"), OrthancException); ASSERT_THROW(DicomPath::Parse("(0010,0010)[-1].PatientID"), OrthancException); - ASSERT_TRUE(DicomPath::IsMatch(DicomPath::Parse("(0010,0010)"), - DicomPath::Parse("(0010,0010)"))); - ASSERT_FALSE(DicomPath::IsMatch(DicomPath::Parse("(0010,0010)"), - DicomPath::Parse("(0010,0020)"))); - ASSERT_TRUE(DicomPath::IsMatch(DicomPath::Parse("(0010,0010)"), - DicomPath::Parse("(0010,0010)[1].(0010,0020)"))); - ASSERT_FALSE(DicomPath::IsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)"), - DicomPath::Parse("(0010,0010)"))); - ASSERT_TRUE(DicomPath::IsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)"), - DicomPath::Parse("(0010,0010)[1].(0010,0020)"))); - ASSERT_TRUE(DicomPath::IsMatch(DicomPath::Parse("(0010,0010)[*].(0010,0020)"), - DicomPath::Parse("(0010,0010)[1].(0010,0020)"))); - ASSERT_FALSE(DicomPath::IsMatch(DicomPath::Parse("(0010,0010)[2].(0010,0020)"), - DicomPath::Parse("(0010,0010)[1].(0010,0020)"))); - ASSERT_THROW(DicomPath::IsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)"), - DicomPath::Parse("(0010,0010)[*].(0010,0020)")), OrthancException); - ASSERT_TRUE(DicomPath::IsMatch(DicomPath::Parse("(0010,0010)[*].(0010,0020)[*].(0010,0030)"), - DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)[3].(0010,0040)"))); - ASSERT_TRUE(DicomPath::IsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)"), - DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)[3].(0010,0040)"))); - ASSERT_FALSE(DicomPath::IsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)[3].(0010,0030)"), - DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)[3].(0010,0040)"))); - ASSERT_FALSE(DicomPath::IsMatch(DicomPath::Parse("(0010,0010)[2].(0010,0020)[2].(0010,0030)"), - DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)[3].(0010,0040)"))); + ASSERT_TRUE(MyIsMatch(DicomPath::Parse("(0010,0010)"), + DicomPath::Parse("(0010,0010)"))); + ASSERT_FALSE(MyIsMatch(DicomPath::Parse("(0010,0010)"), + DicomPath::Parse("(0010,0020)"))); + ASSERT_TRUE(MyIsMatch(DicomPath::Parse("(0010,0010)"), + DicomPath::Parse("(0010,0010)[1].(0010,0020)"))); + ASSERT_FALSE(MyIsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)"), + DicomPath::Parse("(0010,0010)"))); + ASSERT_TRUE(MyIsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)"), + DicomPath::Parse("(0010,0010)[1].(0010,0020)"))); + ASSERT_TRUE(MyIsMatch(DicomPath::Parse("(0010,0010)[*].(0010,0020)"), + DicomPath::Parse("(0010,0010)[1].(0010,0020)"))); + ASSERT_FALSE(MyIsMatch(DicomPath::Parse("(0010,0010)[2].(0010,0020)"), + DicomPath::Parse("(0010,0010)[1].(0010,0020)"))); + ASSERT_THROW(MyIsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)"), + DicomPath::Parse("(0010,0010)[*].(0010,0020)")), OrthancException); + ASSERT_TRUE(MyIsMatch(DicomPath::Parse("(0010,0010)[*].(0010,0020)[*].(0010,0030)"), + DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)[3].(0010,0040)"))); + ASSERT_TRUE(MyIsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)"), + DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)[3].(0010,0040)"))); + ASSERT_FALSE(MyIsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)[3].(0010,0030)"), + DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)[3].(0010,0040)"))); + ASSERT_FALSE(MyIsMatch(DicomPath::Parse("(0010,0010)[2].(0010,0020)[2].(0010,0030)"), + DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)[3].(0010,0040)"))); } @@ -2686,7 +2714,10 @@ ASSERT_NE("1.2.840.113619.2.176.2025.1499492.7040.1171286241.719", vv1[REF_IM_SEQ][0][REF_SOP_INSTANCE].asString()); ASSERT_NE("1.2.840.113619.2.176.2025.1499492.7040.1171286241.726", vv1[REF_IM_SEQ][1][REF_SOP_INSTANCE].asString()); ASSERT_NE("1.2.840.113704.1.111.7016.1342451220.40", vv1[REL_SERIES_SEQ][0][STUDY_INSTANCE_UID].asString()); - ASSERT_EQ("WORLD", vv1[REL_SERIES_SEQ][0][PURPOSE_CODE_SEQ][0][SERIES_DESCRIPTION].asString()); + + // Contrarily to Orthanc 1.9.4, the "SERIES_DESCRIPTION" is also removed from nested sequences + ASSERT_EQ(1u, vv1[REL_SERIES_SEQ][0][PURPOSE_CODE_SEQ][0].size()); + ASSERT_EQ("122403", vv1[REL_SERIES_SEQ][0][PURPOSE_CODE_SEQ][0]["0008,0100"].asString()); } { @@ -2710,6 +2741,258 @@ } +TEST(FromDcmtkBridge, VisitorRemoveTag) +{ + class V : public ITagVisitor + { + private: + uint32_t seen_; + + public: + V() : seen_(0) + { + } + + unsigned int GetSeen() const + { + return seen_; + } + + virtual Action VisitNotSupported(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr) ORTHANC_OVERRIDE + { + seen_ |= (1 << 0); + + if (parentTags.size() == 0u && + parentIndexes.size() == 0u && + DcmTagKey(tag.GetGroup(), tag.GetElement()) == DCM_PixelData) + { + return Action_Remove; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual Action VisitSequence(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + size_t countItems) ORTHANC_OVERRIDE + { + seen_ |= (1 << 1); + + if (parentTags.size() == 0u && + parentIndexes.size() == 0u && + tag == DICOM_TAG_REFERENCED_IMAGE_SEQUENCE && + countItems == 1) + { + return Action_None; + } + else if (parentTags.size() == 1u && + parentIndexes.size() == 1u && + parentTags[0] == DICOM_TAG_REFERENCED_IMAGE_SEQUENCE && + parentIndexes[0] == 0u && + countItems == 0 && + DcmTagKey(tag.GetGroup(), tag.GetElement()) == DCM_ReferencedPatientSequence) + { + return Action_Remove; + } + else if (parentTags.size() == 1u && + parentIndexes.size() == 1u && + parentTags[0] == DICOM_TAG_REFERENCED_IMAGE_SEQUENCE && + parentIndexes[0] == 0u && + countItems == 1 && + DcmTagKey(tag.GetGroup(), tag.GetElement()) == DCM_ReferencedStudySequence) + { + return Action_Remove; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual Action VisitIntegers(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + const std::vector& values) ORTHANC_OVERRIDE + { + seen_ |= (1 << 2); + + if (parentTags.size() == 0u && + parentIndexes.size() == 0u && + DcmTagKey(tag.GetGroup(), tag.GetElement()) == DCM_TagAngleSecondAxis && + values.size() == 2 && + values[0] == 12 && + values[1] == 13) + { + return Action_Remove; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual Action VisitDoubles(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + const std::vector& values) ORTHANC_OVERRIDE + { + seen_ |= (1 << 3); + + if (parentTags.size() == 1u && + parentIndexes.size() == 1u && + parentTags[0] == DICOM_TAG_REFERENCED_IMAGE_SEQUENCE && + parentIndexes[0] == 0u && + DcmTagKey(tag.GetGroup(), tag.GetElement()) == DCM_ExaminedBodyThickness && + values.size() == 3 && + std::abs(values[0] - 42.0f) <= 0.001f && + std::abs(values[1] - 43.0f) <= 0.001f && + std::abs(values[2] - 47.0f) <= 0.001f) + { + return Action_Remove; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual Action VisitAttributes(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + const std::vector& values) ORTHANC_OVERRIDE + { + seen_ |= (1 << 4); + + if (parentTags.size() == 1u && + parentIndexes.size() == 1u && + parentTags[0] == DICOM_TAG_REFERENCED_IMAGE_SEQUENCE && + parentIndexes[0] == 0u && + DcmTagKey(tag.GetGroup(), tag.GetElement()) == DCM_DimensionIndexPointer && + values.size() == 2 && + values[0] == DICOM_TAG_STUDY_DATE && + values[1] == DICOM_TAG_STUDY_TIME) + { + return Action_Remove; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual Action VisitBinary(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + const void* data, + size_t size) ORTHANC_OVERRIDE + { + seen_ |= (1 << 5); + + if (parentTags.size() == 1u && + parentIndexes.size() == 1u && + parentTags[0] == DICOM_TAG_REFERENCED_IMAGE_SEQUENCE && + parentIndexes[0] == 0u && + tag.GetGroup() == 0x0011 && + tag.GetElement() == 0x1311 && + size == 4u && + memcmp(data, "abcd", 4) == 0) + { + return Action_Remove; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual Action VisitString(std::string& newValue, + const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + const std::string& value) ORTHANC_OVERRIDE + { + seen_ |= (1 << 6); + return Action_Remove; + } + }; + + + std::unique_ptr dicom; + + { + Json::Value v = Json::objectValue; + v["PatientName"] = "Hello"; + v["ReferencedSOPClassUID"] = "1.2.840.10008.5.1.4.1.1.4"; + v["ReferencedImageSequence"][0]["ReferencedSOPClassUID"] = "1.2.840.10008.5.1.4.1.1.4"; + v["ReferencedImageSequence"][0]["ReferencedSOPInstanceUID"] = "1.2.840.113619.2.176.2025.1499492.7040.1171286241.719"; + v["ReferencedImageSequence"][0]["ReferencedPatientSequence"] = Json::arrayValue; // Empty nested sequence + v["ReferencedImageSequence"][0]["ReferencedStudySequence"][0]["PatientID"] = "Hello"; // Non-empty nested sequence + v["ReferencedImageSequence"][0]["0011,1311"] = "abcd"; // Binary + + dicom.reset(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "PrivateCreator")); + + { + // Test value multiplicity (cannot be done using "ParsedDicomFile::CreateFromJson()") + const int16_t a[] = { 12, 13 }; + std::unique_ptr s(new DcmSignedShort(DCM_TagAngleSecondAxis)); // VisitIntegers() + ASSERT_TRUE(s->putSint16Array(a, 2).good()); + dicom->GetDcmtkObject().getDataset()->insert(s.release()); + } + + DcmItem *parent = NULL; + ASSERT_TRUE(dicom->GetDcmtkObject().getDataset()->findAndGetSequenceItem(DCM_ReferencedImageSequence, parent, 0).good()); + + { + const float a[] = { 42, 43, 47 }; + std::unique_ptr s(new DcmFloatingPointSingle(DCM_ExaminedBodyThickness)); // VisitDoubles() + ASSERT_TRUE(s->putFloat32Array(a, 3).good()); + parent->insert(s.release()); + } + + { + const uint16_t a[] = { 0x0008, 0x0020, 0x0008, 0x0030 }; + std::unique_ptr s(new DcmAttributeTag(DCM_DimensionIndexPointer)); // VisitAttributes() + ASSERT_TRUE(s->putUint16Array(a, 2).good()); + parent->insert(s.release()); + } + + ASSERT_TRUE(dicom->GetDcmtkObject().getDataset()->insert(new DcmPixelItem(DCM_PixelData)).good()); // VisitNotSupported() + } + + { + V visitor; + dicom->Apply(visitor); + ASSERT_EQ(127u, visitor.GetSeen()); // Make sure all the methods have been applied + } + + { + Json::Value b; + dicom->DatasetToJson(b, DicomToJsonFormat_Short, DicomToJsonFlags_Default, 0); + ASSERT_EQ(Json::objectValue, b.type()); + + Json::Value::Members members = b.getMemberNames(); + ASSERT_EQ(1u, members.size()); + ASSERT_EQ("0008,1140", members[0]); + + // Check that "b["0008,1140"]" is a sequence with one single empty object + ASSERT_EQ(Json::arrayValue, b["0008,1140"].type()); + ASSERT_EQ(1u, b["0008,1140"].size()); + ASSERT_EQ(Json::objectValue, b["0008,1140"][0].type()); + ASSERT_EQ(0u, b["0008,1140"][0].size()); + } +} + + #if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 diff -r 8cc9137b5c2e -r c1d6ce00be3f OrthancServer/OrthancExplorer/explorer.js --- a/OrthancServer/OrthancExplorer/explorer.js Tue Jul 06 08:36:54 2021 +0200 +++ b/OrthancServer/OrthancExplorer/explorer.js Tue Jul 06 08:40:43 2021 +0200 @@ -511,7 +511,8 @@ 'Limit' : LIMIT_RESOURCES + 1, 'Query' : { 'StudyDate' : $('#lookup-study-date').val() - } + }, + 'Full' : true }; $('#lookup-form input').each(function(index, input) { diff -r 8cc9137b5c2e -r c1d6ce00be3f OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp Tue Jul 06 08:36:54 2021 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp Tue Jul 06 08:40:43 2021 +0200 @@ -379,7 +379,7 @@ } Json::Value result; - answers.ToJson(result, true); + answers.ToJson(result, DicomToJsonFormat_Human); call.GetOutput().AnswerJson(result); } @@ -422,7 +422,7 @@ } Json::Value result; - answers.ToJson(result, true); + answers.ToJson(result, DicomToJsonFormat_Human); call.GetOutput().AnswerJson(result); } @@ -466,7 +466,7 @@ } Json::Value result; - answers.ToJson(result, true); + answers.ToJson(result, DicomToJsonFormat_Human); call.GetOutput().AnswerJson(result); } @@ -511,7 +511,7 @@ } Json::Value result; - answers.ToJson(result, true); + answers.ToJson(result, DicomToJsonFormat_Human); call.GetOutput().AnswerJson(result); } @@ -565,7 +565,7 @@ for (size_t i = 0; i < patients.GetSize(); i++) { Json::Value patient; - patients.ToJson(patient, i, true); + patients.ToJson(patient, i, DicomToJsonFormat_Human); DicomMap::SetupFindStudyTemplate(m); if (!MergeQueryAndTemplate(m, call)) @@ -584,7 +584,7 @@ for (size_t j = 0; j < studies.GetSize(); j++) { Json::Value study; - studies.ToJson(study, j, true); + studies.ToJson(study, j, DicomToJsonFormat_Human); DicomMap::SetupFindSeriesTemplate(m); if (!MergeQueryAndTemplate(m, call)) @@ -603,7 +603,7 @@ for (size_t k = 0; k < series.GetSize(); k++) { Json::Value series2; - series.ToJson(series2, k, true); + series.ToJson(series2, k, DicomToJsonFormat_Human); study["Series"].append(series2); } @@ -924,6 +924,7 @@ } std::unique_ptr job(new DicomMoveScuJob(context)); + job->SetQueryFormat(OrthancRestApi::GetDicomFormat(body, DicomToJsonFormat_Short)); { QueryAccessor query(call); @@ -967,6 +968,8 @@ static void DocumentRetrieveShared(RestApiPostCall& call) { OrthancRestApi::DocumentSubmitCommandsJob(call); + OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Short); + call.GetDocumentation() .SetTag("Networking") .SetUriArgument("id", "Identifier of the query of interest") @@ -1511,7 +1514,6 @@ { if (call.IsDocumentation()) { - OrthancRestApi::DocumentSubmitCommandsJob(call); call.GetDocumentation() .SetTag("Networking") .SetSummary("Trigger C-MOVE SCU") @@ -1560,7 +1562,7 @@ MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); DicomAssociationParameters params(localAet, source); - InjectAssociationTimeout(params, request); + InjectAssociationTimeout(params, request); // Handles KEY_TIMEOUT DicomControlUserConnection connection(params); @@ -2128,13 +2130,16 @@ { if (call.IsDocumentation()) { + OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Human); + call.GetDocumentation() .SetTag("Networking") .SetSummary("C-FIND SCU for worklist") .SetDescription("Trigger C-FIND SCU command against the remote worklists of the DICOM modality " "whose identifier is provided in URL") .SetUriArgument("id", "Identifier of the modality of interest") - .AddRequestType(MimeType_Json, "Associative array containing the query on the values of the DICOM tags") + .SetRequestField(KEY_QUERY, RestApiCallDocumentation::Type_JsonObject, + "Associative array containing the filter on the values of the DICOM tags", true) .AddAnswerType(MimeType_Json, "JSON array describing the DICOM tags of the matching worklists"); return; } @@ -2142,9 +2147,23 @@ Json::Value json; if (call.ParseJsonRequest(json)) { - std::unique_ptr query - (ParsedDicomFile::CreateFromJson(json, static_cast(0), - "" /* no private creator */)); + std::unique_ptr query; + DicomToJsonFormat format; + + if (json.isMember(KEY_QUERY)) + { + // New in Orthanc 1.9.5 + query.reset(ParsedDicomFile::CreateFromJson(json[KEY_QUERY], static_cast(0), + "" /* no private creator */)); + format = OrthancRestApi::GetDicomFormat(json, DicomToJsonFormat_Human); + } + else + { + // Compatibility with Orthanc <= 1.9.4 + query.reset(ParsedDicomFile::CreateFromJson(json, static_cast(0), + "" /* no private creator */)); + format = DicomToJsonFormat_Human; + } DicomFindAnswers answers(true); @@ -2154,7 +2173,7 @@ } Json::Value result; - answers.ToJson(result, true); + answers.ToJson(result, format); call.GetOutput().AnswerJson(result); } else diff -r 8cc9137b5c2e -r c1d6ce00be3f OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Tue Jul 06 08:36:54 2021 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Tue Jul 06 08:40:43 2021 +0200 @@ -381,7 +381,6 @@ Json::Value json; if (call.ParseJsonRequest(json)) { - std::cout << json.toStyledString(); OrthancConfiguration::ParseAcceptedTransferSyntaxes(syntaxes, json); } else diff -r 8cc9137b5c2e -r c1d6ce00be3f OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp --- a/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp Tue Jul 06 08:36:54 2021 +0200 +++ b/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp Tue Jul 06 08:40:43 2021 +0200 @@ -21,13 +21,15 @@ #include "DicomMoveScuJob.h" +#include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" #include "../../../OrthancFramework/Sources/SerializationToolbox.h" #include "../ServerContext.h" static const char* const LOCAL_AET = "LocalAet"; -static const char* const TARGET_AET = "TargetAet"; +static const char* const QUERY = "Query"; +static const char* const QUERY_FORMAT = "QueryFormat"; // New in 1.9.5 static const char* const REMOTE = "Remote"; -static const char* const QUERY = "Query"; +static const char* const TARGET_AET = "TargetAet"; static const char* const TIMEOUT = "Timeout"; namespace Orthanc @@ -80,7 +82,6 @@ }; - void DicomMoveScuJob::Retrieve(const DicomMap& findAnswer) { if (connection_.get() == NULL) @@ -92,33 +93,30 @@ } - static void AddTagIfString(Json::Value& target, - const DicomMap& answer, - const DicomTag& tag) + static void AddToQuery(DicomFindAnswers& query, + const DicomMap& item) { - const DicomValue* value = answer.TestAndGetValue(tag); - if (value != NULL && - !value->IsNull() && - !value->IsBinary()) - { - target[tag.Format()] = value->GetContent(); - } + query.Add(item); + + /** + * Compatibility with Orthanc <= 1.9.4: Remove the + * "SpecificCharacterSet" (0008,0005) tag that is automatically + * added if creating a ParsedDicomFile object from a DicomMap. + **/ + query.GetAnswer(query.GetSize() - 1).Remove(DICOM_TAG_SPECIFIC_CHARACTER_SET); } - + void DicomMoveScuJob::AddFindAnswer(const DicomMap& answer) { - assert(query_.type() == Json::arrayValue); - - // Copy the identifiers tags, if they exist - Json::Value item = Json::objectValue; - AddTagIfString(item, answer, DICOM_TAG_QUERY_RETRIEVE_LEVEL); - AddTagIfString(item, answer, DICOM_TAG_PATIENT_ID); - AddTagIfString(item, answer, DICOM_TAG_STUDY_INSTANCE_UID); - AddTagIfString(item, answer, DICOM_TAG_SERIES_INSTANCE_UID); - AddTagIfString(item, answer, DICOM_TAG_SOP_INSTANCE_UID); - AddTagIfString(item, answer, DICOM_TAG_ACCESSION_NUMBER); - query_.append(item); + DicomMap item; + item.CopyTagIfExists(answer, DICOM_TAG_QUERY_RETRIEVE_LEVEL); + item.CopyTagIfExists(answer, DICOM_TAG_PATIENT_ID); + item.CopyTagIfExists(answer, DICOM_TAG_STUDY_INSTANCE_UID); + item.CopyTagIfExists(answer, DICOM_TAG_SERIES_INSTANCE_UID); + item.CopyTagIfExists(answer, DICOM_TAG_SOP_INSTANCE_UID); + item.CopyTagIfExists(answer, DICOM_TAG_ACCESSION_NUMBER); + AddToQuery(query_, item); AddCommand(new Command(*this, answer)); } @@ -191,13 +189,28 @@ } + void DicomMoveScuJob::SetQueryFormat(DicomToJsonFormat format) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + queryFormat_ = format; + } + } + + void DicomMoveScuJob::GetPublicContent(Json::Value& value) { SetOfCommandsJob::GetPublicContent(value); - value["LocalAet"] = parameters_.GetLocalApplicationEntityTitle(); + value[LOCAL_AET] = parameters_.GetLocalApplicationEntityTitle(); value["RemoteAet"] = parameters_.GetRemoteModality().GetApplicationEntityTitle(); - value["Query"] = query_; + + value[QUERY] = Json::objectValue; + query_.ToJson(value[QUERY], queryFormat_); } @@ -207,12 +220,26 @@ context_(context), parameters_(DicomAssociationParameters::UnserializeJob(serialized)), targetAet_(SerializationToolbox::ReadString(serialized, TARGET_AET)), - query_(Json::arrayValue) + query_(true), + queryFormat_(DicomToJsonFormat_Short) { - if (serialized.isMember(QUERY) && - serialized[QUERY].type() == Json::arrayValue) + if (serialized.isMember(QUERY)) { - query_ = serialized[QUERY]; + const Json::Value& query = serialized[QUERY]; + if (query.type() == Json::arrayValue) + { + for (Json::Value::ArrayIndex i = 0; i < query.size(); i++) + { + DicomMap item; + FromDcmtkBridge::FromJson(item, query[i]); + AddToQuery(query_, item); + } + } + } + + if (serialized.isMember(QUERY_FORMAT)) + { + queryFormat_ = StringToDicomToJsonFormat(SerializationToolbox::ReadString(serialized, QUERY_FORMAT)); } } @@ -227,7 +254,13 @@ { parameters_.SerializeJob(target); target[TARGET_AET] = targetAet_; - target[QUERY] = query_; + + // "Short" is for compatibility with Orthanc <= 1.9.4 + target[QUERY] = Json::objectValue; + query_.ToJson(target[QUERY], DicomToJsonFormat_Short); + + target[QUERY_FORMAT] = EnumerationToString(queryFormat_); + return true; } } diff -r 8cc9137b5c2e -r c1d6ce00be3f OrthancServer/Sources/ServerJobs/DicomMoveScuJob.h --- a/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.h Tue Jul 06 08:36:54 2021 +0200 +++ b/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.h Tue Jul 06 08:40:43 2021 +0200 @@ -40,7 +40,8 @@ ServerContext& context_; DicomAssociationParameters parameters_; std::string targetAet_; - Json::Value query_; + DicomFindAnswers query_; + DicomToJsonFormat queryFormat_; // New in 1.9.5 std::unique_ptr connection_; @@ -49,7 +50,8 @@ public: explicit DicomMoveScuJob(ServerContext& context) : context_(context), - query_(Json::arrayValue) + query_(true /* this is for worklists */), + queryFormat_(DicomToJsonFormat_Short) { } @@ -79,6 +81,13 @@ void SetTargetAet(const std::string& aet); + void SetQueryFormat(DicomToJsonFormat format); + + DicomToJsonFormat GetQueryFormat() const + { + return queryFormat_; + } + virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE; virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE diff -r 8cc9137b5c2e -r c1d6ce00be3f OrthancServer/UnitTestsSources/ServerJobsTests.cpp --- a/OrthancServer/UnitTestsSources/ServerJobsTests.cpp Tue Jul 06 08:36:54 2021 +0200 +++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp Tue Jul 06 08:40:43 2021 +0200 @@ -1184,6 +1184,7 @@ ASSERT_EQ(104u, job->GetParameters().GetRemoteModality().GetPortNumber()); ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer()); ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), job->GetParameters().GetTimeout()); + ASSERT_EQ(DicomToJsonFormat_Short, job->GetQueryFormat()); } { @@ -1196,6 +1197,8 @@ job.SetLocalAet("WORLD"); job.SetRemoteModality(r); job.SetTimeout(43); + job.SetQueryFormat(DicomToJsonFormat_Human); + job.Serialize(v); } @@ -1209,5 +1212,63 @@ ASSERT_EQ(42u, job->GetParameters().GetRemoteModality().GetPortNumber()); ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer()); ASSERT_EQ(43u, job->GetParameters().GetTimeout()); + ASSERT_EQ(DicomToJsonFormat_Human, job->GetQueryFormat()); } } + + +TEST_F(OrthancJobsSerialization, DicomMoveScuJob) +{ + Json::Value command = Json::objectValue; + command["0008,0005"]["Type"] = "String"; + command["0008,0005"]["Content"] = "ISO_IR 100"; + command["0010,0020"]["Type"] = "String"; + command["0010,0020"]["Content"] = "1234"; + + Json::Value query = Json::objectValue; + query["0010,0020"] = "456"; + query["0008,0052"] = "STUDY"; + + Json::Value remote = Json::objectValue; + remote["AET"] = "REMOTE"; + remote["Host"] = "192.168.1.1"; + remote["Port"] = 4242; + + Json::Value s = Json::objectValue; + s["Permissive"] = true; + s["Position"] = 1; + s["Description"] = "test"; + s["Remote"] = remote; + s["LocalAet"] = "LOCAL"; + s["TargetAet"] = "TARGET"; + s["QueryFormat"] = "Full"; + s["Query"] = Json::arrayValue; + s["Query"].append(query); + s["Commands"] = Json::arrayValue; + s["Commands"].append(command); + + Json::Value s2; + + { + DicomMoveScuJob job(GetContext(), s); + job.Serialize(s2); + } + + { + DicomMoveScuJob job(GetContext(), s2); + ASSERT_EQ("TARGET", job.GetTargetAet()); + ASSERT_EQ("LOCAL", job.GetParameters().GetLocalApplicationEntityTitle()); + ASSERT_EQ("REMOTE", job.GetParameters().GetRemoteModality().GetApplicationEntityTitle()); + ASSERT_EQ("192.168.1.1", job.GetParameters().GetRemoteModality().GetHost()); + ASSERT_EQ(4242u, job.GetParameters().GetRemoteModality().GetPortNumber()); + ASSERT_EQ("test", job.GetDescription()); + ASSERT_TRUE(job.IsPermissive()); + ASSERT_EQ(1u, job.GetPosition()); + ASSERT_EQ(1u, job.GetCommandsCount()); + ASSERT_EQ(DicomToJsonFormat_Full, job.GetQueryFormat()); + ASSERT_EQ(1u, s2["Commands"].size()); + ASSERT_EQ(command.toStyledString(), s2["Commands"][0].toStyledString()); + ASSERT_EQ(1u, s2["Query"].size()); + ASSERT_EQ(query.toStyledString(), s2["Query"][0].toStyledString()); + } +} diff -r 8cc9137b5c2e -r c1d6ce00be3f TODO --- a/TODO Tue Jul 06 08:36:54 2021 +0200 +++ b/TODO Tue Jul 06 08:40:43 2021 +0200 @@ -35,7 +35,8 @@ https://book.orthanc-server.com/integrations.html * Discuss HL7 in a dedicated page: https://groups.google.com/d/msg/orthanc-users/4dt4992O0lQ/opTjTFU2BgAJ - + https://groups.google.com/g/orthanc-users/c/Spjtcj9vSPo/m/ktUArWxUDQAJ + ======== REST API