changeset 4687:fcd2dc7c8f31

"Replace", "Keep" and "Remove" in "/modify" and "/anonymize" accept paths to subsequences
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 09 Jun 2021 17:24:44 +0200
parents 693f049729ba
children 177ad026d219
files NEWS OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake OrthancFramework/Sources/DicomParsing/DicomModification.cpp OrthancFramework/UnitTestsSources/JobsTests.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp
diffstat 5 files changed, 162 insertions(+), 21 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Tue Jun 08 18:28:57 2021 +0200
+++ b/NEWS	Wed Jun 09 17:24:44 2021 +0200
@@ -10,9 +10,12 @@
 REST API
 --------
 
+* API version upgraded to 13
 * ZIP archive/media generated in synchronous mode are now streamed by default
 * "Replace" tags in "/modify" and "/anonymize" now supports value representation AT
 * "/jobs/..." has new field "ErrorDetails" to help identify the cause of an error
+* "Replace", "Keep" and "Remove" in "/modify" and "/anonymize" accept paths to subsequences
+  using the syntax of the dcmodify command-line tool (wildcards are supported as well)
 
 Maintenance
 -----------
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Tue Jun 08 18:28:57 2021 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Wed Jun 09 17:24:44 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 "12")
+set(ORTHANC_API_VERSION "13")
 
 
 #####################################################################
--- a/OrthancFramework/Sources/DicomParsing/DicomModification.cpp	Tue Jun 08 18:28:57 2021 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DicomModification.cpp	Wed Jun 09 17:24:44 2021 +0200
@@ -1076,9 +1076,11 @@
       
       std::string name = query[i].asString();
 
-      DicomTag tag = FromDcmtkBridge::ParseTag(name);
+      const DicomPath path(DicomPath::Parse(name));
 
-      if (!force && IsDatabaseKey(tag))
+      if (path.GetPrefixLength() == 0 &&
+          !force &&
+          IsDatabaseKey(path.GetFinalTag()))
       {
         throw OrthancException(ErrorCode_BadRequest,
                                "Marking tag \"" + name + "\" as to be " +
@@ -1089,13 +1091,13 @@
       switch (operation)
       {
         case DicomModification::TagOperation_Keep:
-          target.Keep(tag);
-          LOG(TRACE) << "Keep: " << name << " (" << tag.Format() << ")";
+          target.Keep(path);
+          LOG(TRACE) << "Keep: " << name << " = " << path.Format();
           break;
 
         case DicomModification::TagOperation_Remove:
-          target.Remove(tag);
-          LOG(TRACE) << "Remove: " << name << " (" << tag.Format() << ")";
+          target.Remove(path);
+          LOG(TRACE) << "Remove: " << name << " = " << path.Format();
           break;
 
         default:
@@ -1120,19 +1122,21 @@
       const std::string& name = members[i];
       const Json::Value& value = replacements[name];
 
-      DicomTag tag = FromDcmtkBridge::ParseTag(name);
+      const DicomPath path(DicomPath::Parse(name));
 
-      if (!force && IsDatabaseKey(tag))
+      if (path.GetPrefixLength() == 0 &&
+          !force &&
+          IsDatabaseKey(path.GetFinalTag()))
       {
         throw OrthancException(ErrorCode_BadRequest,
                                "Marking tag \"" + name + "\" as to be replaced " +
                                "requires the \"Force\" option to be set to true");
       }
-
-      target.Replace(tag, value, false /* not safe for anonymization */);
+        
+      target.Replace(path, value, false /* not safe for anonymization */);
 
-      LOG(TRACE) << "Replace: " << name << " (" << tag.Format() 
-                 << ") == " << value.toStyledString();
+      LOG(TRACE) << "Replace: " << name << " = " << path.Format() 
+                 << " by: " << value.toStyledString();
     }
   }
 
@@ -1282,9 +1286,12 @@
   static const char* MAP_STUDIES = "MapStudies";
   static const char* MAP_SERIES = "MapSeries";
   static const char* MAP_INSTANCES = "MapInstances";
-  static const char* PRIVATE_CREATOR = "PrivateCreator";  // New in Orthanc 1.6.0
-  static const char* UIDS = "Uids";                       // New in Orthanc 1.9.4
-  static const char* REMOVED_RANGES = "RemovedRanges";    // New in Orthanc 1.9.4
+  static const char* PRIVATE_CREATOR = "PrivateCreator";    // New in Orthanc 1.6.0
+  static const char* UIDS = "Uids";                         // New in Orthanc 1.9.4
+  static const char* REMOVED_RANGES = "RemovedRanges";      // New in Orthanc 1.9.4
+  static const char* KEEP_SEQUENCES = "KeepSequences";      // New in Orthanc 1.9.4
+  static const char* REMOVE_SEQUENCES = "RemoveSequences";  // New in Orthanc 1.9.4
+  static const char* SEQUENCE_REPLACEMENTS = "SequenceReplacements";  // New in Orthanc 1.9.4
   
   void DicomModification::Serialize(Json::Value& value) const
   {
@@ -1377,6 +1384,37 @@
     }
 
     value[REMOVED_RANGES] = ranges;
+
+    // New in Orthanc 1.9.4
+    Json::Value lst = Json::arrayValue;
+    for (ListOfPaths::const_iterator it = keepSequences_.begin(); it != keepSequences_.end(); ++it)
+    {
+      assert(it->GetPrefixLength() > 0);
+      lst.append(it->Format());
+    }
+
+    value[KEEP_SEQUENCES] = lst;
+
+    // New in Orthanc 1.9.4
+    lst = Json::arrayValue;
+    for (ListOfPaths::const_iterator it = removeSequences_.begin(); it != removeSequences_.end(); ++it)
+    {
+      assert(it->GetPrefixLength() > 0);
+      lst.append(it->Format());
+    }
+
+    value[REMOVE_SEQUENCES] = lst;
+
+    // New in Orthanc 1.9.4
+    lst = Json::objectValue;
+    for (SequenceReplacements::const_iterator it = sequenceReplacements_.begin(); it != sequenceReplacements_.end(); ++it)
+    {
+      assert(*it != NULL);
+      assert((*it)->GetPath().GetPrefixLength() > 0);
+      lst[(*it)->GetPath().Format()] = (*it)->GetValue();
+    }
+
+    value[SEQUENCE_REPLACEMENTS] = lst;
   }
 
   void DicomModification::UnserializeUidMap(ResourceType level,
@@ -1511,6 +1549,76 @@
         }
       }
     }
