changeset 696:6e165e40b1df

/rendered for video + use tools/find from 1.12.5 to simplify code and optimize SQL queries
author Alain Mazy <am@orthanc.team>
date Thu, 17 Apr 2025 09:59:46 +0200 (4 weeks ago)
parents ad41d16f36b1
children a3801ea80734
files NEWS Plugin/Plugin.cpp Plugin/WadoRs.cpp Plugin/WadoRs.h Plugin/WadoRsRetrieveFrames.cpp Plugin/WadoRsRetrieveRendered.cpp Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h TODO
diffstat 9 files changed, 236 insertions(+), 103 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Fri Jan 17 12:24:54 2025 +0100
+++ b/NEWS	Thu Apr 17 09:59:46 2025 +0200
@@ -1,10 +1,19 @@
 Pending changes in the mainline
 ===============================
 
+=> Minimum Orthanc version: 1.12.5 <=
+=> Minimum SDK version: 1.12.1 <=
+
+* If calling /rendered route on a video, the plugin will now return the video file (MP4 or ...).
+  This notably enables display of videos in OHIF 3.10.1.
+
 
 Version 1.18 (2024-12-18)
 =========================
 
+=> Minimum Orthanc version: 1.11.0 <=
+=> Minimum SDK version: 1.12.1 <=
+
 * Added a "Server" entry in the DICOMweb job content
 * Fixed parsing of numerical values in QIDO-RS response that prevented, among other,
   the retrieval of "NumberOfStudyRelatedInstances", "NumberOfStudyRelatedSeries",...
--- a/Plugin/Plugin.cpp	Fri Jan 17 12:24:54 2025 +0100
+++ b/Plugin/Plugin.cpp	Thu Apr 17 09:59:46 2025 +0200
@@ -37,9 +37,10 @@
 #include <boost/algorithm/string/predicate.hpp>
 
 
+// we use "ResponseContent" in tools/find -> we need 1.12.5
 #define ORTHANC_CORE_MINIMAL_MAJOR     1
-#define ORTHANC_CORE_MINIMAL_MINOR     11
-#define ORTHANC_CORE_MINIMAL_REVISION  0
+#define ORTHANC_CORE_MINIMAL_MINOR     12
+#define ORTHANC_CORE_MINIMAL_REVISION  5
 
 static const char* const HAS_DELETE = "HasDelete";
 static const char* const SYSTEM_CAPABILITIES = "Capabilities";
@@ -486,7 +487,7 @@
           if (hasExtendedFind)
           {
             LOG(INFO) << "Orthanc supports ExtendedFind.";
-            SetPluginCanUseExtendedFile(true);
+            SetPluginCanUseExtendedFind(true);
           }
           else
           {
--- a/Plugin/WadoRs.cpp	Fri Jan 17 12:24:54 2025 +0100
+++ b/Plugin/WadoRs.cpp	Thu Apr 17 09:59:46 2025 +0200
@@ -53,12 +53,12 @@
 static bool pluginCanUseExtendedFind_ = false;
 static bool isSystemReadOnly_ = false;
 
-void SetPluginCanUseExtendedFile(bool enable)
+void SetPluginCanUseExtendedFind(bool enable)
 {
   pluginCanUseExtendedFind_ = enable;
 }
 
-bool CanUseExtendedFile()
+bool CanUseExtendedFind()
 {
   return pluginCanUseExtendedFind_;
 }
@@ -939,11 +939,13 @@
 
 bool LocateResource(OrthancPluginRestOutput* output,
                     std::string& orthancId,
+                    std::map<std::string, std::string>& metadata, 
                     const std::string& studyInstanceUid,
                     const std::string& seriesInstanceUid,
                     const std::string& sopInstanceUid,
                     const std::string& level,
-                    const OrthancPluginHttpRequest* request)
+                    const OrthancPluginHttpRequest* request,
+                    bool firstResourceOnly)
 {
   OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
 
@@ -969,8 +971,15 @@
       payloadQuery["SeriesInstanceUID"] = seriesInstanceUid;
     }
 
