changeset 5311:79fa77e9fa0d am-experimental

merge default -> am-experimental
author Alain Mazy <am@osimis.io>
date Fri, 16 Jun 2023 09:26:33 +0200
parents 9504de20d43d (diff) b5c502bcaf99 (current diff)
children
files NEWS OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp
diffstat 18 files changed, 426 insertions(+), 83 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Mon Jun 12 18:42:06 2023 +0200
+++ b/NEWS	Fri Jun 16 09:26:33 2023 +0200
@@ -5,6 +5,7 @@
 --------
 
 * API version upgraded to 21
+* New URI /instances/{id}/file-until-pixel-data
 * added a route to delete the output of an asynchronous job (right now only for archive jobs):
   e.g. DELETE /jobs/../archive
 
@@ -26,6 +27,16 @@
   This might have an impact on the image returned by /dicom-web/studies/../series/../instances/../frames/1; the
   image format is now consistent with the PhotometricIntepretation DICOM Tag.
 
+* WIP: new dicomWeb Json format for some of the Rest API routes.
+* WIP: new 'include' get arguments for some of the Rest API routes to define
+  the content of the response.  Useful since the dicomWeb format is very slow
+  to serialize.
+
+* New functions in the SDK:
+  - OrthancPluginEncodeDicomWebJson3()
+  TODO: Xml version
+
+
 
 Version 1.12.0 (2023-04-14)
 ===========================
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Mon Jun 12 18:42:06 2023 +0200
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Fri Jun 16 09:26:33 2023 +0200
@@ -41,6 +41,9 @@
 #include "../Logging.h"
 #include "../Toolbox.h"
 #include "../OrthancException.h"
+#include "DicomWebJsonVisitor.h"
+#include "ParsedDicomFile.h"
+#include <boost/date_time/posix_time/posix_time.hpp>
 
 #if ORTHANC_SANDBOXED == 0
 #  include "../TemporaryFile.h"
@@ -1444,6 +1447,16 @@
 
     result.clear();
 
+    if (format == DicomToJsonFormat_DicomWeb)
+    {
+      DicomWebJsonVisitor visitor;
+
+      ParsedDicomFile dicom(values, Encoding_Utf8, true);
+      dicom.Apply(visitor);
+      result = visitor.GetResult();
+      return;
+    }
+
     for (DicomMap::Content::const_iterator 
            it = values.content_.begin(); it != values.content_.end(); ++it)
     {
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp	Mon Jun 12 18:42:06 2023 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp	Fri Jun 16 09:26:33 2023 +0200
@@ -1755,8 +1755,19 @@
   }
 
 
