changeset 6166:dbe67b2c2d9c attach-custom-data

integration mainline->attach-custom-data
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 11 Jun 2025 16:01:06 +0200
parents 4cf3bfb29474 (current diff) ecd7fdc5f8d4 (diff)
children 86a076ceaf3a
files NEWS OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerEnumerations.h TODO
diffstat 15 files changed, 261 insertions(+), 39 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Mon Jun 09 20:59:35 2025 +0200
+++ b/NEWS	Wed Jun 11 16:01:06 2025 +0200
@@ -23,6 +23,24 @@
     The "LimitMainDicomTagsReconstructLevel" configuration is now ignored when a full processing
     is required.
 * Fix computation of MD5 hashes for memory buffers whose size is larger than 2^31 bytes.
+* Delayed Deletion plugin:
+  - Added an index in the delayed-deletion SQLite external DB to speed up delayed deletions.
+    This new index will only apply to new databases.  If you wish to speed up an existing installation,
+    run "CREATE INDEX PendingIndex ON Pending(uuid)" manually in the plugin SQLite DB.
+    Patch provided by Yurii (George) from ivtech.dev.
+    With this patch, we observed a 100 fold performance improvement when the
+    "Pending" table contains 1-2 millions files.
+* Configuration options "RejectSopClasses" and "RejectedSopClasses" are taken as synonyms.
+  In Orthanc 1.12.6 and 1.12.7, "RejectSopClasses" was used instead of the expected
+  "RejectedSopClasses" spelling.
+* Fix the re-encoding of DICOM files larger than 4GB
+
+REST API
+--------
+
+* If the index database provides the "HasExtendedFind" primitive, the "ResponseContent" option in
+  "/tools/find" now allows to specify "IsProtected" to retrieve the "IsProtected" status of a
+  patient resource.
 
 
 Version 1.12.7 (2025-04-07)
--- a/OrthancFramework/Sources/ChunkedBuffer.cpp	Mon Jun 09 20:59:35 2025 +0200
+++ b/OrthancFramework/Sources/ChunkedBuffer.cpp	Wed Jun 11 16:01:06 2025 +0200
@@ -25,6 +25,8 @@
 #include "PrecompiledHeaders.h"
 #include "ChunkedBuffer.h"
 
+#include "OrthancException.h"
+
 #include <cassert>
 #include <string.h>
 
@@ -54,7 +56,16 @@
     else
     {
       assert(chunkData != NULL);
-      chunks_.push_back(new std::string(reinterpret_cast<const char*>(chunkData), chunkSize));
+
+      try
+      {
+        chunks_.push_back(new std::string(reinterpret_cast<const char*>(chunkData), chunkSize));
+      }
+      catch (...)
+      {
+        throw OrthancException(ErrorCode_NotEnoughMemory);
+      }
+
       numBytes_ += chunkSize;
     }
   }
@@ -172,24 +183,59 @@
   void ChunkedBuffer::Flatten(std::string& result)
   {
     FlushPendingBuffer();
-    result.resize(numBytes_);
 
-    size_t pos = 0;
-    for (Chunks::iterator it = chunks_.begin(); 
-         it != chunks_.end(); ++it)
+    if (chunks_.empty())
     {
-      assert(*it != NULL);
-
-      size_t s = (*it)->size();
-      if (s != 0)
+      if (numBytes_ != 0)
       {
-        memcpy(&result[pos], (*it)->c_str(), s);
-        pos += s;
+        throw OrthancException(ErrorCode_InternalError);
       }
 
-      delete *it;
+      result.clear();
+    }
+    else if (chunks_.size() == 1)
+    {
+      // Avoid reallocating a buffer if there is a single chunk
+      assert(chunks_.front() != NULL);
+      if (chunks_.front()->size() != numBytes_)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+      else
+      {
+        chunks_.front()->swap(result);
+        delete chunks_.front();
+      }
+    }
+    else
+    {
+      try
+      {
+        result.resize(numBytes_);
+      }
+      catch (...)
+      {
+        throw OrthancException(ErrorCode_NotEnoughMemory);
+      }
+
+      size_t pos = 0;
+      for (Chunks::iterator it = chunks_.begin();
+           it != chunks_.end(); ++it)
+      {
+        assert(*it != NULL);
+
+        size_t s = (*it)->size();
+        if (s != 0)
+        {
+          memcpy(&result[pos], (*it)->c_str(), s);
+          pos += s;
+        }
+
+        delete *it;
+      }
     }
 
+    // Reset the data structure
     chunks_.clear();
     numBytes_ = 0;
   }
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Mon Jun 09 20:59:35 2025 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Wed Jun 11 16:01:06 2025 +0200
@@ -31,7 +31,6 @@
 
 #include "../Compatibility.h"
 #include "../Endianness.h"