+    if (firstResourceOnly)
+    {
+      payload["Limit"] = 1;
+    }
+
     payloadQuery["StudyInstanceUID"] = studyInstanceUid;
     payload["Query"] = payloadQuery;
+    payload["ResponseContent"] = Json::arrayValue;
+    payload["ResponseContent"].append("Metadata");
 
     std::map<std::string, std::string> httpHeaders;
     OrthancPlugins::GetHttpHeaders(httpHeaders, request);
@@ -992,7 +1001,14 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem,
                                       "Multiple " + level + " found for WADO-RS: " + studyInstanceUid + "/" + seriesInstanceUid + "/" + sopInstanceUid);
     }
-    orthancId = resources[0].asString();
+    orthancId = resources[0]["ID"].asString();
+    Json::Value::Members metadataMembers = resources[0]["Metadata"].getMemberNames();
+    
+    for (size_t i = 0; i < metadataMembers.size(); ++i)
+    {
+      metadata[metadataMembers[i]] = resources[0]["Metadata"][metadataMembers[i]].asString();
+    }
+    
     return true;
   }
 }
@@ -1000,64 +1016,115 @@
 
 
 bool LocateStudy(OrthancPluginRestOutput* output,
-                 std::string& orthancId,
+                 std::string& studyOrthancId,
                  std::string& studyInstanceUid,
                  const OrthancPluginHttpRequest* request)
 {
   std::string sopInstanceUid;
   std::string seriesInstanceUid;
+  std::map<std::string, std::string> metadata;
   studyInstanceUid = request->groups[0];
 
   return LocateResource(output,
-                        orthancId,
+                        studyOrthancId,
+                        metadata,
                         studyInstanceUid,
                         seriesInstanceUid,
                         sopInstanceUid,
                         "Study",
-                        request);
+                        request,
+                        false);
 }
 
 
 bool LocateSeries(OrthancPluginRestOutput* output,
-                  std::string& orthancId,
+                  std::string& seriesOrthancId,
                   std::string& studyInstanceUid,
                   std::string& seriesInstanceUid,
                   const OrthancPluginHttpRequest* request)
 {
   std::string sopInstanceUid;
+  std::map<std::string, std::string> metadata;
   studyInstanceUid = request->groups[0];
   seriesInstanceUid = request->groups[1];
 
   return LocateResource(output,
-                        orthancId,
+                        seriesOrthancId,
+                        metadata,
                         studyInstanceUid,
                         seriesInstanceUid,
                         sopInstanceUid,
                         "Series",
-                        request);
+                        request,
+                        false);
 }
 
 
 bool LocateInstance(OrthancPluginRestOutput* output,
-                    std::string& orthancId,
+                    std::string& instanceOrthancId,
                     std::string& studyInstanceUid,
                     std::string& seriesInstanceUid,
                     std::string& sopInstanceUid,
+                    std::string& transferSyntaxMetadata,
                     const OrthancPluginHttpRequest* request)
 {
+  std::map<std::string, std::string> metadata;
   studyInstanceUid = request->groups[0];
   seriesInstanceUid = request->groups[1];
   sopInstanceUid = request->groups[2];
 
-  return LocateResource(output,
-                        orthancId,
-                        studyInstanceUid,
-                        seriesInstanceUid,
-                        sopInstanceUid,
-                        "Instance",
-                        request);
+  bool ret = LocateResource(output,
+                            instanceOrthancId,
+                            metadata,
+                            studyInstanceUid,
+                            seriesInstanceUid,
+                            sopInstanceUid,
+                            "Instance",
+                            request,
+                            false);
+  
+  if (ret && metadata.find("TransferSyntax") != metadata.end())
+  {
+    transferSyntaxMetadata = metadata["TransferSyntax"];
+  }
+  
+  return ret;
 }
 