-  void ParsedDicomFile::Apply(ITagVisitor& visitor) const
+  void ParsedDicomFile::Apply(ITagVisitor& visitor, bool injectEmptyPixelData) const
   {
+    DcmItem& dataset = *GetDcmtkObjectConst().getDataset();
+
+    if (injectEmptyPixelData)
+    {
+      DcmTag emptyPixelData(DCM_PixelData, EVR_PixelData);
+      if (!dataset.insertEmptyElement(emptyPixelData, false).good())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
     FromDcmtkBridge::Apply(*GetDcmtkObjectConst().getDataset(), visitor, GetDefaultDicomEncoding());
   }
 
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h	Mon Jun 12 18:42:06 2023 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h	Fri Jun 16 09:26:33 2023 +0200
@@ -273,7 +273,7 @@
 
     bool LookupPhotometricInterpretation(PhotometricInterpretation& result) const;
 
-    void Apply(ITagVisitor& visitor) const;
+    void Apply(ITagVisitor& visitor, bool injectEmptyPixelData = false) const;
 
     // Decode the given frame, using the built-in DICOM decoder of Orthanc
     ImageAccessor* DecodeFrame(unsigned int frame) const;
--- a/OrthancFramework/Sources/Enumerations.cpp	Mon Jun 12 18:42:06 2023 +0200
+++ b/OrthancFramework/Sources/Enumerations.cpp	Fri Jun 16 09:26:33 2023 +0200
@@ -1184,6 +1184,9 @@
       case DicomToJsonFormat_Short:
         return "Short";
 
+      case DicomToJsonFormat_DicomWeb:
+        return "DicomWeb";
+
       default:
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
@@ -1885,6 +1888,10 @@
     {
       return DicomToJsonFormat_Human;
     }
+    else if (format == "DicomWeb")
+    {
+      return DicomToJsonFormat_DicomWeb;
+    }
     else
     {
       throw OrthancException(ErrorCode_ParameterOutOfRange);
--- a/OrthancFramework/Sources/Enumerations.h	Mon Jun 12 18:42:06 2023 +0200
+++ b/OrthancFramework/Sources/Enumerations.h	Fri Jun 16 09:26:33 2023 +0200
@@ -581,7 +581,8 @@
   {
     DicomToJsonFormat_Full,
     DicomToJsonFormat_Short,
-    DicomToJsonFormat_Human
+    DicomToJsonFormat_Human,
+    DicomToJsonFormat_DicomWeb
   };
 
   enum DicomToJsonFlags
--- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Mon Jun 12 18:42:06 2023 +0200
+++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Fri Jun 16 09:26:33 2023 +0200
@@ -3220,7 +3220,129 @@
   m.FromDicomWeb(v);
 }
 
-
+TEST(DicomMap, MainDicomTagsDicomWebJson)
+{
+  std::unique_ptr<ParsedDicomFile> dicom;
+
+  {
+    std::string source = "{"
+      "\"0008,0005\" : {"
+         "\"Name\" : \"SpecificCharacterSet\","
+         "\"Type\" : \"String\","
+         "\"Value\" : \"ISO_IR 192\""
+      "},"
+      "\"0008,0008\" : {"
+         "\"Name\" : \"ImageType\","
+         "\"Type\" : \"String\","
+         "\"Value\" : \"DERIVED\\\\PRIMARY\\\\AXIAL\""
+      "},"
+      "\"0008,0016\" : {"
+         "\"Name\" : \"SOPClassUID\","
+         "\"Type\" : \"String\","
+         "\"Value\" : \"1.2.840.10008.5.1.4.1.1.2\""
+      "},"
+      "\"0008,0018\" : {"
+         "\"Name\" : \"SOPInstanceUID\","
+         "\"Type\" : \"String\","
+         "\"Value\" : \"1.2.276.0.7230010.3.1.4.1215942821.4756.1664833117.11987\""
+      "},"
+      "\"0018,0050\" : {"
+         "\"Name\" : \"SliceThickness\","
+         "\"Type\" : \"String\","
+         "\"Value\" : \"0.19816064757161\""
+      "},"
+      "\"0020,0013\" : {"
+         "\"Name\" : \"InstanceNumber\","
+         "\"Type\" : \"String\","
+         "\"Value\" : \"0\""
+      "},"
+      "\"0020,0032\" : {"
+         "\"Name\" : \"ImagePositionPatient\","
+         "\"Type\" : \"String\","
+         "\"Value\" : \"-79.462419676214\\\\-79.462419676214\\\\-59.150953300125\""
+      "},"
+      "\"0020,0037\" : {"
+         "\"Name\" : \"ImageOrientationPatient\","
+         "\"Type\" : \"String\","
+         "\"Value\" : \"1\\\\-0\\\\0\\\\-0\\\\1\\\\-0\""
+      "},"
+      "\"0028,0004\" : {"
+         "\"Name\" : \"PhotometricInterpretation\","
+         "\"Type\" : \"String\","
+         "\"Value\" : \"MONOCHROME2\""
+      "},"
+      "\"0028,0010\" : {"
+         "\"Name\" : \"Rows\","
+         "\"Type\" : \"String\","
+         "\"Value\" : \"803\""
+      "},"
+      "\"0028,0011\" : {"
+         "\"Name\" : \"Columns\","
+         "\"Type\" : \"String\","
+         "\"Value\" : \"803\""
+      "},"
+      "\"0028,0030\" : {"
+         "\"Name\" : \"PixelSpacing\","
+         "\"Type\" : \"String\","
+         "\"Value\" : \"0.19816064757161\\\\0.19816064757161\""
+      "},"
+      "\"0028,0100\" : {"
+         "\"Name\" : \"BitsAllocated\","
+         "\"Type\" : \"String\","
+         "\"Value\" : \"16\""
+      "},"
+      "\"0028,0101\" : {"
+         "\"Name\" : \"BitsStored\","
+         "\"Type\" : \"String\","
+         "\"Value\" : \"16\""
+      "},"
+      "\"0028,0103\" : {"
+         "\"Name\" : \"PixelRepresentation\","
+         "\"Type\" : \"String\","
+         "\"Value\" : \"0\""
+      "},"
+      "\"0028,1050\" : {"
+         "\"Name\" : \"WindowCenter\","
+         "\"Type\" : \"String\","
+         "\"Value\" : \"1023.96875\""
+      "},"
+      "\"0028,1051\" : {"
+         "\"Name\" : \"WindowWidth\","
+         "\"Type\" : \"String\","
+         "\"Value\" : \"4095.9375\""
+      "},"
+      "\"0028,1052\" : {"
+         "\"Name\" : \"RescaleIntercept\","
+         "\"Type\" : \"String\","
+         "\"Value\" : \"-1024\""
+      "},"
+      "\"0028,1053\" : {"
+         "\"Name\" : \"RescaleSlope\","
+         "\"Type\" : \"String\","
+         "\"Value\" : \"0.0625\""
+      "}"
+   "}";
+
+    Json::Value v;
+    Orthanc::Toolbox::ReadJson(v, source);
+
+//    LOG(INFO) << source;
+//    LOG(INFO) << v.toStyledString();
+    Json::Value result = Json::objectValue;
+    DicomMap map;
+    map.FromDicomAsJson(v, false, true);
+
+// for profiling    for (int i=0; i < 100; ++i)
+    {
+      result = Json::objectValue;
+      FromDcmtkBridge::ToJson(result, map, Orthanc::DicomToJsonFormat_DicomWeb);
+    }
+
+    LOG(INFO) << result.toStyledString();
+    ASSERT_TRUE(result.isMember("00281053"));
+    ASSERT_DOUBLE_EQ(0.0625, result["00281053"]["Value"][0].asDouble());
+  }
+}
 
 
 #if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Mon Jun 12 18:42:06 2023 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Fri Jun 16 09:26:33 2023 +0200
@@ -1052,12 +1052,13 @@
 
       void Apply(char** target,
                  bool isJson,
-                 const ParsedDicomFile& dicom)
+                 const ParsedDicomFile& dicom,
+                 bool injectEmptyPixelData)
       {
         DicomWebJsonVisitor visitor;
         visitor.SetFormatter(*this);
 
-        dicom.Apply(visitor);
+        dicom.Apply(visitor, injectEmptyPixelData);
 
         std::string s;
 
@@ -1077,10 +1078,11 @@
       void Apply(char** target,
                  bool isJson,
                  const void* dicom,
-                 size_t dicomSize) 
+                 size_t dicomSize,
+                 bool injectEmptyPixelData) 
       {
         ParsedDicomFile parsed(dicom, dicomSize);
-        Apply(target, isJson, parsed);
+        Apply(target, isJson, parsed, injectEmptyPixelData);
       }
     };
   }
