changeset 4514:5b929e6b3c36

removal of "dicom-as-json" attachments
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 16 Feb 2021 12:18:41 +0100
parents 1f455b86b054
children 8734caa12448
files NEWS OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h OrthancFramework/Sources/Enumerations.h OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabase.h OrthancServer/Plugins/Engine/OrthancPlugins.cpp OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h OrthancServer/Plugins/Samples/StorageArea/CMakeLists.txt OrthancServer/Plugins/Samples/StorageArea/Plugin.cpp OrthancServer/Sources/Database/IDatabaseWrapper.h OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerEnumerations.cpp OrthancServer/Sources/ServerIndex.cpp OrthancServer/Sources/ServerIndex.h OrthancServer/UnitTestsSources/ServerIndexTests.cpp OrthancServer/UnitTestsSources/UnitTestsMain.cpp
diffstat 21 files changed, 387 insertions(+), 93 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Fri Feb 12 12:13:19 2021 +0100
+++ b/NEWS	Tue Feb 16 12:18:41 2021 +0100
@@ -1,7 +1,28 @@
 Pending changes in the mainline
 ===============================
 
+General
+-------
+
+* BREAKING CHANGE: The "dicom-as-json" attachments are not explicitly stored anymore.
+  If the storage area doesn't support range reading, or if "StorageCompression"
+  is enabled, a new attachment "dicom-until-pixel-data" is generated.
 * New metadata automatically computed at the instance level: "PixelDataOffset"
+
+REST API
+--------
+
+* BREAKING CHANGE: The "/instances/.../tags" route does not report the tags
+  after "Pixel Data" (7fe0,0010) anymore
+
+Plugins
+-------
+
+* New value in enumeration: OrthancPluginDicomToJsonFlags_StopAfterPixelData
+
+Maintenance
+-----------
+
 * Optimization in C-STORE SCP by avoiding an unnecessary DICOM parsing
 * Fix build on big-endian architectures
 * Possibility to generate a static library containing the Orthanc Framework
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Fri Feb 12 12:13:19 2021 +0100
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Tue Feb 16 12:18:41 2021 +0100
@@ -886,7 +886,8 @@
                                       unsigned int maxStringLength,
                                       Encoding encoding,
                                       bool hasCodeExtensions,
-                                      const std::set<DicomTag>& ignoreTagLength)
+                                      const std::set<DicomTag>& ignoreTagLength,
+                                      unsigned int depth)
   {
     if (parent.type() == Json::nullValue)
     {
@@ -925,7 +926,8 @@
       {
         DcmItem* child = sequence.getItem(i);
         Json::Value& v = target.append(Json::objectValue);
-        DatasetToJson(v, *child, format, flags, maxStringLength, encoding, hasCodeExtensions, ignoreTagLength);
+        DatasetToJson(v, *child, format, flags, maxStringLength, encoding, hasCodeExtensions,
+                      ignoreTagLength, depth + 1);
       }
     }
   }
@@ -938,7 +940,8 @@
                                       unsigned int maxStringLength,
                                       Encoding encoding,
                                       bool hasCodeExtensions,
-                                      const std::set<DicomTag>& ignoreTagLength)
+                                      const std::set<DicomTag>& ignoreTagLength,
+                                      unsigned int depth)
   {
     assert(parent.type() == Json::objectValue);
 
@@ -952,6 +955,14 @@
 
       DicomTag tag(FromDcmtkBridge::Convert(element->getTag()));
 
+      // New flag in Orthanc 1.9.1
+      if (depth == 0 &&
+          (flags & DicomToJsonFlags_StopAfterPixelData) &&
+          tag > DICOM_TAG_PIXEL_DATA)
+      {
+        continue;
+      }
+
       /*element->getTag().isPrivate()*/
       if (tag.IsPrivate() &&
           !(flags & DicomToJsonFlags_IncludePrivateTags))    
@@ -978,8 +989,8 @@
         }
       }
 
-      FromDcmtkBridge::ElementToJson(parent, *element, format, flags,
-                                     maxStringLength, encoding, hasCodeExtensions, ignoreTagLength);
+      FromDcmtkBridge::ElementToJson(parent, *element, format, flags, maxStringLength, encoding,
+                                     hasCodeExtensions, ignoreTagLength, depth);
     }
   }
 
@@ -997,7 +1008,7 @@
     Encoding encoding = DetectEncoding(hasCodeExtensions, dataset, defaultEncoding);
 
     target = Json::objectValue;
-    DatasetToJson(target, dataset, format, flags, maxStringLength, encoding, hasCodeExtensions, ignoreTagLength);
+    DatasetToJson(target, dataset, format, flags, maxStringLength, encoding, hasCodeExtensions, ignoreTagLength, 0);
   }
 
 