+bool LocateOneInstance(OrthancPluginRestOutput* output,
+                       std::string& instanceOrthancId,
+                       std::string& studyInstanceUid,
+                       std::string& seriesInstanceUid,
+                       std::string& transferSyntaxMetadata,
+                       const OrthancPluginHttpRequest* request)
+{
+  std::string sopInstanceUid;
+  std::map<std::string, std::string> metadata;
+
+  studyInstanceUid = request->groups[0];
+
+  if (request->groupsCount > 1)
+  {
+    seriesInstanceUid = request->groups[1];
+  }
+
+  bool ret = LocateResource(output,
+                            instanceOrthancId,
+                            metadata,
+                            studyInstanceUid,
+                            seriesInstanceUid,
+                            sopInstanceUid,
+                            "Instance",
+                            request,
+                            false);
+  
+  if (ret && metadata.find("TransferSyntax") != metadata.end())
+  {
+    transferSyntaxMetadata = metadata["TransferSyntax"];
+  }
+  
+  return ret;
+}
 
 void RetrieveDicomStudy(OrthancPluginRestOutput* output,
                         const char* url,
@@ -1099,12 +1166,13 @@
                            const OrthancPluginHttpRequest* request)
 {
   bool transcode;
+  std::string transferSyntax;
   Orthanc::DicomTransferSyntax targetSyntax;
   
   AcceptMultipartDicom(transcode, targetSyntax, request);
   
   std::string orthancId, studyInstanceUid, seriesInstanceUid, sopInstanceUid;
-  if (LocateInstance(output, orthancId, studyInstanceUid, seriesInstanceUid, sopInstanceUid, request))
+  if (LocateInstance(output, orthancId, studyInstanceUid, seriesInstanceUid, sopInstanceUid, transferSyntax, request))
   {
     AnswerListOfDicomInstances(output, Orthanc::ResourceType_Instance, orthancId, transcode, targetSyntax);
   }
@@ -1384,7 +1452,7 @@
     ChildrenMainDicomMaps instancesDicomMaps;
     std::string seriesDicomUid;
 
-    if (CanUseExtendedFile()) // in this case, /series/.../instances?full has been optimized to minimize the SQL queries
+    if (CanUseExtendedFind()) // in this case, /series/.../instances?full has been optimized to minimize the SQL queries
     {
       GetChildrenMainDicomTags(instancesDicomMaps, seriesDicomUid, Orthanc::ResourceType_Series, seriesOrthancId);
       for (ChildrenMainDicomMaps::const_iterator it = instancesDicomMaps.begin(); it != instancesDicomMaps.end(); ++it)
@@ -1410,7 +1478,7 @@
       instancesWorkers.push_back(boost::shared_ptr<boost::thread>(new boost::thread(InstanceWorkerThread, threadData)));
     }
 
-    if (CanUseExtendedFile())  // we must correct the bulkRoot
+    if (CanUseExtendedFind())  // we must correct the bulkRoot
     {
       for (ChildrenMainDicomMaps::const_iterator i = instancesDicomMaps.begin(); i != instancesDicomMaps.end(); ++i)
       {
@@ -1720,12 +1788,14 @@
                               const OrthancPluginHttpRequest* request)
 {
   bool isXml;
+  std::string transferSyntax;
+
   AcceptMetadata(request, isXml);
 
   MainDicomTagsCache cache;
 
   std::string orthancId, studyInstanceUid, seriesInstanceUid, sopInstanceUid;
-  if (LocateInstance(output, orthancId, studyInstanceUid, seriesInstanceUid, sopInstanceUid, request))
+  if (LocateInstance(output, orthancId, studyInstanceUid, seriesInstanceUid, sopInstanceUid, transferSyntax, request))
   {
     OrthancPlugins::DicomWebFormatter::HttpWriter writer(output, isXml);
     WriteInstanceMetadata(writer, OrthancPlugins::MetadataMode_Full, cache, orthancId, studyInstanceUid,
@@ -1740,12 +1810,13 @@
                       const OrthancPluginHttpRequest* request)
 {
   OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
+  std::string transferSyntax;
 
   AcceptBulkData(request);
 
   std::string orthancId, studyInstanceUid, seriesInstanceUid, sopInstanceUid;
   OrthancPlugins::MemoryBuffer content;
-  if (LocateInstance(output, orthancId, studyInstanceUid, seriesInstanceUid, sopInstanceUid, request) &&
+  if (LocateInstance(output, orthancId, studyInstanceUid, seriesInstanceUid, sopInstanceUid, transferSyntax, request) &&
       content.RestApiGet("/instances/" + orthancId + "/file", false))
   {
     std::string bulk(request->groups[3]);
--- a/Plugin/WadoRs.h	Fri Jan 17 12:24:54 2025 +0100
+++ b/Plugin/WadoRs.h	Thu Apr 17 09:59:46 2025 +0200
@@ -27,23 +27,31 @@
 
 
 bool LocateStudy(OrthancPluginRestOutput* output,
-                 std::string& uri,
+                 std::string& studyOrthancId,
                  std::string& studyInstanceUid,
                  const OrthancPluginHttpRequest* request);
 
 bool LocateSeries(OrthancPluginRestOutput* output,
-                  std::string& uri,
+                  std::string& seriesOrthancId,
                   std::string& studyInstanceUid,
                   std::string& seriesInstanceUid,
                   const OrthancPluginHttpRequest* request);
 
 bool LocateInstance(OrthancPluginRestOutput* output,
-                    std::string& uri,
+                    std::string& instanceOrthancId,
                     std::string& studyInstanceUid,
                     std::string& seriesInstanceUid,
                     std::string& sopInstanceUid,
+                    std::string& transferSyntaxMetadata,
                     const OrthancPluginHttpRequest* request);
 
+bool LocateOneInstance(OrthancPluginRestOutput* output,
+                       std::string& instanceOrthancId,
+                       std::string& studyInstanceUid,
+                       std::string& seriesInstanceUid,
+                       std::string& transferSyntaxMetadata,
+                       const OrthancPluginHttpRequest* request);
+
 void RetrieveDicomStudy(OrthancPluginRestOutput* output,
                         const char* url,
                         const OrthancPluginHttpRequest* request);
@@ -104,6 +112,6 @@
 
 void SetPluginCanDownloadTranscodedFile(bool enable);
 
-void SetPluginCanUseExtendedFile(bool enable);
+void SetPluginCanUseExtendedFind(bool enable);
 
 void SetSystemIsReadOnly(bool isReadOnly);
\ No newline at end of file
--- a/Plugin/WadoRsRetrieveFrames.cpp	Fri Jan 17 12:24:54 2025 +0100
+++ b/Plugin/WadoRsRetrieveFrames.cpp	Thu Apr 17 09:59:46 2025 +0200
@@ -476,7 +476,9 @@
                            std::list<unsigned int>& frames)
 {
   std::string orthancId, studyInstanceUid, seriesInstanceUid, sopInstanceUid;
-  if (LocateInstance(output, orthancId, studyInstanceUid, seriesInstanceUid, sopInstanceUid, request))
+  std::string transferSyntax;
+
+  if (LocateInstance(output, orthancId, studyInstanceUid, seriesInstanceUid, sopInstanceUid, transferSyntax, request))
   {
     OrthancPlugins::MemoryBuffer content;
     Orthanc::DicomTransferSyntax currentSyntax;
@@ -500,15 +502,9 @@
       }
     }
 
-    std::string currentSyntaxString;
-    if (!OrthancPlugins::RestApiGetString(currentSyntaxString, "/instances/" + orthancId + "/metadata/TransferSyntax", false))
+    if (!Orthanc::LookupTransferSyntax(currentSyntax, transferSyntax))
     {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "DICOMweb: Unable to get TransferSyntax for instance " + orthancId);
-    }
-
-    if (!Orthanc::LookupTransferSyntax(currentSyntax, currentSyntaxString))
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "Unknown transfer syntax: " + currentSyntaxString);
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "Unknown transfer syntax: " + transferSyntax);
     }
 
     Orthanc::DicomTransferSyntax targetSyntax = currentSyntax;
@@ -518,7 +514,7 @@
       // note: these 2 syntaxes are not supposed to be used in retrieve frames
       // according to https://dicom.nema.org/MEDICAL/dicom/2019a/output/chtml/part18/chapter_6.html#table_6.1.1.8-3b
       // "The Implicit VR Little Endian (1.2.840.10008.1.2), and Explicit VR Big Endian (1.2.840.10008.1.2.2) transfer syntaxes shall not be used with Web Services."
-      LOG(INFO) << "The file is in a transfer syntax " << currentSyntaxString << " that is not allowed by the DICOMweb standard -> it will be transcoded to Little Endian Explicit";
+      LOG(INFO) << "The file is in a transfer syntax " << transferSyntax << " that is not allowed by the DICOMweb standard -> it will be transcoded to Little Endian Explicit";
       targetSyntax = Orthanc::DicomTransferSyntax_LittleEndianExplicit;
     }    
 
--- a/Plugin/WadoRsRetrieveRendered.cpp	Fri Jan 17 12:24:54 2025 +0100
+++ b/Plugin/WadoRsRetrieveRendered.cpp	Thu Apr 17 09:59:46 2025 +0200
@@ -821,10 +821,29 @@
 
 
 static void AnswerFrameRendered(OrthancPluginRestOutput* output,
-                                std::string instanceId,
+                                const std::string& instanceId,
+                                const std::string& transferSyntax,
                                 int frame,
                                 const OrthancPluginHttpRequest* request)
 {
+  // If the instance is a video, we shall provide the video file itself (MP4, ...)
+  Orthanc::DicomTransferSyntax currentSyntax;
+  if (Orthanc::LookupTransferSyntax(currentSyntax, transferSyntax))
+  {
+    if (currentSyntax >= Orthanc::DicomTransferSyntax_MPEG2MainProfileAtMainLevel && currentSyntax <= Orthanc::DicomTransferSyntax_HEVCMain10ProfileLevel5_1)
+    {
+      OrthancPlugins::RestApiClient apiClient;
+      apiClient.SetPath(std::string("/instances/") + instanceId + "/frames/0/raw");
+      if (apiClient.Execute())
+      {
+        apiClient.Forward(OrthancPlugins::GetGlobalContext(), output);
+        return;
+      }
+    }
+  }
+
+  // for other media types, try to generate a single image preview.
+
   static const char* const PHOTOMETRIC_INTERPRETATION = "0028,0004";
   static const char* const PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE = "5200,9230";
   static const char* const PIXEL_VALUE_TRANSFORMATION_SEQUENCE = "0028,9145";
@@ -976,9 +995,11 @@
   else
   {
     std::string instanceId, studyInstanceUid, seriesInstanceUid, sopInstanceUid;
-    if (LocateInstance(output, instanceId, studyInstanceUid, seriesInstanceUid, sopInstanceUid, request))
+    std::string transferSyntax;
+
+    if (LocateInstance(output, instanceId, studyInstanceUid, seriesInstanceUid, sopInstanceUid, transferSyntax, request))
     {
-      AnswerFrameRendered(output, instanceId, frame, request);
+      AnswerFrameRendered(output, instanceId, transferSyntax, frame, request);
     }
     else
     {
@@ -1012,8 +1033,6 @@
                             const char* url,
                             const OrthancPluginHttpRequest* request)
 {
-  static const char* const INSTANCES = "Instances";
-  
   assert(request->groupsCount == 2);
 
   if (request->method != OrthancPluginHttpMethod_Get)
@@ -1022,35 +1041,11 @@
   }
   else
   {
-    std::string orthancId, studyInstanceUid, seriesInstanceUid;
-    if (LocateSeries(output, orthancId, studyInstanceUid, seriesInstanceUid, request))
+    std::string instanceOrthancId, studyInstanceUid, seriesInstanceUid, transferSyntax;
+    if (LocateOneInstance(output, instanceOrthancId, studyInstanceUid, seriesInstanceUid, transferSyntax, request))
     {
-      Json::Value series;
-      if (OrthancPlugins::RestApiGet(series, "/series/" + orthancId, false) &&
-          series.type() == Json::objectValue &&
-          series.isMember(INSTANCES) &&
-          series[INSTANCES].type() == Json::arrayValue &&
-          series[INSTANCES].size() > 0)
-      {
-        std::set<std::string> ids;
-        for (Json::Value::ArrayIndex i = 0; i < series[INSTANCES].size(); i++)
-        {
-          if (series[INSTANCES][i].type() != Json::stringValue)
-          {
-            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-          }
-          else
-          {
-            ids.insert(series[INSTANCES][i].asString());
-          }
-        }
-
-        // Retrieve the first instance in alphanumeric order, in order
-        // to always return the same instance
-        std::string instanceId = *ids.begin();
-        AnswerFrameRendered(output, instanceId, 1 /* first frame */, request);
-        return;  // Success
-      }
+      AnswerFrameRendered(output, instanceOrthancId, transferSyntax, 1 /* first frame */, request);
+      return;  // Success
     }
 
     throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem, "Inexistent series");
@@ -1062,8 +1057,6 @@
                            const char* url,
                            const OrthancPluginHttpRequest* request)
 {
-  static const char* const ID = "ID";
-  
   assert(request->groupsCount == 1);
 
   if (request->method != OrthancPluginHttpMethod_Get)
@@ -1072,35 +1065,11 @@
   }
   else
   {
-    std::string orthancId, studyInstanceUid;
-    if (LocateStudy(output, orthancId, studyInstanceUid, request))
+    std::string instanceOrthancId, studyInstanceUid, seriesInstanceUid, transferSyntax;
+    if (LocateOneInstance(output, instanceOrthancId, studyInstanceUid, seriesInstanceUid, transferSyntax, request))
     {
-      Json::Value instances;
-      if (OrthancPlugins::RestApiGet(instances, "/studies/" + orthancId + "/instances", false) &&
-          instances.type() == Json::arrayValue &&
-          instances.size() > 0)
-      {
-        std::set<std::string> ids;
-        for (Json::Value::ArrayIndex i = 0; i < instances.size(); i++)
-        {
-          if (instances[i].type() != Json::objectValue ||
-              !instances[i].isMember(ID) ||
-              instances[i][ID].type() != Json::stringValue)
-          {
-            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-          }
-          else
-          {
-            ids.insert(instances[i][ID].asString());
-          }
-        }
-
-        // Retrieve the first instance in alphanumeric order, in order
-        // to always return the same instance
-        std::string instanceId = *ids.begin();
-        AnswerFrameRendered(output, instanceId, 1 /* first frame */, request);
-        return;  // Success
-      }
+      AnswerFrameRendered(output, instanceOrthancId, transferSyntax, 1 /* first frame */, request);
+      return;  // Success
     }
 
     throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem, "Inexistent study");
--- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp	Fri Jan 17 12:24:54 2025 +0100
+++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp	Thu Apr 17 09:59:46 2025 +0200
@@ -26,6 +26,7 @@
 #include <boost/algorithm/string/predicate.hpp>
 #include <boost/move/unique_ptr.hpp>
 #include <boost/thread.hpp>
+#include <boost/algorithm/string/join.hpp>
 
 
 #include <json/reader.h>
@@ -4077,6 +4078,26 @@
     }    
   }
 