@@ -3636,7 +3638,8 @@
         DicomWebBinaryFormatter formatter(p.dicomWebCallback, p.dicomWebPayload);
         formatter.Apply(p.targetStringToFree,
                         (service == _OrthancPluginService_GetInstanceDicomWebJson),
-                        instance.GetParsedDicomFile());
+                        instance.GetParsedDicomFile(),
+                        false);
         return;
       }
 
@@ -5195,7 +5198,7 @@
         DicomWebBinaryFormatter formatter(p.callback);
         formatter.Apply(p.target,
                         (service == _OrthancPluginService_EncodeDicomWebJson),
-                        p.dicom, p.dicomSize);
+                        p.dicom, p.dicomSize, false);
         return true;
       }
 
@@ -5208,7 +5211,19 @@
         DicomWebBinaryFormatter formatter(p.callback, p.payload);
         formatter.Apply(p.target,
                         (service == _OrthancPluginService_EncodeDicomWebJson2),
-                        p.dicom, p.dicomSize);
+                        p.dicom, p.dicomSize, false);
+        return true;
+      }
+
+      case _OrthancPluginService_EncodeDicomWebJson3:
+      {
+        const _OrthancPluginEncodeDicomWeb3& p =
+          *reinterpret_cast<const _OrthancPluginEncodeDicomWeb3*>(parameters);
+
+        DicomWebBinaryFormatter formatter(p.callback, p.payload);
+        formatter.Apply(p.target,
+                        (service == _OrthancPluginService_EncodeDicomWebJson3),
+                        p.dicom, p.dicomSize, p.injectEmptyPixelData);
         return true;
       }
 
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Mon Jun 12 18:42:06 2023 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Fri Jun 16 09:26:33 2023 +0200
@@ -447,6 +447,7 @@
     _OrthancPluginService_CreateMemoryBuffer64 = 40, /* New in Orthanc 1.9.0 */
     _OrthancPluginService_CreateDicom2 = 41,         /* New in Orthanc 1.9.0 */
     _OrthancPluginService_GetDatabaseServerIdentifier = 42,         /* New in Orthanc 1.11.1 */