+
+    // New in Orthanc 1.9.4
+    if (serialized.isMember(KEEP_SEQUENCES))
+    {
+      const Json::Value& keep = serialized[KEEP_SEQUENCES];
+      
+      if (keep.type() != Json::arrayValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        for (Json::Value::ArrayIndex i = 0; i < keep.size(); i++)
+        {
+          if (keep[i].type() != Json::stringValue)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat);
+          }
+          else
+          {
+            keepSequences_.push_back(DicomPath::Parse(keep[i].asString()));
+          }
+        }
+      }
+    }
+
+    // New in Orthanc 1.9.4
+    if (serialized.isMember(REMOVE_SEQUENCES))
+    {
+      const Json::Value& remove = serialized[REMOVE_SEQUENCES];
+      
+      if (remove.type() != Json::arrayValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        for (Json::Value::ArrayIndex i = 0; i < remove.size(); i++)
+        {
+          if (remove[i].type() != Json::stringValue)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat);
+          }
+          else
+          {
+            removeSequences_.push_back(DicomPath::Parse(remove[i].asString()));
+          }
+        }
+      }
+    }
+
+    // New in Orthanc 1.9.4
+    if (serialized.isMember(SEQUENCE_REPLACEMENTS))
+    {
+      const Json::Value& replace = serialized[SEQUENCE_REPLACEMENTS];
+      
+      if (replace.type() != Json::objectValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        Json::Value::Members members = replace.getMemberNames();
+        for (size_t i = 0; i < members.size(); i++)
+        {
+          sequenceReplacements_.push_back(
+            new SequenceReplacement(DicomPath::Parse(members[i]), replace[members[i]]));
+        }
+      }
+    }
   }
 
 