+  void SerializeGetArguments(std::string& output, const OrthancPluginHttpRequest* request)
+  {
+    output.clear();
+    std::vector<std::string> arguments;
+    for (uint32_t i = 0; i < request->getCount; ++i)
+    {
+      if (request->getValues[i] && strlen(request->getValues[i]) > 0)
+      {
+        arguments.push_back(std::string(request->getKeys[i]) + "=" + std::string(request->getValues[i]));
+      }
+      else
+      {
+        arguments.push_back(std::string(request->getKeys[i]));
+      }
+    }
+
+    output = boost::algorithm::join(arguments, "&");
+  }
+
+
 #if !ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 4)
   static void SetPluginProperty(const std::string& pluginIdentifier,
                                 _OrthancPluginProperty property,
@@ -4130,6 +4151,24 @@
     httpStatus_(0)
   {
   }
+
+  RestApiClient::RestApiClient(const char* url,
+                               const OrthancPluginHttpRequest* request) :
+    method_(request->method),
+    path_(url),
+    afterPlugins_(false),
+    httpStatus_(0)
+  {
+    OrthancPlugins::GetHttpHeaders(requestHeaders_, request);
+
+    std::string getArguments;
+    OrthancPlugins::SerializeGetArguments(getArguments, request);
+
+    if (!getArguments.empty())
+    {
+      path_ += "?" + getArguments;
+    }
+  }
 #endif
 
 
@@ -4195,6 +4234,32 @@
       }
     }
   }
