[med-svn] [Git][med-team/orthanc-dicomweb][upstream] New upstream version 1.21+dfsg

Sebastien Jodogne (@jodogne-guest) gitlab at salsa.debian.org
Tue Aug 19 14:30:25 BST 2025



Sebastien Jodogne pushed to branch upstream at Debian Med / orthanc-dicomweb


Commits:
fdf3ede8 by jodogne-guest at 2025-08-19T15:29:53+02:00
New upstream version 1.21+dfsg

- - - - -


13 changed files:

- .hg_archival.txt
- CMakeLists.txt
- NEWS
- Plugin/Configuration.cpp
- Plugin/Configuration.h
- Plugin/DicomWebClient.cpp
- Plugin/Plugin.cpp
- Plugin/WadoRs.cpp
- Plugin/WadoRsRetrieveRendered.cpp
- Resources/Orthanc/CMake/DownloadOrthancFramework.cmake
- Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp
- Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h
- TODO


Changes:

=====================================
.hg_archival.txt
=====================================
@@ -1,6 +1,6 @@
 repo: d5f45924411123cfd02d035fd50b8e37536eadef
-node: 10c61ebe30b04c5716c8d333d925c2f1bc5157ac
-branch: OrthancDicomWeb-1.20
+node: 00cae2648bcb72d3ff12e6244ca63a2bc467b935
+branch: OrthancDicomWeb-1.21
 latesttag: null
-latesttagdistance: 625
-changessincelatesttag: 674
+latesttagdistance: 635
+changessincelatesttag: 685


=====================================
CMakeLists.txt
=====================================
@@ -23,7 +23,7 @@ cmake_minimum_required(VERSION 2.8)
 
 project(OrthancDicomWeb)
 
-set(ORTHANC_DICOM_WEB_VERSION "1.20")
+set(ORTHANC_DICOM_WEB_VERSION "1.21")
 
 if (ORTHANC_DICOM_WEB_VERSION STREQUAL "mainline")
   set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "mainline")


=====================================
NEWS
=====================================
@@ -1,3 +1,17 @@
+Version 1.21 (2025-08-14)
+=========================
+
+* New configuration "WadoRsLoaderThreadsCount" to configure how many threads are loading
+  files from the storage when answering to a WADO-RS query.  A value > 1 is meaningful 
+  only if the storage is a distributed network storage (e.g object storage plugin).
+  A value of 0 means reading and writing are performed in sequence (default behaviour).
+* New configuration "EnablePerformanceLogs" to display performance logs.  Currently
+  only showing the time required to execute a WADO-RS query.  For example:
+  WADO-RS: elapsed: 26106623 us, rate: 14.86 instances/s, 155.23Mbps
+* Fix false errors logs generated e.g when OHIF requests the /dicom-web/studies/../metadata route:
+  "dicom-web:/Configuration.cpp:643] Unsupported return MIME type: application/dicom+json, multipart/related; type=application/octet-stream; transfer-syntax=*, will return DICOM+JSON"
+
+
 Version 1.20 (2025-05-12)
 =========================
 


=====================================
Plugin/Configuration.cpp
=====================================
@@ -626,21 +626,21 @@ namespace OrthancPlugins
       std::string accept;
       Orthanc::Toolbox::ToLowerCase(accept, acceptHeader);
   
-      if (accept == "application/dicom+json" ||
-          accept == "application/json" ||
-          accept == "*/*")
+      if (accept.find("application/dicom+json") != std::string::npos ||
+          accept.find("application/json") != std::string::npos ||
+          accept.find("*/*")  != std::string::npos)
       {
         return false;
       }
-      else if (accept == "application/dicom+xml" ||
-               accept == "application/xml" ||
-               accept == "text/xml")
+      else if (accept.find("application/dicom+xml") != std::string::npos ||
+               accept.find("application/xml") != std::string::npos ||
+               accept.find("text/xml")  != std::string::npos)
       {
         return true;
       }
       else
       {
-        LOG(ERROR) << "Unsupported return MIME type: " << accept <<
+        LOG(WARNING) << "Unsupported return MIME type: " << accept <<
                       ", will return DICOM+JSON";
         return false;
       }
@@ -691,6 +691,16 @@ namespace OrthancPlugins
       return globalConfiguration_->GetBooleanValue("ReadOnly", false);
     }
     
+    unsigned int GetWadoRsLoaderThreadsCount()
+    {
+      return GetUnsignedIntegerValue("WadoRsLoaderThreadsCount", 0);
+    }
+
+    bool IsPerformanceLogsEnabled()
+    {
+      return GetBooleanValue("EnablePerformanceLogs", false);
+    }
+
     MetadataMode GetMetadataMode(Orthanc::ResourceType level)
     {
       static const std::string FULL = "Full";


=====================================
Plugin/Configuration.h
=====================================
@@ -141,8 +141,12 @@ namespace OrthancPlugins
 
     unsigned int GetMetadataWorkerThreadsCount();
 
+    unsigned int GetWadoRsLoaderThreadsCount();
+
     bool IsMetadataCacheEnabled();
 
     bool IsReadOnly();
+
+    bool IsPerformanceLogsEnabled();
   }
 }


=====================================
Plugin/DicomWebClient.cpp
=====================================
@@ -386,21 +386,6 @@ static void SubmitJob(OrthancPluginRestOutput* output,
 }
 
 