+    _OrthancPluginService_EncodeDicomWebJson3 = 43,  /* New in Orthanc 1.12.1 */
 
     /* Registration of callbacks */
     _OrthancPluginService_RegisterRestCallback = 1000,
@@ -7256,6 +7257,62 @@
   }
 
 
+  typedef struct
+  {
+    char**                                target;
+    const void*                           dicom;
+    uint32_t                              dicomSize;
+    OrthancPluginDicomWebBinaryCallback2  callback;
+    void*                                 payload;
+    bool                                  injectEmptyPixelData;
+  } _OrthancPluginEncodeDicomWeb3;
+
+  /**
+   * @brief Convert a DICOM instance to DICOMweb JSON.
+   *
+   * This function converts a memory buffer containing a DICOM instance,
+   * into its DICOMweb JSON representation.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param dicom Pointer to the DICOM instance.
+   * @param dicomSize Size of the DICOM instance.
+   * @param callback Callback to set the value of the binary tags.
+   * @param payload User payload.
+   * @return The NULL value in case of error, or the JSON document. This string must
+   * be freed by OrthancPluginFreeString().
+   * @see OrthancPluginCreateDicom()
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebJson3(
+    OrthancPluginContext*                 context,
+    const void*                           dicom,
+    uint32_t                              dicomSize,
+    OrthancPluginDicomWebBinaryCallback2  callback,
+    void*                                 payload,
+    bool                                  injectEmptyPixelData)
+  {
+    char* target = NULL;
+    
+    _OrthancPluginEncodeDicomWeb3 params;
+    params.target = &target;
+    params.dicom = dicom;
+    params.dicomSize = dicomSize;
+    params.callback = callback;
+    params.payload = payload;
+    params.injectEmptyPixelData = injectEmptyPixelData;
+
+
+    if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebJson3, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
   /**
    * @brief Convert a DICOM instance to DICOMweb XML.
    *
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/OrthancFrameworkDependencies.cpp	Mon Jun 12 18:42:06 2023 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/OrthancFrameworkDependencies.cpp	Fri Jun 16 09:26:33 2023 +0200
@@ -64,6 +64,7 @@
 #include "../../../../OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp"
 #include "../../../../OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.cpp"
 #include "../../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp"
+#include "../../../../OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.cpp"
 #include "../../../../OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.cpp"
 #include "../../../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp"
 #include "../../../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp"
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Mon Jun 12 18:42:06 2023 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Fri Jun 16 09:26:33 2023 +0200
@@ -868,7 +868,8 @@
             LookupStringMetadata(target.mainDicomTagsSignature_, target.metadata_, MetadataType_MainDicomTagsSignature);
           }
 
-          if (expandFlags & ExpandResourceFlags_IncludeMainDicomTags)
+          if (expandFlags & ExpandResourceFlags_IncludeMainDicomTags
+              || expandFlags & ExpandResourceFlags_IncludeRequestedTags)
           {
             // read all tags from DB
             transaction.GetMainDicomTags(target.GetMainDicomTags(), internalId);
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Mon Jun 12 18:42:06 2023 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Fri Jun 16 09:26:33 2023 +0200
@@ -115,11 +115,13 @@
     ExpandResourceFlags_IncludeChildren         = (1 << 1),
     ExpandResourceFlags_IncludeMainDicomTags    = (1 << 2),
     ExpandResourceFlags_IncludeLabels           = (1 << 3),
+    ExpandResourceFlags_IncludeRequestedTags    = (1 << 3),
 
     ExpandResourceFlags_Default = (ExpandResourceFlags_IncludeMetadata |
                                      ExpandResourceFlags_IncludeChildren |
                                      ExpandResourceFlags_IncludeMainDicomTags |
-                                     ExpandResourceFlags_IncludeLabels)
+                                     ExpandResourceFlags_IncludeLabels |
+                                     ExpandResourceFlags_IncludeRequestedTags)
   };
 
   class StatelessDatabaseOperations : public boost::noncopyable
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Mon Jun 12 18:42:06 2023 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Fri Jun 16 09:26:33 2023 +0200
@@ -489,7 +489,9 @@
   static const std::string GET_SIMPLIFY = "simplify";
   static const std::string GET_FULL = "full";
   static const std::string GET_SHORT = "short";
+  static const std::string GET_DICOM_WEB = "dicomWeb";
   static const std::string GET_REQUESTED_TAGS = "requestedTags";
+  static const std::string GET_INCLUDE = "include";
 
   static const std::string POST_SIMPLIFY = "Simplify";
   static const std::string POST_FULL = "Full";
@@ -506,6 +508,45 @@
     "report the DICOM tags in full format (tags indexed by their hexadecimal "
     "format, associated with their symbolic name and their value)";
 
+  ExpandResourceFlags OrthancRestApi::GetResponseContent(const RestApiGetCall& call,
+                                                         ExpandResourceFlags defaultContent)
+  {
+    if (call.HasArgument(GET_INCLUDE))
+    {
+      std::vector<std::string> includes;
+      Toolbox::TokenizeString(includes, call.GetArgument(GET_INCLUDE, ""), ',');
+
+      unsigned int result = static_cast<unsigned int>(ExpandResourceFlags_None);
+      for (size_t i = 0; i < includes.size(); ++i)
+      {
+        if (includes[i] == "MainDicomTags")
+        {
+          result |= static_cast<unsigned int>(ExpandResourceFlags_IncludeMainDicomTags);
+        }
+        else if (includes[i] == "RequestedTags")
+        {
+          result |= static_cast<unsigned int>(ExpandResourceFlags_IncludeRequestedTags);
+        }
+        else if (includes[i] == "Labels")
+        {
+          result |= static_cast<unsigned int>(ExpandResourceFlags_IncludeLabels);
+        }
+        else if (includes[i] == "Children")
+        {
+          result |= static_cast<unsigned int>(ExpandResourceFlags_IncludeChildren);
+        }
+        else if (includes[i] == "Metadata")
+        {
+          result |= static_cast<unsigned int>(ExpandResourceFlags_IncludeMetadata);
+        }
+      }
+
+      return static_cast<ExpandResourceFlags>(result);
+    }
+
+    return defaultContent;
+  }
+
 
   DicomToJsonFormat OrthancRestApi::GetDicomFormat(const RestApiGetCall& call,
                                                    DicomToJsonFormat defaultFormat)
@@ -522,6 +563,10 @@
     {
       return DicomToJsonFormat_Full;
     }
+    else if (call.HasArgument(GET_DICOM_WEB))
+    {
+      return DicomToJsonFormat_DicomWeb;
+    }
     else
     {
       return defaultFormat;
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.h	Mon Jun 12 18:42:06 2023 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.h	Fri Jun 16 09:26:33 2023 +0200
@@ -28,7 +28,7 @@
 #include "../../../OrthancFramework/Sources/RestApi/RestApi.h"
 #include "../ServerJobs/ThreadedSetOfInstancesJob.h"
 #include "../ServerEnumerations.h"
-
+#include "../Database/StatelessDatabaseOperations.h"
 #include <set>
 
 namespace Orthanc
@@ -147,6 +147,9 @@
     static DicomToJsonFormat GetDicomFormat(const Json::Value& body,
                                             DicomToJsonFormat defaultFormat);
 
+    static ExpandResourceFlags GetResponseContent(const RestApiGetCall& call,
+                                                  ExpandResourceFlags defaultContent);
+
     static void DocumentDicomFormat(RestApiGetCall& call,
                                     DicomToJsonFormat defaultFormat);
 
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Mon Jun 12 18:42:06 2023 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Fri Jun 16 09:26:33 2023 +0200
@@ -158,11 +158,11 @@
                                  *(mainDicomTags->second.get()),
                                  instanceId->second,
                                  dicomAsJson->second.get(),
-                                 level, format, requestedTags, allowStorageAccess);
+                                 level, format, requestedTags, ExpandResourceFlags_Default, allowStorageAccess);
         }
         else
         {
-          context.ExpandResource(expanded, *resource, level, format, requestedTags, allowStorageAccess);
+          context.ExpandResource(expanded, *resource, level, format, requestedTags, ExpandResourceFlags_Default, allowStorageAccess);
         }
 
         if (expanded.type() == Json::objectValue)
@@ -288,7 +288,7 @@
 
     Json::Value json;
     if (OrthancRestApi::GetContext(call).ExpandResource(
-          json, call.GetUriComponent("id", ""), resourceType, format, requestedTags, true /* allowStorageAccess */))
+          json, call.GetUriComponent("id", ""), resourceType, format, requestedTags, ExpandResourceFlags_Default, true /* allowStorageAccess */))
     {
       call.GetOutput().AnswerJson(json);
     }