@@ -1009,7 +1020,7 @@
   {
     std::set<DicomTag> ignoreTagLength;
     target = Json::objectValue;
-    DatasetToJson(target, dataset, format, flags, maxStringLength, Encoding_Ascii, false, ignoreTagLength);
+    DatasetToJson(target, dataset, format, flags, maxStringLength, Encoding_Ascii, false, ignoreTagLength, 0);
   }
 
 
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h	Fri Feb 12 12:13:19 2021 +0100
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h	Tue Feb 16 12:18:41 2021 +0100
@@ -69,7 +69,8 @@
                               unsigned int maxStringLength,
                               Encoding encoding,
                               bool hasCodeExtensions,
-                              const std::set<DicomTag>& ignoreTagLength);
+                              const std::set<DicomTag>& ignoreTagLength,
+                              unsigned int depth);
 
     static void ElementToJson(Json::Value& parent,
                               DcmElement& element,
@@ -78,7 +79,8 @@
                               unsigned int maxStringLength,
                               Encoding dicomEncoding,
                               bool hasCodeExtensions,
-                              const std::set<DicomTag>& ignoreTagLength);
+                              const std::set<DicomTag>& ignoreTagLength,
+                              unsigned int depth);
 
     static void ChangeStringEncoding(DcmItem& dataset,
                                      Encoding source,
--- a/OrthancFramework/Sources/Enumerations.h	Fri Feb 12 12:13:19 2021 +0100
+++ b/OrthancFramework/Sources/Enumerations.h	Tue Feb 16 12:18:41 2021 +0100
@@ -580,6 +580,7 @@
     DicomToJsonFlags_IncludePixelData      = (1 << 3),
     DicomToJsonFlags_ConvertBinaryToAscii  = (1 << 4),
     DicomToJsonFlags_ConvertBinaryToNull   = (1 << 5),
+    DicomToJsonFlags_StopAfterPixelData    = (1 << 6),  // New in Orthanc 1.9.1
 
     // Some predefined combinations
     DicomToJsonFlags_None     = 0,
@@ -587,7 +588,8 @@
                                  DicomToJsonFlags_IncludePixelData | 
                                  DicomToJsonFlags_IncludePrivateTags | 
                                  DicomToJsonFlags_IncludeUnknownTags | 
-                                 DicomToJsonFlags_ConvertBinaryToNull)
+                                 DicomToJsonFlags_ConvertBinaryToNull |
+                                 DicomToJsonFlags_StopAfterPixelData /* added in 1.9.1 */)
   };
   
   enum DicomFromJsonFlags
--- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Fri Feb 12 12:13:19 2021 +0100
+++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Tue Feb 16 12:18:41 2021 +0100
@@ -628,6 +628,40 @@
 }
 
 
+TEST(ParsedDicomFile, ToJsonFlags3)
+{
+  ParsedDicomFile f(false);
+
+  {
+    Uint8 v[2] = { 0, 0 };
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->putAndInsertString(DCM_PatientName, "HELLO^").good());
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->putAndInsertUint8Array(DCM_PixelData, v, 2).good());
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->putAndInsertString(DcmTag(0x07fe1, 0x0010), "WORLD^").good());
+  }
+
+  std::string s;
+  Toolbox::EncodeDataUriScheme(s, "application/octet-stream", std::string(2, '\0'));
+
+  {
+    Json::Value v;
+    f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_IncludePixelData | DicomToJsonFlags_StopAfterPixelData), 0);
+    ASSERT_EQ(Json::objectValue, v.type());
+    ASSERT_EQ(2u, v.size());
+    ASSERT_EQ("HELLO^", v["0010,0010"].asString());
+    ASSERT_EQ(s, v["7fe0,0010"].asString());
+  }
+
+  {
+    Json::Value v;
+    f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_IncludePrivateTags, 0);
+    ASSERT_EQ(Json::objectValue, v.type());
+    ASSERT_EQ(2u, v.size());
+    ASSERT_EQ("HELLO^", v["0010,0010"].asString());
+    ASSERT_EQ("WORLD^", v["7fe1,0010"].asString());
+  }  
+}
+
+
 TEST(DicomFindAnswers, Basic)
 {
   DicomFindAnswers a(false);
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Fri Feb 12 12:13:19 2021 +0100
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Tue Feb 16 12:18:41 2021 +0100
@@ -670,7 +670,7 @@
   }
 
 
-  void OrthancPluginDatabase::ListAvailableAttachments(std::list<FileContentType>& target,
+  void OrthancPluginDatabase::ListAvailableAttachments(std::set<FileContentType>& target,
                                                        int64_t id)
   {
     ResetAnswers();
@@ -690,7 +690,7 @@
       for (std::list<int32_t>::const_iterator 
              it = answerInt32_.begin(); it != answerInt32_.end(); ++it)
       {
-        target.push_back(static_cast<FileContentType>(*it));
+        target.insert(static_cast<FileContentType>(*it));
       }
     }
   }
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h	Fri Feb 12 12:13:19 2021 +0100
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h	Tue Feb 16 12:18:41 2021 +0100
@@ -230,7 +230,7 @@
     virtual bool IsProtectedPatient(int64_t internalId) 
       ORTHANC_OVERRIDE;
 