-static void AddInstance(std::list<std::string>& target,
-                        const Json::Value& instance)
-{
-  std::string id;
-  if (OrthancPlugins::LookupStringValue(id, instance, "ID"))
-  {
-    target.push_back(id);
-  }
-  else
-  {
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-  }
-}
-
-
 static bool GetSequenceSize(size_t& result,
                             const Json::Value& answer,
                             const std::string& tag,
@@ -556,7 +541,6 @@ static void ParseStowRequest(std::list<std::string>& instances /* out */,
     Json::Value tmpInstances;
     if (OrthancPlugins::RestApiGet(tmpResource, "/instances/" + resource, false))
     {
-      // AddInstance(instances, tmpResource);
       instances.push_back(resource);
       AddResourceForJobContent(resourcesForJobContent, Orthanc::ResourceType_Instance, resource);
     }
@@ -577,7 +561,6 @@ static void ParseStowRequest(std::list<std::string>& instances /* out */,
 
       for (Json::Value::ArrayIndex j = 0; j < tmpInstances.size(); j++)
       {
-        // AddInstance(instances, tmpInstances[j]);
         instances.push_back(tmpInstances[j].asString());
         AddResourceForJobContent(resourcesForJobContent, Orthanc::ResourceType_Instance, tmpInstances[j].asString());
       }


=====================================
Plugin/Plugin.cpp
=====================================
@@ -746,6 +746,8 @@ extern "C"
 
         std::string publicUrlRoot = OrthancPlugins::Configuration::GetPublicRoot();
         LOG(WARNING) << "DICOMweb public root: " << publicUrlRoot;
+
+        LOG(WARNING) << "The DICOMWeb plugin will use " << (OrthancPlugins::Configuration::GetWadoRsLoaderThreadsCount() == 0 ? 1 : OrthancPlugins::Configuration::GetWadoRsLoaderThreadsCount()) << " threads to load DICOM files for WADO-RS queries";
       }
       else
       {


=====================================
Plugin/WadoRs.cpp
=====================================
@@ -32,6 +32,7 @@
 #include <Toolbox.h>
 #include <SerializationToolbox.h>
 #include <MultiThreading/SharedMessageQueue.h>
+#include <MultiThreading/Semaphore.h>
 #include <Compression/GzipCompressor.h>
 
 #include <memory>
@@ -359,6 +360,234 @@ static void AcceptBulkData(const OrthancPluginHttpRequest* request)
   }
 }
 
+class InstanceToPreload : public Orthanc::IDynamicObject
+{
+private:
+  std::string instanceId_;
+  bool transcode_;
+
+public:
+  explicit InstanceToPreload(const std::string& instanceId, bool transcode) : 
+    instanceId_(instanceId),
+    transcode_(transcode)
+  {
+  }
+
+  virtual ~InstanceToPreload() ORTHANC_OVERRIDE
+  {
+  }
+
+  const std::string& GetInstanceId() const {return instanceId_;}
+
+  bool NeedsTranscoding() const {return transcode_;}
+};
+
+
+class LoadedInstance : public Orthanc::IDynamicObject
+{
+private:
+  std::unique_ptr<OrthancPlugins::DicomInstance> dicom_;
+
+public:
+  explicit LoadedInstance(OrthancPlugins::DicomInstance* dicom) : 
+    dicom_(dicom)
+  {
+  }
+
+  virtual ~LoadedInstance() ORTHANC_OVERRIDE
+  {
+  }
+
+  OrthancPlugins::DicomInstance* ReleaseInstance() 
+  {
+    return dicom_.release();
+  }
+};
+
+
+class InstanceLoader : public boost::noncopyable
+{
+protected:
+  bool                                  transcode_;
+  Orthanc::DicomTransferSyntax          targetTransferSyntax_;
+
+  OrthancPlugins::DicomInstance* GetAndTranscodeDicom(InstanceToPreload* instanceToLoad)
+  {
+    std::unique_ptr<OrthancPlugins::DicomInstance> dicom(OrthancPlugins::DicomInstance::Load(instanceToLoad->GetInstanceId(),
+                                                                                             OrthancPluginLoadDicomInstanceMode_WholeDicom));
+    if (transcode_ && instanceToLoad->NeedsTranscoding())
+    {
+      dicom.reset(dicom->Transcode(dicom->GetBuffer(),
+                                    dicom->GetSize(),
+                                    Orthanc::GetTransferSyntaxUid(targetTransferSyntax_)));
+    }
+
+    return dicom.release();
+  }
+
+public:
+  explicit InstanceLoader(bool transcode, Orthanc::DicomTransferSyntax targetTransferSyntax)
+  : transcode_(transcode),
+    targetTransferSyntax_(targetTransferSyntax)
+  {
+  }
+
+  virtual ~InstanceLoader()
+  {
+  }
+
+  virtual void PrepareDicom(const std::string& instanceId, bool transcode) = 0;
+
+  virtual OrthancPlugins::DicomInstance* GetNextDicom() = 0;
+
+};
+
+
+class ThreadedInstanceLoader : public InstanceLoader
+{
+  std::vector<boost::thread*>         threads_;
+
+  Orthanc::SharedMessageQueue         instancesToPreload_;
+
+  Orthanc::SharedMessageQueue         loadedInstances_;
+
+  Orthanc::Semaphore                  bufferSemaphore_;
+
+public:
+  ThreadedInstanceLoader(size_t threadCount, bool transcode, Orthanc::DicomTransferSyntax transferSyntax)
+  : InstanceLoader(transcode, transferSyntax),
+    instancesToPreload_(0),
+    loadedInstances_(0),
+    bufferSemaphore_(3*threadCount) // to limit the number of loaded instances in memory
+  {
+    for (size_t i = 0; i < threadCount; i++)
+    {
+      threads_.push_back(new boost::thread(PreloaderWorkerThread, this));
+    }
+  }
+
+  virtual ~ThreadedInstanceLoader()
+  {
+    Clear();
+  }
+
+  void Clear()
+  {
+    for (size_t i = 0; i < threads_.size(); i++)
+    {
+      instancesToPreload_.Enqueue(NULL);
+    }
+
+    for (size_t i = 0; i < threads_.size(); i++)
+    {
+      if (threads_[i]->joinable())
+      {
+        threads_[i]->join();
+      }
+      delete threads_[i];
+    }
+
+    threads_.clear();
+  }
+
+  static void PreloaderWorkerThread(ThreadedInstanceLoader* that)
+  {
+    static uint16_t threadCounter = 0;
+    Orthanc::Logging::SetCurrentThreadName(std::string("WADO-LOAD-") + boost::lexical_cast<std::string>(threadCounter++));
+
+    while (true)
+    {
+      std::unique_ptr<InstanceToPreload> instanceToPreload(dynamic_cast<InstanceToPreload*>(that->instancesToPreload_.Dequeue(0)));
+      if (instanceToPreload.get() == NULL)  // that's the signal to exit the thread
+      {
+        return;
+      }
+      
+      // wait for the consumers, no need to accumulate instances in memory if loaders are faster than writers
+      that->bufferSemaphore_.Acquire();
+
+      try
+      {
+        std::unique_ptr<OrthancPlugins::DicomInstance> dicom(that->GetAndTranscodeDicom(instanceToPreload.get()));
+        that->loadedInstances_.Enqueue(new LoadedInstance(dicom.release()));
+      }
+      catch (Orthanc::OrthancException& e)
+      {
+        LOG(ERROR) << "Error while loading instances " << e.GetDetails();
+        that->loadedInstances_.Enqueue(NULL);
+      }
+      catch (...)
+      {
+        LOG(ERROR) << "Unknown error while loading instances ";
+        that->loadedInstances_.Enqueue(NULL);
+      }
+
+    }
+  }
+
+  virtual void PrepareDicom(const std::string& instanceId, bool transcode) ORTHANC_OVERRIDE
+  {
+    instancesToPreload_.Enqueue(new InstanceToPreload(instanceId, transcode));
+  }
+
+  virtual OrthancPlugins::DicomInstance* GetNextDicom() ORTHANC_OVERRIDE
+  {
+    std::unique_ptr<LoadedInstance> loadedInstance(dynamic_cast<LoadedInstance*>(loadedInstances_.Dequeue(0)));
+    
+    // unlock preloader threads to buffer the following instances
+    bufferSemaphore_.Release();
+
+    if (loadedInstance.get() != NULL)
+    {
+      return loadedInstance->ReleaseInstance();
+    }
+    else
+    {
+      return NULL;
+    }
+  }
+};
+
+
+class SynchronousInstanceLoader : public InstanceLoader
+{
+  std::list<boost::shared_ptr<InstanceToPreload> >         instancesToLoad_;
+
+public:
+  SynchronousInstanceLoader(bool transcode, Orthanc::DicomTransferSyntax transferSyntax)
+  : InstanceLoader(transcode, transferSyntax)
+  {
+  }
+
+  virtual ~SynchronousInstanceLoader()
+  {
+    Clear();
+  }
+
+  void Clear()
+  {
+    instancesToLoad_.clear();
+  }
+
+  virtual void PrepareDicom(const std::string& instanceId, bool transcode) ORTHANC_OVERRIDE
+  {
+    instancesToLoad_.push_back(boost::shared_ptr<InstanceToPreload>(new InstanceToPreload(instanceId, transcode)));
+  }
+
+  virtual OrthancPlugins::DicomInstance* GetNextDicom() ORTHANC_OVERRIDE
+  {
+    boost::shared_ptr<InstanceToPreload> instanceToLoad(instancesToLoad_.front());
+    instancesToLoad_.pop_front();
+
+    if (instanceToLoad.get() == NULL)
+    {
+      return NULL;
+    }
+
+    return GetAndTranscodeDicom(instanceToLoad.get());
+  }
+};
+
 
 static void AnswerListOfDicomInstances(OrthancPluginRestOutput* output,
                                        Orthanc::ResourceType level,
@@ -366,6 +595,10 @@ static void AnswerListOfDicomInstances(OrthancPluginRestOutput* output,
                                        bool transcode,
                                        Orthanc::DicomTransferSyntax targetSyntax /* only if transcoding */)
 {
+  Orthanc::Toolbox::ElapsedTimer perfTimer;
+  size_t perfTotalSizeInBytes = 0;
+  size_t perfTotalInstancesCount = 0;
+
   if (level != Orthanc::ResourceType_Study &&
       level != Orthanc::ResourceType_Series &&
       level != Orthanc::ResourceType_Instance)
@@ -381,17 +614,76 @@ static void AnswerListOfDicomInstances(OrthancPluginRestOutput* output,
   {
     Json::Value tmp = Json::objectValue;
     tmp["ID"] = publicId;
-    
+
+    if (transcode)
+    {
+      std::string sourceTransferSyntax;
+      if (OrthancPlugins::RestApiGetString(sourceTransferSyntax, "/instances/" + publicId + "/metadata/TransferSyntax", false))
+      {
+        tmp["Metadata"] = Json::objectValue;
+        tmp["Metadata"]["TransferSyntax"] = sourceTransferSyntax;
+      }
+    }
+
     instances = Json::arrayValue;
     instances.append(tmp);
   }
   else
   {
-    if (!OrthancPlugins::RestApiGet(instances, GetResourceUri(level, publicId) + "/instances", false))
+    if (CanUseExtendedFind())
     {
-      // Internal error
-      OrthancPluginSendHttpStatusCode(context, output, 400);
-      return;
+      Json::Value toolsFindPayload;
+
+      toolsFindPayload["Query"] = Json::objectValue;
+      toolsFindPayload["ResponseContent"] = Json::arrayValue;
+      toolsFindPayload["Level"] = "Instance";
+      
+      if (transcode)
+      {
+        toolsFindPayload["ResponseContent"].append("Metadata");
+      }
+      
+      if (level == Orthanc::ResourceType_Patient)
+      {
+        toolsFindPayload["ParentPatient"] = publicId;
+      } 
+      else if (level == Orthanc::ResourceType_Study)
+      {
+        toolsFindPayload["ParentStudy"] = publicId;
+      } 
+      else if (level == Orthanc::ResourceType_Series)
+      {
+        toolsFindPayload["ParentSeries"] = publicId;
+      }
+
+      if (!OrthancPlugins::RestApiPost(instances, "/tools/find", toolsFindPayload, false))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource, "Unable to list instances");
+      }
+    }
+    else
+    {
+      if (!OrthancPlugins::RestApiGet(instances, GetResourceUri(level, publicId) + "/instances", false))
+      {
+        // Internal error
+        OrthancPluginSendHttpStatusCode(context, output, 400);
+        return;
+      }
+
+      if (transcode)
+      {
+        for (Json::Value::ArrayIndex i = 0; i < instances.size(); i++)
+        {
+          const std::string uri = "/instances/" + instances[i]["ID"].asString();
+
+          std::string sourceTransferSyntax;
+          if (OrthancPlugins::RestApiGetString(sourceTransferSyntax, uri + "/metadata/TransferSyntax", false))
+          {
+            instances[i]["Metadata"] = Json::objectValue;
+            instances[i]["Metadata"]["TransferSyntax"] = sourceTransferSyntax;
+          }
+        }
+      }
     }
   }
 
@@ -400,60 +692,68 @@ static void AnswerListOfDicomInstances(OrthancPluginRestOutput* output,
     throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
   }
 
+  // single threaded or multi threaded loading ?
+  std::unique_ptr<InstanceLoader> loader;
+  
+  const unsigned int workersCount = OrthancPlugins::Configuration::GetWadoRsLoaderThreadsCount();
+
+  if (workersCount > 0 && level != Orthanc::ResourceType_Instance)
+  {
+    loader.reset(new ThreadedInstanceLoader(workersCount, transcode, targetSyntax));
+  }
+  else
+  {
+    loader.reset(new SynchronousInstanceLoader(transcode, targetSyntax));
+  }
+  
   for (Json::Value::ArrayIndex i = 0; i < instances.size(); i++)
   {
-    const std::string uri = "/instances/" + instances[i]["ID"].asString();
-
     bool transcodeThisInstance;
-    
-    std::string sourceTransferSyntax;
-    if (!transcode)
+    Orthanc::DicomTransferSyntax currentSyntax;
+
+    if (!transcode || !instances[i].isMember("Metadata") || !instances[i]["Metadata"].isMember("TransferSyntax"))
     {
       transcodeThisInstance = false;      
     }
-    else if (OrthancPlugins::RestApiGetString(sourceTransferSyntax, uri + "/metadata/TransferSyntax", false))
+    else if (Orthanc::LookupTransferSyntax(currentSyntax, instances[i]["Metadata"]["TransferSyntax"].asString()))
     {
-      // Avoid transcoding if the source file already uses the expected transfer syntax
-      Orthanc::DicomTransferSyntax syntax;
-      if (Orthanc::LookupTransferSyntax(syntax, sourceTransferSyntax))
-      {
-        transcodeThisInstance = (syntax != targetSyntax);
-      }
-      else
-      {
-        transcodeThisInstance = true;
-      }
+      transcodeThisInstance = (currentSyntax != targetSyntax);
     }
     else
     {
-      // The transfer syntax of the source file is unknown, transcode it to be sure
       transcodeThisInstance = true;
     }
-    
-    OrthancPlugins::MemoryBuffer dicom;
-    if (dicom.RestApiGet(uri + "/file", false))
-    {
-      if (transcodeThisInstance)
-      {
-        std::unique_ptr<OrthancPlugins::DicomInstance> transcoded(
-          OrthancPlugins::DicomInstance::Transcode(
-            dicom.GetData(), dicom.GetSize(), Orthanc::GetTransferSyntaxUid(targetSyntax)));
 
-        if (OrthancPluginSendMultipartItem(
-              context, output, reinterpret_cast<const char*>(transcoded->GetBuffer()),
-              transcoded->GetSize()) != 0)
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-        }
-      }
-      else
+    loader->PrepareDicom(instances[i]["ID"].asString(), transcodeThisInstance);
+  }
+
+  perfTotalInstancesCount = instances.size();
+
+  for (Json::Value::ArrayIndex i = 0; i < instances.size(); i++)
+  {
+    std::unique_ptr<OrthancPlugins::DicomInstance> dicom(loader->GetNextDicom());
+
+    if (dicom.get() != NULL)
+    {
+      if (OrthancPluginSendMultipartItem(
+          context, output, reinterpret_cast<const char*>(dicom->GetBuffer()),
+          dicom->GetSize()) != 0)
       {
-        if (OrthancPluginSendMultipartItem(context, output, dicom.GetData(), dicom.GetSize()) != 0)
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-        }
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
       }
+      perfTotalSizeInBytes += dicom->GetSize();
     }
+    else
+    {
+      LOG(WARNING) << "Failed to load an instance";
+    }
+  }
+
+  if (OrthancPlugins::Configuration::IsPerformanceLogsEnabled())
+  {
+    uint64_t elapsedMicrosends = perfTimer.GetElapsedMicroseconds();
+    float instancesPerSeconds = float(perfTotalInstancesCount) / (float(elapsedMicrosends) / 1000000.0f);
+    LOG(INFO) << "WADO-RS: elapsed: " << perfTimer.GetElapsedMicroseconds() << " us, rate: " << std::fixed << std::setprecision(2) << instancesPerSeconds << " instances/s, " << Orthanc::Toolbox::GetHumanTransferSpeed(false, perfTotalSizeInBytes, elapsedMicrosends * 1000);
   }
 }
 