-#include "../Logging.h"
 #include "../OrthancException.h"
 #include "../Toolbox.h"
 #include "DicomArray.h"
@@ -1220,7 +1219,7 @@
   }
 
 
-  void DicomMap::LogMissingTagsForStore() const
+  std::string DicomMap::FormatMissingTagsForStore() const
   {
     std::string patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid;
     
@@ -1244,14 +1243,14 @@
       sopInstanceUid = ValueAsString(*this, DICOM_TAG_SOP_INSTANCE_UID);
     }
 
-    LogMissingTagsForStore(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid);
+    return FormatMissingTagsForStore(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid);
   }
 
   
-  void DicomMap::LogMissingTagsForStore(const std::string& patientId,
-                                        const std::string& studyInstanceUid,
-                                        const std::string& seriesInstanceUid,
-                                        const std::string& sopInstanceUid)
+  std::string DicomMap::FormatMissingTagsForStore(const std::string& patientId,
+                                                  const std::string& studyInstanceUid,
+                                                  const std::string& seriesInstanceUid,
+                                                  const std::string& sopInstanceUid)
   {
     std::string s, t;
 
@@ -1309,11 +1308,11 @@
 
     if (t.size() == 0)
     {
-      LOG(ERROR) << "Store has failed because all the required tags (" << s << ") are missing (is it a DICOMDIR file?)";
+      return "Store has failed because all the required tags (" + s + ") are missing (is it a DICOMDIR file?)";
     }
     else
     {
-      LOG(ERROR) << "Store has failed because required tags (" << s << ") are missing for the following instance: " << t;
+      return "Store has failed because required tags (" + s + ") are missing for the following instance: " + t;
     }
   }
 
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.h	Mon Jun 09 20:59:35 2025 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.h	Wed Jun 11 16:01:06 2025 +0200
@@ -171,12 +171,12 @@
                                           const void* dicom,
                                           size_t size);
 
-    void LogMissingTagsForStore() const;
+    std::string FormatMissingTagsForStore() const;
 