--- a/OrthancFramework/UnitTestsSources/JobsTests.cpp	Tue Jun 08 18:28:57 2021 +0200
+++ b/OrthancFramework/UnitTestsSources/JobsTests.cpp	Wed Jun 09 17:24:44 2021 +0200
@@ -1090,6 +1090,32 @@
 }
 
 
+TEST(JobsSerialization, DicomModification2)
+{   
+  Json::Value s;
+
+  {
+    DicomModification modification;
+    modification.SetupAnonymization(DicomVersion_2017c);
+    modification.Remove(DicomPath(DICOM_TAG_REFERENCED_IMAGE_SEQUENCE, 1, DICOM_TAG_SOP_INSTANCE_UID));
+    modification.Replace(DicomPath(DICOM_TAG_REFERENCED_IMAGE_SEQUENCE, 1, DICOM_TAG_SOP_CLASS_UID), "Hello", true);
+    modification.Keep(DicomPath(DICOM_TAG_REFERENCED_IMAGE_SEQUENCE, 1, DICOM_TAG_PATIENT_NAME));
+
+    s = 42;
+    modification.Serialize(s);
+  }
+
+  {
+    DicomModification modification(s);
+
+    // Check idempotent serialization
+    Json::Value ss;
+    modification.Serialize(ss);
+    ASSERT_EQ(s.toStyledString(), ss.toStyledString());
+  }
+}
+
+
 TEST(JobsSerialization, Registry)
 {   
   Json::Value s;
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Tue Jun 08 18:28:57 2021 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Wed Jun 09 17:24:44 2021 +0200
@@ -46,6 +46,10 @@
 #include <boost/lexical_cast.hpp>
 #include <boost/algorithm/string/predicate.hpp>
 
+#define INFO_SUBSEQUENCES \
+  "Starting with Orthanc 1.9.4, paths to subsequences can be provided using the "\
+  "same syntax as the `dcmodify` command-line tool (wildcards are supported as well)."
+
 namespace Orthanc
 {
   // Modification of DICOM instances ------------------------------------------
@@ -71,9 +75,9 @@
       .SetRequestField("RemovePrivateTags", RestApiCallDocumentation::Type_Boolean,
                        "Remove the private tags from the DICOM instances (defaults to `false`)", false)
       .SetRequestField("Replace", RestApiCallDocumentation::Type_JsonObject,
-                       "Associative array to change the value of some DICOM tags in the DICOM instances", false)
+                       "Associative array to change the value of some DICOM tags in the DICOM instances. " INFO_SUBSEQUENCES, false)
       .SetRequestField("Remove", RestApiCallDocumentation::Type_JsonListOfStrings,
-                       "List of tags that must be removed from the DICOM instances", false)
+                       "List of tags that must be removed from the DICOM instances. " INFO_SUBSEQUENCES, false)
       .SetRequestField("Keep", RestApiCallDocumentation::Type_JsonListOfStrings,
                        "Keep the original value of the specified tags, to be chosen among the `StudyInstanceUID`, "
                        "`SeriesInstanceUID` and `SOPInstanceUID` tags. Avoid this feature as much as possible, "
@@ -96,11 +100,11 @@
       .SetRequestField("KeepPrivateTags", RestApiCallDocumentation::Type_Boolean,
                        "Keep the private tags from the DICOM instances (defaults to `false`)", false)
       .SetRequestField("Replace", RestApiCallDocumentation::Type_JsonObject,
-                       "Associative array to change the value of some DICOM tags in the DICOM instances", false)
+                       "Associative array to change the value of some DICOM tags in the DICOM instances. " INFO_SUBSEQUENCES, false)
       .SetRequestField("Remove", RestApiCallDocumentation::Type_JsonListOfStrings,
-                       "List of additional tags to be removed from the DICOM instances", false)
+                       "List of additional tags to be removed from the DICOM instances. " INFO_SUBSEQUENCES, false)
       .SetRequestField("Keep", RestApiCallDocumentation::Type_JsonListOfStrings,
-                       "List of DICOM tags whose value must not be destroyed by the anonymization", false)
+                       "List of DICOM tags whose value must not be destroyed by the anonymization. " INFO_SUBSEQUENCES, false)
       .SetRequestField("PrivateCreator", RestApiCallDocumentation::Type_String,
                        "The private creator to be used for private tags in `Replace`", false);
   }