=====================================
Plugin/WadoRsRetrieveRendered.cpp
=====================================
@@ -849,7 +849,7 @@ static void AnswerFrameRendered(OrthancPluginRestOutput* output,
         apiClient.SetPath(std::string("/instances/") + instanceId + "/frames/0/raw");
         if (apiClient.Execute())
         {
-          apiClient.Forward(OrthancPlugins::GetGlobalContext(), output);
+          apiClient.ExecuteAndForwardAnswer(OrthancPlugins::GetGlobalContext(), output);
           return;
         }
       }


=====================================
Resources/Orthanc/CMake/DownloadOrthancFramework.cmake
=====================================
@@ -171,6 +171,10 @@ if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg" OR
         set(ORTHANC_FRAMEWORK_MD5 "0e971f32f4f3e4951e0f3b5de49a3da6")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.7")
         set(ORTHANC_FRAMEWORK_MD5 "f27c27d7a7a694dab1fd7f0a99d9715a")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.8")
+        set(ORTHANC_FRAMEWORK_MD5 "eb1c719234338e8277b80d3453563e9f")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.9")
+        set(ORTHANC_FRAMEWORK_MD5 "66b5a2ee60706c4a502896083b9e1a01")
 
       # Below this point are development snapshots that were used to
       # release some plugin, before an official release of the Orthanc