-    static void LogMissingTagsForStore(const std::string& patientId,
-                                       const std::string& studyInstanceUid,
-                                       const std::string& seriesInstanceUid,
-                                       const std::string& sopInstanceUid);
+    static std::string FormatMissingTagsForStore(const std::string& patientId,
+                                                 const std::string& studyInstanceUid,
+                                                 const std::string& seriesInstanceUid,
+                                                 const std::string& sopInstanceUid);
 
     bool LookupStringValue(std::string& result,
                            const DicomTag& tag,
--- a/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp	Mon Jun 09 20:59:35 2025 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp	Wed Jun 11 16:01:06 2025 +0200
@@ -192,7 +192,7 @@
 
                   if (e.GetErrorCode() == ErrorCode_InexistentTag)
                   {
-                    FromDcmtkBridge::LogMissingTagsForStore(**imageDataSet);
+                    LOG(ERROR) << FromDcmtkBridge::FormatMissingTagsForStore(**imageDataSet);
                   }
                   else
                   {
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Mon Jun 09 20:59:35 2025 +0200
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Wed Jun 11 16:01:06 2025 +0200
@@ -38,6 +38,7 @@
 
 #include "FromDcmtkBridge.h"
 #include "ToDcmtkBridge.h"
+#include "../ChunkedBuffer.h"
 #include "../Compatibility.h"
 #include "../Logging.h"
 #include "../Toolbox.h"
@@ -167,6 +168,73 @@
 
   namespace
   {
+    class ChunkedBufferStream : public DcmOutputStream
+    {
+    private:
+      class Consumer : public DcmConsumer
+      {
+      private:
+        ChunkedBuffer  buffer_;
+
+      public:
+        void Flatten(std::string& buffer)
+        {
+          buffer_.Flatten(buffer);
+        }
+
+        OFBool good() const ORTHANC_OVERRIDE
+        {
+          return true;
+        }
+
+        OFCondition status() const ORTHANC_OVERRIDE
+        {
+          return EC_Normal;
+        }
+
+        OFBool isFlushed() const ORTHANC_OVERRIDE
+        {
+          return true;
+        }
+
+        offile_off_t avail() const ORTHANC_OVERRIDE
+        {
+          // since we cannot report "unlimited", let's claim that we can still write 10MB.
+          // Note that offile_off_t is a signed type.
+          return 10 * 1024 * 1024;
+        }
+
+        offile_off_t write(const void *buf,
+                           offile_off_t buflen) ORTHANC_OVERRIDE
+        {
+          buffer_.AddChunk(buf, buflen);
+          return buflen;
+        }
+
+        void flush() ORTHANC_OVERRIDE
+        {
+          // Nothing to flush
+        }
+      };
+
+      Consumer consumer_;
+
+    public:
+      ChunkedBufferStream() :
+        DcmOutputStream(&consumer_)
+      {
+      }
+
+      void Flatten(std::string& buffer)
+      {
+        consumer_.Flatten(buffer);
+      }
+    };
+  }
+
+
+  namespace
+  {
     class DictionaryLocker : public boost::noncopyable
     {
     private:
@@ -1572,7 +1640,12 @@
   }
 
 
-  
+#if 0
+  /**
+   * This was the implementation in Orthanc <= 1.12.7. This version
+   * uses "DcmFileFormat::calcElementLength()", which cannot handle
+   * DICOM files whose size cannot be represented on 32 bits.
+   **/
   static bool SaveToMemoryBufferInternal(std::string& buffer,
                                          DcmFileFormat& dicom,
                                          E_TransferSyntax xfer,
@@ -1619,6 +1692,46 @@
       return false;
     }
   }
+#endif
+
+
+#if 1
+  /**
+   * This is the cleaner implementation used in Orthanc >= 1.12.8,
+   * which allows to write DICOM files larger than 4GB.
+   **/
+  static bool SaveToMemoryBufferInternal(std::string& buffer,
+                                         DcmFileFormat& dicom,
+                                         E_TransferSyntax xfer,
+                                         std::string& errorMessage)
+  {
+    ChunkedBufferStream ob;
+
+    // Fill the (chunked) memory buffer with the meta-header and the dataset
+    dicom.transferInit();
+    OFCondition c = dicom.write(ob, xfer, /*opt_sequenceType*/ EET_ExplicitLength, NULL,
+                                /*opt_groupLength*/ EGL_recalcGL,
+                                /*opt_paddingType*/ EPD_noChange,
+                                /*padlen*/ 0, /*subPadlen*/ 0, /*instanceLength*/ 0,
+                                EWM_updateMeta /* creates new SOP instance UID on lossy */);
+    dicom.transferEnd();
+
+    if (c.good())
+    {
+      ob.flush();
+      ob.Flatten(buffer);
+      return true;
+    }
+    else
+    {
+      // Error
+      buffer.clear();
+      errorMessage = std::string(c.text());
+      return false;
+    }
+  }
+#endif
+
 
   bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer,
                                            DcmDataset& dataSet)
@@ -3162,7 +3275,7 @@
   }
 
 
-  void FromDcmtkBridge::LogMissingTagsForStore(DcmDataset& dicom)
+  std::string FromDcmtkBridge::FormatMissingTagsForStore(DcmDataset& dicom)
   {
     std::string patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid;
 
@@ -3194,7 +3307,7 @@
       sopInstanceUid.assign(c);
     }
     
-    DicomMap::LogMissingTagsForStore(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid);
+    return DicomMap::FormatMissingTagsForStore(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid);
   }
 
 
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h	Mon Jun 09 20:59:35 2025 +0200
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h	Wed Jun 11 16:01:06 2025 +0200
@@ -280,7 +280,7 @@
     static bool LookupOrthancTransferSyntax(DicomTransferSyntax& target,
                                             DcmDataset& dicom);
 