-    virtual void ListAvailableAttachments(std::list<FileContentType>& target,
+    virtual void ListAvailableAttachments(std::set<FileContentType>& target,
                                           int64_t id) 
       ORTHANC_OVERRIDE;
 
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Fri Feb 12 12:13:19 2021 +0100
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Tue Feb 16 12:18:41 2021 +0100
@@ -1702,6 +1702,7 @@
         static_cast<int>(OrthancPluginDicomToJsonFlags_IncludePixelData) != static_cast<int>(DicomToJsonFlags_IncludePixelData) ||
         static_cast<int>(OrthancPluginDicomToJsonFlags_ConvertBinaryToNull) != static_cast<int>(DicomToJsonFlags_ConvertBinaryToNull) ||
         static_cast<int>(OrthancPluginDicomToJsonFlags_ConvertBinaryToAscii) != static_cast<int>(DicomToJsonFlags_ConvertBinaryToAscii) ||
+        static_cast<int>(OrthancPluginDicomToJsonFlags_StopAfterPixelData) != static_cast<int>(DicomToJsonFlags_StopAfterPixelData) ||
         static_cast<int>(OrthancPluginCreateDicomFlags_DecodeDataUriScheme) != static_cast<int>(DicomFromJsonFlags_DecodeDataUriScheme) ||
         static_cast<int>(OrthancPluginCreateDicomFlags_GenerateIdentifiers) != static_cast<int>(DicomFromJsonFlags_GenerateIdentifiers))
 
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Fri Feb 12 12:13:19 2021 +0100
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Tue Feb 16 12:18:41 2021 +0100
@@ -835,6 +835,7 @@
     OrthancPluginDicomToJsonFlags_IncludePixelData      = (1 << 3),  /*!< Include the pixel data */
     OrthancPluginDicomToJsonFlags_ConvertBinaryToAscii  = (1 << 4),  /*!< Output binary tags as-is, dropping non-ASCII */
     OrthancPluginDicomToJsonFlags_ConvertBinaryToNull   = (1 << 5),  /*!< Signal binary tags as null values */
+    OrthancPluginDicomToJsonFlags_StopAfterPixelData    = (1 << 6),  /*!< Stop processing after pixel data (new in 1.9.1) */
 
     _OrthancPluginDicomToJsonFlags_INTERNAL = 0x7fffffff
   } OrthancPluginDicomToJsonFlags;
--- a/OrthancServer/Plugins/Samples/StorageArea/CMakeLists.txt	Fri Feb 12 12:13:19 2021 +0100
+++ b/OrthancServer/Plugins/Samples/StorageArea/CMakeLists.txt	Tue Feb 16 12:18:41 2021 +0100
@@ -23,4 +23,12 @@
 
 include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake)
 
+set(USE_LEGACY_API OFF CACHE BOOL "Whether to enable support for read-range")
+
+if (USE_LEGACY_API)
+  add_definitions(-DUSE_LEGACY_API=1)
+else()
+  add_definitions(-DUSE_LEGACY_API=0)
+endif()
+
 add_library(PluginTest SHARED Plugin.cpp)
--- a/OrthancServer/Plugins/Samples/StorageArea/Plugin.cpp	Fri Feb 12 12:13:19 2021 +0100
+++ b/OrthancServer/Plugins/Samples/StorageArea/Plugin.cpp	Tue Feb 16 12:18:41 2021 +0100
@@ -26,9 +26,6 @@
 #include <string>
 
 
-#define USE_LEGACY_API 0
-
-
 static OrthancPluginContext* context = NULL;
 
 
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h	Fri Feb 12 12:13:19 2021 +0100
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Tue Feb 16 12:18:41 2021 +0100
@@ -43,6 +43,7 @@
 
 #include <list>
 #include <boost/noncopyable.hpp>