@@ -515,7 +519,6 @@ if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "system")
   include(${CMAKE_CURRENT_LIST_DIR}/Compiler.cmake)
   include(${CMAKE_CURRENT_LIST_DIR}/DownloadPackage.cmake)
   include(${CMAKE_CURRENT_LIST_DIR}/AutoGeneratedCode.cmake)
-  set(EMBED_RESOURCES_PYTHON ${CMAKE_CURRENT_LIST_DIR}/EmbedResources.py)
 
   if (ORTHANC_FRAMEWORK_USE_SHARED)
     list(GET CMAKE_FIND_LIBRARY_PREFIXES 0 Prefix)


=====================================
Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp
=====================================
@@ -220,37 +220,60 @@ namespace OrthancPlugins
   }
 
 
+  void MemoryBuffer::Clear()
+  {
+    if (buffer_.data != NULL)
+    {
+      OrthancPluginFreeMemoryBuffer(GetGlobalContext(), &buffer_);
+      buffer_.data = NULL;
+      buffer_.size = 0;
+    }
+  }
+
+
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
-  MemoryBuffer::MemoryBuffer(const void* buffer,
-                             size_t size)
+  void MemoryBuffer::Assign(const void* buffer,
+                            size_t size)
   {
     uint32_t s = static_cast<uint32_t>(size);
     if (static_cast<size_t>(s) != size)
     {
       ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory);
     }