-    static void LogMissingTagsForStore(DcmDataset& dicom);
+    static std::string FormatMissingTagsForStore(DcmDataset& dicom);
 
     static void RemovePath(DcmDataset& dataset,
                            const DicomPath& path);
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/PendingDeletionsDatabase.cpp	Mon Jun 09 20:59:35 2025 +0200
+++ b/OrthancServer/Plugins/Samples/DelayedDeletion/PendingDeletionsDatabase.cpp	Wed Jun 11 16:01:06 2025 +0200
@@ -42,6 +42,12 @@
     if (!db_.DoesTableExist("Pending"))
     {
       db_.Execute("CREATE TABLE Pending(uuid TEXT, type INTEGER)");
+      
+      // New in v 1.12.7+
+      // add an index on uuid to speed up the DELETE FROM Pending WHERE uuid=?
+      // With this patch, we observed a 100 fold performance
+      // improvement when the Pending table contains 1-2 millions files.
+      db_.Execute("CREATE INDEX PendingIndex ON Pending(uuid)");
     }
     
     t.Commit();
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Mon Jun 09 20:59:35 2025 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Wed Jun 11 16:01:06 2025 +0200
@@ -705,8 +705,8 @@
     {
       call.GetDocumentation().SetHttpGetArgument(GET_RESPONSE_CONTENT, RestApiCallDocumentation::Type_String,
                             "Defines the content of response for each returned resource.  Allowed values are `MainDicomTags`, "
-                            "`Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `Attachments`.  If not specified, Orthanc "
-                            "will return `MainDicomTags`, `Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`."
+                            "`Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `IsProtected`, `Attachments`.  If not specified, Orthanc "
+                            "will return `MainDicomTags`, `Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `IsProtected`."
                             "e.g: '" + GET_RESPONSE_CONTENT + "=MainDicomTags;Children "
                             "(new in Orthanc 1.12.5 - overrides `expand`)", false);
 
@@ -719,8 +719,8 @@
       call.GetDocumentation().SetRequestField(POST_RESPONSE_CONTENT, RestApiCallDocumentation::Type_JsonListOfStrings,
                             "Defines the content of response for each returned resource. (this field, if present, overrides the \"Expand\" field).  "
                             "Allowed values are `MainDicomTags`, "
-                            "`Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `Attachments`.  If not specified, Orthanc "
-                            "will return `MainDicomTags`, `Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`."
+                            "`Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `IsProtected`, `Attachments`.  If not specified, Orthanc "
+                            "will return `MainDicomTags`, `Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `IsProtected`."
                             "(new in Orthanc 1.12.5)", false);
 
       call.GetDocumentation().SetRequestField(POST_EXPAND, RestApiCallDocumentation::Type_Boolean,
--- a/OrthancServer/Sources/ResourceFinder.cpp	Mon Jun 09 20:59:35 2025 +0200
+++ b/OrthancServer/Sources/ResourceFinder.cpp	Wed Jun 11 16:01:06 2025 +0200
@@ -394,6 +394,14 @@
       }
     }
 
+    if (responseContent_ & ResponseContentFlags_IsProtected)
+    {
+      if (resource.GetLevel() == ResourceType_Patient )
+      {
+        target["IsProtected"] = index.IsProtectedPatient(resource.GetIdentifier());
+      }
+    }
+
     if (responseContent_ & ResponseContentFlags_MainDicomTags)
     {
       DicomMap allMainDicomTags;
--- a/OrthancServer/Sources/ResourceFinder.h	Mon Jun 09 20:59:35 2025 +0200
+++ b/OrthancServer/Sources/ResourceFinder.h	Wed Jun 11 16:01:06 2025 +0200
@@ -178,7 +178,7 @@
       request_.SetRetrieveAttachments(retrieve);
     }
 
