# HG changeset patch # User Sebastien Jodogne # Date 1623078348 -7200 # Node ID c5528c7847a688bbaff0e4a8f8c5c101042b5a78 # Parent 898e8ac8c4534b3feee38cca2feb06a8188e4888 new class DicomPath diff -r 898e8ac8c453 -r c5528c7847a6 OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake --- a/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake Sat Jun 05 12:23:54 2021 +0200 +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake Mon Jun 07 17:05:48 2021 +0200 @@ -151,6 +151,7 @@ ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Cache/MemoryStringCache.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/ChunkedBuffer.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/DicomTag.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/DicomPath.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/EnumerationDictionary.h ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Enumerations.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/FileInfo.cpp diff -r 898e8ac8c453 -r c5528c7847a6 OrthancFramework/Sources/DicomFormat/DicomPath.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/DicomFormat/DicomPath.cpp Mon Jun 07 17:05:48 2021 +0200 @@ -0,0 +1,256 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2021 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "DicomPath.h" + + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error Macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#if ORTHANC_ENABLE_DCMTK == 1 +# include "../DicomParsing/FromDcmtkBridge.h" +#endif + +#include "../OrthancException.h" +#include "../Toolbox.h" + +#include + + +namespace Orthanc +{ + DicomPath::PrefixItem::PrefixItem(DicomTag tag, + bool isUniversal, + size_t index) : + tag_(tag), + isUniversal_(isUniversal), + index_(index) + { + } + + + size_t DicomPath::PrefixItem::GetIndex() const + { + if (isUniversal_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return index_; + } + } + + + DicomTag DicomPath::ParseTag(const std::string& token) + { + DicomTag tag(0,0); + + if (token[0] == '(' && + token[token.size() - 1] == ')') + { + std::string hex = token.substr(1, token.size() - 2); + if (!DicomTag::ParseHexadecimal(tag, hex.c_str())) + { + throw OrthancException(ErrorCode_UnknownDicomTag, "Cannot parse tag: " + token); + } + } + else + { +#if ORTHANC_ENABLE_DCMTK == 1 + tag = FromDcmtkBridge::ParseTag(token); +#else + if (!DicomTag::ParseHexadecimal(tag, token.c_str())) + { + throw OrthancException(ErrorCode_UnknownDicomTag, "Cannot parse tag without DCMTK: " + token); + } +#endif + } + + return tag; + } + + + const DicomPath::PrefixItem& DicomPath::GetLevel(size_t i) const + { + if (i >= prefix_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + return prefix_[i]; + } + } + + + DicomPath::DicomPath(const Orthanc::DicomTag& sequence, + size_t index, + const Orthanc::DicomTag& tag) : + finalTag_(tag) + { + AddIndexedTagToPrefix(sequence, index); + } + + + DicomPath::DicomPath(const Orthanc::DicomTag& sequence1, + size_t index1, + const Orthanc::DicomTag& sequence2, + size_t index2, + const Orthanc::DicomTag& tag) : + finalTag_(tag) + { + AddIndexedTagToPrefix(sequence1, index1); + AddIndexedTagToPrefix(sequence2, index2); + } + + + DicomPath::DicomPath(const Orthanc::DicomTag& sequence1, + size_t index1, + const Orthanc::DicomTag& sequence2, + size_t index2, + const Orthanc::DicomTag& sequence3, + size_t index3, + const Orthanc::DicomTag& tag) : + finalTag_(tag) + { + AddIndexedTagToPrefix(sequence1, index1); + AddIndexedTagToPrefix(sequence2, index2); + AddIndexedTagToPrefix(sequence3, index3); + } + + + void DicomPath::AddIndexedTagToPrefix(const Orthanc::DicomTag& tag, + size_t index) + { + prefix_.push_back(PrefixItem::CreateIndexed(tag, index)); + } + + + void DicomPath::AddUniversalTagToPrefix(const Orthanc::DicomTag& tag) + { + prefix_.push_back(PrefixItem::CreateUniversal(tag)); + } + + + std::string DicomPath::Format() const + { + std::string s; + + for (size_t i = 0; i < prefix_.size(); i++) + { + s += "(" + prefix_[i].GetTag().Format() + ")"; + + if (prefix_[i].IsUniversal()) + { + s += "[*]."; + } + else + { + s += "[" + boost::lexical_cast(prefix_[i].GetIndex()) + "]."; + } + } + + return s + "(" + finalTag_.Format() + ")"; + } + + + DicomPath DicomPath::Parse(const std::string& s, + bool allowUniversal) + { + std::vector tokens; + Toolbox::TokenizeString(tokens, s, '.'); + + if (tokens.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Empty path to DICOM tags"); + } + + const DicomTag finalTag = ParseTag(Toolbox::StripSpaces(tokens[tokens.size() - 1])); + + DicomPath path(finalTag); + + for (size_t i = 0; i < tokens.size() - 1; i++) + { + size_t pos = tokens[i].find('['); + if (pos == std::string::npos) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Parent path doesn't contain an index"); + } + else + { + const std::string left = Orthanc::Toolbox::StripSpaces(tokens[i].substr(0, pos)); + const std::string right = Orthanc::Toolbox::StripSpaces(tokens[i].substr(pos + 1)); + + if (left.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Parent path doesn't contain a tag"); + } + else if (right.empty() || + right[right.size() - 1] != ']') + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Parent path doesn't contain the end of the index"); + } + else + { + DicomTag tag = ParseTag(left); + + try + { + std::string s = Toolbox::StripSpaces(right.substr(0, right.size() - 1)); + if (s == "*") + { + if (allowUniversal) + { + path.AddUniversalTagToPrefix(tag); + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Cannot create an universal parent path"); + } + } + else + { + int index = boost::lexical_cast(s); + if (index < 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Negative index in parent path: " + s); + } + else + { + path.AddIndexedTagToPrefix(tag, static_cast(index)); + } + } + } + catch (boost::bad_lexical_cast&) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Not a valid index in parent path: [" + right); + } + } + } + } + + return path; + } +} diff -r 898e8ac8c453 -r c5528c7847a6 OrthancFramework/Sources/DicomFormat/DicomPath.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/DicomFormat/DicomPath.h Mon Jun 07 17:05:48 2021 +0200 @@ -0,0 +1,137 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2021 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "../OrthancFramework.h" +#include "../DicomFormat/DicomTag.h" + +#include + +namespace Orthanc +{ + class DicomPath + { + private: + class PrefixItem + { + private: + DicomTag tag_; + bool isUniversal_; // Matches any index + size_t index_; + + PrefixItem(DicomTag tag, + bool isUniversal, + size_t index); + + public: + static PrefixItem CreateUniversal(const DicomTag& tag) + { + return PrefixItem(tag, true, 0 /* dummy value */); + } + + static PrefixItem CreateIndexed(const DicomTag& tag, + size_t index) + { + return PrefixItem(tag, false, index); + } + + const DicomTag& GetTag() const + { + return tag_; + } + + bool IsUniversal() const + { + return isUniversal_; + } + + size_t GetIndex() const; + }; + + std::vector prefix_; + Orthanc::DicomTag finalTag_; + + static DicomTag ParseTag(const std::string& token); + + const PrefixItem& GetLevel(size_t i) const; + + public: + explicit DicomPath(const Orthanc::DicomTag& tag) : + finalTag_(tag) + { + } + + DicomPath(const Orthanc::DicomTag& sequence, + size_t index, + const Orthanc::DicomTag& tag); + + DicomPath(const Orthanc::DicomTag& sequence1, + size_t index1, + const Orthanc::DicomTag& sequence2, + size_t index2, + const Orthanc::DicomTag& tag); + + DicomPath(const Orthanc::DicomTag& sequence1, + size_t index1, + const Orthanc::DicomTag& sequence2, + size_t index2, + const Orthanc::DicomTag& sequence3, + size_t index3, + const Orthanc::DicomTag& tag); + + void AddIndexedTagToPrefix(const Orthanc::DicomTag& tag, + size_t index); + + void AddUniversalTagToPrefix(const Orthanc::DicomTag& tag); + + size_t GetPrefixLength() const + { + return prefix_.size(); + } + + const Orthanc::DicomTag& GetFinalTag() const + { + return finalTag_; + } + + const Orthanc::DicomTag& GetPrefixTag(size_t level) const + { + return GetLevel(level).GetTag(); + } + + bool IsPrefixUniversal(size_t level) const + { + return GetLevel(level).IsUniversal(); + } + + size_t GetPrefixIndex(size_t level) const + { + return GetLevel(level).GetIndex(); + } + + std::string Format() const; + + static DicomPath Parse(const std::string& s, + bool allowUniversal); + }; +} diff -r 898e8ac8c453 -r c5528c7847a6 OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp --- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Sat Jun 05 12:23:54 2021 +0200 +++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Mon Jun 07 17:05:48 2021 +0200 @@ -36,12 +36,13 @@ #include #include "../Sources/Compatibility.h" +#include "../Sources/DicomFormat/DicomPath.h" #include "../Sources/DicomNetworking/DicomFindAnswers.h" #include "../Sources/DicomParsing/DicomModification.h" #include "../Sources/DicomParsing/DicomWebJsonVisitor.h" #include "../Sources/DicomParsing/FromDcmtkBridge.h" +#include "../Sources/DicomParsing/ParsedDicomCache.h" #include "../Sources/DicomParsing/ToDcmtkBridge.h" -#include "../Sources/DicomParsing/ParsedDicomCache.h" #include "../Sources/Endianness.h" #include "../Sources/Images/Image.h" #include "../Sources/Images/ImageBuffer.h" @@ -50,6 +51,7 @@ #include "../Sources/Images/PngWriter.h" #include "../Sources/Logging.h" #include "../Sources/OrthancException.h" + #include "../Resources/CodeGeneration/EncodingTests.h" #if ORTHANC_SANDBOXED != 1 @@ -2256,6 +2258,100 @@ } +TEST(DicomModification, DicomPath) +{ + // Those are samples inspired by those from "man dcmodify" + + static const DicomTag DICOM_TAG_ACQUISITION_MATRIX(0x0018, 0x1310); + static const DicomTag DICOM_TAG_REFERENCED_PERFORMED_PROCEDURE_STEP_SEQUENCE(0x0008, 0x1111); + + DicomPath path = DicomPath::Parse("(0010,0010)", true); + ASSERT_EQ(0u, path.GetPrefixLength()); + ASSERT_EQ(DICOM_TAG_PATIENT_NAME, path.GetFinalTag()); + ASSERT_THROW(path.GetPrefixTag(0), OrthancException); + + path = DicomPath::Parse("0018,1310", true); + ASSERT_EQ(0u, path.GetPrefixLength()); + ASSERT_EQ(DICOM_TAG_ACQUISITION_MATRIX, path.GetFinalTag()); + ASSERT_EQ("(0018,1310)", path.Format()); + + // The following sample won't work without DCMTK + path = DicomPath::Parse("PatientID", true); + ASSERT_EQ(0u, path.GetPrefixLength()); + ASSERT_EQ(DICOM_TAG_PATIENT_ID, path.GetFinalTag()); + ASSERT_EQ("(0010,0020)", path.Format()); + + path = DicomPath::Parse("(0018,1310)", true); + ASSERT_EQ(0u, path.GetPrefixLength()); + ASSERT_EQ(DICOM_TAG_ACQUISITION_MATRIX, path.GetFinalTag()); + ASSERT_EQ("(0018,1310)", path.Format()); + + path = DicomPath::Parse("(0008,1111)[0].PatientName", true); + ASSERT_EQ(1u, path.GetPrefixLength()); + ASSERT_EQ(DICOM_TAG_REFERENCED_PERFORMED_PROCEDURE_STEP_SEQUENCE, path.GetPrefixTag(0)); + ASSERT_FALSE(path.IsPrefixUniversal(0)); + ASSERT_EQ(0, path.GetPrefixIndex(0)); + ASSERT_THROW(path.GetPrefixTag(1), OrthancException); + ASSERT_EQ(DICOM_TAG_PATIENT_NAME, path.GetFinalTag()); + + path = DicomPath::Parse("(0008,1111)[1].(0008,1111)[2].(0010,0010)", true); + ASSERT_EQ(2u, path.GetPrefixLength()); + ASSERT_EQ(DICOM_TAG_REFERENCED_PERFORMED_PROCEDURE_STEP_SEQUENCE, path.GetPrefixTag(0)); + ASSERT_FALSE(path.IsPrefixUniversal(0)); + ASSERT_EQ(1, path.GetPrefixIndex(0)); + ASSERT_EQ(DICOM_TAG_REFERENCED_PERFORMED_PROCEDURE_STEP_SEQUENCE, path.GetPrefixTag(1)); + ASSERT_FALSE(path.IsPrefixUniversal(1)); + ASSERT_EQ(2, path.GetPrefixIndex(1)); + ASSERT_THROW(path.GetPrefixTag(2), OrthancException); + ASSERT_EQ(DICOM_TAG_PATIENT_NAME, path.GetFinalTag()); + + path = DicomPath::Parse("(0008,1111)[*].PatientName", true); + ASSERT_EQ(1u, path.GetPrefixLength()); + ASSERT_EQ(DICOM_TAG_REFERENCED_PERFORMED_PROCEDURE_STEP_SEQUENCE, path.GetPrefixTag(0)); + ASSERT_TRUE(path.IsPrefixUniversal(0)); + ASSERT_THROW(path.GetPrefixIndex(0), OrthancException); + ASSERT_THROW(path.GetPrefixTag(1), OrthancException); + ASSERT_EQ(DICOM_TAG_PATIENT_NAME, path.GetFinalTag()); + ASSERT_EQ("(0008,1111)[*].(0010,0010)", path.Format()); + + ASSERT_THROW(DicomPath::Parse("(0008,1111)[*].PatientName", false), OrthancException); + + path = DicomPath::Parse("(0008,1111)[1].(0008,1111)[*].(0010,0010)", true); + ASSERT_EQ(2u, path.GetPrefixLength()); + ASSERT_EQ(DICOM_TAG_REFERENCED_PERFORMED_PROCEDURE_STEP_SEQUENCE, path.GetPrefixTag(0)); + ASSERT_FALSE(path.IsPrefixUniversal(0)); + ASSERT_EQ(1, path.GetPrefixIndex(0)); + ASSERT_EQ(DICOM_TAG_REFERENCED_PERFORMED_PROCEDURE_STEP_SEQUENCE, path.GetPrefixTag(0)); + ASSERT_TRUE(path.IsPrefixUniversal(1)); + ASSERT_THROW(path.GetPrefixIndex(1), OrthancException); + ASSERT_THROW(path.GetPrefixTag(2), OrthancException); + ASSERT_EQ(DICOM_TAG_PATIENT_NAME, path.GetFinalTag()); + + path = DicomPath::Parse("PatientID[1].PatientName", true); + ASSERT_EQ(1u, path.GetPrefixLength()); + ASSERT_EQ(DICOM_TAG_PATIENT_ID, path.GetPrefixTag(0)); + ASSERT_FALSE(path.IsPrefixUniversal(0)); + ASSERT_EQ(1, path.GetPrefixIndex(0)); + ASSERT_THROW(path.GetPrefixTag(1), OrthancException); + ASSERT_EQ(DICOM_TAG_PATIENT_NAME, path.GetFinalTag()); + + path = DicomPath::Parse(" PatientID [ 42 ] . PatientName ", true); + ASSERT_EQ(1u, path.GetPrefixLength()); + ASSERT_EQ(DICOM_TAG_PATIENT_ID, path.GetPrefixTag(0)); + ASSERT_FALSE(path.IsPrefixUniversal(0)); + ASSERT_EQ(42, path.GetPrefixIndex(0)); + ASSERT_THROW(path.GetPrefixTag(1), OrthancException); + ASSERT_EQ(DICOM_TAG_PATIENT_NAME, path.GetFinalTag()); + ASSERT_EQ("(0010,0020)[42].(0010,0010)", path.Format()); + + ASSERT_THROW(DicomPath::Parse("nope", true), OrthancException); + ASSERT_THROW(DicomPath::Parse("(0010,0010)[.PatientID", true), OrthancException); + ASSERT_THROW(DicomPath::Parse("(0010,0010)[].PatientID", true), OrthancException); + ASSERT_THROW(DicomPath::Parse("(0010,0010[].PatientID", true), OrthancException); + ASSERT_THROW(DicomPath::Parse("(0010,0010)0].PatientID", true), OrthancException); + ASSERT_THROW(DicomPath::Parse("(0010,0010)[-1].PatientID", true), OrthancException); +} + #if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1