+#include <set>
 
 namespace Orthanc
 {
@@ -153,7 +154,7 @@
 
     virtual bool IsProtectedPatient(int64_t internalId) = 0;
 
-    virtual void ListAvailableAttachments(std::list<FileContentType>& target,
+    virtual void ListAvailableAttachments(std::set<FileContentType>& target,
                                           int64_t id) = 0;
 
     virtual void LogChange(int64_t internalId,
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Fri Feb 12 12:13:19 2021 +0100
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Tue Feb 16 12:18:41 2021 +0100
@@ -803,7 +803,7 @@
   }
 
 
-  void SQLiteDatabaseWrapper::ListAvailableAttachments(std::list<FileContentType>& target,
+  void SQLiteDatabaseWrapper::ListAvailableAttachments(std::set<FileContentType>& target,
                                                        int64_t id)
   {
     target.clear();
@@ -814,7 +814,7 @@
 
     while (s.Step())
     {
-      target.push_back(static_cast<FileContentType>(s.ColumnInt(0)));
+      target.insert(static_cast<FileContentType>(s.ColumnInt(0)));
     }
   }
 
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h	Fri Feb 12 12:13:19 2021 +0100
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h	Tue Feb 16 12:18:41 2021 +0100
@@ -234,7 +234,7 @@
                                   FileContentType attachment)
       ORTHANC_OVERRIDE;
 
-    virtual void ListAvailableAttachments(std::list<FileContentType>& target,
+    virtual void ListAvailableAttachments(std::set<FileContentType>& target,
                                           int64_t id)
       ORTHANC_OVERRIDE;
 
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Fri Feb 12 12:13:19 2021 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Tue Feb 16 12:18:41 2021 +0100
@@ -1576,12 +1576,12 @@
 
     const std::string resourceType = call.GetFullUri() [0];
     const std::string publicId = call.GetUriComponent("id", "");
-    std::list<FileContentType> attachments;
+    std::set<FileContentType> attachments;
     OrthancRestApi::GetIndex(call).ListAvailableAttachments(attachments, publicId, StringToResourceType(resourceType.c_str()));
 
     Json::Value result = Json::arrayValue;
 
-    for (std::list<FileContentType>::const_iterator 
+    for (std::set<FileContentType>::const_iterator 
            it = attachments.begin(); it != attachments.end(); ++it)
     {
       result.append(EnumerationToString(*it));
--- a/OrthancServer/Sources/ServerContext.cpp	Fri Feb 12 12:13:19 2021 +0100
+++ b/OrthancServer/Sources/ServerContext.cpp	Tue Feb 16 12:18:41 2021 +0100
@@ -583,12 +583,14 @@
       ServerIndex::Attachments attachments;
       attachments.push_back(dicomInfo);
 
-      FileInfo jsonInfo;
-      if (true /* TODO - !area_.HasReadRange() || !hasPixelDataOffset || compression != CompressionType_DicomAsJson */)
+      FileInfo dicomUntilPixelData;
+      if (hasPixelDataOffset &&
+          (!area_.HasReadRange() ||
+           compression != CompressionType_None))
       {
-        jsonInfo = accessor.Write(dicomAsJson.toStyledString(), 
-                                  FileContentType_DicomAsJson, compression, storeMD5_);
-        attachments.push_back(jsonInfo);
+        dicomUntilPixelData = accessor.Write(dicom.GetBufferData(), pixelDataOffset, 
+                                             FileContentType_DicomUntilPixelData, compression, storeMD5_);
+        attachments.push_back(dicomUntilPixelData);
       }
 
       typedef std::map<MetadataType, std::string>  InstanceMetadata;
@@ -610,9 +612,9 @@
       {
         accessor.Remove(dicomInfo);
 
-        if (jsonInfo.IsValid())
+        if (dicomUntilPixelData.IsValid())
         {
-          accessor.Remove(jsonInfo);
+          accessor.Remove(dicomUntilPixelData);
         }
       }
 
@@ -807,61 +809,178 @@
   }
 
 
+  static void InjectEmptyPixelData(Json::Value& dicomAsJson)
+  {
+    // This is for backward compatibility with Orthanc <= 1.9.0
+    Json::Value pixelData = Json::objectValue;
+    pixelData["Name"] = "PixelData";
+    pixelData["Type"] = "Null";
+    pixelData["Value"] = Json::nullValue;
+
+    dicomAsJson["7fe0,0010"] = pixelData;
+  }
+
+  
   void ServerContext::ReadDicomAsJson(Json::Value& result,
                                       const std::string& instancePublicId,
                                       const std::set<DicomTag>& ignoreTagLength)
   {
-    if (ignoreTagLength.empty())
+    /**
+     * CASE 1: The DICOM file, truncated at pixel data, is available
+     * as an attachment (it was created either because the storage
+     * area does not support range reads, or it "StorageCompression"
+     * is enabled). Simply return this attachment.
+     **/
+    
+    FileInfo attachment;
+
+    if (index_.LookupAttachment(attachment, instancePublicId, FileContentType_DicomUntilPixelData))
     {
-      std::string tmp;
+      std::string dicom;
 
       {
-        FileInfo attachment;
-        if (index_.LookupAttachment(attachment, instancePublicId, FileContentType_DicomAsJson))
+        StorageAccessor accessor(area_, GetMetricsRegistry());
+        accessor.Read(dicom, attachment);
+      }
+
+      ParsedDicomFile parsed(dicom);
+      OrthancConfiguration::DefaultDicomDatasetToJson(result, parsed, ignoreTagLength);
+      InjectEmptyPixelData(result);
+    }
+    else
+    {
+      /**
+       * The truncated DICOM file is not stored as a standalone
+       * attachment. Lookup whether the pixel data offset has already
+       * been computed for this instance.
+       **/
+    
+      bool hasPixelDataOffset;
+      uint64_t pixelDataOffset;
+
+      {
+        std::string s;
+        if (index_.LookupMetadata(s, instancePublicId, ResourceType_Instance,
+                                  MetadataType_Instance_PixelDataOffset))
         {
-          StorageAccessor accessor(area_, GetMetricsRegistry());
-          accessor.Read(tmp, attachment);
+          hasPixelDataOffset = false;
+
+          if (!s.empty())
+          {
+            try
+            {
+              pixelDataOffset = boost::lexical_cast<uint64_t>(s);
+              hasPixelDataOffset = true;
+            }
+            catch (boost::bad_lexical_cast&)
+            {
+            }
+          }
+
+          if (!hasPixelDataOffset)
+          {
+            LOG(ERROR) << "Metadata \"PixelDataOffset\" is corrupted for instance: " << instancePublicId;
+          }
         }
         else
         {
-          // The "DICOM as JSON" summary is not available from the Orthanc
-          // store (most probably deleted), reconstruct it from the DICOM file
-          std::string dicom;
-          ReadDicom(dicom, instancePublicId);
+          // This instance was created by a version of Orthanc <= 1.9.0
+          hasPixelDataOffset = false;
+        }
+      }
+
+
+      if (hasPixelDataOffset &&
+          area_.HasReadRange() &&
+          index_.LookupAttachment(attachment, instancePublicId, FileContentType_Dicom) &&
+          attachment.GetCompressionType() == CompressionType_None)
+      {
+        /**
+         * CASE 2: The pixel data offset is known, AND that a range read
+         * can be used to retrieve the truncated DICOM file. Note that
+         * this case cannot be used if "StorageCompression" option is
+         * "true".
+         **/
+      
+        StorageAccessor accessor(area_, GetMetricsRegistry());
+        std::unique_ptr<IMemoryBuffer> dicom(
+          area_.ReadRange(attachment.GetUuid(), FileContentType_Dicom, 0, pixelDataOffset));
 
-          LOG(INFO) << "Reconstructing the missing DICOM-as-JSON summary for instance: "
-                    << instancePublicId;
-    
-          ParsedDicomFile parsed(dicom);
+        if (dicom.get() == NULL)
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+        else
+        {
+          assert(dicom->GetSize() == pixelDataOffset);
+          ParsedDicomFile parsed(dicom->GetData(), dicom->GetSize());
+          OrthancConfiguration::DefaultDicomDatasetToJson(result, parsed, ignoreTagLength);
+          InjectEmptyPixelData(result);
+        }
+      }
+      else if (ignoreTagLength.empty() &&
+               index_.LookupAttachment(attachment, instancePublicId, FileContentType_DicomAsJson))
+      {
+        /**
+         * CASE 3: This instance was created using Orthanc <=
+         * 1.9.0. Reuse the old "DICOM-as-JSON" attachment if available.
+         * This is for backward compatibility: A call to
+         * "/tools/invalidate-tags" or to one flavors of
+         * "/.../.../reconstruct" will disable this case.
+         **/
+      
+        std::string dicomAsJson;
 
-          Json::Value summary;
-          OrthancConfiguration::DefaultDicomDatasetToJson(summary, parsed);
+        {
+          StorageAccessor accessor(area_, GetMetricsRegistry());
+          accessor.Read(dicomAsJson, attachment);
+        }
 
-          tmp = summary.toStyledString();
+        if (!Toolbox::ReadJson(result, dicomAsJson))
+        {
+          throw OrthancException(ErrorCode_CorruptedFile,
+                                 "Corrupted DICOM-as-JSON attachment of instance: " + instancePublicId);
+        }
+      }
+      else
+      {
+        /**
+         * CASE 4: Neither the truncated DICOM file is accessible, nor
+         * the DICOM-as-JSON summary. We have to retrieve the full DICOM
+         * file from the storage area.
+         **/
+
+        std::string dicom;
+        ReadDicom(dicom, instancePublicId);
 
-          if (!AddAttachment(instancePublicId, FileContentType_DicomAsJson,
-                             tmp.c_str(), tmp.size()))
+        ParsedDicomFile parsed(dicom);
+        OrthancConfiguration::DefaultDicomDatasetToJson(result, parsed, ignoreTagLength);
+
+        if (!hasPixelDataOffset)
+        {
+          /**
+           * The pixel data offset was never computed for this
+           * instance, which indicates that it was created using
+           * Orthanc <= 1.9.0, or that calls to
+           * "LookupPixelDataOffset()" from earlier versions of
+           * Orthanc have failed. Try again this precomputation now
+           * for future calls.
+           **/
+          if (DicomStreamReader::LookupPixelDataOffset(pixelDataOffset, dicom) &&
+              pixelDataOffset < dicom.size())
           {
-            throw OrthancException(ErrorCode_InternalError,
-                                   "Cannot associate the DICOM-as-JSON summary to instance: " + instancePublicId);
+            index_.SetMetadata(instancePublicId, MetadataType_Instance_PixelDataOffset,
+                               boost::lexical_cast<std::string>(pixelDataOffset));
+
+            if (!area_.HasReadRange() ||
+                compressionEnabled_ != CompressionType_None)
+            {
+              AddAttachment(instancePublicId, FileContentType_DicomUntilPixelData,
+                            dicom.empty() ? NULL: dicom.c_str(), pixelDataOffset);
+            }
           }
         }
       }
-
-      if (!Toolbox::ReadJson(result, tmp))
-      {
-        throw OrthancException(ErrorCode_CorruptedFile);
-      }
-    }
-    else
-    {
-      // The "DicomAsJson" attachment might have stored some tags as
-      // "too long". We are forced to re-parse the DICOM file.
-      std::string dicom;
-      ReadDicom(dicom, instancePublicId);
-
-      ParsedDicomFile parsed(dicom);
-      OrthancConfiguration::DefaultDicomDatasetToJson(result, parsed, ignoreTagLength);
     }
   }
 
--- a/OrthancServer/Sources/ServerEnumerations.cpp	Fri Feb 12 12:13:19 2021 +0100
+++ b/OrthancServer/Sources/ServerEnumerations.cpp	Tue Feb 16 12:18:41 2021 +0100
@@ -74,6 +74,7 @@
 
     dictContentType_.Add(FileContentType_Dicom, "dicom");
     dictContentType_.Add(FileContentType_DicomAsJson, "dicom-as-json");
+    dictContentType_.Add(FileContentType_DicomUntilPixelData, "dicom-until-pixel-data");
   }
 
   void RegisterUserMetadata(int metadata,
--- a/OrthancServer/Sources/ServerIndex.cpp	Fri Feb 12 12:13:19 2021 +0100
+++ b/OrthancServer/Sources/ServerIndex.cpp	Tue Feb 16 12:18:41 2021 +0100
@@ -975,11 +975,13 @@
           }
         }
 
-        // New in Orthanc 1.9.1
-        SetInstanceMetadata(content, instanceMetadata, instanceId,
-                            MetadataType_Instance_PixelDataOffset,
-                            (hasPixelDataOffset ? 
-                             boost::lexical_cast<std::string>(pixelDataOffset) : ""));
+        if (hasPixelDataOffset)
+        {
+          // New in Orthanc 1.9.1
+          SetInstanceMetadata(content, instanceMetadata, instanceId,
+                              MetadataType_Instance_PixelDataOffset,
+                              boost::lexical_cast<std::string>(pixelDataOffset));
+        }
         
         const DicomValue* value;
         if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL &&
@@ -1900,7 +1902,7 @@
   }
 
 
-  void ServerIndex::ListAvailableAttachments(std::list<FileContentType>& target,
+  void ServerIndex::ListAvailableAttachments(std::set<FileContentType>& target,
                                              const std::string& publicId,
                                              ResourceType expectedType)
   {
@@ -2030,10 +2032,10 @@
 
       ResourceType thisType = db_.GetResourceType(resource);
 
-      std::list<FileContentType> f;
+      std::set<FileContentType> f;
       db_.ListAvailableAttachments(f, resource);
 
-      for (std::list<FileContentType>::const_iterator
+      for (std::set<FileContentType>::const_iterator
              it = f.begin(); it != f.end(); ++it)
       {
         FileInfo attachment;
--- a/OrthancServer/Sources/ServerIndex.h	Fri Feb 12 12:13:19 2021 +0100
+++ b/OrthancServer/Sources/ServerIndex.h	Tue Feb 16 12:18:41 2021 +0100
@@ -218,7 +218,7 @@
                         ResourceType expectedType,
                         MetadataType type);
 
-    void ListAvailableAttachments(std::list<FileContentType>& target,
+    void ListAvailableAttachments(std::set<FileContentType>& target,
                                   const std::string& publicId,
                                   ResourceType expectedType);
 
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Fri Feb 12 12:13:19 2021 +0100
+++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Tue Feb 16 12:18:41 2021 +0100
@@ -37,6 +37,7 @@
 #include "../../OrthancFramework/Sources/Compatibility.h"
 #include "../../OrthancFramework/Sources/FileStorage/FilesystemStorage.h"
 #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h"
+#include "../../OrthancFramework/Sources/Images/Image.h"
 #include "../../OrthancFramework/Sources/Logging.h"
 
 #include "../Sources/Database/SQLiteDatabaseWrapper.h"
@@ -734,6 +735,7 @@
     {
       DicomMap summary;
       OrthancConfiguration::DefaultExtractDicomSummary(summary, toStore->GetParsedDicomFile());
+      toStore->SetOrigin(DicomInstanceOrigin::FromPlugins());
 
       DicomTransferSyntax transferSyntax;
       bool hasTransferSyntax = dicom.LookupTransferSyntax(transferSyntax);
@@ -798,6 +800,10 @@
 
 TEST(ServerIndex, Overwrite)
 {
+  // Create a dummy 1x1 image
+  Image image(PixelFormat_Grayscale8, 1, 1, false);
+  reinterpret_cast<uint8_t*>(image.GetBuffer()) [0] = 128;
+
   for (unsigned int i = 0; i < 2; i++)
   {
     bool overwrite = (i == 0);
@@ -831,6 +837,10 @@
     {
       ParsedDicomFile dicom(instance, GetDefaultDicomEncoding(), false /* be strict */);
 
+      // Add a pixel data so as to have one "FileContentType_DicomUntilPixelData"
+      // (because of "context.SetCompressionEnabled(true)")
+      dicom.EmbedImage(image);
+      
       DicomInstanceHasher hasher(instance);
       
       std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromParsedDicomFile(dicom));
@@ -842,15 +852,20 @@
       ASSERT_EQ(id, id2);
     }
 
-    FileInfo dicom1, json1;
+    {
+      FileInfo nope;
+      ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, id, FileContentType_DicomAsJson));
+    }
+
+    FileInfo dicom1, pixelData1;
     ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom1, id, FileContentType_Dicom));
-    ASSERT_TRUE(context.GetIndex().LookupAttachment(json1, id, FileContentType_DicomAsJson));
+    ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData1, id, FileContentType_DicomUntilPixelData));
 
     context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
                                            countStudies, countSeries, countInstances);
     ASSERT_EQ(1u, countInstances);