-    else if (OrthancPluginCreateMemoryBuffer(GetGlobalContext(), &buffer_, s) !=
-             OrthancPluginErrorCode_Success)
+
+    Clear();
+
+    if (OrthancPluginCreateMemoryBuffer(GetGlobalContext(), &buffer_, s) !=
+        OrthancPluginErrorCode_Success)
     {
       ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory);
     }
     else
     {
-      memcpy(buffer_.data, buffer, size);
+      if (size > 0)
+      {
+        memcpy(buffer_.data, buffer, size);
+      }
     }
   }
 #endif
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+  void MemoryBuffer::Assign(const std::string& s)
+  {
+    Assign(s.empty() ? NULL : s.c_str(), s.size());
+  }
+#endif
+
 
-  void MemoryBuffer::Clear()
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+  void MemoryBuffer::AssignJson(const Json::Value& value)
   {
-    if (buffer_.data != NULL)
-    {
-      OrthancPluginFreeMemoryBuffer(GetGlobalContext(), &buffer_);
-      buffer_.data = NULL;
-      buffer_.size = 0;
-    }
+    std::string s;
+    WriteFastJson(s, value);
+    Assign(s);
   }
+#endif
 
 
   void MemoryBuffer::Assign(OrthancPluginMemoryBuffer& other)
@@ -673,7 +696,7 @@ namespace OrthancPlugins
   {
     OrthancString str;
     str.Assign(OrthancPluginDicomBufferToJson
-               (GetGlobalContext(), GetData(), GetSize(), format, flags, maxStringLength));
+               (GetGlobalContext(), reinterpret_cast<const char*>(GetData()), GetSize(), format, flags, maxStringLength));
     str.ToJson(target);
   }
 