-    // NB: "index" is only used in this method to fill the "IsStable" information
+    // NB: "index" is used in this method to fill the "IsStable" and "IsProtected" information
     void Expand(Json::Value& target,
                 const FindResponse::Resource& resource,
                 ServerIndex& index,
--- a/OrthancServer/Sources/ServerContext.cpp	Mon Jun 09 20:59:35 2025 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Wed Jun 11 16:01:06 2025 +0200
@@ -495,7 +495,27 @@
         std::list<std::string> acceptedSopClasses;
         std::set<std::string> rejectedSopClasses;
         lock.GetConfiguration().GetListOfStringsParameter(acceptedSopClasses, "AcceptedSopClasses");
-        lock.GetConfiguration().GetSetOfStringsParameter(rejectedSopClasses, "RejectSopClasses");
+
+        static const char* const REJECTED_SOP_CLASS = "RejectedSopClasses";
+        if (lock.GetJson().isMember(REJECTED_SOP_CLASS))
+        {
+          lock.GetConfiguration().GetSetOfStringsParameter(rejectedSopClasses, REJECTED_SOP_CLASS);
+        }
+        else
+        {
+          static const char* const REJECT_SOP_CLASS = "RejectSopClasses";
+          if (lock.GetJson().isMember(REJECT_SOP_CLASS))
+          {
+            /**
+             * This is for backward compatibility. In Orthanc 1.12.6,
+             * there was a typo: "RejectedSopClasses" was spelled as
+             * "RejectSopClasses".
+             * https://discourse.orthanc-server.org/t/fix-for-config-param-rejectsopclasses-vs-rejectedsopclasses/5900
+             **/
+            lock.GetConfiguration().GetSetOfStringsParameter(rejectedSopClasses, REJECT_SOP_CLASS);
+          }
+        }
+
         SetAcceptedSopClasses(acceptedSopClasses, rejectedSopClasses);
 
         defaultDicomRetrieveMethod_ = StringToRetrieveMethod(lock.GetConfiguration().GetStringParameter("DicomDefaultRetrieveMethod", "C-MOVE"));
@@ -874,7 +894,7 @@
     {
       if (e.GetErrorCode() == ErrorCode_InexistentTag)
       {
-        summary.LogMissingTagsForStore();
+        LOG(ERROR) << summary.FormatMissingTagsForStore();
       }
       
       throw;
--- a/OrthancServer/Sources/ServerEnumerations.cpp	Mon Jun 09 20:59:35 2025 +0200
+++ b/OrthancServer/Sources/ServerEnumerations.cpp	Wed Jun 11 16:01:06 2025 +0200
@@ -655,6 +655,10 @@
     {
       return ResponseContentFlags_IsStable;
     }
+    else if (value == "IsProtected")
+    {
+      return ResponseContentFlags_IsProtected;
+    }
     else
     {
       throw OrthancException(ErrorCode_ParameterOutOfRange,
--- a/OrthancServer/Sources/ServerEnumerations.h	Mon Jun 09 20:59:35 2025 +0200
+++ b/OrthancServer/Sources/ServerEnumerations.h	Wed Jun 11 16:01:06 2025 +0200
@@ -136,6 +136,7 @@
     ResponseContentFlags_Children             = (1 << 10),
     ResponseContentFlags_Labels               = (1 << 11),
     ResponseContentFlags_IsStable             = (1 << 12),
+    ResponseContentFlags_IsProtected          = (1 << 13),
 
     ResponseContentFlags_INTERNAL_CountResources = (1 << 30),
     
@@ -150,7 +151,8 @@
                                         ResponseContentFlags_Parent | 
                                         ResponseContentFlags_Children | 
                                         ResponseContentFlags_Labels |
-                                        ResponseContentFlags_IsStable),  // equivalent to "Expand": true
+                                        ResponseContentFlags_IsStable |
+                                        ResponseContentFlags_IsProtected),  // equivalent to "Expand": true
     
     ResponseContentFlags_Default = (ResponseContentFlags_ID |
                                     ResponseContentFlags_Type |
--- a/TODO	Mon Jun 09 20:59:35 2025 +0200
+++ b/TODO	Wed Jun 11 16:01:06 2025 +0200
@@ -66,6 +66,12 @@
 * Allow saving PrivateTags in ExtraMainDicomTags.
   Note: they can actually be stored but they then appear as "Unknown Tag & Data" in the responses.
   If we try to add the PrivateCreator in the ExtraMainDicomTags, then, the DICOMWeb plugin fails to initialize because the private tags are not known.
+* Support hashed passwords in RegisteredUsers.  E.g:
+  "RegisteredUsers": { 
+    "admin": {
+      "Password": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", 
+      "Hashing": "sha1"} }
+  
 
 ============================
 Documentation (Orthanc Book)