-    ASSERT_EQ(dicom1.GetCompressedSize() + json1.GetCompressedSize(), diskSize);
-    ASSERT_EQ(dicom1.GetUncompressedSize() + json1.GetUncompressedSize(), uncompressedSize);
+    ASSERT_EQ(dicom1.GetCompressedSize() + pixelData1.GetCompressedSize(), diskSize);
+    ASSERT_EQ(dicom1.GetUncompressedSize() + pixelData1.GetUncompressedSize(), uncompressedSize);
 
     Json::Value tmp;
     context.ReadDicomAsJson(tmp, id);
@@ -870,6 +885,9 @@
 
       ParsedDicomFile dicom(instance2, GetDefaultDicomEncoding(), false /* be strict */);
 
+      // Add a pixel data so as to have one "FileContentType_DicomUntilPixelData"
+      dicom.EmbedImage(image);
+
       std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromParsedDicomFile(dicom));
       toStore->SetOrigin(DicomInstanceOrigin::FromPlugins());
 
@@ -879,22 +897,27 @@
       ASSERT_EQ(id, id2);
     }
 
-    FileInfo dicom2, json2;
+    {
+      FileInfo nope;
+      ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, id, FileContentType_DicomAsJson));
+    }
+
+    FileInfo dicom2, pixelData2;
     ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom2, id, FileContentType_Dicom));
