changeset 4681:c5528c7847a6

new class DicomPath
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 07 Jun 2021 17:05:48 +0200
parents 898e8ac8c453
children d38a7040474a
files OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake OrthancFramework/Sources/DicomFormat/DicomPath.cpp OrthancFramework/Sources/DicomFormat/DicomPath.h OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp
diffstat 4 files changed, 491 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- 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
--- /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
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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 <boost/lexical_cast.hpp>
+
+
+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<std::string>(prefix_[i].GetIndex()) + "].";
+      }
+    }
+
+    return s + "(" + finalTag_.Format() + ")";
+  }
+
+  
+  DicomPath DicomPath::Parse(const std::string& s,
+                             bool allowUniversal)
+  {
+    std::vector<std::string> 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<int>(s);
+              if (index < 0)
+              {
+                throw OrthancException(ErrorCode_ParameterOutOfRange, "Negative index in parent path: " + s);
+              }
+              else
+              {
+                path.AddIndexedTagToPrefix(tag, static_cast<size_t>(index));
+              }
+            }
+          }
+          catch (boost::bad_lexical_cast&)
+          {
+            throw OrthancException(ErrorCode_ParameterOutOfRange, "Not a valid index in parent path: [" + right);
+          }
+        }
+      }
+    }
+
+    return path;
+  }
+}
--- /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
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../OrthancFramework.h"
+#include "../DicomFormat/DicomTag.h"
+
+#include <vector>
+
+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<PrefixItem>  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);
+  };
+}
--- 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 <gtest/gtest.h>
 
 #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