@@ -421,6 +421,30 @@
   }
 
 
+  static void GetInstanceFileUntiPixelData(RestApiGetCall& call)
+  {
+    if (call.IsDocumentation())
+    {
+      call.GetDocumentation()
+        .SetTag("Instances")
+        .SetSummary("Download DICOM Header (without the pixel data)")
+        .SetDescription("Download one DICOM instance header")
+        .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest")
+        .SetHttpHeader("Accept", "This HTTP header can be set to retrieve the DICOM instance in DICOMweb format")
+        .AddAnswerType(MimeType_Dicom, "The DICOM instance");
+      return;
+    }
+
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    std::string publicId = call.GetUriComponent("id", "");
+
+    std::string buffer;
+    context.ReadDicomUntilPixelData(buffer, publicId);
+    call.GetOutput().AnswerBuffer(buffer, MimeType_Binary);
+  }
+
+
   static void ExportInstanceFile(RestApiPostCall& call)
   {
     if (call.IsDocumentation())
@@ -3355,12 +3379,13 @@
     Json::Value result = Json::arrayValue;
 
     const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human);
+    ExpandResourceFlags expandResource = OrthancRestApi::GetResponseContent(call, ExpandResourceFlags_Default);
 
     for (std::list<std::string>::const_iterator
            it = a.begin(); it != a.end(); ++it)
     {
       Json::Value resource;
-      if (OrthancRestApi::GetContext(call).ExpandResource(resource, *it, end, format, requestedTags, true /* allowStorageAccess */))
+      if (OrthancRestApi::GetContext(call).ExpandResource(resource, *it, end, format, requestedTags, expandResource, true /* allowStorageAccess */))
       {
         result.append(resource);
       }
@@ -3479,7 +3504,7 @@
     const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human);
 
     Json::Value resource;