-    ASSERT_TRUE(context.GetIndex().LookupAttachment(json2, id, FileContentType_DicomAsJson));
+    ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData2, id, FileContentType_DicomUntilPixelData));
 
     context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
                                            countStudies, countSeries, countInstances);
     ASSERT_EQ(1u, countInstances);
-    ASSERT_EQ(dicom2.GetCompressedSize() + json2.GetCompressedSize(), diskSize);
-    ASSERT_EQ(dicom2.GetUncompressedSize() + json2.GetUncompressedSize(), uncompressedSize);
+    ASSERT_EQ(dicom2.GetCompressedSize() + pixelData2.GetCompressedSize(), diskSize);
+    ASSERT_EQ(dicom2.GetUncompressedSize() + pixelData2.GetUncompressedSize(), uncompressedSize);
 
     if (overwrite)
     {
       ASSERT_NE(dicom1.GetUuid(), dicom2.GetUuid());
-      ASSERT_NE(json1.GetUuid(), json2.GetUuid());
+      ASSERT_NE(pixelData1.GetUuid(), pixelData2.GetUuid());
       ASSERT_NE(dicom1.GetUncompressedSize(), dicom2.GetUncompressedSize());
-      ASSERT_NE(json1.GetUncompressedSize(), json2.GetUncompressedSize());
+      ASSERT_NE(pixelData1.GetUncompressedSize(), pixelData2.GetUncompressedSize());
     
       context.ReadDicomAsJson(tmp, id);
       ASSERT_EQ("overwritten", tmp["0010,0010"]["Value"].asString());
@@ -909,9 +932,9 @@
     else
     {
       ASSERT_EQ(dicom1.GetUuid(), dicom2.GetUuid());
-      ASSERT_EQ(json1.GetUuid(), json2.GetUuid());
+      ASSERT_EQ(pixelData1.GetUuid(), pixelData2.GetUuid());
       ASSERT_EQ(dicom1.GetUncompressedSize(), dicom2.GetUncompressedSize());
-      ASSERT_EQ(json1.GetUncompressedSize(), json2.GetUncompressedSize());
+      ASSERT_EQ(pixelData1.GetUncompressedSize(), pixelData2.GetUncompressedSize());
 
       context.ReadDicomAsJson(tmp, id);
       ASSERT_EQ("name", tmp["0010,0010"]["Value"].asString());
@@ -930,3 +953,74 @@
 }
 
 
+TEST(ServerIndex, DicomUntilPixelData)
+{
+  // Create a dummy 1x1 image
+  Image image(PixelFormat_Grayscale8, 1, 1, false);
+  reinterpret_cast<uint8_t*>(image.GetBuffer()) [0] = 128;
+
+  for (unsigned int i = 0; i < 2; i++)
+  {
+    const bool compression = (i == 0);
+    
+    MemoryStorageArea storage;
+    SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
+    db.Open();
+    ServerContext context(db, storage, true /* running unit tests */, 10);
+    context.SetupJobsEngine(true, false);
+    context.SetCompressionEnabled(compression);
+
+    for (unsigned int j = 0; j < 2; j++)
+    {
+      const bool withPixelData = (j == 0);
+
+      ParsedDicomFile dicom(true);
+      
+      if (withPixelData)
+      {
+        dicom.EmbedImage(image);
+      }
+
+      std::string id;
+      size_t dicomSize;
+
+      {
+        std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromParsedDicomFile(dicom));
+        dicomSize = toStore->GetBufferSize();
+        toStore->SetOrigin(DicomInstanceOrigin::FromPlugins());
+        ASSERT_EQ(StoreStatus_Success, context.Store(id, *toStore, StoreInstanceMode_Default));
+      }
+
+      std::set<FileContentType> attachments;
+      context.GetIndex().ListAvailableAttachments(attachments, id, ResourceType_Instance);
+
+      ASSERT_TRUE(attachments.find(FileContentType_Dicom) != attachments.end());
+      
+      if (compression &&
+          withPixelData)
+      {
+        ASSERT_EQ(2u, attachments.size());
+        ASSERT_TRUE(attachments.find(FileContentType_DicomUntilPixelData) != attachments.end());
+      }
+      else
+      {
+        ASSERT_EQ(1u, attachments.size());
+      }
+
+      std::string s;
+      bool found = context.GetIndex().LookupMetadata(s, id, ResourceType_Instance,
+                                                     MetadataType_Instance_PixelDataOffset);
+      
+      if (withPixelData)
+      {
+        ASSERT_TRUE(found);
+        ASSERT_GT(boost::lexical_cast<int>(s), 128 /* length of the DICOM preamble */);
+        ASSERT_LT(boost::lexical_cast<size_t>(s), dicomSize);
+      }
+      else
+      {
+        ASSERT_FALSE(found);        
+      }
+    }
+  }
+}
--- a/OrthancServer/UnitTestsSources/UnitTestsMain.cpp	Fri Feb 12 12:13:19 2021 +0100
+++ b/OrthancServer/UnitTestsSources/UnitTestsMain.cpp	Tue Feb 16 12:18:41 2021 +0100
@@ -332,16 +332,16 @@
       ignoreTagLength.insert(DICOM_TAG_PATIENT_ID);
 
       FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