+
+  void RestApiClient::Forward(OrthancPluginContext* context, OrthancPluginRestOutput* output)
+  {
+    if (Execute() && httpStatus_ == 200)
+    {
+      const char* mimeType = NULL;
+      for (HttpHeaders::const_iterator h = answerHeaders_.begin(); h != answerHeaders_.end(); ++h)
+      {
+        if (h->first == "content-type")
+        {
+          mimeType = h->second.c_str();
+        }
+      }
+      
+      AnswerString(answerBody_, mimeType, output);
+    }
+    else
+    {
+      AnswerHttpError(httpStatus_, output);
+    }
+  }
+
+  bool RestApiClient::GetAnswerJson(Json::Value& output) const
+  {
+    return ReadJson(output, answerBody_);
+  }
 #endif
 
 
--- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h	Fri Jan 17 12:24:54 2025 +0100
+++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h	Thu Apr 17 09:59:46 2025 +0200
@@ -1399,6 +1399,9 @@
 // helper method to convert Http headers from the plugin SDK to a std::map
 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);
+
 #if HAS_ORTHANC_PLUGIN_WEBDAV == 1
   class IWebDavCollection : public boost::noncopyable
   {
@@ -1528,6 +1531,10 @@
 
   public:
     RestApiClient();
+    
+    // used to forward a call from the plugin to the core
+    RestApiClient(const char* url,
+                  const OrthancPluginHttpRequest* request);
 
     void SetMethod(OrthancPluginHttpMethod method)
     {
@@ -1584,12 +1591,17 @@
 
     bool Execute();
 
+    // Execute and forward the response as is
+    void Forward(OrthancPluginContext* context, OrthancPluginRestOutput* output);
+
     uint16_t GetHttpStatus() const;
 
     bool LookupAnswerHeader(std::string& value,
                             const std::string& key) const;
 
     const std::string& GetAnswerBody() const;
+
+    bool GetAnswerJson(Json::Value& output) const;
   };
 #endif
 }
--- a/TODO	Fri Jan 17 12:24:54 2025 +0100
+++ b/TODO	Thu Apr 17 09:59:46 2025 +0200
@@ -31,6 +31,8 @@
 
 * Add support for application/zip in /dicom-web/studies/ (aka sup 211: https://www.dicomstandard.org/docs/librariesprovider2/dicomdocuments/news/ftsup/docs/sups/sup211.pdf?sfvrsn=9fe9edae_2)
 
+* Add support for thumbnails (aka sup 203: https://www.dicomstandard.org/docs/librariesprovider2/dicomdocuments/news/progress/docs/sups/sup203.pdf)
+
 * Support private tags in search fields:
   https://discourse.orthanc-server.org/t/dicomweb-plugin-exception-of-unknown-dicom-tag-for-private-data-element-tags-while-using-query-parameters/3998