-    if (OrthancRestApi::GetContext(call).ExpandResource(resource, current, end, format, requestedTags, true /* allowStorageAccess */))
+    if (OrthancRestApi::GetContext(call).ExpandResource(resource, current, end, format, requestedTags, ExpandResourceFlags_Default, true /* allowStorageAccess */))
     {
       call.GetOutput().AnswerJson(resource);
     }
@@ -3886,7 +3911,7 @@
           Json::Value item;
           std::set<DicomTag> emptyRequestedTags;  // not supported for bulk content
 
-          if (OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format, emptyRequestedTags, true /* allowStorageAccess */))
+          if (OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format, emptyRequestedTags, ExpandResourceFlags_Default, true /* allowStorageAccess */))
           {
             if (metadata)
             {
@@ -3911,7 +3936,7 @@
           std::set<DicomTag> emptyRequestedTags;  // not supported for bulk content
 
           if (index.LookupResourceType(level, *it) &&
-              OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format, emptyRequestedTags, true /* allowStorageAccess */))
+              OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format, emptyRequestedTags, ExpandResourceFlags_Default, true /* allowStorageAccess */))
           {
             if (metadata)
             {
@@ -4010,6 +4035,7 @@
     Register("/studies/{id}/module-patient", GetModule<ResourceType_Study, DicomModule_Patient>);
 
     Register("/instances/{id}/file", GetInstanceFile);
+    Register("/instances/{id}/file-until-pixel-data", GetInstanceFileUntiPixelData);
     Register("/instances/{id}/export", ExportInstanceFile);
     Register("/instances/{id}/tags", GetInstanceTags);
     Register("/instances/{id}/simplified-tags", GetInstanceSimplifiedTags);
--- a/OrthancServer/Sources/OrthancWebDav.cpp	Mon Jun 12 18:42:06 2023 +0200
+++ b/OrthancServer/Sources/OrthancWebDav.cpp	Fri Jun 16 09:26:33 2023 +0200
@@ -261,7 +261,7 @@
       Json::Value resource;
       std::set<DicomTag> emptyRequestedTags;  // not supported for webdav
 
-      if (context_.ExpandResource(resource, publicId, level_, DicomToJsonFormat_Human, emptyRequestedTags, true /* allowStorageAccess */))
+      if (context_.ExpandResource(resource, publicId, level_, DicomToJsonFormat_Human, emptyRequestedTags, ExpandResourceFlags_Default, true /* allowStorageAccess */))
       {
         if (success_)
         {
--- a/OrthancServer/Sources/ServerContext.cpp	Mon Jun 12 18:42:06 2023 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Fri Jun 16 09:26:33 2023 +0200
@@ -1133,32 +1133,47 @@
   void ServerContext::ReadDicomForHeader(std::string& dicom,
                                          const std::string& instancePublicId)
   {
-    if (!ReadDicomUntilPixelData(dicom, instancePublicId))
-    {
-      ReadDicom(dicom, instancePublicId);
-    }
+    ReadDicomUntilPixelData(dicom, instancePublicId);
   }
 
   bool ServerContext::ReadDicomUntilPixelData(std::string& dicom,
                                               const std::string& instancePublicId)
   {
+    FileInfo attachment;
+    int64_t revision;  // Ignored
+
+    // if the attachment exists as such return it directly
+    if (index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_DicomUntilPixelData))
+    {
+      StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
+      accessor.Read(dicom, attachment);
+      return true;
+    }
+
+    // if the storage area can not read part of files, return the whole file
     if (!area_.HasReadRange())
     {
-      return false;
+      ReadDicom(dicom, instancePublicId);
+      return true;
     }
-    
-    FileInfo attachment;
-    int64_t revision;  // Ignored
+
+    // else, read the start of the dicom file
+
     if (!index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_Dicom))
     {
       throw OrthancException(ErrorCode_InternalError,
-                             "Unable to read the DICOM file of instance " + instancePublicId);
+                            "Unable to read the DICOM file of instance " + instancePublicId);
+    }
+
+    // if the attachment is compressed, return the whole file
+    if (attachment.GetCompressionType() != CompressionType_None)
+    {
+      ReadDicom(dicom, instancePublicId);
+      return true;
     }
 
     std::string s;
-
-    if (attachment.GetCompressionType() == CompressionType_None &&
-        index_.LookupMetadata(s, revision, instancePublicId, ResourceType_Instance,
+    if (index_.LookupMetadata(s, revision, instancePublicId, ResourceType_Instance,
                               MetadataType_Instance_PixelDataOffset) &&
         !s.empty())
     {
@@ -1179,7 +1194,8 @@
       }
     }
 
-    return false;
+    ReadDicom(dicom, instancePublicId);
+    return true;
   }
   
 
@@ -1991,7 +2007,8 @@
   static void SerializeExpandedResource(Json::Value& target,
                                         const ExpandedResource& resource,
                                         DicomToJsonFormat format,
-                                        const std::set<DicomTag>& requestedTags)
+                                        const std::set<DicomTag>& requestedTags,
+                                        ExpandResourceFlags expandFlags)
   {
     target = Json::objectValue;
 
@@ -2019,42 +2036,45 @@
         throw OrthancException(ErrorCode_InternalError);
     }
 
-    switch (resource.GetLevel())
+    if (expandFlags & ExpandResourceFlags_IncludeChildren)
     {
-      case ResourceType_Patient:
-      case ResourceType_Study:
-      case ResourceType_Series:
+      switch (resource.GetLevel())
       {
-        Json::Value c = Json::arrayValue;
+        case ResourceType_Patient:
+        case ResourceType_Study:
+        case ResourceType_Series:
+        {
+          Json::Value c = Json::arrayValue;
+
+          for (std::list<std::string>::const_iterator
+                  it = resource.childrenIds_.begin(); it != resource.childrenIds_.end(); ++it)
+          {
+            c.append(*it);
+          }
 
-        for (std::list<std::string>::const_iterator
-                it = resource.childrenIds_.begin(); it != resource.childrenIds_.end(); ++it)
-        {
-          c.append(*it);
+          if (resource.GetLevel() == ResourceType_Patient)
+          {
+            target["Studies"] = c;
+          }
+          else if (resource.GetLevel() == ResourceType_Study)
+          {
+            target["Series"] = c;
+          }
+          else
+          {
+            target["Instances"] = c;
+          }
+          break;
         }
 
-        if (resource.GetLevel() == ResourceType_Patient)
-        {
-          target["Studies"] = c;
-        }
-        else if (resource.GetLevel() == ResourceType_Study)
-        {
-          target["Series"] = c;
-        }
-        else
-        {
-          target["Instances"] = c;
-        }
-        break;
+        case ResourceType_Instance:
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
       }
-
-      case ResourceType_Instance:
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
     }
-
+    
     switch (resource.GetLevel())
     {
       case ResourceType_Patient:
@@ -2117,26 +2137,29 @@
     }
 
     // serialize tags
+    if (expandFlags & ExpandResourceFlags_IncludeMainDicomTags)
+    {
+      static const char* const MAIN_DICOM_TAGS = "MainDicomTags";
+      static const char* const PATIENT_MAIN_DICOM_TAGS = "PatientMainDicomTags";
 
-    static const char* const MAIN_DICOM_TAGS = "MainDicomTags";
-    static const char* const PATIENT_MAIN_DICOM_TAGS = "PatientMainDicomTags";
-
-    DicomMap mainDicomTags;
-    resource.GetMainDicomTags().ExtractResourceInformation(mainDicomTags, resource.GetLevel());
+      DicomMap mainDicomTags;
+      resource.GetMainDicomTags().ExtractResourceInformation(mainDicomTags, resource.GetLevel());
 
-    target[MAIN_DICOM_TAGS] = Json::objectValue;
-    FromDcmtkBridge::ToJson(target[MAIN_DICOM_TAGS], mainDicomTags, format);
-    
-    if (resource.GetLevel() == ResourceType_Study)
-    {
-      DicomMap patientMainDicomTags;
-      resource.GetMainDicomTags().ExtractPatientInformation(patientMainDicomTags);
+      target[MAIN_DICOM_TAGS] = Json::objectValue;
+      FromDcmtkBridge::ToJson(target[MAIN_DICOM_TAGS], mainDicomTags, format);
+      
+      if (resource.GetLevel() == ResourceType_Study)
+      {
+        DicomMap patientMainDicomTags;
+        resource.GetMainDicomTags().ExtractPatientInformation(patientMainDicomTags);
 
-      target[PATIENT_MAIN_DICOM_TAGS] = Json::objectValue;
-      FromDcmtkBridge::ToJson(target[PATIENT_MAIN_DICOM_TAGS], patientMainDicomTags, format);
+        target[PATIENT_MAIN_DICOM_TAGS] = Json::objectValue;
+        FromDcmtkBridge::ToJson(target[PATIENT_MAIN_DICOM_TAGS], patientMainDicomTags, format);
+      }
     }
 
-    if (requestedTags.size() > 0)
+    if ((expandFlags & ExpandResourceFlags_IncludeRequestedTags)
+        && requestedTags.size() > 0)
     {
       static const char* const REQUESTED_TAGS = "RequestedTags";
 
@@ -2148,6 +2171,7 @@
 
     }
 
+    if (expandFlags & ExpandResourceFlags_IncludeLabels)
     {
       Json::Value labels = Json::arrayValue;
 
@@ -2382,13 +2406,14 @@
                                      ResourceType level,
                                      DicomToJsonFormat format,
                                      const std::set<DicomTag>& requestedTags,
+                                     ExpandResourceFlags expandFlags,
                                      bool allowStorageAccess)
   {
     std::string unusedInstanceId;
     Json::Value* unusedDicomAsJson = NULL;
     DicomMap unusedMainDicomTags;
 
-    return ExpandResource(target, publicId, unusedMainDicomTags, unusedInstanceId, unusedDicomAsJson, level, format, requestedTags, allowStorageAccess);
+    return ExpandResource(target, publicId, unusedMainDicomTags, unusedInstanceId, unusedDicomAsJson, level, format, requestedTags, expandFlags, allowStorageAccess);
   }
 
   bool ServerContext::ExpandResource(Json::Value& target,
@@ -2399,13 +2424,14 @@
                                      ResourceType level,
                                      DicomToJsonFormat format,
                                      const std::set<DicomTag>& requestedTags,
+                                     ExpandResourceFlags expandFlags,
                                      bool allowStorageAccess)
   {
     ExpandedResource resource;
 
-    if (ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, level, requestedTags, ExpandResourceFlags_Default, allowStorageAccess))
+    if (ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, level, requestedTags, expandFlags, allowStorageAccess))
     {
-      SerializeExpandedResource(target, resource, format, requestedTags);
+      SerializeExpandedResource(target, resource, format, requestedTags, expandFlags);
       return true;
     }
 
--- a/OrthancServer/Sources/ServerContext.h	Mon Jun 12 18:42:06 2023 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Fri Jun 16 09:26:33 2023 +0200
@@ -566,6 +566,7 @@
                         ResourceType level,
                         DicomToJsonFormat format,
                         const std::set<DicomTag>& requestedTags,
+                        ExpandResourceFlags expandFlags,
                         bool allowStorageAccess);
 
     bool ExpandResource(Json::Value& target,
@@ -576,6 +577,7 @@
                         ResourceType level,
                         DicomToJsonFormat format,
                         const std::set<DicomTag>& requestedTags,
+                        ExpandResourceFlags expandFlags,
                         bool allowStorageAccess);
 
     bool ExpandResource(ExpandedResource& target,