-                                     DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength);
+                                     DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength, 0);
       ASSERT_TRUE(b.isMember("0010,0010"));
       ASSERT_EQ("Hello", b["0010,0010"].asString());
 
       FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
-                                     DicomToJsonFlags_Default, 3, Encoding_Ascii, false, ignoreTagLength);
+                                     DicomToJsonFlags_Default, 3, Encoding_Ascii, false, ignoreTagLength, 0);
       ASSERT_TRUE(b["0010,0010"].isNull()); // "Hello" has more than 3 characters
 
       FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Full,
-                                     DicomToJsonFlags_Default, 3, Encoding_Ascii, false, ignoreTagLength);
+                                     DicomToJsonFlags_Default, 3, Encoding_Ascii, false, ignoreTagLength, 0);
       ASSERT_TRUE(b["0010,0010"].isObject());
       ASSERT_EQ("PatientName", b["0010,0010"]["Name"].asString());
       ASSERT_EQ("TooLong", b["0010,0010"]["Type"].asString());
@@ -349,7 +349,7 @@
 
       ignoreTagLength.insert(DICOM_TAG_PATIENT_NAME);
       FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
-                                     DicomToJsonFlags_Default, 3, Encoding_Ascii, false, ignoreTagLength);
+                                     DicomToJsonFlags_Default, 3, Encoding_Ascii, false, ignoreTagLength, 0);
       ASSERT_EQ("Hello", b["0010,0010"].asString());
     }
 
@@ -375,7 +375,7 @@
       Json::Value b;
       std::set<DicomTag> ignoreTagLength;
       FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
-                                     DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength);
+                                     DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength, 0);
       ASSERT_EQ("Hello", b["0010,0010"].asString());
     }
 
@@ -388,7 +388,7 @@
         Json::Value b;
         std::set<DicomTag> ignoreTagLength;
         FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
-                                       DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength);
+                                       DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength, 0);
         ASSERT_EQ(Json::arrayValue, b["0008,1110"].type());
         ASSERT_EQ(2u, b["0008,1110"].size());
       
@@ -407,7 +407,7 @@
         Json::Value b;
         std::set<DicomTag> ignoreTagLength;
         FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Full,
-                                       DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength);
+                                       DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength, 0);
 
         Json::Value c;
         Toolbox::SimplifyDicomAsJson(c, b, DicomToJsonFormat_Human);