@@ -1566,7 +1589,7 @@ namespace OrthancPlugins
     {
       if (!answer.IsEmpty())
       {
-        result.assign(answer.GetData(), answer.GetSize());
+        result.assign(reinterpret_cast<const char*>(answer.GetData()), answer.GetSize());
       }
       return true;
     }
@@ -2052,6 +2075,26 @@ namespace OrthancPlugins
             DoPost(target, index, uri, body, headers));
   }
 
+  bool OrthancPeers::DoPost(Json::Value& target,
+                            size_t index,
+                            const std::string& uri,
+                            const std::string& body,
+                            const HttpHeaders& headers, 
+                            unsigned int timeout) const
+  {
+    MemoryBuffer buffer;
+
+    if (DoPost(buffer, index, uri, body, headers, timeout))
+    {
+      buffer.ToJson(target);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
 
   bool OrthancPeers::DoPost(Json::Value& target,
                             size_t index,
@@ -2098,6 +2141,17 @@ namespace OrthancPlugins
                             const std::string& uri,
                             const std::string& body,
                             const HttpHeaders& headers) const
+  {
+    return DoPost(target, index, uri, body, headers, timeout_);
+  }
+
+
+  bool OrthancPeers::DoPost(MemoryBuffer& target,
+                            size_t index,
+                            const std::string& uri,
+                            const std::string& body,
+                            const HttpHeaders& headers,
+                            unsigned int timeout) const
   {
     if (index >= index_.size())
     {
@@ -2117,7 +2171,7 @@ namespace OrthancPlugins
     OrthancPluginErrorCode code = OrthancPluginCallPeerApi
       (GetGlobalContext(), *answer, NULL, &status, peers_,
        static_cast<uint32_t>(index), OrthancPluginHttpMethod_Post, uri.c_str(),
-       pluginHeaders.GetSize(), pluginHeaders.GetKeys(), pluginHeaders.GetValues(), body.empty() ? NULL : body.c_str(), body.size(), timeout_);
+       pluginHeaders.GetSize(), pluginHeaders.GetKeys(), pluginHeaders.GetValues(), body.empty() ? NULL : body.c_str(), body.size(), timeout);
 
     if (code == OrthancPluginErrorCode_Success)
     {
@@ -4068,6 +4122,16 @@ namespace OrthancPlugins
   }
 #endif
 
+  void GetGetArguments(GetArguments& result, const OrthancPluginHttpRequest* request)
+  {
+    result.clear();
+
+    for (uint32_t i = 0; i < request->getCount; ++i)
+    {
+      result[request->getKeys[i]] = request->getValues[i];
+    }    
+  }
+
   void GetHttpHeaders(HttpHeaders& result, const OrthancPluginHttpRequest* request)
   {
     result.clear();
@@ -4168,6 +4232,11 @@ namespace OrthancPlugins
     {
       path_ += "?" + getArguments;
     }
+
+    if (request->bodySize > 0 && request->body != NULL)
+    {
+      requestBody_.assign(reinterpret_cast<const char*>(request->body), request->bodySize);
+    }
   }
 #endif
 
@@ -4188,6 +4257,15 @@ namespace OrthancPlugins
 #endif
 
 
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+  void RestApiClient::SetRequestHeader(const std::string& key,
+                                       const std::string& value)
+  {
+    requestHeaders_[key] = value;
+  }
+#endif
+
+
 #if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
   bool RestApiClient::Execute()
   {
@@ -4235,9 +4313,17 @@ namespace OrthancPlugins
     }
   }
 
-  void RestApiClient::Forward(OrthancPluginContext* context, OrthancPluginRestOutput* output)
+  void RestApiClient::ExecuteAndForwardAnswer(OrthancPluginContext* context, OrthancPluginRestOutput* output)
+  {
+    if (Execute())
+    {
+      ForwardAnswer(context, output);
+    }
+  }
+
+  void RestApiClient::ForwardAnswer(OrthancPluginContext* context, OrthancPluginRestOutput* output)
   {
-    if (Execute() && httpStatus_ == 200)
+    if (httpStatus_ == 200)
     {
       const char* mimeType = NULL;
       for (HttpHeaders::const_iterator h = answerHeaders_.begin(); h != answerHeaders_.end(); ++h)
@@ -4316,4 +4402,209 @@ namespace OrthancPlugins
     }
   }
 #endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  KeyValueStore::Iterator::Iterator(OrthancPluginKeysValuesIterator  *iterator) :
+    iterator_(iterator)
+  {
+    if (iterator_ == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  KeyValueStore::Iterator::~Iterator()
+  {
+    OrthancPluginFreeKeysValuesIterator(OrthancPlugins::GetGlobalContext(), iterator_);
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  bool KeyValueStore::Iterator::Next()
+  {
+    uint8_t done;
+    OrthancPluginErrorCode code = OrthancPluginKeysValuesIteratorNext(OrthancPlugins::GetGlobalContext(), &done, iterator_);
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+    else
+    {
+      return (done != 0);
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  std::string KeyValueStore::Iterator::GetKey() const
+  {
+    const char* s = OrthancPluginKeysValuesIteratorGetKey(OrthancPlugins::GetGlobalContext(), iterator_);
+    if (s == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+    else
+    {
+      return s;
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  void KeyValueStore::Iterator::GetValue(std::string& value) const
+  {
+    OrthancPlugins::MemoryBuffer valueBuffer;
+    OrthancPluginErrorCode code = OrthancPluginKeysValuesIteratorGetValue(OrthancPlugins::GetGlobalContext(), *valueBuffer, iterator_);
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+    else
+    {
+      valueBuffer.ToString(value);
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  void KeyValueStore::Store(const std::string& key,
+                            const void* value,
+                            size_t valueSize)
+  {
+    if (static_cast<size_t>(static_cast<uint32_t>(valueSize)) != valueSize)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory);
+    }
+
+    OrthancPluginErrorCode code = OrthancPluginStoreKeyValue(OrthancPlugins::GetGlobalContext(), storeId_.c_str(),
+                                                             key.c_str(), value, static_cast<uint32_t>(valueSize));
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  bool KeyValueStore::GetValue(std::string& value,
+                               const std::string& key)
+  {
+    uint8_t found = false;
+    OrthancPlugins::MemoryBuffer valueBuffer;
+    OrthancPluginErrorCode code = OrthancPluginGetKeyValue(OrthancPlugins::GetGlobalContext(), &found,
+                                                           *valueBuffer, storeId_.c_str(), key.c_str());
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+    else if (found)
+    {
+      valueBuffer.ToString(value);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  void KeyValueStore::DeleteKey(const std::string& key)
+  {
+    OrthancPluginErrorCode code = OrthancPluginDeleteKeyValue(OrthancPlugins::GetGlobalContext(),
+                                                              storeId_.c_str(), key.c_str());
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  KeyValueStore::Iterator* KeyValueStore::CreateIterator()
+  {
+    return new Iterator(OrthancPluginCreateKeysValuesIterator(OrthancPlugins::GetGlobalContext(), storeId_.c_str()));
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_QUEUES == 1
+  void Queue::Enqueue(const void* value,
+                      size_t valueSize)
+  {
+    if (static_cast<size_t>(static_cast<uint32_t>(valueSize)) != valueSize)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory);
+    }
+
+    OrthancPluginErrorCode code = OrthancPluginEnqueueValue(OrthancPlugins::GetGlobalContext(),
+                                                            queueId_.c_str(), value, static_cast<uint32_t>(valueSize));
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_QUEUES == 1
+  bool Queue::DequeueInternal(std::string& value,
+                              OrthancPluginQueueOrigin origin)
+  {
+    uint8_t found = false;
+    OrthancPlugins::MemoryBuffer valueBuffer;
+
+    OrthancPluginErrorCode code = OrthancPluginDequeueValue(OrthancPlugins::GetGlobalContext(), &found,
+                                                            *valueBuffer, queueId_.c_str(), origin);
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+    else if (found)
+    {
+      valueBuffer.ToString(value);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_QUEUES == 1
+  uint64_t Queue::GetSize()
+  {
+    uint64_t size = 0;
+    OrthancPluginErrorCode code = OrthancPluginGetQueueSize(OrthancPlugins::GetGlobalContext(), queueId_.c_str(), &size);
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+    else
+    {
+      return size;
+    }
+  }
+#endif
 }


=====================================
Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h
=====================================
@@ -134,6 +134,14 @@
 #  define HAS_ORTHANC_PLUGIN_LOG_MESSAGE  0
 #endif
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8)
+#  define HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES  1
+#  define HAS_ORTHANC_PLUGIN_QUEUES            1
+#else
+#  define HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES  0
+#  define HAS_ORTHANC_PLUGIN_QUEUES            0
+#endif
+
 
 // Macro to tag a function as having been deprecated
 #if (__cplusplus >= 201402L)  // C++14
@@ -172,6 +180,8 @@ namespace OrthancPlugins
 {
   typedef std::map<std::string, std::string>  HttpHeaders;
 
+  typedef std::map<std::string, std::string>  GetArguments;
+
   typedef void (*RestCallback) (OrthancPluginRestOutput* output,
                                 const char* url,
                                 const OrthancPluginHttpRequest* request);
@@ -203,13 +213,6 @@ namespace OrthancPlugins
   public:
     MemoryBuffer();
 
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
-    // This constructor makes a copy of the given buffer in the memory
-    // handled by the Orthanc core
-    MemoryBuffer(const void* buffer,
-                 size_t size);
-#endif
-
     ~MemoryBuffer()
     {
       Clear();
@@ -220,6 +223,20 @@ namespace OrthancPlugins
       return &buffer_;
     }
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+    // Copy of the given buffer into the memory managed by the Orthanc core
+    void Assign(const void* buffer,
+                size_t size);
+#endif
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+    void Assign(const std::string& s);
+#endif
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+    void AssignJson(const Json::Value& value);
+#endif
+
     // This transfers ownership from "other" to "this"
     void Assign(OrthancPluginMemoryBuffer& other);
 
@@ -227,11 +244,11 @@ namespace OrthancPlugins
 
     OrthancPluginMemoryBuffer Release();
 
-    const char* GetData() const
+    const void* GetData() const
     {
       if (buffer_.size > 0)
       {
-        return reinterpret_cast<const char*>(buffer_.data);
+        return buffer_.data;
       }
       else
       {
@@ -854,6 +871,13 @@ namespace OrthancPlugins
                 const std::string& body,
                 const HttpHeaders& headers) const;
 
+    bool DoPost(MemoryBuffer& target,
+                size_t index,
+                const std::string& uri,
+                const std::string& body,
+                const HttpHeaders& headers,
+                unsigned int timeout) const;
+
     bool DoPost(MemoryBuffer& target,
                 const std::string& name,
                 const std::string& uri,
@@ -866,6 +890,13 @@ namespace OrthancPlugins
                 const std::string& body,
                 const HttpHeaders& headers) const;
 
+    bool DoPost(Json::Value& target,
+                size_t index,
+                const std::string& uri,
+                const std::string& body,
+                const HttpHeaders& headers,
+                unsigned int timeout) const;
+
     bool DoPost(Json::Value& target,
                 const std::string& name,
                 const std::string& uri,
@@ -1402,6 +1433,9 @@ void GetHttpHeaders(HttpHeaders& result, const OrthancPluginHttpRequest* request
 // helper method to re-serialize the get arguments from the SDK into a string
 void SerializeGetArguments(std::string& output, const OrthancPluginHttpRequest* request);
 
+// helper method to convert Get arguments from the plugin SDK to a std::map
+void GetGetArguments(GetArguments& result, const OrthancPluginHttpRequest* request);
+
 #if HAS_ORTHANC_PLUGIN_WEBDAV == 1
   class IWebDavCollection : public boost::noncopyable
   {
@@ -1559,6 +1593,9 @@ void SerializeGetArguments(std::string& output, const OrthancPluginHttpRequest*
     void AddRequestHeader(const std::string& key,
                           const std::string& value);
 
+    void SetRequestHeader(const std::string& key,
+                          const std::string& value);
+
     const HttpHeaders& GetRequestHeaders() const
     {
       return requestHeaders_;
@@ -1589,10 +1626,14 @@ void SerializeGetArguments(std::string& output, const OrthancPluginHttpRequest*
       return requestBody_;
     }
 
+    // Execute only
     bool Execute();
 
+    // Forward response as is
+    void ForwardAnswer(OrthancPluginContext* context, OrthancPluginRestOutput* output);
+
     // Execute and forward the response as is
-    void Forward(OrthancPluginContext* context, OrthancPluginRestOutput* output);
+    void ExecuteAndForwardAnswer(OrthancPluginContext* context, OrthancPluginRestOutput* output);
 
     uint16_t GetHttpStatus() const;
 
@@ -1604,4 +1645,101 @@ void SerializeGetArguments(std::string& output, const OrthancPluginHttpRequest*
     bool GetAnswerJson(Json::Value& output) const;
   };
 #endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  class KeyValueStore : public boost::noncopyable
+  {
+  public:
+    class Iterator : public boost::noncopyable
+    {
+    private:
+      OrthancPluginKeysValuesIterator  *iterator_;
+
+    public:
+      explicit Iterator(OrthancPluginKeysValuesIterator *iterator);
+
+      ~Iterator();
+
+      bool Next();
+
+      std::string GetKey() const;
+
+      void GetValue(std::string& target) const;
+    };
+
+  private:
+    std::string storeId_;
+
+  public:
+    explicit KeyValueStore(const std::string& storeId) :
+      storeId_(storeId)
+    {
+    }
+
+    const std::string& GetStoreId() const
+    {
+      return storeId_;
+    }
+
+    void Store(const std::string& key,
+               const void* value,
+               size_t valueSize);
+
+    void Store(const std::string& key,
+               const std::string& value)
+    {
+      Store(key, value.empty() ? NULL : value.c_str(), value.size());
+    }
+
+    bool GetValue(std::string& value,
+                  const std::string& key);
+
+    void DeleteKey(const std::string& key);
+
+    Iterator* CreateIterator();
+  };
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_QUEUES == 1
+  class Queue : public boost::noncopyable
+  {
+  private:
+    std::string queueId_;
+
+    bool DequeueInternal(std::string& value, OrthancPluginQueueOrigin origin);
+
+  public:
+    explicit Queue(const std::string& queueId) :
+      queueId_(queueId)
+    {
+    }
+
+    const std::string& GetQueueId() const
+    {
+      return queueId_;
+    }
+
+    void Enqueue(const void* value,
+                 size_t valueSize);
+
+    void Enqueue(const std::string& value)
+    {
+      Enqueue(value.empty() ? NULL : value.c_str(), value.size());
+    }
+
+    bool DequeueBack(std::string& value)
+    {
+      return DequeueInternal(value, OrthancPluginQueueOrigin_Back);
+    }
+
+    bool DequeueFront(std::string& value)
+    {
+      return DequeueInternal(value, OrthancPluginQueueOrigin_Front);
+    }
+
+    uint64_t GetSize();
+  };
+#endif
 }


=====================================
TODO
=====================================
@@ -1,3 +1,6 @@
+* Force usage of StudyInstanceUID & SeriesInstanceUID in WADO-URI for single instances:
+  https://discourse.orthanc-server.org/t/dicomweb-wado-uri-does-not-work-if-duplicated-instances/5863
+
 
 
 * https://orthanc.uclouvain.be/book/plugins/dicomweb.html#retrieving-dicom-resources-from-a-wado-rs-server



View it on GitLab: https://salsa.debian.org/med-team/orthanc-dicomweb/-/commit/fdf3ede877d7e781983431995d735e2ed6fc1ecd

-- 
View it on GitLab: https://salsa.debian.org/med-team/orthanc-dicomweb/-/commit/fdf3ede877d7e781983431995d735e2ed6fc1ecd
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/debian-med-commit/attachments/20250819/54a913d0/attachment-0001.htm>


More information about the debian-med-commit mailing list