Mercurial > hg > orthanc
changeset 5044:6fed78e13233
Refactored DicomMap to handle sequences when needed
line wrap: on
line diff
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake Mon Jun 27 15:22:19 2022 +0200 +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake Tue Jun 28 17:45:09 2022 +0200 @@ -197,7 +197,6 @@ ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/DicomInstanceHasher.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/DicomIntegerPixelAccessor.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/DicomMap.cpp - ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/DicomSequencesMap.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/DicomStreamReader.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/DicomValue.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/StreamBlockReader.cpp
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.cpp Mon Jun 27 15:22:19 2022 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp Tue Jun 28 17:45:09 2022 +0200 @@ -359,6 +359,11 @@ SetValueInternal(group, element, new DicomValue(str, isBinary)); } + void DicomMap::SetValue(const DicomTag& tag, const Json::Value& value) + { + SetValueInternal(tag.GetGroup(), tag.GetElement(), new DicomValue(value)); + } + bool DicomMap::HasTag(uint16_t group, uint16_t element) const { return HasTag(DicomTag(group, element)); @@ -1328,7 +1333,7 @@ } - void DicomMap::FromDicomAsJson(const Json::Value& dicomAsJson, bool append) + void DicomMap::FromDicomAsJson(const Json::Value& dicomAsJson, bool append, bool parseSequences) { if (dicomAsJson.type() != Json::objectValue) { @@ -1371,6 +1376,18 @@ SetValue(tag, value["Value"].asString(), false /* not binary */); } } + else if (value["Type"] == "Sequence" && parseSequences) + { + if (value["Value"].type() != Json::arrayValue) + { + printf("%s", dicomAsJson.toStyledString().c_str()); + throw OrthancException(ErrorCode_CorruptedFile); + } + else + { + SetValue(tag, value["Value"]); + } + } } } @@ -1435,21 +1452,18 @@ return true; } -#if ORTHANC_ENABLE_DCMTK == 1 - void DicomMap::ExtractSequences(std::set<DicomTag>& sequences, const std::set<DicomTag>& tags) + void DicomMap::ExtractSequences(DicomMap& result) const { - sequences.clear(); + result.Clear(); - for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it) + for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it) { - ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(*it); - if (vr == ValueRepresentation_Sequence) + if (it->second->IsSequence()) { - sequences.insert(*it); + result.SetValue(it->first, it->second->GetSequenceContent()); } } } -#endif void DicomMap::Serialize(Json::Value& target) const { @@ -1673,6 +1687,27 @@ } + void DicomMap::RemoveSequences() + { + Content kept; + + for (Content::iterator it = content_.begin(); it != content_.end(); ++it) + { + assert(it->second != NULL); + + if (!it->second->IsSequence()) + { + kept[it->first] = it->second; + } + else + { + delete it->second; + } + } + + content_ = kept; + } + void DicomMap::DumpMainDicomTags(Json::Value& target, ResourceType level) const {
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.h Mon Jun 27 15:22:19 2022 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomMap.h Tue Jun 28 17:45:09 2022 +0200 @@ -94,6 +94,9 @@ const std::string& str, bool isBinary); + void SetValue(const DicomTag& tag, + const Json::Value& value); + bool HasTag(uint16_t group, uint16_t element) const; bool HasTag(const DicomTag& tag) const; @@ -124,6 +127,8 @@ void ExtractTags(DicomMap& result, const std::set<DicomTag>& tags) const; + void ExtractSequences(DicomMap& result) const; + static void SetupFindPatientTemplate(DicomMap& result); static void SetupFindStudyTemplate(DicomMap& result); @@ -149,10 +154,6 @@ static bool HasComputedTags(const std::set<DicomTag>& tags); -#if ORTHANC_ENABLE_DCMTK == 1 - static void ExtractSequences(std::set<DicomTag>& sequences, const std::set<DicomTag>& tags); -#endif - static const std::set<DicomTag>& GetMainDicomTags(ResourceType level); // returns a string uniquely identifying the list of main dicom tags for a level @@ -208,7 +209,8 @@ const DicomTag& tag) const; void FromDicomAsJson(const Json::Value& dicomAsJson, - bool append = false); + bool append = false, + bool parseSequences = false); void Merge(const DicomMap& other); @@ -231,6 +233,8 @@ void RemoveBinaryTags(); + void RemoveSequences(); + void DumpMainDicomTags(Json::Value& target, ResourceType level) const;
--- a/OrthancFramework/Sources/DicomFormat/DicomSequencesMap.cpp Mon Jun 27 15:22:19 2022 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,100 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2022 Osimis S.A., Belgium - * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - -#include "../PrecompiledHeaders.h" -#include "DicomSequencesMap.h" -#include "../Toolbox.h" - -namespace Orthanc -{ - -#if ORTHANC_ENABLE_DCMTK == 1 - - // copy all tags from Json (used to read from metadata) - void DicomSequencesMap::Deserialize(const Json::Value& serialized) - { - Json::Value::Members members = serialized.getMemberNames(); - for (size_t i = 0; i < members.size(); i++) - { - DicomTag tag(0, 0); - if (DicomTag::ParseHexadecimal(tag, members[i].c_str())) - { - sequences_[tag] = serialized[members[i]]; - } - } - } - -#endif - - // serialize a subet of tags (used to store in the metadata) - void DicomSequencesMap::Serialize(Json::Value& target, const std::set<DicomTag>& tags) const - { - // add the sequences to "target" - for (std::map<DicomTag, Json::Value>::const_iterator it = sequences_.begin(); - it != sequences_.end(); ++it) - { - if (tags.find(it->first) != tags.end()) - { - target[it->first.Format()] = it->second; - } - } - } - - // copy a subset of tags from Json - void DicomSequencesMap::FromDicomAsJson(const Json::Value& dicomAsJson, const std::set<DicomTag>& tags) - { - for (std::set<DicomTag>::const_iterator it = tags.begin(); - it != tags.end(); ++it) - { - std::string tag = it->Format(); - if (dicomAsJson.isMember(tag)) - { - sequences_[*it] = dicomAsJson[tag]; - } - } - } - - void DicomSequencesMap::ToJson(Json::Value& target, DicomToJsonFormat format, const std::set<DicomTag>& tags) const - { - // add the sequences to "target" - for (std::map<DicomTag, Json::Value>::const_iterator it = sequences_.begin(); - it != sequences_.end(); ++it) - { - Json::Value sequenceFullJson = Json::objectValue; - sequenceFullJson[it->first.Format()] = it->second; - - Json::Value& requestedFormatJson = sequenceFullJson; - Json::Value convertedJson; - - if (format != DicomToJsonFormat_Full) - { - Toolbox::SimplifyDicomAsJson(convertedJson, sequenceFullJson, format); - requestedFormatJson = convertedJson; - } - - Json::Value::Members keys = requestedFormatJson.getMemberNames(); - for (size_t i = 0; i < keys.size(); i++) // there should always be only one member in this JSON - { - target[keys[i]] = requestedFormatJson[keys[i]]; - } - } - } -} \ No newline at end of file
--- a/OrthancFramework/Sources/DicomFormat/DicomSequencesMap.h Mon Jun 27 15:22:19 2022 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,57 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2022 Osimis S.A., Belgium - * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#pragma once - -#include "../../../OrthancFramework/Sources/DicomFormat/DicomTag.h" - -#include <json/value.h> -#include <boost/noncopyable.hpp> - -#include <map> - -namespace Orthanc -{ - class DatabaseLookup; - class ParsedDicomFile; - struct ServerIndexChange; - - /* - * contains a map of dicom sequences where: - * the key is a DicomTag - * the sequence is serialized in Json "full" format - */ - struct DicomSequencesMap : public boost::noncopyable - { - std::map<DicomTag, Json::Value> sequences_; - - void Deserialize(const Json::Value& serialized); - void Serialize(Json::Value& target, const std::set<DicomTag>& tagsSubset) const; - void FromDicomAsJson(const Json::Value& dicomAsJson, const std::set<DicomTag>& tagsSubset); - void ToJson(Json::Value& target, DicomToJsonFormat format, const std::set<DicomTag>& tagsSubset) const; - - size_t GetSize() const - { - return sequences_.size(); - } - }; -}
--- a/OrthancFramework/Sources/DicomFormat/DicomValue.cpp Mon Jun 27 15:22:19 2022 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomValue.cpp Tue Jun 28 17:45:09 2022 +0200 @@ -40,7 +40,8 @@ DicomValue::DicomValue(const DicomValue& other) : type_(other.type_), - content_(other.content_) + content_(other.content_), + sequenceJson_(other.sequenceJson_) { } @@ -61,10 +62,15 @@ content_.assign(data, size); } + DicomValue::DicomValue(const Json::Value& value) : + type_(Type_SequenceAsJson), + sequenceJson_(value) + { + } const std::string& DicomValue::GetContent() const { - if (type_ == Type_Null) + if (type_ == Type_Null || type_ == Type_SequenceAsJson) { throw OrthancException(ErrorCode_BadParameterType); } @@ -74,6 +80,19 @@ } } + const Json::Value& DicomValue::GetSequenceContent() const + { + if (type_ != Type_SequenceAsJson) + { + throw OrthancException(ErrorCode_BadParameterType); + } + else + { + return sequenceJson_; + } + } + + bool DicomValue::IsNull() const { return type_ == Type_Null; @@ -84,6 +103,15 @@ return type_ == Type_Binary; } + bool DicomValue::IsString() const + { + return type_ == Type_String; + } + + bool DicomValue::IsSequence() const + { + return type_ == Type_SequenceAsJson; + } DicomValue* DicomValue::Clone() const { @@ -107,8 +135,7 @@ bool DicomValue::ParseInteger32(int32_t& result) const { - if (IsBinary() || - IsNull()) + if (!IsString()) { return false; } @@ -120,8 +147,7 @@ bool DicomValue::ParseInteger64(int64_t& result) const { - if (IsBinary() || - IsNull()) + if (!IsString()) { return false; } @@ -133,8 +159,7 @@ bool DicomValue::ParseUnsignedInteger32(uint32_t& result) const { - if (IsBinary() || - IsNull()) + if (!IsString()) { return false; } @@ -146,8 +171,7 @@ bool DicomValue::ParseUnsignedInteger64(uint64_t& result) const { - if (IsBinary() || - IsNull()) + if (!IsString()) { return false; } @@ -159,8 +183,7 @@ bool DicomValue::ParseFloat(float& result) const { - if (IsBinary() || - IsNull()) + if (!IsString()) { return false; } @@ -172,8 +195,7 @@ bool DicomValue::ParseDouble(double& result) const { - if (IsBinary() || - IsNull()) + if (!IsString()) { return false; } @@ -185,8 +207,7 @@ bool DicomValue::ParseFirstFloat(float& result) const { - if (IsBinary() || - IsNull()) + if (!IsString()) { return false; } @@ -200,8 +221,7 @@ { uint64_t value; - if (IsBinary() || - IsNull()) + if (!IsString()) { return false; } @@ -223,6 +243,10 @@ { return false; } + else if (IsSequence()) + { + return false; + } else if (IsBinary() && !allowBinary) { return false; @@ -263,6 +287,11 @@ break; } + case Type_SequenceAsJson: + { + throw OrthancException(ErrorCode_NotImplemented); + } + default: throw OrthancException(ErrorCode_InternalError); } @@ -289,6 +318,10 @@ const std::string base64 =SerializationToolbox::ReadString(source, KEY_CONTENT); Toolbox::DecodeBase64(content_, base64); } + else if (type == "Sequence") + { + throw OrthancException(ErrorCode_NotImplemented); + } else { throw OrthancException(ErrorCode_BadFileFormat);
--- a/OrthancFramework/Sources/DicomFormat/DicomValue.h Mon Jun 27 15:22:19 2022 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomValue.h Tue Jun 28 17:45:09 2022 +0200 @@ -43,11 +43,13 @@ { Type_Null, Type_String, - Type_Binary + Type_Binary, + Type_SequenceAsJson }; Type type_; std::string content_; + Json::Value sequenceJson_; DicomValue(const DicomValue& other); @@ -61,11 +63,19 @@ size_t size, bool isBinary); + DicomValue(const Json::Value& value); + const std::string& GetContent() const; + const Json::Value& GetSequenceContent() const; + bool IsNull() const; bool IsBinary() const; + + bool IsString() const; + + bool IsSequence() const; DicomValue* Clone() const;
--- a/OrthancFramework/Sources/DicomNetworking/Internals/FindScp.cpp Mon Jun 27 15:22:19 2022 +0200 +++ b/OrthancFramework/Sources/DicomNetworking/Internals/FindScp.cpp Tue Jun 28 17:45:09 2022 +0200 @@ -275,6 +275,7 @@ DicomMap input; std::set<DicomTag> ignoreTagLength; FromDcmtkBridge::ExtractDicomSummary(input, *requestIdentifiers, 0 /* don't truncate tags */, ignoreTagLength); + input.RemoveSequences(); DicomMap filtered; FixFindQuery(filtered, input);
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Mon Jun 27 15:22:19 2022 +0200 +++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Tue Jun 28 17:45:09 2022 +0200 @@ -564,6 +564,26 @@ ConvertLeafElement(*element, DicomToJsonFlags_Default, maxStringLength, encoding, hasCodeExtensions, ignoreTagLength)); } + else + { + DcmSequenceOfItems* sequence = dynamic_cast<DcmSequenceOfItems*>(element); + + if (sequence) + { + Json::Value jsonSequence = Json::arrayValue; + for (unsigned long i = 0; i < sequence->card(); i++) + { + DcmItem* child = sequence->getItem(i); + Json::Value& v = jsonSequence.append(Json::objectValue); + DatasetToJson(v, *child, DicomToJsonFormat_Full, DicomToJsonFlags_Default, + maxStringLength, encoding, hasCodeExtensions, + ignoreTagLength, 1); + } + + target.SetValue(DicomTag(element->getTag().getGTag(), element->getTag().getETag()), + jsonSequence); + } + } } } @@ -1417,6 +1437,18 @@ { result[tagName] = Json::nullValue; } + else if (it->second->IsSequence()) + { + result[tagName] = Json::arrayValue; + const Json::Value& jsonSequence = it->second->GetSequenceContent(); + + for (Json::Value::ArrayIndex i = 0; i < jsonSequence.size(); ++i) + { + Json::Value target = Json::objectValue; + Toolbox::SimplifyDicomAsJson(target, jsonSequence[i], DicomToJsonFormat_Human); + result[tagName].append(target); + } + } else { // TODO IsBinary @@ -1439,6 +1471,11 @@ value["Type"] = "Null"; value["Value"] = Json::nullValue; } + else if (it->second->IsSequence()) + { + value["Type"] = "Sequence"; + value["Value"] = it->second->GetSequenceContent(); + } else { // TODO IsBinary @@ -1458,6 +1495,18 @@ { result[hex] = Json::nullValue; } + else if (it->second->IsSequence()) + { + result[hex] = Json::arrayValue; + const Json::Value& jsonSequence = it->second->GetSequenceContent(); + + for (Json::Value::ArrayIndex i = 0; i < jsonSequence.size(); ++i) + { + Json::Value target = Json::objectValue; + Toolbox::SimplifyDicomAsJson(target, jsonSequence[i], DicomToJsonFormat_Short); + result[hex].append(target); + } + } else { // TODO IsBinary
--- a/OrthancFramework/UnitTestsSources/DicomMapTests.cpp Mon Jun 27 15:22:19 2022 +0200 +++ b/OrthancFramework/UnitTestsSources/DicomMapTests.cpp Tue Jun 28 17:45:09 2022 +0200 @@ -167,26 +167,6 @@ } -TEST(DicomMap, ExtractSequences) -{ - std::set<DicomTag> allTags; - std::set<DicomTag> sequences; - - // empty list - DicomMap::ExtractSequences(sequences, allTags); - ASSERT_EQ(0u, sequences.size()); - - // one tag, no sequence - allTags.insert(DICOM_TAG_PATIENT_NAME); - DicomMap::ExtractSequences(sequences, allTags); - ASSERT_EQ(0u, sequences.size()); - - // one sequence - allTags.insert(DICOM_TAG_REFERENCED_IMAGE_SEQUENCE); - DicomMap::ExtractSequences(sequences, allTags); - ASSERT_EQ(1u, sequences.size()); - ASSERT_TRUE(sequences.find(DICOM_TAG_REFERENCED_IMAGE_SEQUENCE) != sequences.end()); -} TEST(DicomMap, Tags) { @@ -642,6 +622,168 @@ } +TEST(DicomMap, FromDicomAsJsonAndSequences) +{ + DicomMap m; + std::string jsonFullString = "{" + "\"0008,1090\" : " + "{" + "\"Name\" : \"ManufacturerModelName\"," + "\"Type\" : \"String\"," + "\"Value\" : \"MyModel\"" + "}," + "\"0008,1111\" : " + "{" + "\"Name\" : \"ReferencedPerformedProcedureStepSequence\"," + "\"Type\" : \"Sequence\"," + "\"Value\" : " + "[" + "{" + "\"0008,1150\" : " + "{" + "\"Name\" : \"ReferencedSOPClassUID\"," + "\"Type\" : \"String\"," + "\"Value\" : \"1.2.4\"" + "}," + "\"0008,1155\" : " + "{" + "\"Name\" : \"ReferencedSOPInstanceUID\"," + "\"Type\" : \"String\"," + "\"Value\" : \"1.2.3\"" + "}" + "}" + "]" + "}}"; + + Json::Value parsedJson; + bool ret = Toolbox::ReadJson(parsedJson, jsonFullString); + + m.FromDicomAsJson(parsedJson, false /* append */, true /* parseSequences*/); + ASSERT_TRUE(ret); + + ASSERT_TRUE(m.HasTag(DicomTag(0x0008, 0x1090))); + ASSERT_EQ("MyModel", m.GetValue(0x0008,0x1090).GetContent()); + + ASSERT_TRUE(m.HasTag(DicomTag(0x0008, 0x1111))); + const Json::Value& jsonSequence = m.GetValue(0x0008, 0x1111).GetSequenceContent(); + ASSERT_EQ("ReferencedSOPClassUID", jsonSequence[0]["0008,1150"]["Name"].asString()); + + {// serialize to human dicomAsJson + Json::Value dicomAsJson = Json::objectValue; + FromDcmtkBridge::ToJson(dicomAsJson, m, DicomToJsonFormat_Human); + // printf("%s", dicomAsJson.toStyledString().c_str()); + + ASSERT_TRUE(dicomAsJson.isMember("ManufacturerModelName")); + ASSERT_TRUE(dicomAsJson.isMember("ReferencedPerformedProcedureStepSequence")); + ASSERT_TRUE(dicomAsJson["ReferencedPerformedProcedureStepSequence"][0].isMember("ReferencedSOPClassUID")); + ASSERT_EQ("1.2.4", dicomAsJson["ReferencedPerformedProcedureStepSequence"][0]["ReferencedSOPClassUID"].asString()); + } + + {// serialize to full dicomAsJson + Json::Value dicomAsJson = Json::objectValue; + FromDcmtkBridge::ToJson(dicomAsJson, m, DicomToJsonFormat_Full); + // printf("%s", dicomAsJson.toStyledString().c_str()); + + ASSERT_TRUE(dicomAsJson.isMember("0008,1090")); + ASSERT_TRUE(dicomAsJson.isMember("0008,1111")); + ASSERT_TRUE(dicomAsJson["0008,1111"]["Value"][0].isMember("0008,1150")); + ASSERT_EQ("1.2.4", dicomAsJson["0008,1111"]["Value"][0]["0008,1150"]["Value"].asString()); + ASSERT_EQ("MyModel", dicomAsJson["0008,1090"]["Value"].asString()); + } + + {// serialize to short dicomAsJson + Json::Value dicomAsJson = Json::objectValue; + FromDcmtkBridge::ToJson(dicomAsJson, m, DicomToJsonFormat_Short); + // printf("%s", dicomAsJson.toStyledString().c_str()); + + ASSERT_TRUE(dicomAsJson.isMember("0008,1090")); + ASSERT_TRUE(dicomAsJson.isMember("0008,1111")); + ASSERT_TRUE(dicomAsJson["0008,1111"][0].isMember("0008,1150")); + ASSERT_EQ("1.2.4", dicomAsJson["0008,1111"][0]["0008,1150"].asString()); + ASSERT_EQ("MyModel", dicomAsJson["0008,1090"].asString()); + } + + {// extract sequence + DicomMap sequencesOnly; + m.ExtractSequences(sequencesOnly); + + ASSERT_EQ(1, sequencesOnly.GetSize()); + ASSERT_TRUE(sequencesOnly.HasTag(0x0008, 0x1111)); + ASSERT_TRUE(sequencesOnly.GetValue(0x0008, 0x1111).GetSequenceContent()[0].isMember("0008,1150")); + + // copy sequence + DicomMap sequencesCopy; + sequencesCopy.SetValue(0x0008, 0x1111, sequencesOnly.GetValue(0x0008, 0x1111)); + + ASSERT_EQ(1, sequencesCopy.GetSize()); + ASSERT_TRUE(sequencesCopy.HasTag(0x0008, 0x1111)); + ASSERT_TRUE(sequencesCopy.GetValue(0x0008, 0x1111).GetSequenceContent()[0].isMember("0008,1150")); + } +} + +TEST(DicomMap, ExtractSummary) +{ + Json::Value v = Json::objectValue; + v["PatientName"] = "Hello"; + v["ReferencedSOPClassUID"] = "1.2.840.10008.5.1.4.1.1.4"; + + { + Json::Value a = Json::arrayValue; + + { + Json::Value item = Json::objectValue; + item["ReferencedSOPClassUID"] = "1.2.840.10008.5.1.4.1.1.4"; + item["ReferencedSOPInstanceUID"] = "1.2.840.113619.2.176.2025.1499492.7040.1171286241.719"; + a.append(item); + } + + { + Json::Value item = Json::objectValue; + item["ReferencedSOPClassUID"] = "1.2.840.10008.5.1.4.1.1.4"; // ReferencedSOPClassUID + item["ReferencedSOPInstanceUID"] = "1.2.840.113619.2.176.2025.1499492.7040.1171286241.726"; + a.append(item); + } + + v["ReferencedImageSequence"] = a; + } + + { + Json::Value a = Json::arrayValue; + + { + Json::Value item = Json::objectValue; + item["StudyInstanceUID"] = "1.2.840.113704.1.111.7016.1342451220.40"; + + { + Json::Value b = Json::arrayValue; + + { + Json::Value c = Json::objectValue; + c["CodeValue"] = "122403"; + c["0008,103e"] = "WORLD"; // Series description + b.append(c); + } + + item["PurposeOfReferenceCodeSequence"] = b; + } + + a.append(item); + } + + v["RelatedSeriesSequence"] = a; + } + + std::unique_ptr<ParsedDicomFile> dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + + DicomMap summary; + std::set<DicomTag> ignoreTagLength; + dicom->ExtractDicomSummary(summary, ORTHANC_MAXIMUM_TAG_LENGTH, ignoreTagLength); + + ASSERT_TRUE(summary.HasTag(0x0008, 0x1140)); + ASSERT_EQ("1.2.840.10008.5.1.4.1.1.4", summary.GetValue(0x0008, 0x1140).GetSequenceContent()[0]["0008,1150"]["Value"].asString()); +} + + TEST(DicomWebJson, Multiplicity) {
--- a/OrthancServer/Sources/Database/ResourcesContent.cpp Mon Jun 27 15:22:19 2022 +0200 +++ b/OrthancServer/Sources/Database/ResourcesContent.cpp Tue Jun 28 17:45:09 2022 +0200 @@ -28,6 +28,7 @@ #include "../../../OrthancFramework/Sources/DicomFormat/DicomArray.h" #include "../../../OrthancFramework/Sources/OrthancException.h" +#include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" #include <cassert> @@ -45,8 +46,7 @@ const DicomElement& element = flattened.GetElement(i); const DicomTag& tag = element.GetTag(); const DicomValue& value = element.GetValue(); - if (!value.IsNull() && - !value.IsBinary()) + if (value.IsString()) { target.AddMainDicomTag(resource, tag, element.GetValue().GetContent()); } @@ -70,9 +70,7 @@ assert(DicomMap::IsMainDicomTag(tags[i])); const DicomValue* value = map.TestAndGetValue(tags[i]); - if (value != NULL && - !value->IsNull() && - !value->IsBinary()) + if (value != NULL && value->IsString()) { std::string s = ServerToolbox::NormalizeIdentifier(value->GetContent()); target.AddIdentifierTag(resource, tags[i], s); @@ -133,7 +131,7 @@ throw OrthancException(ErrorCode_InternalError); } - StoreMainDicomTagsInternal(*this, resource, tags); + StoreMainDicomTagsInternal(*this, resource, tags); // saves only leaf tags, not sequences }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Mon Jun 27 15:22:19 2022 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Tue Jun 28 17:45:09 2022 +0200 @@ -881,7 +881,7 @@ Toolbox::ReadJson(jsonMetadata, serializedSequences); assert(jsonMetadata["Version"].asInt() == 1); - target.sequences_.Deserialize(jsonMetadata["Sequences"]); + target.tags_.FromDicomAsJson(jsonMetadata["Sequences"], true /* append */, true /* parseSequences */); } // check if we have access to all requestedTags or if we must get tags from parents @@ -2629,6 +2629,28 @@ } + static void GetMainDicomSequenceMetadataContent(std::string& result, + const DicomMap& dicomSummary, + ResourceType level) + { + DicomMap levelSummary; + DicomMap levelSequences; + + dicomSummary.ExtractResourceInformation(levelSummary, level); + levelSummary.ExtractSequences(levelSequences); + + if (levelSequences.GetSize() > 0) + { + Json::Value jsonMetadata; + jsonMetadata["Version"] = 1; + jsonMetadata["Sequences"] = Json::objectValue; + FromDcmtkBridge::ToJson(jsonMetadata["Sequences"], levelSequences, DicomToJsonFormat_Full); + + Toolbox::WriteFastJson(result, jsonMetadata); + } + } + + void StatelessDatabaseOperations::ReconstructInstance(const ParsedDicomFile& dicom) { class Operations : public IReadWriteOperations @@ -2657,6 +2679,25 @@ } } + static void SetMainDicomSequenceMetadata(ReadWriteTransaction& transaction, + int64_t instance, + const DicomMap& dicomSummary, + ResourceType level) + { + std::string serialized; + GetMainDicomSequenceMetadataContent(serialized, dicomSummary, level); + + if (!serialized.empty()) + { + ReplaceMetadata(transaction, instance, MetadataType_MainDicomSequences, serialized); + } + else + { + transaction.DeleteMetadata(instance, MetadataType_MainDicomSequences); + } + + } + public: explicit Operations(const ParsedDicomFile& dicom) { @@ -2704,6 +2745,11 @@ ReplaceMetadata(transaction, study, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Study)); // New in Orthanc 1.11.0 ReplaceMetadata(transaction, series, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Series)); // New in Orthanc 1.11.0 ReplaceMetadata(transaction, instance, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Instance)); // New in Orthanc 1.11.0 + + SetMainDicomSequenceMetadata(transaction, patient, summary_, ResourceType_Patient); + SetMainDicomSequenceMetadata(transaction, study, summary_, ResourceType_Study); + SetMainDicomSequenceMetadata(transaction, series, summary_, ResourceType_Series); + SetMainDicomSequenceMetadata(transaction, instance, summary_, ResourceType_Instance); } if (hasTransferSyntax_) @@ -2852,7 +2898,6 @@ StoreStatus StatelessDatabaseOperations::Store(std::map<MetadataType, std::string>& instanceMetadata, const DicomMap& dicomSummary, - const DicomSequencesMap& sequencesToStore, const Attachments& attachments, const MetadataMap& metadata, const DicomInstanceOrigin& origin, @@ -2871,7 +2916,6 @@ StoreStatus storeStatus_; std::map<MetadataType, std::string>& instanceMetadata_; const DicomMap& dicomSummary_; - const DicomSequencesMap& sequencesToStore_; const Attachments& attachments_; const MetadataMap& metadata_; const DicomInstanceOrigin& origin_; @@ -2904,32 +2948,17 @@ } static void SetMainDicomSequenceMetadata(ResourcesContent& content, - int64_t resource, - const DicomSequencesMap& sequencesToStore, // all sequences for all levels ! - ResourceType level) + int64_t resource, + const DicomMap& dicomSummary, + ResourceType level) { - if (sequencesToStore.GetSize() > 0) + std::string serialized; + GetMainDicomSequenceMetadataContent(serialized, dicomSummary, level); + + if (!serialized.empty()) { - const std::set<DicomTag>& levelTags = DicomMap::GetMainDicomTags(level); - std::set<DicomTag> levelSequences; - DicomMap::ExtractSequences(levelSequences, levelTags); - - if (levelSequences.size() == 0) - { - return; - } - - Json::Value jsonMetadata; - jsonMetadata["Version"] = 1; - jsonMetadata["Sequences"] = Json::objectValue; - sequencesToStore.Serialize(jsonMetadata["Sequences"], levelSequences); - - std::string serialized; - Toolbox::WriteFastJson(serialized, jsonMetadata); - content.AddMetadata(resource, MetadataType_MainDicomSequences, serialized); } - } static bool ComputeExpectedNumberOfInstances(int64_t& target, @@ -2989,7 +3018,6 @@ public: Operations(std::map<MetadataType, std::string>& instanceMetadata, const DicomMap& dicomSummary, - const DicomSequencesMap& sequencesToStore, const Attachments& attachments, const MetadataMap& metadata, const DicomInstanceOrigin& origin, @@ -3004,7 +3032,6 @@ storeStatus_(StoreStatus_Failure), instanceMetadata_(instanceMetadata), dicomSummary_(dicomSummary), - sequencesToStore_(sequencesToStore), attachments_(attachments), metadata_(metadata), origin_(origin), @@ -3155,27 +3182,27 @@ content.AddResource(instanceId, ResourceType_Instance, dicomSummary_); SetInstanceMetadata(content, instanceMetadata_, instanceId, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Instance)); // New in Orthanc 1.11.0 - SetMainDicomSequenceMetadata(content, instanceId, sequencesToStore_, ResourceType_Instance); // new in Orthanc 1.11.1 + SetMainDicomSequenceMetadata(content, instanceId, dicomSummary_, ResourceType_Instance); // new in Orthanc 1.11.1 if (status.isNewSeries_) { content.AddResource(status.seriesId_, ResourceType_Series, dicomSummary_); content.AddMetadata(status.seriesId_, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Series)); // New in Orthanc 1.11.0 - SetMainDicomSequenceMetadata(content, status.seriesId_, sequencesToStore_, ResourceType_Series); // new in Orthanc 1.11.1 + SetMainDicomSequenceMetadata(content, status.seriesId_, dicomSummary_, ResourceType_Series); // new in Orthanc 1.11.1 } if (status.isNewStudy_) { content.AddResource(status.studyId_, ResourceType_Study, dicomSummary_); content.AddMetadata(status.studyId_, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Study)); // New in Orthanc 1.11.0 - SetMainDicomSequenceMetadata(content, status.studyId_, sequencesToStore_, ResourceType_Study); // new in Orthanc 1.11.1 + SetMainDicomSequenceMetadata(content, status.studyId_, dicomSummary_, ResourceType_Study); // new in Orthanc 1.11.1 } if (status.isNewPatient_) { content.AddResource(status.patientId_, ResourceType_Patient, dicomSummary_); content.AddMetadata(status.patientId_, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Patient)); // New in Orthanc 1.11.0 - SetMainDicomSequenceMetadata(content, status.patientId_, sequencesToStore_, ResourceType_Patient); // new in Orthanc 1.11.1 + SetMainDicomSequenceMetadata(content, status.patientId_, dicomSummary_, ResourceType_Patient); // new in Orthanc 1.11.1 } // Attach the auto-computed metadata for the patient/study/series levels @@ -3320,7 +3347,7 @@ }; - Operations operations(instanceMetadata, dicomSummary, sequencesToStore, attachments, metadata, origin, + Operations operations(instanceMetadata, dicomSummary, attachments, metadata, origin, overwrite, hasTransferSyntax, transferSyntax, hasPixelDataOffset, pixelDataOffset, maximumStorageSize, maximumPatients, isReconstruct); Apply(operations);
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Mon Jun 27 15:22:19 2022 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Tue Jun 28 17:45:09 2022 +0200 @@ -23,7 +23,6 @@ #pragma once #include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h" -#include "../../../OrthancFramework/Sources/DicomFormat/DicomSequencesMap.h" #include "IDatabaseWrapper.h" #include "../DicomInstanceOrigin.h" @@ -41,8 +40,7 @@ struct ExpandedResource : public boost::noncopyable { std::string id_; - DicomMap tags_; // all tags from DB (only leaf tags, not sequences !) - DicomSequencesMap sequences_; // the requested sequences (from MainDicomTags or RequestedTags) + DicomMap tags_; // all main tags and main sequences from DB std::string mainDicomTagsSignature_; std::string parentId_; std::list<std::string> childrenIds_; @@ -652,7 +650,6 @@ StoreStatus Store(std::map<MetadataType, std::string>& instanceMetadata, const DicomMap& dicomSummary, - const DicomSequencesMap& sequencesToStore, const Attachments& attachments, const MetadataMap& metadata, const DicomInstanceOrigin& origin,
--- a/OrthancServer/Sources/ServerContext.cpp Mon Jun 27 15:22:19 2022 +0200 +++ b/OrthancServer/Sources/ServerContext.cpp Tue Jun 28 17:45:09 2022 +0200 @@ -526,12 +526,9 @@ bool hasTransferSyntax = dicom.LookupTransferSyntax(transferSyntax); DicomMap summary; - dicom.GetSummary(summary); // -> this includes only the leaf nodes + dicom.GetSummary(summary); // -> from Orthanc 1.11.1, this includes the leaf nodes and sequences std::set<DicomTag> allMainDicomTags = DicomMap::GetAllMainDicomTags(); - std::set<DicomTag> mainDicomSequences; - DicomMap::ExtractSequences(mainDicomSequences, allMainDicomTags); - DicomSequencesMap sequencesToStore; try { @@ -541,10 +538,8 @@ DicomInstanceHasher hasher(summary); resultPublicId = hasher.HashInstance(); - Json::Value dicomAsJson; // -> this includes the sequences - - dicom.GetDicomAsJson(dicomAsJson, mainDicomSequences /*ignoreTagLength*/); // make sure that sequences that we wish to store in DB are not 'cropped' - sequencesToStore.FromDicomAsJson(dicomAsJson, mainDicomSequences); + Json::Value dicomAsJson; + dicom.GetDicomAsJson(dicomAsJson, allMainDicomTags); // don't crop any main dicom tags Json::Value simplifiedTags; Toolbox::SimplifyDicomAsJson(simplifiedTags, dicomAsJson, DicomToJsonFormat_Human); @@ -623,7 +618,7 @@ typedef std::map<MetadataType, std::string> InstanceMetadata; InstanceMetadata instanceMetadata; result.SetStatus(index_.Store( - instanceMetadata, summary, sequencesToStore, attachments, dicom.GetMetadata(), dicom.GetOrigin(), overwrite, + instanceMetadata, summary, attachments, dicom.GetMetadata(), dicom.GetOrigin(), overwrite, hasTransferSyntax, transferSyntax, hasPixelDataOffset, pixelDataOffset, isReconstruct)); // Only keep the metadata for the "instance" level @@ -1901,8 +1896,14 @@ const std::string& ServerContext::GetDeidentifiedContent(const DicomElement &element) const { static const std::string redactedContent = "*** POTENTIAL PHI ***"; + static const std::string emptyContent = ""; const DicomTag& tag = element.GetTag(); + if (element.GetValue().IsSequence()) + { + return emptyContent; + } + if (deidentifyLogs_ && !element.GetValue().GetContent().empty() && logsDeidentifierRules_.IsAlteredTag(tag)) @@ -2083,13 +2084,6 @@ target[MAIN_DICOM_TAGS] = Json::objectValue; FromDcmtkBridge::ToJson(target[MAIN_DICOM_TAGS], mainDicomTags, format); - {// add the main DICOM sequences to the main dicom tags - const std::set<DicomTag>& mainDicomTags = DicomMap::GetMainDicomTags(resource.type_); - std::set<DicomTag> mainDicomSequences; - DicomMap::ExtractSequences(mainDicomSequences, mainDicomTags); - resource.sequences_.ToJson(target[MAIN_DICOM_TAGS], format, mainDicomSequences); - } - if (resource.type_ == ResourceType_Study) { DicomMap patientMainDicomTags; @@ -2109,12 +2103,6 @@ target[REQUESTED_TAGS] = Json::objectValue; FromDcmtkBridge::ToJson(target[REQUESTED_TAGS], tags, format); - {// add the requested sequences to the requested tags - std::set<DicomTag> requestedDicomSequences; - DicomMap::ExtractSequences(requestedDicomSequences, requestedTags); - resource.sequences_.ToJson(target[REQUESTED_TAGS], format, requestedDicomSequences); - } - } } @@ -2426,9 +2414,6 @@ // possibly merge missing requested tags from dicom-as-json if (!resource.missingRequestedTags_.empty() && !DicomMap::HasOnlyComputedTags(resource.missingRequestedTags_)) { - std::set<DicomTag> missingSequences; - DicomMap::ExtractSequences(missingSequences, resource.missingRequestedTags_); - OrthancConfiguration::ReaderLock lock; if (lock.GetConfiguration().IsWarningEnabled(Warnings_001_TagsBeingReadFromStorage)) { @@ -2474,13 +2459,11 @@ Json::Value tmpDicomAsJson; ReadDicomAsJson(tmpDicomAsJson, instanceId_, resource.missingRequestedTags_ /* ignoreTagLength */); // read all tags from DICOM and avoid cropping requested tags - tagsFromJson.FromDicomAsJson(tmpDicomAsJson); - resource.sequences_.FromDicomAsJson(tmpDicomAsJson, missingSequences); + tagsFromJson.FromDicomAsJson(tmpDicomAsJson, false /* append */, true /* parseSequences*/); } else { - tagsFromJson.FromDicomAsJson(*dicomAsJson); - resource.sequences_.FromDicomAsJson(*dicomAsJson, missingSequences); + tagsFromJson.FromDicomAsJson(*dicomAsJson, false /* append */, true /* parseSequences*/); } resource.tags_.Merge(tagsFromJson);
--- a/OrthancServer/Sources/ServerIndex.cpp Mon Jun 27 15:22:19 2022 +0200 +++ b/OrthancServer/Sources/ServerIndex.cpp Tue Jun 28 17:45:09 2022 +0200 @@ -511,7 +511,6 @@ StoreStatus ServerIndex::Store(std::map<MetadataType, std::string>& instanceMetadata, const DicomMap& dicomSummary, - const DicomSequencesMap& sequencesToStore, const ServerIndex::Attachments& attachments, const ServerIndex::MetadataMap& metadata, const DicomInstanceOrigin& origin, @@ -532,7 +531,7 @@ } return StatelessDatabaseOperations::Store( - instanceMetadata, dicomSummary, sequencesToStore, attachments, metadata, origin, overwrite, hasTransferSyntax, + instanceMetadata, dicomSummary, attachments, metadata, origin, overwrite, hasTransferSyntax, transferSyntax, hasPixelDataOffset, pixelDataOffset, maximumStorageSize, maximumPatients, isReconstruct); }
--- a/OrthancServer/Sources/ServerIndex.h Mon Jun 27 15:22:19 2022 +0200 +++ b/OrthancServer/Sources/ServerIndex.h Tue Jun 28 17:45:09 2022 +0200 @@ -77,7 +77,6 @@ StoreStatus Store(std::map<MetadataType, std::string>& instanceMetadata, const DicomMap& dicomSummary, - const DicomSequencesMap& sequencesToStore, const Attachments& attachments, const MetadataMap& metadata, const DicomInstanceOrigin& origin,
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Mon Jun 27 15:22:19 2022 +0200 +++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Tue Jun 28 17:45:09 2022 +0200 @@ -732,14 +732,13 @@ { DicomMap summary; - DicomSequencesMap sequences; OrthancConfiguration::DefaultExtractDicomSummary(summary, toStore->GetParsedDicomFile()); toStore->SetOrigin(DicomInstanceOrigin::FromPlugins()); DicomTransferSyntax transferSyntax; bool hasTransferSyntax = dicom.LookupTransferSyntax(transferSyntax); ASSERT_EQ(StoreStatus_Success, index.Store( - instanceMetadata, summary, sequences, attachments, toStore->GetMetadata(), + instanceMetadata, summary, attachments, toStore->GetMetadata(), toStore->GetOrigin(), false /* don't overwrite */, hasTransferSyntax, transferSyntax, true /* pixel data offset */, 42, false)); }