changeset 3958:596912ebab5f c-get

integration mainline->c-get
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 20 May 2020 17:03:24 +0200
parents 66879215cbf3 (current diff) 6e14f2da7c7e (diff)
children 76a24be12912
files CMakeLists.txt NEWS OrthancServer/DefaultDicomImageDecoder.h Plugins/Include/orthanc/OrthancCPlugin.h Plugins/Samples/GdcmDecoder/CMakeLists.txt Plugins/Samples/GdcmDecoder/GdcmConfiguration.cmake Plugins/Samples/GdcmDecoder/GdcmDecoderCache.cpp Plugins/Samples/GdcmDecoder/GdcmDecoderCache.h Plugins/Samples/GdcmDecoder/GdcmImageDecoder.cpp Plugins/Samples/GdcmDecoder/GdcmImageDecoder.h Plugins/Samples/GdcmDecoder/Plugin.cpp Plugins/Samples/GdcmDecoder/README Resources/Configuration.json
diffstat 55 files changed, 2527 insertions(+), 2054 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Wed May 20 16:38:33 2020 +0200
+++ b/CMakeLists.txt	Wed May 20 17:03:24 2020 +0200
@@ -91,6 +91,7 @@
   OrthancServer/ServerEnumerations.cpp
   OrthancServer/ServerIndex.cpp
   OrthancServer/ServerJobs/ArchiveJob.cpp
+  OrthancServer/ServerJobs/CleaningInstancesJob.cpp
   OrthancServer/ServerJobs/DicomModalityStoreJob.cpp
   OrthancServer/ServerJobs/DicomMoveScuJob.cpp
   OrthancServer/ServerJobs/LuaJobManager.cpp
--- a/Core/Compression/HierarchicalZipWriter.h	Wed May 20 16:38:33 2020 +0200
+++ b/Core/Compression/HierarchicalZipWriter.h	Wed May 20 17:03:24 2020 +0200
@@ -140,7 +140,7 @@
       return indexer_.GetCurrentDirectoryPath();
     }
 
-    void Write(const char* data, size_t length)
+    void Write(const void* data, size_t length)
     {
       writer_.Write(data, length);
     }
--- a/Core/Compression/ZipWriter.cpp	Wed May 20 16:38:33 2020 +0200
+++ b/Core/Compression/ZipWriter.cpp	Wed May 20 17:03:24 2020 +0200
@@ -227,7 +227,7 @@
   }
 
 
-  void ZipWriter::Write(const char* data, size_t length)
+  void ZipWriter::Write(const void* data, size_t length)
   {
     if (!hasFileInZip_)
     {
@@ -236,17 +236,19 @@
 
     const size_t maxBytesInAStep = std::numeric_limits<int32_t>::max();
 
+    const char* p = reinterpret_cast<const char*>(data);
+    
     while (length > 0)
     {
       int bytes = static_cast<int32_t>(length <= maxBytesInAStep ? length : maxBytesInAStep);
 
-      if (zipWriteInFileInZip(pimpl_->file_, data, bytes))
+      if (zipWriteInFileInZip(pimpl_->file_, p, bytes))
       {
         throw OrthancException(ErrorCode_CannotWriteFile,
                                "Cannot write data to ZIP archive: " + path_);
       }
       
-      data += bytes;
+      p += bytes;
       length -= bytes;
     }
   }
--- a/Core/Compression/ZipWriter.h	Wed May 20 16:38:33 2020 +0200
+++ b/Core/Compression/ZipWriter.h	Wed May 20 17:03:24 2020 +0200
@@ -101,7 +101,7 @@
 
     void OpenFile(const char* path);
 
-    void Write(const char* data, size_t length);
+    void Write(const void* data, size_t length);
 
     void Write(const std::string& data);
   };
--- a/Core/DicomNetworking/DicomStoreUserConnection.cpp	Wed May 20 16:38:33 2020 +0200
+++ b/Core/DicomNetworking/DicomStoreUserConnection.cpp	Wed May 20 17:03:24 2020 +0200
@@ -491,40 +491,35 @@
         uncompressedSyntaxes.insert(DicomTransferSyntax_BigEndianExplicit);
       }
 
-      std::unique_ptr<IDicomTranscoder::TranscodedDicom> transcoded(
-        transcoder.TranscodeToParsed(*dicom, buffer, size, uncompressedSyntaxes, false));
+      IDicomTranscoder::DicomImage source;
+      source.AcquireParsed(dicom.release());
+      source.SetExternalBuffer(buffer, size);
 
-      // WARNING: Below this point, "transcoded->GetDicom()" is possibly
-      // a reference to "*dicom", if the DCMTK transcoder was used
+      const std::string sourceUid = IDicomTranscoder::GetSopInstanceUid(source.GetParsed());
       
-      if (transcoded.get() == NULL ||
-          transcoded->GetDicom().getDataset() == NULL)
+      IDicomTranscoder::DicomImage transcoded;
+      if (transcoder.Transcode(transcoded, source, uncompressedSyntaxes, false))
       {
-        throw OrthancException(
-          ErrorCode_NotImplemented,
-          "Cannot transcode from \"" + std::string(GetTransferSyntaxUid(inputSyntax)) +
-          "\" to an uncompressed syntax for modality: " +
-          GetParameters().GetRemoteModality().GetApplicationEntityTitle());
-      }
-      else if (transcoded->HasSopInstanceUidChanged())
-      {
-        throw OrthancException(ErrorCode_Plugin, "The transcoder has changed the SOP "
-                               "instance UID while transcoding to an uncompressed transfer syntax");
-      }
-      else
-      {
-        DicomTransferSyntax transcodedSyntax;
-
-        // Sanity check
-        if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transcodedSyntax, transcoded->GetDicom()) ||
-            accepted.find(transcodedSyntax) == accepted.end())
+        if (sourceUid != IDicomTranscoder::GetSopInstanceUid(transcoded.GetParsed()))
         {
-          throw OrthancException(ErrorCode_InternalError);
+          throw OrthancException(ErrorCode_Plugin, "The transcoder has changed the SOP "
+                                 "instance UID while transcoding to an uncompressed transfer syntax");
         }
         else
         {
-          Store(sopClassUid, sopInstanceUid, transcoded->GetDicom(),
-                hasMoveOriginator, moveOriginatorAET, moveOriginatorID);
+          DicomTransferSyntax transcodedSyntax;
+          
+          // Sanity check
+          if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transcodedSyntax, transcoded.GetParsed()) ||
+              accepted.find(transcodedSyntax) == accepted.end())
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+          else
+          {
+            Store(sopClassUid, sopInstanceUid, transcoded.GetParsed(),
+                  hasMoveOriginator, moveOriginatorAET, moveOriginatorID);
+          }
         }
       }
     }
--- a/Core/DicomParsing/DcmtkTranscoder.cpp	Wed May 20 16:38:33 2020 +0200
+++ b/Core/DicomParsing/DcmtkTranscoder.cpp	Wed May 20 17:03:24 2020 +0200
@@ -55,81 +55,32 @@
 
 namespace Orthanc
 {
-  static uint16_t GetBitsStored(DcmDataset& dataset)
+  static bool GetBitsStored(uint16_t& bitsStored,
+                            DcmDataset& dataset)
   {
-    uint16_t bitsStored;
-    if (dataset.findAndGetUint16(DCM_BitsStored, bitsStored).good())
-    {
-      return bitsStored;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadFileFormat,
-                             "Missing \"Bits Stored\" tag in DICOM instance");
-    }      
+    return dataset.findAndGetUint16(DCM_BitsStored, bitsStored).good();
   }
 
   
-  static std::string GetSopInstanceUid(DcmDataset& dataset)
-  {
-    const char* v = NULL;
-
-    if (dataset.findAndGetString(DCM_SOPInstanceUID, v).good() &&
-        v != NULL)
-    {
-      return std::string(v);
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadFileFormat, "File without SOP instance UID");
-    }
-  }
-
-  
-  static void CheckSopInstanceUid(DcmFileFormat& dicom,
-                                  const std::string& sopInstanceUid,
-                                  bool mustEqual)
-  {
-    if (dicom.getDataset() == NULL)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-      
-    bool ok;
-      
-    if (mustEqual)
-    {
-      ok = (GetSopInstanceUid(*dicom.getDataset()) == sopInstanceUid);
-    }
-    else
-    {
-      ok = (GetSopInstanceUid(*dicom.getDataset()) != sopInstanceUid);
-    }
-
-    if (!ok)
-    {
-      throw OrthancException(ErrorCode_InternalError,
-                             mustEqual ? "The SOP instance UID has changed unexpectedly during transcoding" :
-                             "The SOP instance UID has not changed as expected during transcoding");
-    }
-  }
-    
-
   void DcmtkTranscoder::SetLossyQuality(unsigned int quality)
   {
     if (quality <= 0 ||
         quality > 100)
     {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
+      throw OrthancException(
+        ErrorCode_ParameterOutOfRange,
+        "The quality for lossy transcoding must be an integer between 1 and 100, received: " +
+        boost::lexical_cast<std::string>(quality));
     }
     else
     {
+      LOG(INFO) << "Quality for lossy transcoding using DCMTK is set to: " << quality;
       lossyQuality_ = quality;
     }
   }
 
     
-  bool DcmtkTranscoder::InplaceTranscode(bool& hasSopInstanceUidChanged /* out */,
+  bool DcmtkTranscoder::InplaceTranscode(DicomTransferSyntax& selectedSyntax /* out */,
                                          DcmFileFormat& dicom,
                                          const std::set<DicomTransferSyntax>& allowedSyntaxes,
                                          bool allowNewSopInstanceUid) 
@@ -139,8 +90,6 @@
       throw OrthancException(ErrorCode_InternalError);
     }
 
-    hasSopInstanceUidChanged = false;
-
     DicomTransferSyntax syntax;
     if (!FromDcmtkBridge::LookupOrthancTransferSyntax(syntax, dicom))
     {
@@ -148,8 +97,10 @@
                              "Cannot determine the transfer syntax");
     }
 
-    const uint16_t bitsStored = GetBitsStored(*dicom.getDataset());
-    std::string sourceSopInstanceUid = GetSopInstanceUid(*dicom.getDataset());
+    uint16_t bitsStored;
+    bool hasBitsStored = GetBitsStored(bitsStored, *dicom.getDataset());
+    
+    std::string sourceSopInstanceUid = IDicomTranscoder::GetSopInstanceUid(dicom);
     
     if (allowedSyntaxes.find(syntax) != allowedSyntaxes.end())
     {
@@ -160,43 +111,42 @@
     if (allowedSyntaxes.find(DicomTransferSyntax_LittleEndianImplicit) != allowedSyntaxes.end() &&
         FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_LittleEndianImplicit, NULL))
     {
-      CheckSopInstanceUid(dicom, sourceSopInstanceUid, true);
+      selectedSyntax = DicomTransferSyntax_LittleEndianImplicit;
       return true;
     }
 
     if (allowedSyntaxes.find(DicomTransferSyntax_LittleEndianExplicit) != allowedSyntaxes.end() &&
         FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_LittleEndianExplicit, NULL))
     {
-      CheckSopInstanceUid(dicom, sourceSopInstanceUid, true);
+      selectedSyntax = DicomTransferSyntax_LittleEndianExplicit;
       return true;
     }
       
     if (allowedSyntaxes.find(DicomTransferSyntax_BigEndianExplicit) != allowedSyntaxes.end() &&
         FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_BigEndianExplicit, NULL))
     {
-      CheckSopInstanceUid(dicom, sourceSopInstanceUid, true);
+      selectedSyntax = DicomTransferSyntax_BigEndianExplicit;
       return true;
     }
 
     if (allowedSyntaxes.find(DicomTransferSyntax_DeflatedLittleEndianExplicit) != allowedSyntaxes.end() &&
         FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_DeflatedLittleEndianExplicit, NULL))
     {
-      CheckSopInstanceUid(dicom, sourceSopInstanceUid, true);
+      selectedSyntax = DicomTransferSyntax_DeflatedLittleEndianExplicit;
       return true;
     }
 
 #if ORTHANC_ENABLE_DCMTK_JPEG == 1
     if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess1) != allowedSyntaxes.end() &&
         allowNewSopInstanceUid &&
-        bitsStored == 8)
+        (!hasBitsStored || bitsStored == 8))
     {
       // Check out "dcmjpeg/apps/dcmcjpeg.cc"
       DJ_RPLossy parameters(lossyQuality_);
         
       if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess1, &parameters))
       {
-        CheckSopInstanceUid(dicom, sourceSopInstanceUid, false);
-        hasSopInstanceUidChanged = true;
+        selectedSyntax = DicomTransferSyntax_JPEGProcess1;
         return true;
       }
     }
@@ -205,14 +155,13 @@
 #if ORTHANC_ENABLE_DCMTK_JPEG == 1
     if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess2_4) != allowedSyntaxes.end() &&
         allowNewSopInstanceUid &&
-        bitsStored <= 12)
+        (!hasBitsStored || bitsStored <= 12))
     {
       // Check out "dcmjpeg/apps/dcmcjpeg.cc"
       DJ_RPLossy parameters(lossyQuality_);
       if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess2_4, &parameters))
       {
-        CheckSopInstanceUid(dicom, sourceSopInstanceUid, false);
-        hasSopInstanceUidChanged = true;
+        selectedSyntax = DicomTransferSyntax_JPEGProcess2_4;
         return true;
       }
     }
@@ -226,7 +175,7 @@
                                0 /* opt_point_transform */);
       if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess14, &parameters))
       {
-        CheckSopInstanceUid(dicom, sourceSopInstanceUid, true);
+        selectedSyntax = DicomTransferSyntax_JPEGProcess14;
         return true;
       }
     }
@@ -240,7 +189,7 @@
                                0 /* opt_point_transform */);
       if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess14SV1, &parameters))
       {
-        CheckSopInstanceUid(dicom, sourceSopInstanceUid, true);
+        selectedSyntax = DicomTransferSyntax_JPEGProcess14SV1;
         return true;
       }
     }
@@ -259,7 +208,7 @@
        **/              
       if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGLSLossless, &parameters))
       {
-        CheckSopInstanceUid(dicom, sourceSopInstanceUid, true);
+        selectedSyntax = DicomTransferSyntax_JPEGLSLossless;
         return true;
       }
     }
@@ -279,8 +228,7 @@
        **/              
       if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGLSLossy, &parameters))
       {
-        CheckSopInstanceUid(dicom, sourceSopInstanceUid, false);
-        hasSopInstanceUidChanged = true;
+        selectedSyntax = DicomTransferSyntax_JPEGLSLossy;
         return true;
       }
     }
@@ -322,81 +270,61 @@
   }
 
 
-
-  bool DcmtkTranscoder::TranscodeParsedToBuffer(
-    std::string& target /* out */,
-    DicomTransferSyntax& sourceSyntax /* out */,
-    bool& hasSopInstanceUidChanged /* out */,
-    DcmFileFormat& dicom /* in, possibly modified */,
-    DicomTransferSyntax targetSyntax,
-    bool allowNewSopInstanceUid)
+  bool DcmtkTranscoder::Transcode(DicomImage& target,
+                                  DicomImage& source /* in, "GetParsed()" possibly modified */,
+                                  const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                                  bool allowNewSopInstanceUid)
   {
-    if (dicom.getDataset() == NULL)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, dicom))
+    target.Clear();
+    
+    DicomTransferSyntax sourceSyntax;
+    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, source.GetParsed()))
     {
       LOG(ERROR) << "Unsupport transfer syntax for transcoding";
       return false;
     }
 
-    std::set<DicomTransferSyntax> tmp;
-    tmp.insert(targetSyntax);
+#if !defined(NDEBUG)
+    const std::string sourceSopInstanceUid = GetSopInstanceUid(source.GetParsed());
+#endif
 
-    if (InplaceTranscode(hasSopInstanceUidChanged, dicom, tmp, allowNewSopInstanceUid))
+    DicomTransferSyntax targetSyntax;
+    if (allowedSyntaxes.find(sourceSyntax) != allowedSyntaxes.end())
     {
+      // No transcoding is needed
+      target.AcquireParsed(source);
+      target.AcquireBuffer(source);
+      return true;
+    }
+    else if (InplaceTranscode(targetSyntax, source.GetParsed(),
+                              allowedSyntaxes, allowNewSopInstanceUid))
+    {   
+      // Sanity check
       DicomTransferSyntax targetSyntax2;
-      if (FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax2, dicom) &&
+      if (FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax2, source.GetParsed()) &&
           targetSyntax == targetSyntax2 &&
-          dicom.getDataset() != NULL)
+          allowedSyntaxes.find(targetSyntax2) != allowedSyntaxes.end())
       {
-        FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom.getDataset());
+        target.AcquireParsed(source);
+        source.Clear();
+        
+#if !defined(NDEBUG)
+        // Only run the sanity check in debug mode
+        CheckTranscoding(target, sourceSyntax, sourceSopInstanceUid,
+                         allowedSyntaxes, allowNewSopInstanceUid);
+#endif
+        
         return true;
       }
       else
       {
         throw OrthancException(ErrorCode_InternalError);
-      }      
-    }
-    else
-    {
-      return false;
-    }    
-  }
-
-
-  IDicomTranscoder::TranscodedDicom* DcmtkTranscoder::TranscodeToParsed(
-    DcmFileFormat& dicom /* in, possibly modified */,
-    const void* buffer /* in, same DICOM file as "dicom" */,
-    size_t size,
-    const std::set<DicomTransferSyntax>& allowedSyntaxes,
-    bool allowNewSopInstanceUid)
-  {
-    DicomTransferSyntax sourceSyntax;
-    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, dicom))
-    {
-      LOG(ERROR) << "Unsupport transfer syntax for transcoding";
-      return NULL;
-    }
-
-    bool hasSopInstanceUidChanged;
-    
-    if (allowedSyntaxes.find(sourceSyntax) != allowedSyntaxes.end())
-    {
-      // No transcoding is needed
-      return TranscodedDicom::CreateFromExternal(dicom, false /* no change in UID */);
-    }
-    else if (InplaceTranscode(hasSopInstanceUidChanged, dicom,
-                              allowedSyntaxes, allowNewSopInstanceUid))
-    {
-      return TranscodedDicom::CreateFromExternal(dicom, hasSopInstanceUidChanged);
+      }  
     }
     else
     {
       // Cannot transcode
-      return NULL;
+      return false;
     }
   }
 }
--- a/Core/DicomParsing/DcmtkTranscoder.h	Wed May 20 16:38:33 2020 +0200
+++ b/Core/DicomParsing/DcmtkTranscoder.h	Wed May 20 17:03:24 2020 +0200
@@ -50,6 +50,11 @@
   private:
     unsigned int  lossyQuality_;
     
+    bool InplaceTranscode(DicomTransferSyntax& selectedSyntax /* out */,
+                          DcmFileFormat& dicom,
+                          const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                          bool allowNewSopInstanceUid);
+    
   public:
     DcmtkTranscoder() :
       lossyQuality_(90)
@@ -63,25 +68,11 @@
       return lossyQuality_;
     }
     
-    bool InplaceTranscode(bool& hasSopInstanceUidChanged /* out */,
-                          DcmFileFormat& dicom,
-                          const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                          bool allowNewSopInstanceUid);
-    
     static bool IsSupported(DicomTransferSyntax syntax);
 
-    virtual bool TranscodeParsedToBuffer(std::string& target /* out */,
-                                         DicomTransferSyntax& sourceSyntax /* out */,
-                                         bool& hasSopInstanceUidChanged /* out */,
-                                         DcmFileFormat& dicom /* in, possibly modified */,
-                                         DicomTransferSyntax targetSyntax,
-                                         bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
-
-    virtual TranscodedDicom* TranscodeToParsed(
-      DcmFileFormat& dicom /* in, possibly modified */,
-      const void* buffer /* in, same DICOM file as "dicom" */,
-      size_t size,
-      const std::set<DicomTransferSyntax>& allowedSyntaxes,
-      bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
+    virtual bool Transcode(DicomImage& target,
+                           DicomImage& source /* in, "GetParsed()" possibly modified */,
+                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                           bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
   };
 }
--- a/Core/DicomParsing/FromDcmtkBridge.cpp	Wed May 20 16:38:33 2020 +0200
+++ b/Core/DicomParsing/FromDcmtkBridge.cpp	Wed May 20 17:03:24 2020 +0200
@@ -1299,9 +1299,9 @@
     {
       DicomTransferSyntax sourceSyntax;
       bool known = LookupOrthancTransferSyntax(sourceSyntax, dicom);
-
-      if (!dicom.getDataset()->chooseRepresentation(xfer, representation).good() ||
-          !dicom.getDataset()->canWriteXfer(xfer) ||
+      
+      if (!dicom.chooseRepresentation(xfer, representation).good() ||
+          !dicom.canWriteXfer(xfer) ||
           !dicom.validateMetaInfo(xfer, EWM_updateMeta).good())
       {
         return false;
@@ -1780,9 +1780,13 @@
     }
 
     DcmPixelData& pixelData = dynamic_cast<DcmPixelData&>(*element);
+
+    E_TransferSyntax repType;
+    const DcmRepresentationParameter *repParam = NULL;
+    pixelData.getCurrentRepresentationKey(repType, repParam);
+    
     DcmPixelSequence* pixelSequence = NULL;
-    if (!pixelData.getEncapsulatedRepresentation
-        (dataset.getCurrentXfer(), NULL, pixelSequence).good())
+    if (!pixelData.getEncapsulatedRepresentation(repType, repParam, pixelSequence).good())
     {
       return NULL;
     }
--- a/Core/DicomParsing/IDicomTranscoder.cpp	Wed May 20 16:38:33 2020 +0200
+++ b/Core/DicomParsing/IDicomTranscoder.cpp	Wed May 20 17:03:24 2020 +0200
@@ -35,77 +35,404 @@
 #include "IDicomTranscoder.h"
 
 #include "../OrthancException.h"
+#include "FromDcmtkBridge.h"
+#include "ParsedDicomFile.h"
 
 #include <dcmtk/dcmdata/dcfilefo.h>
+#include <dcmtk/dcmdata/dcdeftag.h>
 
 namespace Orthanc
 {
-  IDicomTranscoder::TranscodedDicom::TranscodedDicom(bool hasSopInstanceUidChanged) :
-    external_(NULL),
-    hasSopInstanceUidChanged_(hasSopInstanceUidChanged)
+  IDicomTranscoder::TranscodingType IDicomTranscoder::GetTranscodingType(DicomTransferSyntax target,
+                                                                         DicomTransferSyntax source)
   {
+    if (target == source)
+    {
+      return TranscodingType_Lossless;
+    }
+    else if (target == DicomTransferSyntax_LittleEndianImplicit ||
+             target == DicomTransferSyntax_LittleEndianExplicit ||
+             target == DicomTransferSyntax_BigEndianExplicit ||
+             target == DicomTransferSyntax_DeflatedLittleEndianExplicit ||
+             target == DicomTransferSyntax_JPEGProcess14 ||
+             target == DicomTransferSyntax_JPEGProcess14SV1 ||
+             target == DicomTransferSyntax_JPEGLSLossless ||
+             target == DicomTransferSyntax_JPEG2000LosslessOnly ||
+             target == DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly)
+    {
+      return TranscodingType_Lossless;
+    }
+    else if (target == DicomTransferSyntax_JPEGProcess1 ||
+             target == DicomTransferSyntax_JPEGProcess2_4 ||
+             target == DicomTransferSyntax_JPEGLSLossy ||
+             target == DicomTransferSyntax_JPEG2000 ||
+             target == DicomTransferSyntax_JPEG2000Multicomponent)
+    {
+      return TranscodingType_Lossy;
+    }
+    else
+    {
+      return TranscodingType_Unknown;
+    }
   }
-  
+
 
-  IDicomTranscoder::TranscodedDicom*
-  IDicomTranscoder::TranscodedDicom::CreateFromExternal(DcmFileFormat& dicom,
-                                                        bool hasSopInstanceUidChanged)
+  std::string IDicomTranscoder::GetSopInstanceUid(DcmFileFormat& dicom)
   {
-    std::unique_ptr<TranscodedDicom> transcoded(new TranscodedDicom(hasSopInstanceUidChanged));
-    transcoded->external_ = &dicom;
-    return transcoded.release();
-  }        
+    if (dicom.getDataset() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    
+    DcmDataset& dataset = *dicom.getDataset();
+    
+    const char* v = NULL;
+
+    if (dataset.findAndGetString(DCM_SOPInstanceUID, v).good() &&
+        v != NULL)
+    {
+      return std::string(v);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "File without SOP instance UID");
+    }
+  }
+
 
-  
-  IDicomTranscoder::TranscodedDicom*
-  IDicomTranscoder::TranscodedDicom::CreateFromInternal(DcmFileFormat* dicom,
-                                                        bool hasSopInstanceUidChanged)
+  void IDicomTranscoder::CheckTranscoding(IDicomTranscoder::DicomImage& transcoded,
+                                          DicomTransferSyntax sourceSyntax,
+                                          const std::string& sourceSopInstanceUid,
+                                          const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                                          bool allowNewSopInstanceUid)
   {
-    if (dicom == NULL)
+    DcmFileFormat& parsed = transcoded.GetParsed();
+    
+    if (parsed.getDataset() == NULL)
     {
-      throw OrthancException(ErrorCode_NullPointer);
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    std::string targetSopInstanceUid = GetSopInstanceUid(parsed);
+
+    if (parsed.getDataset()->tagExists(DCM_PixelData))
+    {
+      if (!allowNewSopInstanceUid && (targetSopInstanceUid != sourceSopInstanceUid))
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
     }
     else
     {
-      std::unique_ptr<TranscodedDicom> transcoded(new TranscodedDicom(hasSopInstanceUidChanged));
-      transcoded->internal_.reset(dicom);
-      return transcoded.release();
+      if (targetSopInstanceUid != sourceSopInstanceUid)
+      {
+        throw OrthancException(ErrorCode_InternalError,
+                               "No pixel data: Transcoding must not change the SOP instance UID");
+      }
+    }
+
+    DicomTransferSyntax targetSyntax;
+    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax, parsed))
+    {
+      return;  // Unknown transfer syntax, cannot do further test
+    }
+
+    if (allowedSyntaxes.find(sourceSyntax) != allowedSyntaxes.end())
+    {
+      // No transcoding should have happened
+      if (targetSopInstanceUid != sourceSopInstanceUid)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+        
+    if (allowedSyntaxes.find(targetSyntax) == allowedSyntaxes.end())
+    {
+      throw OrthancException(ErrorCode_InternalError, "An incorrect output transfer syntax was chosen");
+    }
+    
+    if (parsed.getDataset()->tagExists(DCM_PixelData))
+    {
+      switch (GetTranscodingType(targetSyntax, sourceSyntax))
+      {
+        case TranscodingType_Lossy:
+          if (targetSopInstanceUid == sourceSopInstanceUid)
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+          break;
+
+        case TranscodingType_Lossless:
+          if (targetSopInstanceUid != sourceSopInstanceUid)
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+          break;
+
+        default:
+          break;
+      }
+    }
+  }
+    
+
+  void IDicomTranscoder::DicomImage::Parse()
+  {
+    if (parsed_.get() != NULL)
+    {
+      // Already parsed
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (buffer_.get() != NULL)
+    {
+      if (isExternalBuffer_)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+      else
+      {
+        parsed_.reset(FromDcmtkBridge::LoadFromMemoryBuffer(
+                        buffer_->empty() ? NULL : buffer_->c_str(), buffer_->size()));
+        
+        if (parsed_.get() == NULL)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat);
+        }      
+      }
+    }
+    else if (isExternalBuffer_)
+    {
+      parsed_.reset(FromDcmtkBridge::LoadFromMemoryBuffer(externalBuffer_, externalSize_));
+      
+      if (parsed_.get() == NULL)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }      
+    }
+    else
+    {
+      // No buffer is available
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+  
+  
+  void IDicomTranscoder::DicomImage::Serialize()
+  {
+    if (parsed_.get() == NULL ||
+        buffer_.get() != NULL ||
+        isExternalBuffer_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (parsed_->getDataset() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      buffer_.reset(new std::string);
+      FromDcmtkBridge::SaveToMemoryBuffer(*buffer_, *parsed_->getDataset());
     }
   }
 
   
-  DcmFileFormat& IDicomTranscoder::TranscodedDicom::GetDicom() const
+  IDicomTranscoder::DicomImage::DicomImage() :
+    isExternalBuffer_(false)
+  {
+  }
+
+
+  void IDicomTranscoder::DicomImage::Clear()
   {
-    if (internal_.get() != NULL)
+    parsed_.reset(NULL);
+    buffer_.reset(NULL);
+    isExternalBuffer_ = false;
+  }
+
+  
+  void IDicomTranscoder::DicomImage::AcquireParsed(ParsedDicomFile& parsed)
+  {
+    AcquireParsed(parsed.ReleaseDcmtkObject());
+  }
+  
+      
+  void IDicomTranscoder::DicomImage::AcquireParsed(DcmFileFormat* parsed)
+  {
+    if (parsed == NULL)
     {
-      return *internal_.get();
+      throw OrthancException(ErrorCode_NullPointer);
     }
-    else if (external_ != NULL)
+    else if (parsed->getDataset() == NULL)
     {
-      return *external_;
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else if (parsed_.get() != NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
     }
     else
     {
-      // Probably results from a call to "ReleaseDicom()"
+      parsed_.reset(parsed);
+    }
+  }
+  
+
+  void IDicomTranscoder::DicomImage::AcquireParsed(DicomImage& other)
+  {
+    AcquireParsed(other.ReleaseParsed());
+  }
+  
+
+  void IDicomTranscoder::DicomImage::AcquireBuffer(std::string& buffer /* will be swapped */)
+  {
+    if (buffer_.get() != NULL ||
+        isExternalBuffer_)
+    {
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
     }
+    else
+    {
+      buffer_.reset(new std::string);
+      buffer_->swap(buffer);
+    }
   }
 
 
-  DcmFileFormat* IDicomTranscoder::TranscodedDicom::ReleaseDicom()
+  void IDicomTranscoder::DicomImage::AcquireBuffer(DicomImage& other)
   {
-    if (internal_.get() != NULL)
+    if (buffer_.get() != NULL ||
+        isExternalBuffer_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (other.isExternalBuffer_)
+    {
+      assert(other.buffer_.get() == NULL);
+      isExternalBuffer_ = true;
+      externalBuffer_ = other.externalBuffer_;
+      externalSize_ = other.externalSize_;
+    }
+    else if (other.buffer_.get() != NULL)
+    {
+      buffer_.reset(other.buffer_.release());
+    }
+    else
     {
-      return internal_.release();
+      buffer_.reset(NULL);
+    }    
+  }
+
+  
+  void IDicomTranscoder::DicomImage::SetExternalBuffer(const void* buffer,
+                                                       size_t size)
+  {
+    if (buffer_.get() != NULL ||
+        isExternalBuffer_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      isExternalBuffer_ = true;
+      externalBuffer_ = buffer;
+      externalSize_ = size;
     }
-    else if (external_ != NULL)
+  }
+
+
+  void IDicomTranscoder::DicomImage::SetExternalBuffer(const std::string& buffer)
+  {
+    SetExternalBuffer(buffer.empty() ? NULL : buffer.c_str(), buffer.size());
+  }
+
+
+  DcmFileFormat& IDicomTranscoder::DicomImage::GetParsed()
+  {
+    if (parsed_.get() != NULL)
     {
-      return new DcmFileFormat(*external_);  // Clone
+      return *parsed_;
+    }
+    else if (buffer_.get() != NULL ||
+             isExternalBuffer_)
+    {
+      Parse();
+      return *parsed_;
     }
     else
     {
-      // Probably results from a call to "ReleaseDicom()"
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);      
-    }        
+      throw OrthancException(
+        ErrorCode_BadSequenceOfCalls,
+        "AcquireParsed(), AcquireBuffer() or SetExternalBuffer() should have been called");
+    }
+  }
+  
+
+  DcmFileFormat* IDicomTranscoder::DicomImage::ReleaseParsed()
+  {
+    if (parsed_.get() != NULL)
+    {
+      buffer_.reset(NULL);
+      return parsed_.release();
+    }
+    else if (buffer_.get() != NULL ||
+             isExternalBuffer_)
+    {
+      Parse();
+      buffer_.reset(NULL);
+      return parsed_.release();
+    }
+    else
+    {
+      throw OrthancException(
+        ErrorCode_BadSequenceOfCalls,
+        "AcquireParsed(), AcquireBuffer() or SetExternalBuffer() should have been called");
+    }
+  }
+
+
+  ParsedDicomFile* IDicomTranscoder::DicomImage::ReleaseAsParsedDicomFile()
+  {
+    return ParsedDicomFile::AcquireDcmtkObject(ReleaseParsed());
+  }
+
+  
+  const void* IDicomTranscoder::DicomImage::GetBufferData()
+  {
+    if (isExternalBuffer_)
+    {
+      assert(buffer_.get() == NULL);
+      return externalBuffer_;
+    }
+    else
+    {    
+      if (buffer_.get() == NULL)
+      {
+        Serialize();
+      }
+
+      assert(buffer_.get() != NULL);
+      return buffer_->empty() ? NULL : buffer_->c_str();
+    }
+  }
+
+  
+  size_t IDicomTranscoder::DicomImage::GetBufferSize()
+  {
+    if (isExternalBuffer_)
+    {
+      assert(buffer_.get() == NULL);
+      return externalSize_;
+    }
+    else
+    {    
+      if (buffer_.get() == NULL)
+      {
+        Serialize();
+      }
+
+      assert(buffer_.get() != NULL);
+      return buffer_->size();
+    }
   }
 }
--- a/Core/DicomParsing/IDicomTranscoder.h	Wed May 20 16:38:33 2020 +0200
+++ b/Core/DicomParsing/IDicomTranscoder.h	Wed May 20 17:03:24 2020 +0200
@@ -47,60 +47,85 @@
    * WARNING: This class might be called from several threads at
    * once. Make sure to implement proper locking.
    **/
+
+  class ParsedDicomFile;
   
   class IDicomTranscoder : public boost::noncopyable
   {
   public:
+    class DicomImage : public boost::noncopyable
+    {
+    private:
+      std::unique_ptr<DcmFileFormat>  parsed_;
+      std::unique_ptr<std::string>    buffer_;
+      bool                            isExternalBuffer_;
+      const void*                     externalBuffer_;
+      size_t                          externalSize_;
+
+      void Parse();
+
+      void Serialize();
+
+      DcmFileFormat* ReleaseParsed();
+
+    public:
+      DicomImage();
+      
+      void Clear();
+      
+      // Calling this method will invalidate the "ParsedDicomFile" object
+      void AcquireParsed(ParsedDicomFile& parsed);
+      
+      void AcquireParsed(DcmFileFormat* parsed);
+
+      void AcquireParsed(DicomImage& other);
+
+      void AcquireBuffer(std::string& buffer /* will be swapped */);
+
+      void AcquireBuffer(DicomImage& other);
+
+      void SetExternalBuffer(const void* buffer,
+                             size_t size);
+
+      void SetExternalBuffer(const std::string& buffer);
+
+      DcmFileFormat& GetParsed();
+
+      ParsedDicomFile* ReleaseAsParsedDicomFile();
+
+      const void* GetBufferData();
+
+      size_t GetBufferSize();
+    };
+
+
+  protected:
+    enum TranscodingType
+    {
+      TranscodingType_Lossy,
+      TranscodingType_Lossless,
+      TranscodingType_Unknown
+    };
+
+    static TranscodingType GetTranscodingType(DicomTransferSyntax target,
+                                              DicomTransferSyntax source);
+
+    static void CheckTranscoding(DicomImage& transcoded,
+                                 DicomTransferSyntax sourceSyntax,
+                                 const std::string& sourceSopInstanceUid,
+                                 const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                                 bool allowNewSopInstanceUid);
+    
+  public:    
     virtual ~IDicomTranscoder()
     {
     }
 
-    virtual bool TranscodeParsedToBuffer(std::string& target /* out */,
-                                         DicomTransferSyntax& sourceSyntax /* out */,
-                                         bool& hasSopInstanceUidChanged /* out */,
-                                         DcmFileFormat& dicom /* in, possibly modified */,
-                                         DicomTransferSyntax targetSyntax,
-                                         bool allowNewSopInstanceUid) = 0;
-
-
-    class TranscodedDicom : public boost::noncopyable
-    {
-    private:
-      std::unique_ptr<DcmFileFormat>  internal_;
-      DcmFileFormat*                  external_;
-      bool                            hasSopInstanceUidChanged_;
-
-      TranscodedDicom(bool hasSopInstanceUidChanged);
-
-    public:
-      static TranscodedDicom* CreateFromExternal(DcmFileFormat& dicom,
-                                                 bool hasSopInstanceUidChanged);
-        
-      static TranscodedDicom* CreateFromInternal(DcmFileFormat* dicom,
-                                                 bool hasSopInstanceUidChanged);
+    virtual bool Transcode(DicomImage& target,
+                           DicomImage& source /* in, "GetParsed()" possibly modified */,
+                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                           bool allowNewSopInstanceUid) = 0;
 
-      // TODO - Is this information used somewhere?
-      bool HasSopInstanceUidChanged() const
-      {
-        return hasSopInstanceUidChanged_;
-      }
-      
-      DcmFileFormat& GetDicom() const;
-
-      DcmFileFormat* ReleaseDicom();
-    };
-    
-    /**
-     * Transcoding flavor that creates a new parsed DICOM file. A
-     * "std::set<>" is used to give the possible plugin the
-     * possibility to do a single parsing for all the possible
-     * transfer syntaxes. This flavor is used by C-STORE.
-     **/
-    virtual TranscodedDicom* TranscodeToParsed(
-      DcmFileFormat& dicom /* in, possibly modified */,
-      const void* buffer /* in, same DICOM file as "dicom" */,
-      size_t size,
-      const std::set<DicomTransferSyntax>& allowedSyntaxes,
-      bool allowNewSopInstanceUid) = 0;
+    static std::string GetSopInstanceUid(DcmFileFormat& dicom);
   };
 }
--- a/Core/DicomParsing/Internals/DicomImageDecoder.cpp	Wed May 20 16:38:33 2020 +0200
+++ b/Core/DicomParsing/Internals/DicomImageDecoder.cpp	Wed May 20 17:03:24 2020 +0200
@@ -810,7 +810,7 @@
      **/
     
     {
-      LOG(INFO) << "Decoding a compressed image by converting its transfer syntax to Little Endian";
+      LOG(INFO) << "Trying to decode a compressed image by transcoding it to Little Endian Explicit";
 
       std::unique_ptr<DcmDataset> converted(dynamic_cast<DcmDataset*>(dataset.clone()));
       converted->chooseRepresentation(EXS_LittleEndianExplicit, NULL);
@@ -821,8 +821,18 @@
       }
     }
 
-    throw OrthancException(ErrorCode_BadFileFormat,
-                           "Cannot decode a DICOM image with the built-in decoder");
+    DicomTransferSyntax s;
+    if (FromDcmtkBridge::LookupOrthancTransferSyntax(s, dataset.getCurrentXfer()))
+    {
+      throw OrthancException(ErrorCode_NotImplemented,
+                             "The built-in DCMTK decoder cannot decode some DICOM instance "
+                             "whose transfer syntax is: " + std::string(GetTransferSyntaxUid(s)));
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NotImplemented,
+                             "The built-in DCMTK decoder cannot decode some DICOM instance");
+    }
   }
 
 
--- a/Core/DicomParsing/MemoryBufferTranscoder.cpp	Wed May 20 16:38:33 2020 +0200
+++ b/Core/DicomParsing/MemoryBufferTranscoder.cpp	Wed May 20 17:03:24 2020 +0200
@@ -37,105 +37,73 @@
 #include "../OrthancException.h"
 #include "FromDcmtkBridge.h"
 
+#if !defined(NDEBUG)  // For debugging
+#  include "ParsedDicomFile.h"
+#endif
+
 namespace Orthanc
 {
-  MemoryBufferTranscoder::MemoryBufferTranscoder()
+  static void CheckTargetSyntax(const std::string& transcoded,
+                                const std::set<DicomTransferSyntax>& allowedSyntaxes)
   {
-#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
-    useDcmtk_ = true;
-#else
-    useDcmtk_ = false;
+#if !defined(NDEBUG)
+    // Debug mode
+    ParsedDicomFile parsed(transcoded);
+
+    std::string s;
+    DicomTransferSyntax a, b;
+    if (!parsed.LookupTransferSyntax(s) ||
+        !FromDcmtkBridge::LookupOrthancTransferSyntax(a, parsed.GetDcmtkObject()) ||
+        !LookupTransferSyntax(b, s) ||
+        a != b ||
+        allowedSyntaxes.find(a) == allowedSyntaxes.end())
+    {
+      throw OrthancException(
+        ErrorCode_Plugin,
+        "DEBUG - The transcoding plugin has not written to one of the allowed transfer syntaxes");
+    }
 #endif
   }
-
-
-  void MemoryBufferTranscoder::SetDcmtkUsed(bool used)
-  {
-#if ORTHANC_ENABLE_DCMTK_TRANSCODING != 1
-    if (useDcmtk)
-    {
-      throw OrthancException(ErrorCode_NotImplemented,
-                             "Orthanc was built without support for DMCTK transcoding");
-    }
-#endif    
-
-    useDcmtk_ = used;
-  }
-
+    
 
-  bool MemoryBufferTranscoder::TranscodeParsedToBuffer(
-    std::string& target /* out */,
-    DicomTransferSyntax& sourceSyntax /* out */,
-    bool& hasSopInstanceUidChanged /* out */,
-    DcmFileFormat& dicom /* in, possibly modified */,
-    DicomTransferSyntax targetSyntax,
-    bool allowNewSopInstanceUid)
+  bool MemoryBufferTranscoder::Transcode(DicomImage& target,
+                                         DicomImage& source,
+                                         const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                                         bool allowNewSopInstanceUid)
   {
-    if (dicom.getDataset() == NULL)
+    target.Clear();
+    
+#if !defined(NDEBUG)
+    // Don't run this code in release mode, as it implies parsing the DICOM file
+    DicomTransferSyntax sourceSyntax;
+    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, source.GetParsed()))
     {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    std::string source;
-    FromDcmtkBridge::SaveToMemoryBuffer(source, *dicom.getDataset());
-
-    const void* data = source.empty() ? NULL : source.c_str();
-
-    std::set<DicomTransferSyntax> tmp;
-    tmp.insert(targetSyntax);
-
-    DicomTransferSyntax targetSyntax2;
-    bool success = Transcode(target, sourceSyntax, targetSyntax2, hasSopInstanceUidChanged,
-                             data, source.size(), tmp, allowNewSopInstanceUid);
-
-    if (success &&
-        targetSyntax != targetSyntax2)
-    {
-      throw OrthancException(ErrorCode_InternalError);
+      LOG(ERROR) << "Unsupport transfer syntax for transcoding";
+      return false;
     }
     
-#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
-    if (!success &&
-        useDcmtk_ &&
-        dcmtk_.TranscodeParsedToBuffer(
-          target, sourceSyntax, hasSopInstanceUidChanged,
-          dicom, targetSyntax, allowNewSopInstanceUid))
-    {
-      success = true;
-    }
+    const std::string sourceSopInstanceUid = GetSopInstanceUid(source.GetParsed());
 #endif
 
-    return success;
-  }
-  
+    std::string buffer;
+    if (TranscodeBuffer(buffer, source.GetBufferData(), source.GetBufferSize(),
+                        allowedSyntaxes, allowNewSopInstanceUid))
+    {
+      CheckTargetSyntax(buffer, allowedSyntaxes);  // For debug only
 
-  IDicomTranscoder::TranscodedDicom* MemoryBufferTranscoder::TranscodeToParsed(
-    DcmFileFormat& dicom /* in, possibly modified */,
-    const void* buffer /* in, same DICOM file as "dicom" */,
-    size_t size,
-    const std::set<DicomTransferSyntax>& allowedSyntaxes,
-    bool allowNewSopInstanceUid)
-  {
-    DicomTransferSyntax sourceSyntax, targetSyntax;
-    bool hasSopInstanceUidChanged;
-    
-    std::string target;
-    if (Transcode(target, sourceSyntax, targetSyntax, hasSopInstanceUidChanged,
-                  buffer, size, allowedSyntaxes, allowNewSopInstanceUid))
-    {
-      const void* data = target.empty() ? NULL : target.c_str();
-      return IDicomTranscoder::TranscodedDicom::CreateFromInternal(
-        FromDcmtkBridge::LoadFromMemoryBuffer(data, target.size()), hasSopInstanceUidChanged);
+      target.AcquireBuffer(buffer);
+      
+#if !defined(NDEBUG)
+      // Only run the sanity check in debug mode
+      CheckTranscoding(target, sourceSyntax, sourceSopInstanceUid,
+                       allowedSyntaxes, allowNewSopInstanceUid);
+#endif
+
+      return true;
     }
-#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
-    else if (useDcmtk_)
-    {
-      return dcmtk_.TranscodeToParsed(dicom, buffer, size, allowedSyntaxes, allowNewSopInstanceUid);
-    }
-#endif
     else
     {
-      return NULL;
+      return false;
     }
   }
 }
--- a/Core/DicomParsing/MemoryBufferTranscoder.h	Wed May 20 16:38:33 2020 +0200
+++ b/Core/DicomParsing/MemoryBufferTranscoder.h	Wed May 20 17:03:24 2020 +0200
@@ -33,62 +33,24 @@
 
 #pragma once
 
-#if !defined(ORTHANC_ENABLE_DCMTK_TRANSCODING)
-#  error Macro ORTHANC_ENABLE_DCMTK_TRANSCODING must be defined to use this file
-#endif
-
-#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
-#  include "DcmtkTranscoder.h"
-#endif
+#include "IDicomTranscoder.h"
 
 namespace Orthanc
 {
   // This is the basis class for transcoding plugins
   class MemoryBufferTranscoder : public IDicomTranscoder
   {
-  private:
-    bool  useDcmtk_;
-
-#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
-    DcmtkTranscoder  dcmtk_;
-#endif
-
   protected:
-    virtual bool Transcode(std::string& target,
-                           DicomTransferSyntax& sourceSyntax /* out */,
-                           DicomTransferSyntax& targetSyntax /* out */,
-                           bool& hasSopInstanceUidChanged /* out */,
-                           const void* buffer,
-                           size_t size,
-                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                           bool allowNewSopInstanceUid) = 0;
+    virtual bool TranscodeBuffer(std::string& target,
+                                 const void* buffer,
+                                 size_t size,
+                                 const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                                 bool allowNewSopInstanceUid) = 0;
     
   public:
-    /**
-     * If "useDcmtk" is "true", the transcoder will first try and call
-     * DCMTK, before calling its own "Transcode()" implementation.
-     **/
-    MemoryBufferTranscoder();
-
-    void SetDcmtkUsed(bool used);
-
-    bool IsDcmtkUsed() const
-    {
-      return useDcmtk_;
-    }
-    
-    virtual bool TranscodeParsedToBuffer(std::string& target /* out */,
-                                         DicomTransferSyntax& sourceSyntax /* out */,
-                                         bool& hasSopInstanceUidChanged /* out */,
-                                         DcmFileFormat& dicom /* in, possibly modified */,
-                                         DicomTransferSyntax targetSyntax,
-                                         bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
-
-    virtual TranscodedDicom* TranscodeToParsed(
-      DcmFileFormat& dicom /* in, possibly modified */,
-      const void* buffer /* in, same DICOM file as "dicom" */,
-      size_t size,
-      const std::set<DicomTransferSyntax>& allowedSyntaxes,
-      bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
+    virtual bool Transcode(DicomImage& target /* out */,
+                           DicomImage& source,
+                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                           bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
   };
 }
--- a/Core/DicomParsing/ParsedDicomFile.cpp	Wed May 20 16:38:33 2020 +0200
+++ b/Core/DicomParsing/ParsedDicomFile.cpp	Wed May 20 17:03:24 2020 +0200
@@ -455,8 +455,8 @@
   void ParsedDicomFile::SendPathValue(RestApiOutput& output,
                                       const UriComponents& uri)
   {
-    DcmItem* dicom = pimpl_->file_->getDataset();
-    E_TransferSyntax transferSyntax = pimpl_->file_->getDataset()->getCurrentXfer();
+    DcmItem* dicom = GetDcmtkObject().getDataset();
+    E_TransferSyntax transferSyntax = GetDcmtkObject().getDataset()->getCurrentXfer();
 
     // Special case: Accessing the pixel data
     if (uri.size() == 1 || 
@@ -516,7 +516,7 @@
     InvalidateCache();
 
     DcmTagKey key(tag.GetGroup(), tag.GetElement());
-    DcmElement* element = pimpl_->file_->getDataset()->remove(key);
+    DcmElement* element = GetDcmtkObject().getDataset()->remove(key);
     if (element != NULL)
     {
       delete element;
@@ -536,7 +536,7 @@
 
     InvalidateCache();
 
-    DcmItem* dicom = pimpl_->file_->getDataset();
+    DcmItem* dicom = GetDcmtkObject().getDataset();
     DcmTagKey key(tag.GetGroup(), tag.GetElement());
 
     if (onlyIfExists &&
@@ -558,7 +558,7 @@
   {
     InvalidateCache();
 
-    DcmDataset& dataset = *pimpl_->file_->getDataset();
+    DcmDataset& dataset = *GetDcmtkObject().getDataset();
 
     // Loop over the dataset to detect its private tags
     typedef std::list<DcmElement*> Tags;
@@ -629,7 +629,7 @@
       return;
     }
 
-    if (pimpl_->file_->getDataset()->tagExists(ToDcmtkBridge::Convert(tag)))
+    if (GetDcmtkObject().getDataset()->tagExists(ToDcmtkBridge::Convert(tag)))
     {
       throw OrthancException(ErrorCode_AlreadyExistingTag);
     }
@@ -650,7 +650,7 @@
     bool hasCodeExtensions;
     Encoding encoding = DetectEncoding(hasCodeExtensions);
     std::unique_ptr<DcmElement> element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding, privateCreator));
-    InsertInternal(*pimpl_->file_->getDataset(), element.release());
+    InsertInternal(*GetDcmtkObject().getDataset(), element.release());
   }
 
 
@@ -782,7 +782,7 @@
 
     InvalidateCache();
 
-    DcmDataset& dicom = *pimpl_->file_->getDataset();
+    DcmDataset& dicom = *GetDcmtkObject().getDataset();
     if (CanReplaceProceed(dicom, ToDcmtkBridge::Convert(tag), mode))
     {
       // Either the tag was previously existing (and now removed), or
@@ -828,7 +828,7 @@
 
     InvalidateCache();
 
-    DcmDataset& dicom = *pimpl_->file_->getDataset();
+    DcmDataset& dicom = *GetDcmtkObject().getDataset();
     if (CanReplaceProceed(dicom, ToDcmtkBridge::Convert(tag), mode))
     {
       // Either the tag was previously existing (and now removed), or
@@ -867,7 +867,7 @@
   void ParsedDicomFile::Answer(RestApiOutput& output)
   {
     std::string serialized;
-    if (FromDcmtkBridge::SaveToMemoryBuffer(serialized, *pimpl_->file_->getDataset()))
+    if (FromDcmtkBridge::SaveToMemoryBuffer(serialized, *GetDcmtkObject().getDataset()))
     {
       output.AnswerBuffer(serialized, MimeType_Dicom);
     }
@@ -879,7 +879,7 @@
                                     const DicomTag& tag)
   {
     DcmTagKey k(tag.GetGroup(), tag.GetElement());
-    DcmDataset& dataset = *pimpl_->file_->getDataset();
+    DcmDataset& dataset = *GetDcmtkObject().getDataset();
 
     if (tag.IsPrivate() ||
         FromDcmtkBridge::IsUnknownTag(tag) ||
@@ -970,7 +970,7 @@
 
   void ParsedDicomFile::SaveToMemoryBuffer(std::string& buffer)
   {
-    FromDcmtkBridge::SaveToMemoryBuffer(buffer, *pimpl_->file_->getDataset());
+    FromDcmtkBridge::SaveToMemoryBuffer(buffer, *GetDcmtkObject().getDataset());
   }
 
 
@@ -1004,6 +1004,7 @@
                                            bool permissive)
   {
     pimpl_->file_.reset(new DcmFileFormat);
+    pimpl_->frameIndex_.reset(NULL);
 
     const DicomValue* tmp = source.TestAndGetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET);
 
@@ -1091,7 +1092,7 @@
                                    bool keepSopInstanceUid) : 
     pimpl_(new PImpl)
   {
-    pimpl_->file_.reset(dynamic_cast<DcmFileFormat*>(other.pimpl_->file_->clone()));
+    pimpl_->file_.reset(dynamic_cast<DcmFileFormat*>(other.GetDcmtkObject().clone()));
 
     if (!keepSopInstanceUid)
     {
@@ -1121,7 +1122,30 @@
 
   DcmFileFormat& ParsedDicomFile::GetDcmtkObject() const
   {
-    return *pimpl_->file_.get();
+    if (pimpl_->file_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "ReleaseDcmtkObject() was called");
+    }
+    else
+    {
+      return *pimpl_->file_;
+    }
+  }
+
+
+  DcmFileFormat* ParsedDicomFile::ReleaseDcmtkObject()
+  {
+    if (pimpl_->file_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "ReleaseDcmtkObject() was called");
+    }
+    else
+    {
+      pimpl_->frameIndex_.reset(NULL);
+      return pimpl_->file_.release();
+    }
   }
 
 
@@ -1354,7 +1378,7 @@
       }
     }
 
-    if (!pimpl_->file_->getDataset()->insert(pixels.release(), false, false).good())
+    if (!GetDcmtkObject().getDataset()->insert(pixels.release(), false, false).good())
     {
       throw OrthancException(ErrorCode_InternalError);
     }    
@@ -1364,7 +1388,7 @@
   Encoding ParsedDicomFile::DetectEncoding(bool& hasCodeExtensions) const
   {
     return FromDcmtkBridge::DetectEncoding(hasCodeExtensions,
-                                           *pimpl_->file_->getDataset(),
+                                           *GetDcmtkObject().getDataset(),
                                            GetDefaultDicomEncoding());
   }
 
@@ -1388,7 +1412,7 @@
                                       unsigned int maxStringLength)
   {
     std::set<DicomTag> ignoreTagLength;
-    FromDcmtkBridge::ExtractDicomAsJson(target, *pimpl_->file_->getDataset(),
+    FromDcmtkBridge::ExtractDicomAsJson(target, *GetDcmtkObject().getDataset(),
                                         format, flags, maxStringLength,
                                         GetDefaultDicomEncoding(), ignoreTagLength);
   }
@@ -1400,7 +1424,7 @@
                                       unsigned int maxStringLength,
                                       const std::set<DicomTag>& ignoreTagLength)
   {
-    FromDcmtkBridge::ExtractDicomAsJson(target, *pimpl_->file_->getDataset(),
+    FromDcmtkBridge::ExtractDicomAsJson(target, *GetDcmtkObject().getDataset(),
                                         format, flags, maxStringLength,
                                         GetDefaultDicomEncoding(), ignoreTagLength);
   }
@@ -1409,28 +1433,28 @@
   void ParsedDicomFile::DatasetToJson(Json::Value& target,
                                       const std::set<DicomTag>& ignoreTagLength)
   {
-    FromDcmtkBridge::ExtractDicomAsJson(target, *pimpl_->file_->getDataset(), ignoreTagLength);
+    FromDcmtkBridge::ExtractDicomAsJson(target, *GetDcmtkObject().getDataset(), ignoreTagLength);
   }
 
 
   void ParsedDicomFile::DatasetToJson(Json::Value& target)
   {
     const std::set<DicomTag> ignoreTagLength;
-    FromDcmtkBridge::ExtractDicomAsJson(target, *pimpl_->file_->getDataset(), ignoreTagLength);
+    FromDcmtkBridge::ExtractDicomAsJson(target, *GetDcmtkObject().getDataset(), ignoreTagLength);
   }
 
 
   void ParsedDicomFile::HeaderToJson(Json::Value& target, 
                                      DicomToJsonFormat format)
   {
-    FromDcmtkBridge::ExtractHeaderAsJson(target, *pimpl_->file_->getMetaInfo(), format, DicomToJsonFlags_None, 0);
+    FromDcmtkBridge::ExtractHeaderAsJson(target, *GetDcmtkObject().getMetaInfo(), format, DicomToJsonFlags_None, 0);
   }
 
 
   bool ParsedDicomFile::HasTag(const DicomTag& tag) const
   {
     DcmTag key(tag.GetGroup(), tag.GetElement());
-    return pimpl_->file_->getDataset()->tagExists(key);
+    return GetDcmtkObject().getDataset()->tagExists(key);
   }
 
 
@@ -1472,7 +1496,7 @@
     memcpy(bytes, pdf.c_str(), pdf.size());
       
     DcmPolymorphOBOW* obj = element.release();
-    result = pimpl_->file_->getDataset()->insert(obj);
+    result = GetDcmtkObject().getDataset()->insert(obj);
 
     if (!result.good())
     {
@@ -1564,13 +1588,13 @@
     if (pimpl_->frameIndex_.get() == NULL)
     {
       assert(pimpl_->file_ != NULL &&
-             pimpl_->file_->getDataset() != NULL);
-      pimpl_->frameIndex_.reset(new DicomFrameIndex(*pimpl_->file_->getDataset()));
+             GetDcmtkObject().getDataset() != NULL);
+      pimpl_->frameIndex_.reset(new DicomFrameIndex(*GetDcmtkObject().getDataset()));
     }
 
     pimpl_->frameIndex_->GetRawFrame(target, frameId);
 
-    E_TransferSyntax transferSyntax = pimpl_->file_->getDataset()->getCurrentXfer();
+    E_TransferSyntax transferSyntax = GetDcmtkObject().getDataset()->getCurrentXfer();
     switch (transferSyntax)
     {
       case EXS_JPEGProcess1:
@@ -1598,8 +1622,8 @@
   unsigned int ParsedDicomFile::GetFramesCount() const
   {
     assert(pimpl_->file_ != NULL &&
-           pimpl_->file_->getDataset() != NULL);
-    return DicomFrameIndex::GetFramesCount(*pimpl_->file_->getDataset());
+           GetDcmtkObject().getDataset() != NULL);
+    return DicomFrameIndex::GetFramesCount(*GetDcmtkObject().getDataset());
   }
 
 
@@ -1611,33 +1635,35 @@
     if (source != target)  // Avoid unnecessary conversion
     {
       ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, GetDicomSpecificCharacterSet(target));
-      FromDcmtkBridge::ChangeStringEncoding(*pimpl_->file_->getDataset(), source, hasCodeExtensions, target);
+      FromDcmtkBridge::ChangeStringEncoding(*GetDcmtkObject().getDataset(), source, hasCodeExtensions, target);
     }
   }
 
 
   void ParsedDicomFile::ExtractDicomSummary(DicomMap& target) const
   {
-    FromDcmtkBridge::ExtractDicomSummary(target, *pimpl_->file_->getDataset());
+    FromDcmtkBridge::ExtractDicomSummary(target, *GetDcmtkObject().getDataset());
   }
 
 
   void ParsedDicomFile::ExtractDicomSummary(DicomMap& target,
                                             const std::set<DicomTag>& ignoreTagLength) const
   {
-    FromDcmtkBridge::ExtractDicomSummary(target, *pimpl_->file_->getDataset(), ignoreTagLength);
+    FromDcmtkBridge::ExtractDicomSummary(target, *GetDcmtkObject().getDataset(), ignoreTagLength);
   }
 
 
   bool ParsedDicomFile::LookupTransferSyntax(std::string& result)
   {
+#if 0
+    // This was the implementation in Orthanc <= 1.6.1
+
     // TODO - Shouldn't "dataset.getCurrentXfer()" be used instead of
     // using the meta header?
     const char* value = NULL;
 
-    assert(pimpl_->file_ != NULL);
-    if (pimpl_->file_->getMetaInfo() != NULL &&
-        pimpl_->file_->getMetaInfo()->findAndGetString(DCM_TransferSyntaxUID, value).good() &&
+    if (GetDcmtkObject().getMetaInfo() != NULL &&
+        GetDcmtkObject().getMetaInfo()->findAndGetString(DCM_TransferSyntaxUID, value).good() &&
         value != NULL)
     {
       result.assign(value);
@@ -1647,6 +1673,18 @@
     {
       return false;
     }
+#else
+    DicomTransferSyntax s;
+    if (FromDcmtkBridge::LookupOrthancTransferSyntax(s, GetDcmtkObject()))
+    {
+      result.assign(GetTransferSyntaxUid(s));
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+#endif
   }
 
 
@@ -1655,7 +1693,7 @@
     DcmTagKey k(DICOM_TAG_PHOTOMETRIC_INTERPRETATION.GetGroup(),
                 DICOM_TAG_PHOTOMETRIC_INTERPRETATION.GetElement());
 
-    DcmDataset& dataset = *pimpl_->file_->getDataset();
+    DcmDataset& dataset = *GetDcmtkObject().getDataset();
 
     const char *c = NULL;
     if (dataset.findAndGetString(k, c).good() &&
@@ -1673,6 +1711,6 @@
 
   void ParsedDicomFile::Apply(ITagVisitor& visitor)
   {
-    FromDcmtkBridge::Apply(*pimpl_->file_->getDataset(), visitor, GetDefaultDicomEncoding());
+    FromDcmtkBridge::Apply(*GetDcmtkObject().getDataset(), visitor, GetDefaultDicomEncoding());
   }
 }
--- a/Core/DicomParsing/ParsedDicomFile.h	Wed May 20 16:38:33 2020 +0200
+++ b/Core/DicomParsing/ParsedDicomFile.h	Wed May 20 17:03:24 2020 +0200
@@ -127,6 +127,9 @@
 
     DcmFileFormat& GetDcmtkObject() const;
 
+    // The "ParsedDicomFile" object cannot be used after calling this method
+    DcmFileFormat* ReleaseDcmtkObject();
+
     ParsedDicomFile* Clone(bool keepSopInstanceUid);
 
 #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
--- a/NEWS	Wed May 20 16:38:33 2020 +0200
+++ b/NEWS	Wed May 20 17:03:24 2020 +0200
@@ -4,12 +4,18 @@
 General
 -------
 
-* Support of DICOM C-GET SCP
+* DICOM transcoding over the REST API
+* Transcoding from compressed to uncompressed transfer syntaxes over DICOM
+  C-STORE SCU (if the remote modality doesn't support compressed syntaxes)
+* New configuration options related to transcoding:
+  "TranscodeDicomProtocol", "BuiltinDecoderTranscoderOrder",
+  "IngestTranscoding" and "DicomLossyTranscodingQuality"
+* Support of DICOM C-GET SCP (contribution by Varian)
 
 REST API
 --------
 
-* API version has been upgraded to 7
+* API version upgraded to 7
 * Improved:
   - "/instances/../modify": it is now possible to "Keep" the "SOPInstanceUID".  
     Note that it was already possible to "Replace" it.
@@ -17,8 +23,11 @@
   - "/queries/.../answers/../retrieve": "TargetAet" not mandatory anymore
     (defaults to the local AET)
 * Changes:
-  - "/instances/.../modify", ".../archive", ".../media",
-    "/tools/create-media" and "/tools/create-archive": New option "Transcode"
+  - "/{patients|studies|series}/.../modify": New option "KeepSource"
+  - "/{patients|studies|series|instances}/.../modify": New option "Transcode"
+  - "/peers/{id}/store": New option "Transcode"
+  - ".../archive", ".../media", "/tools/create-media" and
+    "/tools/create-archive": New option "Transcode"
   - "/ordered-slices": reverted the change introduced in 1.5.8 and go-back 
     to 1.5.7 behaviour.
 
@@ -27,19 +36,29 @@
 
 * New functions in the SDK:
   - OrthancPluginCreateDicomInstance()
+  - OrthancPluginCreateMemoryBuffer()
+  - OrthancPluginEncodeDicomWebJson2()
+  - OrthancPluginEncodeDicomWebXml2()
   - OrthancPluginFreeDicomInstance()
+  - OrthancPluginGetInstanceAdvancedJson()
+  - OrthancPluginGetInstanceDecodedFrame()
+  - OrthancPluginGetInstanceDicomWebJson()
+  - OrthancPluginGetInstanceDicomWebXml()
   - OrthancPluginGetInstanceFramesCount()
   - OrthancPluginGetInstanceRawFrame()
-  - OrthancPluginGetInstanceDecodedFrame()
+  - OrthancPluginRegisterTranscoderCallback()
+  - OrthancPluginSerializeDicomInstance()
   - OrthancPluginTranscodeDicomInstance()
-  - OrthancPluginSerializeDicomInstance()
 * "OrthancPluginDicomInstance" structure wrapped in "OrthancPluginCppWrapper.h"
+* Allow concurrent calls to the custom image decoders provided by the plugins
 
 Maintenance
 -----------
 
+* Moved the GDCM sample plugin out of the Orthanc repository as a separate plugin
 * Fix missing body in "OrthancPluginHttpPost()" and "OrthancPluginHttpPut()"
 * Fix issue #169 (TransferSyntaxUID change from Explicit to Implicit during C-STORE SCU)
+* Fix issue #179 (deadlock in Python plugins)
 * Upgraded dependencies for static builds (notably on Windows and LSB):
   - openssl 1.1.1g
 
--- a/OrthancServer/DefaultDicomImageDecoder.h	Wed May 20 16:38:33 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "IDicomImageDecoder.h"
-#include "../Core/DicomParsing/ParsedDicomFile.h"
-#include "../Core/DicomParsing/Internals/DicomImageDecoder.h"
-
-namespace Orthanc
-{
-  class DefaultDicomImageDecoder : public IDicomImageDecoder
-  {
-  public:
-    virtual ImageAccessor* Decode(const void* dicom,
-                                  size_t size,
-                                  unsigned int frame)
-    {
-      ParsedDicomFile parsed(dicom, size);
-      return DicomImageDecoder::Decode(parsed, frame);
-    }
-  };
-}
--- a/OrthancServer/OrthancConfiguration.cpp	Wed May 20 16:38:33 2020 +0200
+++ b/OrthancServer/OrthancConfiguration.cpp	Wed May 20 17:03:24 2020 +0200
@@ -422,8 +422,8 @@
   }
 
 
-  std::string OrthancConfiguration::GetStringParameter(const std::string& parameter,
-                                                       const std::string& defaultValue) const
+  bool OrthancConfiguration::LookupStringParameter(std::string& target,
+                                                   const std::string& parameter) const
   {
     if (json_.isMember(parameter))
     {
@@ -434,11 +434,27 @@
       }
       else
       {
-        return json_[parameter].asString();
+        target = json_[parameter].asString();
+        return true;
       }
     }
     else
     {
+      return false;
+    }
+  }
+
+
+  std::string OrthancConfiguration::GetStringParameter(const std::string& parameter,
+                                                       const std::string& defaultValue) const
+  {
+    std::string value;
+    if (LookupStringParameter(value, parameter))
+    {
+      return value;
+    }
+    else
+    {
       return defaultValue;
     }
   }
--- a/OrthancServer/OrthancConfiguration.h	Wed May 20 16:38:33 2020 +0200
+++ b/OrthancServer/OrthancConfiguration.h	Wed May 20 17:03:24 2020 +0200
@@ -163,6 +163,9 @@
       fontRegistry_.AddFromResource(resource);
     }
 
+    bool LookupStringParameter(std::string& target,
+                               const std::string& parameter) const;
+
     std::string GetStringParameter(const std::string& parameter,
                                    const std::string& defaultValue) const;
     
--- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Wed May 20 16:38:33 2020 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Wed May 20 17:03:24 2020 +0200
@@ -130,15 +130,18 @@
 
     if (transcode)
     {
-      std::string transcoded;
-      DicomTransferSyntax sourceSyntax;
-      bool hasSopInstanceUidChanged;
+      IDicomTranscoder::DicomImage source;
+      source.AcquireParsed(*modified);  // "modified" is invalid below this point
+      
+      IDicomTranscoder::DicomImage transcoded;
 
-      if (context.GetTranscoder().TranscodeParsedToBuffer(
-            transcoded, sourceSyntax, hasSopInstanceUidChanged,
-            modified->GetDcmtkObject(), targetSyntax, true))
+      std::set<DicomTransferSyntax> s;
+      s.insert(targetSyntax);
+      
+      if (context.Transcode(transcoded, source, s, true))
       {      
-        call.GetOutput().AnswerBuffer(transcoded, MimeType_Dicom);
+        call.GetOutput().AnswerBuffer(transcoded.GetBufferData(),
+                                      transcoded.GetBufferSize(), MimeType_Dicom);
       }
       else
       {
@@ -179,9 +182,10 @@
       modification.SetLevel(ResourceType_Instance);
     }
 
-    if (request.isMember("Transcode"))
+    static const char* TRANSCODE = "Transcode";
+    if (request.isMember(TRANSCODE))
     {
-      std::string s = SerializationToolbox::ReadString(request, "Transcode");
+      std::string s = SerializationToolbox::ReadString(request, TRANSCODE);
       
       DicomTransferSyntax syntax;
       if (LookupTransferSyntax(syntax, s))
@@ -214,6 +218,17 @@
   }
 
 
+  static void SetKeepSource(CleaningInstancesJob& job,
+                            const Json::Value& body)
+  {
+    static const char* KEEP_SOURCE = "KeepSource";
+    if (body.isMember(KEEP_SOURCE))
+    {
+      job.SetKeepSource(SerializationToolbox::ReadBoolean(body, KEEP_SOURCE));
+    }
+  }
+
+
   static void SubmitModificationJob(std::unique_ptr<DicomModification>& modification,
                                     bool isAnonymization,
                                     RestApiPostCall& call,
@@ -223,11 +238,19 @@
     ServerContext& context = OrthancRestApi::GetContext(call);
 
     std::unique_ptr<ResourceModificationJob> job(new ResourceModificationJob(context));
-    
+
     job->SetModification(modification.release(), level, isAnonymization);
     job->SetOrigin(call);
+    SetKeepSource(*job, body);
+
+    static const char* TRANSCODE = "Transcode";
+    if (body.isMember(TRANSCODE))
+    {
+      job->SetTranscode(SerializationToolbox::ReadString(body, TRANSCODE));
+    }
     
     context.AddChildInstances(*job, call.GetUriComponent("id", ""));
+    job->AddTrailingStep();
 
     OrthancRestApi::GetApi(call).SubmitCommandsJob
       (call, job.release(), true /* synchronous by default */, body);
@@ -723,14 +746,10 @@
     {
       job->AddSourceSeries(series[i]);
     }
-
+    
     job->AddTrailingStep();
 
-    static const char* KEEP_SOURCE = "KeepSource";
-    if (request.isMember(KEEP_SOURCE))
-    {
-      job->SetKeepSource(SerializationToolbox::ReadBoolean(request, KEEP_SOURCE));
-    }
+    SetKeepSource(*job, request);
 
     static const char* REMOVE = "Remove";
     if (request.isMember(REMOVE))
@@ -809,11 +828,7 @@
 
     job->AddTrailingStep();
 
-    static const char* KEEP_SOURCE = "KeepSource";
-    if (request.isMember(KEEP_SOURCE))
-    {
-      job->SetKeepSource(SerializationToolbox::ReadBoolean(request, KEEP_SOURCE));
-    }
+    SetKeepSource(*job, request);
 
     OrthancRestApi::GetApi(call).SubmitCommandsJob
       (call, job.release(), true /* synchronous by default */, request);
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Wed May 20 16:38:33 2020 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Wed May 20 17:03:24 2020 +0200
@@ -1136,21 +1136,31 @@
     std::unique_ptr<OrthancPeerStoreJob> job(new OrthancPeerStoreJob(context));
 
     GetInstancesToExport(request, *job, remote, call);
+
+    static const char* TRANSCODE = "Transcode";
+    if (request.type() == Json::objectValue &&
+        request.isMember(TRANSCODE))
+    {
+      job->SetTranscode(SerializationToolbox::ReadString(request, TRANSCODE));
+    }
     
-    OrthancConfiguration::ReaderLock lock;
-
-    WebServiceParameters peer;
-    if (lock.GetConfiguration().LookupOrthancPeer(peer, remote))
     {
-      job->SetPeer(peer);    
-      OrthancRestApi::GetApi(call).SubmitCommandsJob
-        (call, job.release(), true /* synchronous by default */, request);
+      OrthancConfiguration::ReaderLock lock;
+      
+      WebServiceParameters peer;
+      if (lock.GetConfiguration().LookupOrthancPeer(peer, remote))
+      {
+        job->SetPeer(peer);    
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_UnknownResource,
+                               "No peer with symbolic name: " + remote);
+      }
     }
-    else
-    {
-      throw OrthancException(ErrorCode_UnknownResource,
-                             "No peer with symbolic name: " + remote);
-    }
+
+    OrthancRestApi::GetApi(call).SubmitCommandsJob
+      (call, job.release(), true /* synchronous by default */, request);
   }
 
   static void PeerSystem(RestApiGetCall& call)
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Wed May 20 16:38:33 2020 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Wed May 20 17:03:24 2020 +0200
@@ -43,7 +43,7 @@
 #include "../../Core/Images/Image.h"
 #include "../../Core/Images/ImageProcessing.h"
 #include "../../Core/Logging.h"
-#include "../DefaultDicomImageDecoder.h"
+#include "../../Core/MultiThreading/Semaphore.h"
 #include "../OrthancConfiguration.h"
 #include "../Search/DatabaseLookup.h"
 #include "../ServerContext.h"
@@ -56,6 +56,14 @@
 #include <boost/math/special_functions/round.hpp>
 
 
+/**
+ * This semaphore is used to limit the number of concurrent HTTP
+ * requests on CPU-intensive routes of the REST API, in order to
+ * prevent exhaustion of resources (new in Orthanc 1.7.0).
+ **/
+static Orthanc::Semaphore throttlingSemaphore_(4);  // TODO => PARAMETER?
+
+
 namespace Orthanc
 {
   static void AnswerDicomAsJson(RestApiCall& call,
@@ -547,44 +555,23 @@
         {
           std::string publicId = call.GetUriComponent("id", "");
 
-#if ORTHANC_ENABLE_PLUGINS == 1
-          if (context.GetPlugins().HasCustomImageDecoder())
-          {
-            // TODO create a cache of file
-            std::string dicomContent;
-            context.ReadDicom(dicomContent, publicId);
-            decoded.reset(context.GetPlugins().DecodeUnsafe(dicomContent.c_str(), dicomContent.size(), frame));
-
-            /**
-             * Note that we call "DecodeUnsafe()": We do not fallback to
-             * the builtin decoder if no installed decoder plugin is able
-             * to decode the image. This allows us to take advantage of
-             * the cache below.
-             **/
-
-            if (handler.RequiresDicomTags() &&
-                decoded.get() != NULL)
-            {
-              // TODO Optimize this lookup for photometric interpretation:
-              // It should be implemented by the plugin to avoid parsing
-              // twice the DICOM file
-              ParsedDicomFile parsed(dicomContent);
-              parsed.ExtractDicomSummary(dicom);
-            }
-          }
-#endif
+          decoded.reset(context.DecodeDicomFrame(publicId, frame));
 
           if (decoded.get() == NULL)
           {
-            // Use Orthanc's built-in decoder, using the cache to speed-up
-            // things on multi-frame images
-            ServerContext::DicomCacheLocker locker(context, publicId);        
-            decoded.reset(DicomImageDecoder::Decode(locker.GetDicom(), frame));
-
-            if (handler.RequiresDicomTags())
-            {
-              locker.GetDicom().ExtractDicomSummary(dicom);
-            }
+            throw OrthancException(ErrorCode_NotImplemented,
+                                   "Cannot decode DICOM instance with ID: " + publicId);
+          }
+          
+          if (handler.RequiresDicomTags())
+          {
+            /**
+             * Retrieve a summary of the DICOM tags, which is
+             * necessary to deal with MONOCHROME1 photometric
+             * interpretation, and with windowing parameters.
+             **/ 
+            ServerContext::DicomCacheLocker locker(context, publicId);
+            locker.GetDicom().ExtractDicomSummary(dicom);
           }
         }
         catch (OrthancException& e)
@@ -938,6 +925,8 @@
   template <enum ImageExtractionMode mode>
   static void GetImage(RestApiGetCall& call)
   {
+    Semaphore::Locker locker(throttlingSemaphore_);
+        
     GetImageHandler handler(mode);
     IDecodedFrameHandler::Apply(call, handler);
   }
@@ -945,6 +934,8 @@
 
   static void GetRenderedFrame(RestApiGetCall& call)
   {
+    Semaphore::Locker locker(throttlingSemaphore_);
+        
     RenderedFrameHandler handler;
     IDecodedFrameHandler::Apply(call, handler);
   }
@@ -952,6 +943,8 @@
 
   static void GetMatlabImage(RestApiGetCall& call)
   {
+    Semaphore::Locker locker(throttlingSemaphore_);
+        
     ServerContext& context = OrthancRestApi::GetContext(call);
 
     std::string frameId = call.GetUriComponent("frame", "0");
@@ -967,21 +960,19 @@
     }
 
     std::string publicId = call.GetUriComponent("id", "");
-    std::string dicomContent;
-    context.ReadDicom(dicomContent, publicId);
+    std::unique_ptr<ImageAccessor> decoded(context.DecodeDicomFrame(publicId, frame));
 
-#if ORTHANC_ENABLE_PLUGINS == 1
-    IDicomImageDecoder& decoder = context.GetPlugins();
-#else
-    DefaultDicomImageDecoder decoder;  // This is Orthanc's built-in decoder
-#endif
-
-    std::unique_ptr<ImageAccessor> decoded(decoder.Decode(dicomContent.c_str(), dicomContent.size(), frame));
-
-    std::string result;
-    decoded->ToMatlabString(result);
-
-    call.GetOutput().AnswerBuffer(result, MimeType_PlainText);
+    if (decoded.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_NotImplemented,
+                             "Cannot decode DICOM instance with ID: " + publicId);
+    }
+    else
+    {
+      std::string result;
+      decoded->ToMatlabString(result);
+      call.GetOutput().AnswerBuffer(result, MimeType_PlainText);
+    }
   }
 
 
--- a/OrthancServer/ServerContext.cpp	Wed May 20 16:38:33 2020 +0200
+++ b/OrthancServer/ServerContext.cpp	Wed May 20 17:03:24 2020 +0200
@@ -34,6 +34,7 @@
 #include "PrecompiledHeadersServer.h"
 #include "ServerContext.h"
 
+#include "../Core/DicomParsing/Internals/DicomImageDecoder.h"
 #include "../Core/Cache/SharedArchive.h"
 #include "../Core/DicomParsing/DcmtkTranscoder.h"
 #include "../Core/DicomParsing/FromDcmtkBridge.h"
@@ -83,7 +84,7 @@
       {
         const ServerIndexChange& change = dynamic_cast<const ServerIndexChange&>(*obj.get());
 
-        boost::recursive_mutex::scoped_lock lock(that->listenersMutex_);
+        boost::shared_lock<boost::shared_mutex> lock(that->listenersMutex_);
         for (ServerListeners::iterator it = that->listeners_.begin(); 
              it != that->listeners_.end(); ++it)
         {
@@ -245,36 +246,72 @@
     isHttpServerSecure_(true),
     isExecuteLuaEnabled_(false),
     overwriteInstances_(false),
-    dcmtkTranscoder_(new DcmtkTranscoder)
+    dcmtkTranscoder_(new DcmtkTranscoder),
+    isIngestTranscoding_(false)
   {
+    try
     {
-      OrthancConfiguration::ReaderLock lock;
+      unsigned int lossyQuality;
+
+      {
+        OrthancConfiguration::ReaderLock lock;
 
-      queryRetrieveArchive_.reset(
-        new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("QueryRetrieveSize", 100)));
-      mediaArchive_.reset(
-        new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("MediaArchiveSize", 1)));
-      defaultLocalAet_ = lock.GetConfiguration().GetStringParameter("DicomAet", "ORTHANC");
-      jobsEngine_.SetWorkersCount(lock.GetConfiguration().GetUnsignedIntegerParameter("ConcurrentJobs", 2));
-      saveJobs_ = lock.GetConfiguration().GetBooleanParameter("SaveJobs", true);
-      metricsRegistry_->SetEnabled(lock.GetConfiguration().GetBooleanParameter("MetricsEnabled", true));
+        queryRetrieveArchive_.reset(
+          new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("QueryRetrieveSize", 100)));
+        mediaArchive_.reset(
+          new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("MediaArchiveSize", 1)));
+        defaultLocalAet_ = lock.GetConfiguration().GetStringParameter("DicomAet", "ORTHANC");
+        jobsEngine_.SetWorkersCount(lock.GetConfiguration().GetUnsignedIntegerParameter("ConcurrentJobs", 2));
+        saveJobs_ = lock.GetConfiguration().GetBooleanParameter("SaveJobs", true);
+        metricsRegistry_->SetEnabled(lock.GetConfiguration().GetBooleanParameter("MetricsEnabled", true));
+
+        // New configuration options in Orthanc 1.5.1
+        findStorageAccessMode_ = StringToFindStorageAccessMode(lock.GetConfiguration().GetStringParameter("StorageAccessOnFind", "Always"));
+        limitFindInstances_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances", 0);
+        limitFindResults_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults", 0);
+
+        // New configuration option in Orthanc 1.6.0
+        storageCommitmentReports_.reset(new StorageCommitmentReports(lock.GetConfiguration().GetUnsignedIntegerParameter("StorageCommitmentReportsSize", 100)));
+
+        // New options in Orthanc 1.7.0
+        transcodeDicomProtocol_ = lock.GetConfiguration().GetBooleanParameter("TranscodeDicomProtocol", true);
+        builtinDecoderTranscoderOrder_ = StringToBuiltinDecoderTranscoderOrder(lock.GetConfiguration().GetStringParameter("BuiltinDecoderTranscoderOrder", "After"));
+        lossyQuality = lock.GetConfiguration().GetUnsignedIntegerParameter("DicomLossyTranscodingQuality", 90);
 
-      // New configuration options in Orthanc 1.5.1
-      findStorageAccessMode_ = StringToFindStorageAccessMode(lock.GetConfiguration().GetStringParameter("StorageAccessOnFind", "Always"));
-      limitFindInstances_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances", 0);
-      limitFindResults_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults", 0);
-
-      // New configuration option in Orthanc 1.6.0
-      storageCommitmentReports_.reset(new StorageCommitmentReports(lock.GetConfiguration().GetUnsignedIntegerParameter("StorageCommitmentReportsSize", 100)));
+        std::string s;
+        if (lock.GetConfiguration().LookupStringParameter(s, "IngestTranscoding"))
+        {
+          if (LookupTransferSyntax(ingestTransferSyntax_, s))
+          {
+            isIngestTranscoding_ = true;
+            LOG(WARNING) << "Incoming DICOM instances will automatically be transcoded to "
+                         << "transfer syntax: " << GetTransferSyntaxUid(ingestTransferSyntax_);
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                   "Unknown transfer syntax for ingest transcoding: " + s);
+          }
+        }
+        else
+        {
+          isIngestTranscoding_ = false;
+          LOG(INFO) << "Automated transcoding of incoming DICOM instances is disabled";
+        }
+      }
 
-      // New option in Orthanc 1.7.0
-      transcodeDicomProtocol_ = lock.GetConfiguration().GetBooleanParameter("TranscodeDicomProtocol", true);
-    }
+      jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200);
 
-    jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200);
-
-    listeners_.push_back(ServerListener(luaListener_, "Lua"));
-    changeThread_ = boost::thread(ChangeThread, this, (unitTesting ? 20 : 100));
+      listeners_.push_back(ServerListener(luaListener_, "Lua"));
+      changeThread_ = boost::thread(ChangeThread, this, (unitTesting ? 20 : 100));
+    
+      dynamic_cast<DcmtkTranscoder&>(*dcmtkTranscoder_).SetLossyQuality(lossyQuality);
+    }
+    catch (OrthancException&)
+    {
+      Stop();
+      throw;
+    }
   }
 
 
@@ -294,7 +331,7 @@
     if (!done_)
     {
       {
-        boost::recursive_mutex::scoped_lock lock(listenersMutex_);
+        boost::unique_lock<boost::shared_mutex> lock(listenersMutex_);
         listeners_.clear();
       }
 
@@ -381,7 +418,7 @@
       bool accepted = true;
 
       {
-        boost::recursive_mutex::scoped_lock lock(listenersMutex_);
+        boost::shared_lock<boost::shared_mutex> lock(listenersMutex_);
 
         for (ServerListeners::iterator it = listeners_.begin(); it != listeners_.end(); ++it)
         {
@@ -471,7 +508,7 @@
       if (status == StoreStatus_Success ||
           status == StoreStatus_AlreadyStored)
       {
-        boost::recursive_mutex::scoped_lock lock(listenersMutex_);
+        boost::shared_lock<boost::shared_mutex> lock(listenersMutex_);
 
         for (ServerListeners::iterator it = listeners_.begin(); it != listeners_.end(); ++it)
         {
@@ -506,21 +543,19 @@
                                    DicomInstanceToStore& dicom,
                                    StoreInstanceMode mode)
   {
-    //const DicomTransferSyntax option = DicomTransferSyntax_JPEGProcess1;
-    const DicomTransferSyntax option = DicomTransferSyntax_LittleEndianExplicit;
-    
-    if (1)
+    if (!isIngestTranscoding_)
     {
+      // No automated transcoding. This was the only path in Orthanc <= 1.6.1.
       return StoreAfterTranscoding(resultPublicId, dicom, mode);
     }
     else
     {
-      // TODO => Automated transcoding of incoming DICOM files
-      
+      // Automated transcoding of incoming DICOM files
+
       DicomTransferSyntax sourceSyntax;
       if (!FromDcmtkBridge::LookupOrthancTransferSyntax(
             sourceSyntax, dicom.GetParsedDicomFile().GetDcmtkObject()) ||
-          sourceSyntax == option)
+          sourceSyntax == ingestTransferSyntax_)
       {
         // No transcoding
         return StoreAfterTranscoding(resultPublicId, dicom, mode);
@@ -528,31 +563,30 @@
       else
       {      
         std::set<DicomTransferSyntax> syntaxes;
-        syntaxes.insert(option);
+        syntaxes.insert(ingestTransferSyntax_);
 
-        std::unique_ptr<IDicomTranscoder::TranscodedDicom> transcoded(
-          GetTranscoder().TranscodeToParsed(dicom.GetParsedDicomFile().GetDcmtkObject(),
-                                            dicom.GetBufferData(), dicom.GetBufferSize(),
-                                            syntaxes, true /* allow new SOP instance UID */));
+        IDicomTranscoder::DicomImage source;
+        source.SetExternalBuffer(dicom.GetBufferData(), dicom.GetBufferSize());
 
-        if (transcoded.get() == NULL)
+        IDicomTranscoder::DicomImage transcoded;
+        if (Transcode(transcoded, source, syntaxes, true /* allow new SOP instance UID */))
         {
-          // Cannot transcode => store the original file
-          return StoreAfterTranscoding(resultPublicId, dicom, mode);
-        }
-        else
-        {
-          std::unique_ptr<ParsedDicomFile> tmp(
-            ParsedDicomFile::AcquireDcmtkObject(transcoded->ReleaseDicom()));
-      
+          std::unique_ptr<ParsedDicomFile> tmp(transcoded.ReleaseAsParsedDicomFile());
+
           DicomInstanceToStore toStore;
           toStore.SetParsedDicomFile(*tmp);
           toStore.SetOrigin(dicom.GetOrigin());
 
           StoreStatus ok = StoreAfterTranscoding(resultPublicId, toStore, mode);
-          printf(">> %s\n", resultPublicId.c_str());
+          assert(resultPublicId == tmp->GetHasher().HashInstance());
+
           return ok;
         }
+        else
+        {
+          // Cannot transcode => store the original file
+          return StoreAfterTranscoding(resultPublicId, dicom, mode);
+        }
       }
     }
   }
@@ -822,7 +856,7 @@
 #if ORTHANC_ENABLE_PLUGINS == 1
   void ServerContext::SetPlugins(OrthancPlugins& plugins)
   {
-    boost::recursive_mutex::scoped_lock lock(listenersMutex_);
+    boost::unique_lock<boost::shared_mutex> lock(listenersMutex_);
 
     plugins_ = &plugins;
 
@@ -835,7 +869,7 @@
 
   void ServerContext::ResetPlugins()
   {
-    boost::recursive_mutex::scoped_lock lock(listenersMutex_);
+    boost::unique_lock<boost::shared_mutex> lock(listenersMutex_);
 
     plugins_ = NULL;
 
@@ -1171,26 +1205,95 @@
   }
 
 
-  IDicomTranscoder& ServerContext::GetTranscoder()
+  ImageAccessor* ServerContext::DecodeDicomFrame(const std::string& publicId,
+                                                 unsigned int frameIndex)
   {
-    IDicomTranscoder* transcoder = dcmtkTranscoder_.get();
+    if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before)
+    {
+      // Use Orthanc's built-in decoder, using the cache to speed-up
+      // things on multi-frame images
+      ServerContext::DicomCacheLocker locker(*this, publicId);        
+      std::unique_ptr<ImageAccessor> decoded(
+        DicomImageDecoder::Decode(locker.GetDicom(), frameIndex));
+      if (decoded.get() != NULL)
+      {
+        return decoded.release();
+      }
+    }
 
 #if ORTHANC_ENABLE_PLUGINS == 1
-    if (HasPlugins())
+    if (HasPlugins() &&
+        GetPlugins().HasCustomImageDecoder())
     {
-      transcoder = &GetPlugins();
+      // TODO: Store the raw buffer in the DicomCacheLocker
+      std::string dicomContent;
+      ReadDicom(dicomContent, publicId);
+      std::unique_ptr<ImageAccessor> decoded(
+        GetPlugins().Decode(dicomContent.c_str(), dicomContent.size(), frameIndex));
+      if (decoded.get() != NULL)
+      {
+        return decoded.release();
+      }
+      else if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
+      {
+        LOG(INFO) << "The installed image decoding plugins cannot handle an image, "
+                  << "fallback to the built-in DCMTK decoder";
+      }
     }
 #endif
 
-    if (transcoder == NULL)
+    if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
     {
-      throw OrthancException(ErrorCode_InternalError);
+      ServerContext::DicomCacheLocker locker(*this, publicId);        
+      return DicomImageDecoder::Decode(locker.GetDicom(), frameIndex);
     }
     else
     {
-      return *transcoder;
+      return NULL;  // Built-in decoder is disabled
+    }
+  }
+
+
+  ImageAccessor* ServerContext::DecodeDicomFrame(const DicomInstanceToStore& dicom,
+                                                 unsigned int frameIndex)
+  {
+    if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before)
+    {
+      std::unique_ptr<ImageAccessor> decoded(
+        DicomImageDecoder::Decode(dicom.GetParsedDicomFile(), frameIndex));
+      if (decoded.get() != NULL)
+      {
+        return decoded.release();
+      }
     }
-  }   
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+    if (HasPlugins() &&
+        GetPlugins().HasCustomImageDecoder())
+    {
+      std::unique_ptr<ImageAccessor> decoded(
+        GetPlugins().Decode(dicom.GetBufferData(), dicom.GetBufferSize(), frameIndex));
+      if (decoded.get() != NULL)
+      {
+        return decoded.release();
+      }
+      else if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
+      {
+        LOG(INFO) << "The installed image decoding plugins cannot handle an image, "
+                  << "fallback to the built-in DCMTK decoder";
+      }
+    }
+#endif
+
+    if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
+    {
+      return DicomImageDecoder::Decode(dicom.GetParsedDicomFile(), frameIndex);
+    }
+    else
+    {
+      return NULL;
+    }
+  }
 
 
   void ServerContext::StoreWithTranscoding(std::string& sopClassUid,
@@ -1211,8 +1314,48 @@
     }
     else
     {
-      connection.Transcode(sopClassUid, sopInstanceUid, GetTranscoder(), data, dicom.size(),
+      connection.Transcode(sopClassUid, sopInstanceUid, *this, data, dicom.size(),
                            hasMoveOriginator, moveOriginatorAet, moveOriginatorId);
     }
   }
+
+
+  bool ServerContext::Transcode(DicomImage& target,
+                                DicomImage& source /* in, "GetParsed()" possibly modified */,
+                                const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                                bool allowNewSopInstanceUid)
+  {
+    if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before)
+    {
+      if (dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid))
+      {
+        return true;
+      }
+    }
+      
+#if ORTHANC_ENABLE_PLUGINS == 1
+    if (HasPlugins() &&
+        GetPlugins().HasCustomTranscoder())
+    {
+      if (GetPlugins().Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid))
+      {
+        return true;
+      }
+      else if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
+      {
+        LOG(INFO) << "The installed transcoding plugins cannot handle an image, "
+                  << "fallback to the built-in DCMTK transcoder";
+      }
+    }
+#endif
+
+    if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
+    {
+      return dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid);
+    }
+    else
+    {
+      return false;
+    }
+  }
 }
--- a/OrthancServer/ServerContext.h	Wed May 20 16:38:33 2020 +0200
+++ b/OrthancServer/ServerContext.h	Wed May 20 17:03:24 2020 +0200
@@ -65,6 +65,7 @@
    **/
   class ServerContext :
     public IStorageCommitmentFactory,
+    public IDicomTranscoder,
     private JobsRegistry::IObserver
   {
   public:
@@ -202,7 +203,7 @@
 #endif
 
     ServerListeners listeners_;
-    boost::recursive_mutex listenersMutex_;
+    boost::shared_mutex listenersMutex_;
 
     bool done_;
     bool haveJobsChanged_;
@@ -228,6 +229,9 @@
 
     bool transcodeDicomProtocol_;
     std::unique_ptr<IDicomTranscoder>  dcmtkTranscoder_;
+    BuiltinDecoderTranscoderOrder builtinDecoderTranscoderOrder_;
+    bool isIngestTranscoding_;
+    DicomTransferSyntax ingestTransferSyntax_;
 
     StoreStatus StoreAfterTranscoding(std::string& resultPublicId,
                                       DicomInstanceToStore& dicom,
@@ -459,6 +463,12 @@
       return *storageCommitmentReports_;
     }
 
+    ImageAccessor* DecodeDicomFrame(const std::string& publicId,
+                                    unsigned int frameIndex);
+
+    ImageAccessor* DecodeDicomFrame(const DicomInstanceToStore& dicom,
+                                    unsigned int frameIndex);
+
     void StoreWithTranscoding(std::string& sopClassUid,
                               std::string& sopInstanceUid,
                               DicomStoreUserConnection& connection,
@@ -467,8 +477,11 @@
                               const std::string& moveOriginatorAet,
                               uint16_t moveOriginatorId);
 
-    // This accessor can be used even if the global option
+    // This method can be used even if the global option
     // "TranscodeDicomProtocol" is set to "false"
-    IDicomTranscoder& GetTranscoder();
+    virtual bool Transcode(DicomImage& target,
+                           DicomImage& source /* in, "GetParsed()" possibly modified */,
+                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                           bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancServer/ServerEnumerations.cpp	Wed May 20 16:38:33 2020 +0200
+++ b/OrthancServer/ServerEnumerations.cpp	Wed May 20 17:03:24 2020 +0200
@@ -214,6 +214,29 @@
                              "should be \"Always\", \"Never\" or \"Answers\": " + value);
     }    
   }
+
+
+  BuiltinDecoderTranscoderOrder StringToBuiltinDecoderTranscoderOrder(const std::string& value)
+  {
+    if (value == "Before")
+    {
+      return BuiltinDecoderTranscoderOrder_Before;
+    }
+    else if (value == "After")
+    {
+      return BuiltinDecoderTranscoderOrder_After;
+    }
+    else if (value == "Disabled")
+    {
+      return BuiltinDecoderTranscoderOrder_Disabled;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Configuration option \"BuiltinDecoderTranscoderOrder\" "
+                             "should be \"After\", \"Before\" or \"Disabled\": " + value);
+    }    
+  }
   
 
   std::string GetBasePath(ResourceType type,
--- a/OrthancServer/ServerEnumerations.h	Wed May 20 16:38:33 2020 +0200
+++ b/OrthancServer/ServerEnumerations.h	Wed May 20 17:03:24 2020 +0200
@@ -175,6 +175,13 @@
     ChangeType_NewChildInstance = 4097
   };
 
+  enum BuiltinDecoderTranscoderOrder
+  {
+    BuiltinDecoderTranscoderOrder_Before,
+    BuiltinDecoderTranscoderOrder_After,
+    BuiltinDecoderTranscoderOrder_Disabled
+  };
+
 
 
   void InitializeServerEnumerations();
@@ -194,6 +201,8 @@
 
   FindStorageAccessMode StringToFindStorageAccessMode(const std::string& str);
 
+  BuiltinDecoderTranscoderOrder StringToBuiltinDecoderTranscoderOrder(const std::string& str);
+
   std::string EnumerationToString(FileContentType type);
 
   std::string GetFileContentMime(FileContentType type);
--- a/OrthancServer/ServerJobs/ArchiveJob.cpp	Wed May 20 16:38:33 2020 +0200
+++ b/OrthancServer/ServerJobs/ArchiveJob.cpp	Wed May 20 17:03:24 2020 +0200
@@ -442,28 +442,17 @@
               // New in Orthanc 1.7.0
               std::set<DicomTransferSyntax> syntaxes;
               syntaxes.insert(transferSyntax);
-              
-              parsed.reset(new ParsedDicomFile(content));
-              const char* data = content.empty() ? NULL : content.c_str();
-              
-              std::unique_ptr<IDicomTranscoder::TranscodedDicom> transcodedDicom(
-                context.GetTranscoder().TranscodeToParsed(
-                  parsed->GetDcmtkObject(), data, content.size(),
-                  syntaxes, true /* allow new SOP instance UID */));
+
+              IDicomTranscoder::DicomImage source, transcoded;
+              source.SetExternalBuffer(content);
 
-              if (transcodedDicom.get() != NULL &&
-                  transcodedDicom->GetDicom().getDataset() != NULL)
+              if (context.Transcode(transcoded, source, syntaxes, true /* allow new SOP instance UID */))
               {
-                std::string transcoded;
-                FromDcmtkBridge::SaveToMemoryBuffer(
-                  transcoded, *transcodedDicom->GetDicom().getDataset());
-              
-                writer.Write(transcoded);
+                writer.Write(transcoded.GetBufferData(), transcoded.GetBufferSize());
 
                 if (dicomDir != NULL)
                 {
-                  std::unique_ptr<ParsedDicomFile> tmp(
-                    ParsedDicomFile::AcquireDcmtkObject(transcodedDicom->ReleaseDicom()));
+                  std::unique_ptr<ParsedDicomFile> tmp(transcoded.ReleaseAsParsedDicomFile());
                   dicomDir->Add(dicomDirFolder, filename_, *tmp);
                 }
                 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/CleaningInstancesJob.cpp	Wed May 20 17:03:24 2020 +0200
@@ -0,0 +1,120 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "CleaningInstancesJob.h"
+
+#include "../../Core/SerializationToolbox.h"
+#include "../ServerContext.h"
+
+
+namespace Orthanc
+{
+  bool CleaningInstancesJob::HandleTrailingStep()
+  {
+    if (!keepSource_)
+    {
+      const size_t n = GetInstancesCount();
+
+      for (size_t i = 0; i < n; i++)
+      {
+        Json::Value tmp;
+        context_.DeleteResource(tmp, GetInstance(i), ResourceType_Instance);
+      }
+    }
+
+    return true;
+  }
+
+  
+  void CleaningInstancesJob::SetKeepSource(bool keep)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    keepSource_ = keep;
+  }
+
+
+  static const char* KEEP_SOURCE = "KeepSource";
+
+
+  CleaningInstancesJob::CleaningInstancesJob(ServerContext& context,
+                                             const Json::Value& serialized,
+                                             bool defaultKeepSource) :
+    SetOfInstancesJob(serialized),  // (*)
+    context_(context)
+  {
+    if (!HasTrailingStep())
+    {
+      // Should have been set by (*)
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    if (serialized.isMember(KEEP_SOURCE))
+    {
+      keepSource_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SOURCE);
+    }
+    else
+    {
+      keepSource_ = defaultKeepSource;
+    }
+  }
+
+  
+  bool CleaningInstancesJob::Serialize(Json::Value& target)
+  {
+    if (!SetOfInstancesJob::Serialize(target))
+    {
+      return false;
+    }
+    else
+    {
+      target[KEEP_SOURCE] = keepSource_;
+      return true;
+    }
+  }
+
+
+  void CleaningInstancesJob::Start()
+  {
+    if (!HasTrailingStep())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "AddTrailingStep() should have been called before submitting the job");
+    }
+
+    SetOfInstancesJob::Start();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/CleaningInstancesJob.h	Wed May 20 17:03:24 2020 +0200
@@ -0,0 +1,79 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Core/JobsEngine/SetOfInstancesJob.h"
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  class CleaningInstancesJob : public SetOfInstancesJob
+  {
+  private:
+    ServerContext&  context_;
+    bool            keepSource_;
+    
+  protected:
+    virtual bool HandleTrailingStep();
+    
+  public:
+    CleaningInstancesJob(ServerContext& context,
+                         bool keepSource) :
+      context_(context),
+      keepSource_(keepSource)
+    {
+    }
+
+    CleaningInstancesJob(ServerContext& context,
+                         const Json::Value& serialized,
+                         bool defaultKeepSource);
+
+    ServerContext& GetContext() const
+    {
+      return context_;
+    }
+    
+    bool IsKeepSource() const
+    {
+      return keepSource_;
+    }
+    
+    void SetKeepSource(bool keep);
+
+    virtual bool Serialize(Json::Value& target);
+
+    virtual void Start();
+  };
+}
--- a/OrthancServer/ServerJobs/MergeStudyJob.cpp	Wed May 20 16:38:33 2020 +0200
+++ b/OrthancServer/ServerJobs/MergeStudyJob.cpp	Wed May 20 17:03:24 2020 +0200
@@ -48,7 +48,7 @@
 
     // Add all the instances of the series as to be processed
     std::list<std::string> instances;
-    context_.GetIndex().GetChildren(instances, series);
+    GetContext().GetIndex().GetChildren(instances, series);
 
     for (std::list<std::string>::const_iterator
            it = instances.begin(); it != instances.end(); ++it)
@@ -68,7 +68,7 @@
     else
     {
       std::list<std::string> series;
-      context_.GetIndex().GetChildren(series, study);
+      GetContext().GetIndex().GetChildren(series, study);
 
       for (std::list<std::string>::const_iterator
              it = series.begin(); it != series.end(); ++it)
@@ -81,6 +81,12 @@
 
   bool MergeStudyJob::HandleInstance(const std::string& instance)
   {
+    if (!HasTrailingStep())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "AddTrailingStep() should have been called after AddSourceXXX()");
+    }
+    
     /**
      * Retrieve the DICOM instance to be modified
      **/
@@ -89,7 +95,7 @@
 
     try
     {
-      ServerContext::DicomCacheLocker locker(context_, instance);
+      ServerContext::DicomCacheLocker locker(GetContext(), instance);
       modified.reset(locker.GetDicom().Clone(true));
     }
     catch (OrthancException&)
@@ -145,7 +151,7 @@
     toStore.SetParsedDicomFile(*modified);
 
     std::string modifiedInstance;
-    if (context_.Store(modifiedInstance, toStore,
+    if (GetContext().Store(modifiedInstance, toStore,
                        StoreInstanceMode_Default) != StoreStatus_Success)
     {
       LOG(ERROR) << "Error while storing a modified instance " << instance;
@@ -156,27 +162,9 @@
   }
 
   
-  bool MergeStudyJob::HandleTrailingStep()
-  {
-    if (!keepSource_)
-    {
-      const size_t n = GetInstancesCount();
-
-      for (size_t i = 0; i < n; i++)
-      {
-        Json::Value tmp;
-        context_.DeleteResource(tmp, GetInstance(i), ResourceType_Instance);
-      }
-    }
-
-    return true;
-  }
-
-  
   MergeStudyJob::MergeStudyJob(ServerContext& context,
                                const std::string& targetStudy) :
-    context_(context),
-    keepSource_(false),
+    CleaningInstancesJob(context, false /* by default, remove source instances */),
     targetStudy_(targetStudy)
   {
     /**
@@ -185,7 +173,7 @@
     
     ResourceType type;
 
-    if (!context_.GetIndex().LookupResourceType(type, targetStudy) ||
+    if (!GetContext().GetIndex().LookupResourceType(type, targetStudy) ||
         type != ResourceType_Study)
     {
       throw OrthancException(ErrorCode_UnknownResource,
@@ -202,7 +190,7 @@
     DicomTag::AddTagsForModule(removals_, DicomModule_Study);
     
     std::list<std::string> instances;
-    context_.GetIndex().GetChildInstances(instances, targetStudy);
+    GetContext().GetIndex().GetChildInstances(instances, targetStudy);
     
     if (instances.empty())
     {
@@ -212,7 +200,7 @@
     DicomMap dicom;
 
     {
-      ServerContext::DicomCacheLocker locker(context_, instances.front());
+      ServerContext::DicomCacheLocker locker(GetContext(), instances.front());
       locker.GetDicom().ExtractDicomSummary(dicom);
     }
 
@@ -260,7 +248,7 @@
     {
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
     }
-    else if (!context_.GetIndex().LookupResourceType(level, studyOrSeries))
+    else if (!GetContext().GetIndex().LookupResourceType(level, studyOrSeries))
     {
       throw OrthancException(ErrorCode_UnknownResource,
                              "Cannot find this resource: " + studyOrSeries);
@@ -295,7 +283,7 @@
     {
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
     }
-    else if (!context_.GetIndex().LookupParent(parent, series, ResourceType_Study))
+    else if (!GetContext().GetIndex().LookupParent(parent, series, ResourceType_Study))
     {
       throw OrthancException(ErrorCode_UnknownResource,
                              "This resource is not a series: " + series);
@@ -321,7 +309,7 @@
     {
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
     }
-    else if (!context_.GetIndex().LookupResourceType(actualLevel, study) ||
+    else if (!GetContext().GetIndex().LookupResourceType(actualLevel, study) ||
              actualLevel != ResourceType_Study)
     {
       throw OrthancException(ErrorCode_UnknownResource,
@@ -334,25 +322,13 @@
   }
 
 
-  void MergeStudyJob::SetKeepSource(bool keep)
-  {
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    keepSource_ = keep;
-  }
-
-
   void MergeStudyJob::GetPublicContent(Json::Value& value)
   {
-    SetOfInstancesJob::GetPublicContent(value);
+    CleaningInstancesJob::GetPublicContent(value);
     value["TargetStudy"] = targetStudy_;
   }
 
 
-  static const char* KEEP_SOURCE = "KeepSource";
   static const char* TARGET_STUDY = "TargetStudy";
   static const char* REPLACEMENTS = "Replacements";
   static const char* REMOVALS = "Removals";
@@ -362,8 +338,8 @@
 
   MergeStudyJob::MergeStudyJob(ServerContext& context,
                                const Json::Value& serialized) :
-    SetOfInstancesJob(serialized),  // (*)
-    context_(context)
+    CleaningInstancesJob(context, serialized,
+                         false /* by default, remove source instances */)  // (*)
   {
     if (!HasTrailingStep())
     {
@@ -371,7 +347,6 @@
       throw OrthancException(ErrorCode_InternalError);
     }
 
-    keepSource_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SOURCE);
     targetStudy_ = SerializationToolbox::ReadString(serialized, TARGET_STUDY);
     SerializationToolbox::ReadMapOfTags(replacements_, serialized, REPLACEMENTS);
     SerializationToolbox::ReadSetOfTags(removals_, serialized, REMOVALS);
@@ -382,13 +357,12 @@
   
   bool MergeStudyJob::Serialize(Json::Value& target)
   {
-    if (!SetOfInstancesJob::Serialize(target))
+    if (!CleaningInstancesJob::Serialize(target))
     {
       return false;
     }
     else
     {
-      target[KEEP_SOURCE] = keepSource_;
       target[TARGET_STUDY] = targetStudy_;
       SerializationToolbox::WriteMapOfTags(target, replacements_, REPLACEMENTS);
       SerializationToolbox::WriteSetOfTags(target, removals_, REMOVALS);
--- a/OrthancServer/ServerJobs/MergeStudyJob.h	Wed May 20 16:38:33 2020 +0200
+++ b/OrthancServer/ServerJobs/MergeStudyJob.h	Wed May 20 17:03:24 2020 +0200
@@ -34,22 +34,20 @@
 #pragma once
 
 #include "../../Core/DicomFormat/DicomMap.h"
-#include "../../Core/JobsEngine/SetOfInstancesJob.h"
 #include "../DicomInstanceOrigin.h"
+#include "CleaningInstancesJob.h"
 
 namespace Orthanc
 {
   class ServerContext;
   
-  class MergeStudyJob : public SetOfInstancesJob
+  class MergeStudyJob : public CleaningInstancesJob
   {
   private:
     typedef std::map<std::string, std::string>  SeriesUidMap;
     typedef std::map<DicomTag, std::string>     Replacements;
     
     
-    ServerContext&         context_;
-    bool                   keepSource_;
     std::string            targetStudy_;
     Replacements           replacements_;
     std::set<DicomTag>     removals_;
@@ -61,12 +59,9 @@
 
     void AddSourceStudyInternal(const std::string& study);
 
-
   protected:
     virtual bool HandleInstance(const std::string& instance);
 
-    virtual bool HandleTrailingStep();
-    
   public:
     MergeStudyJob(ServerContext& context,
                   const std::string& targetStudy);
@@ -85,13 +80,6 @@
 
     void AddSourceSeries(const std::string& series);
 
-    bool IsKeepSource() const
-    {
-      return keepSource_;
-    }
-    
-    void SetKeepSource(bool keep);
-
     void SetOrigin(const DicomInstanceOrigin& origin);
 
     void SetOrigin(const RestApiCall& call);
--- a/OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp	Wed May 20 16:38:33 2020 +0200
+++ b/OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp	Wed May 20 17:03:24 2020 +0200
@@ -35,8 +35,11 @@
 #include "OrthancPeerStoreJob.h"
 
 #include "../../Core/Logging.h"
+#include "../../Core/SerializationToolbox.h"
 #include "../ServerContext.h"
 
+#include <dcmtk/dcmdata/dcfilefo.h>
+
 
 namespace Orthanc
 {
@@ -55,7 +58,31 @@
 
     try
     {
-      context_.ReadDicom(client_->GetBody(), instance);
+      if (transcode_)
+      {
+        std::string dicom;
+        context_.ReadDicom(dicom, instance);
+
+        std::set<DicomTransferSyntax> syntaxes;
+        syntaxes.insert(transferSyntax_);
+        
+        IDicomTranscoder::DicomImage source, transcoded;
+        source.SetExternalBuffer(dicom);
+
+        if (context_.Transcode(transcoded, source, syntaxes, true))
+        {
+          client_->GetBody().assign(reinterpret_cast<const char*>(transcoded.GetBufferData()),
+                                    transcoded.GetBufferSize());
+        }
+        else
+        {
+          client_->GetBody().swap(dicom);
+        }
+      }
+      else
+      {
+        context_.ReadDicom(client_->GetBody(), instance);
+      }
     }
     catch (OrthancException& e)
     {
@@ -94,6 +121,61 @@
   }
 
 
+  DicomTransferSyntax OrthancPeerStoreJob::GetTransferSyntax() const
+  {
+    if (transcode_)
+    {
+      return transferSyntax_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+  
+
+  void OrthancPeerStoreJob::SetTranscode(DicomTransferSyntax syntax)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      transcode_ = true;
+      transferSyntax_ = syntax;
+    }    
+  }
+  
+
+  void OrthancPeerStoreJob::SetTranscode(const std::string& transferSyntaxUid)
+  {
+    DicomTransferSyntax s;
+    if (LookupTransferSyntax(s, transferSyntaxUid))
+    {
+      SetTranscode(s);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Unknown transfer syntax UID: " + transferSyntaxUid);
+    }
+  }
+
+
+  void OrthancPeerStoreJob::ClearTranscode()
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      transcode_ = false;
+    }
+  }
+
+
   void OrthancPeerStoreJob::Stop(JobStopReason reason)   // For pausing jobs
   {
     client_.reset(NULL);
@@ -109,17 +191,33 @@
                     false /* allow simple format if possible */,
                     false /* don't include passwords */);
     value["Peer"] = v;
+    
+    if (transcode_)
+    {
+      value["Transcode"] = GetTransferSyntaxUid(transferSyntax_);
+    }
   }
 
 
   static const char* PEER = "Peer";
+  static const char* TRANSCODE = "Transcode";
 
   OrthancPeerStoreJob::OrthancPeerStoreJob(ServerContext& context,
                                            const Json::Value& serialized) :
     SetOfInstancesJob(serialized),
     context_(context)
   {
+    assert(serialized.type() == Json::objectValue);
     peer_ = WebServiceParameters(serialized[PEER]);
+
+    if (serialized.isMember(TRANSCODE))
+    {
+      SetTranscode(SerializationToolbox::ReadString(serialized, TRANSCODE));
+    }
+    else
+    {
+      transcode_ = false;
+    }
   }
 
 
@@ -131,9 +229,16 @@
     }
     else
     {
+      assert(target.type() == Json::objectValue);
       peer_.Serialize(target[PEER],
                       true /* force advanced format */,
                       true /* include passwords */);
+
+      if (transcode_)
+      {
+        target[TRANSCODE] = GetTransferSyntaxUid(transferSyntax_);
+      }
+      
       return true;
     }
   }  
--- a/OrthancServer/ServerJobs/OrthancPeerStoreJob.h	Wed May 20 16:38:33 2020 +0200
+++ b/OrthancServer/ServerJobs/OrthancPeerStoreJob.h	Wed May 20 17:03:24 2020 +0200
@@ -48,6 +48,8 @@
     ServerContext&               context_;
     WebServiceParameters         peer_;
     std::unique_ptr<HttpClient>  client_;
+    bool                         transcode_;
+    DicomTransferSyntax          transferSyntax_;
 
   protected:
     virtual bool HandleInstance(const std::string& instance);
@@ -56,7 +58,8 @@
 
   public:
     OrthancPeerStoreJob(ServerContext& context) :
-      context_(context)
+      context_(context),
+      transcode_(false)
     {
     }
 
@@ -70,6 +73,19 @@
       return peer_;
     }
 
+    bool IsTranscode() const
+    {
+      return transcode_;
+    }
+
+    DicomTransferSyntax GetTransferSyntax() const;
+
+    void SetTranscode(DicomTransferSyntax syntax);
+
+    void SetTranscode(const std::string& transferSyntaxUid);
+
+    void ClearTranscode();
+
     virtual void Stop(JobStopReason reason);   // For pausing jobs
 
     virtual void GetJobType(std::string& target)
--- a/OrthancServer/ServerJobs/ResourceModificationJob.cpp	Wed May 20 16:38:33 2020 +0200
+++ b/OrthancServer/ServerJobs/ResourceModificationJob.cpp	Wed May 20 17:03:24 2020 +0200
@@ -38,6 +38,10 @@
 #include "../../Core/SerializationToolbox.h"
 #include "../ServerContext.h"
 
+#include <dcmtk/dcmdata/dcfilefo.h>
+#include <dcmtk/dcmdata/dcdeftag.h>
+#include <cassert>
+
 namespace Orthanc
 {
   class ResourceModificationJob::Output : public boost::noncopyable
@@ -152,7 +156,7 @@
 
     try
     {
-      ServerContext::DicomCacheLocker locker(context_, instance);
+      ServerContext::DicomCacheLocker locker(GetContext(), instance);
       ParsedDicomFile& original = locker.GetDicom();
 
       originalHasher.reset(new DicomInstanceHasher(original.GetHasher()));
@@ -171,6 +175,41 @@
 
     modification_->Apply(*modified);
 
+    const std::string modifiedUid = IDicomTranscoder::GetSopInstanceUid(modified->GetDcmtkObject());
+    
+    if (transcode_)
+    {
+      std::set<DicomTransferSyntax> syntaxes;
+      syntaxes.insert(transferSyntax_);
+
+      IDicomTranscoder::DicomImage source;
+      source.AcquireParsed(*modified);  // "modified" is invalid below this point
+      
+      IDicomTranscoder::DicomImage transcoded;
+      if (GetContext().Transcode(transcoded, source, syntaxes, true))
+      {
+        modified.reset(transcoded.ReleaseAsParsedDicomFile());
+
+        // Fix the SOP instance UID in order the preserve the
+        // references between instance UIDs in the DICOM hierarchy
+        // (the UID might have changed in the case of lossy transcoding)
+        if (modified.get() == NULL ||
+            modified->GetDcmtkObject().getDataset() == NULL ||
+            !modified->GetDcmtkObject().getDataset()->putAndInsertString(
+              DCM_SOPInstanceUID, modifiedUid.c_str(), OFTrue /* replace */).good())
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+      else
+      {
+        LOG(WARNING) << "Cannot transcode instance, keeping original transfer syntax: " << instance;
+        modified.reset(source.ReleaseAsParsedDicomFile());
+      }
+    }
+
+    assert(modifiedUid == IDicomTranscoder::GetSopInstanceUid(modified->GetDcmtkObject()));
+
     DicomInstanceToStore toStore;
     toStore.SetOrigin(origin_);
     toStore.SetParsedDicomFile(*modified);
@@ -211,24 +250,32 @@
      **/
 
     std::string modifiedInstance;
-    if (context_.Store(modifiedInstance, toStore,
-                       StoreInstanceMode_Default) != StoreStatus_Success)
+    if (GetContext().Store(modifiedInstance, toStore,
+                           StoreInstanceMode_Default) != StoreStatus_Success)
     {
       throw OrthancException(ErrorCode_CannotStoreInstance,
                              "Error while storing a modified instance " + instance);
     }
 
-    assert(modifiedInstance == modifiedHasher.HashInstance());
+    /**
+     * The assertion below will fail if automated transcoding to a
+     * lossy transfer syntax is enabled in the Orthanc core, and if
+     * the source instance is not in this transfer syntax.
+     **/
+    // assert(modifiedInstance == modifiedHasher.HashInstance());
 
     output_->Update(modifiedHasher);
 
     return true;
   }
 
-  
-  bool ResourceModificationJob::HandleTrailingStep()
+
+  ResourceModificationJob::ResourceModificationJob(ServerContext& context) :
+    CleaningInstancesJob(context, true /* by default, keep source */),
+    modification_(new DicomModification),
+    isAnonymization_(false),
+    transcode_(false)
   {
-    throw OrthancException(ErrorCode_InternalError);
   }
 
 
@@ -285,9 +332,64 @@
   }
 
 
+  DicomTransferSyntax ResourceModificationJob::GetTransferSyntax() const
+  {
+    if (transcode_)
+    {
+      return transferSyntax_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+  
+
+  void ResourceModificationJob::SetTranscode(DicomTransferSyntax syntax)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      transcode_ = true;
+      transferSyntax_ = syntax;
+    }    
+  }
+
+
+  void ResourceModificationJob::SetTranscode(const std::string& transferSyntaxUid)
+  {
+    DicomTransferSyntax s;
+    if (LookupTransferSyntax(s, transferSyntaxUid))
+    {
+      SetTranscode(s);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Unknown transfer syntax UID: " + transferSyntaxUid);
+    }
+  }
+
+
+  void ResourceModificationJob::ClearTranscode()
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      transcode_ = false;
+    }
+  }
+
+
   void ResourceModificationJob::GetPublicContent(Json::Value& value)
   {
-    SetOfInstancesJob::GetPublicContent(value);
+    CleaningInstancesJob::GetPublicContent(value);
 
     value["IsAnonymization"] = isAnonymization_;
 
@@ -295,33 +397,57 @@
     {
       output_->Format(value);
     }
+
+    if (transcode_)
+    {
+      value["Transcode"] = GetTransferSyntaxUid(transferSyntax_);
+    }
   }
 
 
   static const char* MODIFICATION = "Modification";
   static const char* ORIGIN = "Origin";
   static const char* IS_ANONYMIZATION = "IsAnonymization";
+  static const char* TRANSCODE = "Transcode";
   
 
   ResourceModificationJob::ResourceModificationJob(ServerContext& context,
                                                    const Json::Value& serialized) :
-    SetOfInstancesJob(serialized),
-    context_(context)
+    CleaningInstancesJob(context, serialized, true /* by default, keep source */)
   {
+    assert(serialized.type() == Json::objectValue);
+
     isAnonymization_ = SerializationToolbox::ReadBoolean(serialized, IS_ANONYMIZATION);
     origin_ = DicomInstanceOrigin(serialized[ORIGIN]);
     modification_.reset(new DicomModification(serialized[MODIFICATION]));
+
+    if (serialized.isMember(TRANSCODE))
+    {
+      SetTranscode(SerializationToolbox::ReadString(serialized, TRANSCODE));
+    }
+    else
+    {
+      transcode_ = false;
+    }
   }
   
   bool ResourceModificationJob::Serialize(Json::Value& value)
   {
-    if (!SetOfInstancesJob::Serialize(value))
+    if (!CleaningInstancesJob::Serialize(value))
     {
       return false;
     }
     else
     {
+      assert(value.type() == Json::objectValue);
+      
       value[IS_ANONYMIZATION] = isAnonymization_;
+
+      if (transcode_)
+      {
+        value[TRANSCODE] = GetTransferSyntaxUid(transferSyntax_);
+      }
+      
       origin_.Serialize(value[ORIGIN]);
       
       Json::Value tmp;
--- a/OrthancServer/ServerJobs/ResourceModificationJob.h	Wed May 20 16:38:33 2020 +0200
+++ b/OrthancServer/ServerJobs/ResourceModificationJob.h	Wed May 20 17:03:24 2020 +0200
@@ -33,36 +33,31 @@
 
 #pragma once
 
-#include "../../Core/JobsEngine/SetOfInstancesJob.h"
 #include "../../Core/DicomParsing/DicomModification.h"
 #include "../DicomInstanceOrigin.h"
+#include "CleaningInstancesJob.h"
 
 namespace Orthanc
 {
   class ServerContext;
   
-  class ResourceModificationJob : public SetOfInstancesJob
+  class ResourceModificationJob : public CleaningInstancesJob
   {
   private:
     class Output;
     
-    ServerContext&                      context_;
     std::unique_ptr<DicomModification>  modification_;
     boost::shared_ptr<Output>           output_;
     bool                                isAnonymization_;
     DicomInstanceOrigin                 origin_;
+    bool                                transcode_;
+    DicomTransferSyntax                 transferSyntax_;
 
   protected:
     virtual bool HandleInstance(const std::string& instance);
     
-    virtual bool HandleTrailingStep();
-
   public:
-    ResourceModificationJob(ServerContext& context) :
-      context_(context),
-      isAnonymization_(false)
-    {
-    }
+    ResourceModificationJob(ServerContext& context);
 
     ResourceModificationJob(ServerContext& context,
                             const Json::Value& serialized);
@@ -87,6 +82,19 @@
       return origin_;
     }
 
+    bool IsTranscode() const
+    {
+      return transcode_;
+    }
+
+    DicomTransferSyntax GetTransferSyntax() const;
+
+    void SetTranscode(DicomTransferSyntax syntax);
+
+    void SetTranscode(const std::string& transferSyntaxUid);
+
+    void ClearTranscode();
+
     virtual void Stop(JobStopReason reason)
     {
     }
--- a/OrthancServer/ServerJobs/SplitStudyJob.cpp	Wed May 20 16:38:33 2020 +0200
+++ b/OrthancServer/ServerJobs/SplitStudyJob.cpp	Wed May 20 17:03:24 2020 +0200
@@ -67,6 +67,12 @@
   
   bool SplitStudyJob::HandleInstance(const std::string& instance)
   {
+    if (!HasTrailingStep())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "AddTrailingStep() should have been called after AddSourceSeries()");
+    }
+    
     /**
      * Retrieve the DICOM instance to be modified
      **/
@@ -75,7 +81,7 @@
 
     try
     {
-      ServerContext::DicomCacheLocker locker(context_, instance);
+      ServerContext::DicomCacheLocker locker(GetContext(), instance);
       modified.reset(locker.GetDicom().Clone(true));
     }
     catch (OrthancException&)
@@ -138,8 +144,8 @@
     toStore.SetParsedDicomFile(*modified);
 
     std::string modifiedInstance;
-    if (context_.Store(modifiedInstance, toStore,
-                       StoreInstanceMode_Default) != StoreStatus_Success)
+    if (GetContext().Store(modifiedInstance, toStore,
+                           StoreInstanceMode_Default) != StoreStatus_Success)
     {
       LOG(ERROR) << "Error while storing a modified instance " << instance;
       return false;
@@ -149,27 +155,9 @@
   }
 
   
-  bool SplitStudyJob::HandleTrailingStep()
-  {
-    if (!keepSource_)
-    {
-      const size_t n = GetInstancesCount();
-
-      for (size_t i = 0; i < n; i++)
-      {
-        Json::Value tmp;
-        context_.DeleteResource(tmp, GetInstance(i), ResourceType_Instance);
-      }
-    }
-
-    return true;
-  }
-
-  
   SplitStudyJob::SplitStudyJob(ServerContext& context,
                                const std::string& sourceStudy) :
-    context_(context),
-    keepSource_(false),
+    CleaningInstancesJob(context, false /* by default, remove source instances */),
     sourceStudy_(sourceStudy),
     targetStudyUid_(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study))
   {
@@ -177,7 +165,7 @@
     
     ResourceType type;
     
-    if (!context_.GetIndex().LookupResourceType(type, sourceStudy) ||
+    if (!GetContext().GetIndex().LookupResourceType(type, sourceStudy) ||
         type != ResourceType_Study)
     {
       throw OrthancException(ErrorCode_UnknownResource,
@@ -213,7 +201,7 @@
     {
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
     }
-    else if (!context_.GetIndex().LookupParent(parent, series, ResourceType_Study) ||
+    else if (!GetContext().GetIndex().LookupParent(parent, series, ResourceType_Study) ||
              parent != sourceStudy_)
     {
       throw OrthancException(ErrorCode_UnknownResource,
@@ -226,7 +214,7 @@
 
       // Add all the instances of the series as to be processed
       std::list<std::string> instances;
-      context_.GetIndex().GetChildren(instances, series);
+      GetContext().GetIndex().GetChildren(instances, series);
 
       for (std::list<std::string>::const_iterator
              it = instances.begin(); it != instances.end(); ++it)
@@ -237,17 +225,6 @@
   }
 
 
-  void SplitStudyJob::SetKeepSource(bool keep)
-  {
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    keepSource_ = keep;
-  }
-
-
   bool SplitStudyJob::LookupTargetSeriesUid(std::string& uid,
                                             const std::string& series) const
   {
@@ -309,7 +286,7 @@
     
   void SplitStudyJob::GetPublicContent(Json::Value& value)
   {
-    SetOfInstancesJob::GetPublicContent(value);
+    CleaningInstancesJob::GetPublicContent(value);
 
     if (!targetStudy_.empty())
     {
@@ -320,7 +297,6 @@
   }
 
 
-  static const char* KEEP_SOURCE = "KeepSource";
   static const char* SOURCE_STUDY = "SourceStudy";
   static const char* TARGET_STUDY = "TargetStudy";
   static const char* TARGET_STUDY_UID = "TargetStudyUID";
@@ -332,8 +308,8 @@
 
   SplitStudyJob::SplitStudyJob(ServerContext& context,
                                const Json::Value& serialized) :
-    SetOfInstancesJob(serialized),  // (*)
-    context_(context)
+    CleaningInstancesJob(context, serialized,
+                         false /* by default, remove source instances */)  // (*)
   {
     if (!HasTrailingStep())
     {
@@ -343,7 +319,6 @@
 
     Setup();
 
-    keepSource_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SOURCE);
     sourceStudy_ = SerializationToolbox::ReadString(serialized, SOURCE_STUDY);
     targetStudy_ = SerializationToolbox::ReadString(serialized, TARGET_STUDY);
     targetStudyUid_ = SerializationToolbox::ReadString(serialized, TARGET_STUDY_UID);
@@ -356,13 +331,12 @@
   
   bool SplitStudyJob::Serialize(Json::Value& target)
   {
-    if (!SetOfInstancesJob::Serialize(target))
+    if (!CleaningInstancesJob::Serialize(target))
     {
       return false;
     }
     else
     {
-      target[KEEP_SOURCE] = keepSource_;
       target[SOURCE_STUDY] = sourceStudy_;
       target[TARGET_STUDY] = targetStudy_;
       target[TARGET_STUDY_UID] = targetStudyUid_;
--- a/OrthancServer/ServerJobs/SplitStudyJob.h	Wed May 20 16:38:33 2020 +0200
+++ b/OrthancServer/ServerJobs/SplitStudyJob.h	Wed May 20 17:03:24 2020 +0200
@@ -33,24 +33,22 @@
 
 #pragma once
 
-#include "../../Core/JobsEngine/SetOfInstancesJob.h"
 #include "../../Core/DicomFormat/DicomTag.h"
 #include "../DicomInstanceOrigin.h"
+#include "CleaningInstancesJob.h"
 
 namespace Orthanc
 {
   class ServerContext;
   
-  class SplitStudyJob : public SetOfInstancesJob
+  class SplitStudyJob : public CleaningInstancesJob
   {
   private:
     typedef std::map<std::string, std::string>  SeriesUidMap;
     typedef std::map<DicomTag, std::string>     Replacements;
     
     
-    ServerContext&         context_;
     std::set<DicomTag>     allowedTags_;
-    bool                   keepSource_;
     std::string            sourceStudy_;
     std::string            targetStudy_;
     std::string            targetStudyUid_;
@@ -66,8 +64,6 @@
   protected:
     virtual bool HandleInstance(const std::string& instance);
 
-    virtual bool HandleTrailingStep();
-    
   public:
     SplitStudyJob(ServerContext& context,
                   const std::string& sourceStudy);
@@ -92,13 +88,6 @@
 
     void AddSourceSeries(const std::string& series);
 
-    bool IsKeepSource() const
-    {
-      return keepSource_;
-    }
-    
-    void SetKeepSource(bool keep);
-
     bool LookupTargetSeriesUid(std::string& uid,
                                const std::string& series) const;
 
--- a/Plugins/Engine/OrthancPlugins.cpp	Wed May 20 16:38:33 2020 +0200
+++ b/Plugins/Engine/OrthancPlugins.cpp	Wed May 20 17:03:24 2020 +0200
@@ -62,7 +62,6 @@
 #include "../../Core/OrthancException.h"
 #include "../../Core/SerializationToolbox.h"
 #include "../../Core/Toolbox.h"
-#include "../../OrthancServer/DefaultDicomImageDecoder.h"
 #include "../../OrthancServer/OrthancConfiguration.h"
 #include "../../OrthancServer/OrthancFindRequestHandler.h"
 #include "../../OrthancServer/Search/HierarchicalMatcher.h"
@@ -71,12 +70,13 @@
 #include "PluginsEnumerations.h"
 #include "PluginsJob.h"
 
-#include <boost/regex.hpp> 
+#include <boost/regex.hpp>
 #include <dcmtk/dcmdata/dcdict.h>
 #include <dcmtk/dcmdata/dcdicent.h>
 
 #define ERROR_MESSAGE_64BIT "A 64bit version of the Orthanc API is necessary"
 
+
 namespace Orthanc
 {
   static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target,
@@ -330,9 +330,11 @@
     class DicomWebBinaryFormatter : public DicomWebJsonVisitor::IBinaryFormatter
     {
     private:
-      OrthancPluginDicomWebBinaryCallback  callback_;
-      DicomWebJsonVisitor::BinaryMode      currentMode_;
-      std::string                          currentBulkDataUri_;
+      OrthancPluginDicomWebBinaryCallback   oldCallback_;
+      OrthancPluginDicomWebBinaryCallback2  newCallback_;  // New in Orthanc 1.7.0
+      void*                                 newPayload_;   // New in Orthanc 1.7.0
+      DicomWebJsonVisitor::BinaryMode       currentMode_;
+      std::string                           currentBulkDataUri_;
 
       static void Setter(OrthancPluginDicomWebNode*       node,
                          OrthancPluginDicomWebBinaryMode  mode,
@@ -366,8 +368,18 @@
       }
       
     public:
-      DicomWebBinaryFormatter(const _OrthancPluginEncodeDicomWeb& parameters) :
-        callback_(parameters.callback)
+      DicomWebBinaryFormatter(OrthancPluginDicomWebBinaryCallback callback) :
+        oldCallback_(callback),
+        newCallback_(NULL),
+        newPayload_(NULL)
+      {
+      }
+      
+      DicomWebBinaryFormatter(OrthancPluginDicomWebBinaryCallback2 callback,
+                              void* payload) :
+        oldCallback_(NULL),
+        newCallback_(callback),
+        newPayload_(payload)
       {
       }
       
@@ -377,7 +389,8 @@
                                                      const DicomTag& tag,
                                                      ValueRepresentation vr)
       {
-        if (callback_ == NULL)
+        if (oldCallback_ == NULL &&
+            newCallback_ == NULL)
         {
           return DicomWebJsonVisitor::BinaryMode_InlineBinary;
         }
@@ -398,20 +411,70 @@
 
           currentMode_ = DicomWebJsonVisitor::BinaryMode_Ignore;
 
-          callback_(reinterpret_cast<OrthancPluginDicomWebNode*>(this),
-                    DicomWebBinaryFormatter::Setter,
-                    static_cast<uint32_t>(parentTags.size()),
-                    (empty ? NULL : &groups[0]),
-                    (empty ? NULL : &elements[0]),
-                    (empty ? NULL : &indexes[0]),
-                    tag.GetGroup(),
-                    tag.GetElement(),
-                    Plugins::Convert(vr));
+          if (oldCallback_ != NULL)
+          {
+            oldCallback_(reinterpret_cast<OrthancPluginDicomWebNode*>(this),
+                         DicomWebBinaryFormatter::Setter,
+                         static_cast<uint32_t>(parentTags.size()),
+                         (empty ? NULL : &groups[0]),
+                         (empty ? NULL : &elements[0]),
+                         (empty ? NULL : &indexes[0]),
+                         tag.GetGroup(),
+                         tag.GetElement(),
+                         Plugins::Convert(vr));
+          }
+          else
+          {
+            assert(newCallback_ != NULL);
+            newCallback_(reinterpret_cast<OrthancPluginDicomWebNode*>(this),
+                         DicomWebBinaryFormatter::Setter,
+                         static_cast<uint32_t>(parentTags.size()),
+                         (empty ? NULL : &groups[0]),
+                         (empty ? NULL : &elements[0]),
+                         (empty ? NULL : &indexes[0]),
+                         tag.GetGroup(),
+                         tag.GetElement(),
+                         Plugins::Convert(vr),
+                         newPayload_);
+          }          
 
           bulkDataUri = currentBulkDataUri_;          
           return currentMode_;
         }
       }
+
+      void Apply(char** target,
+                 bool isJson,
+                 ParsedDicomFile& dicom)
+      {
+        DicomWebJsonVisitor visitor;
+        visitor.SetFormatter(*this);
+
+        dicom.Apply(visitor);
+
+        std::string s;
+
+        if (isJson)
+        {
+          s = visitor.GetResult().toStyledString();
+        }
+        else
+        {
+          visitor.FormatXml(s);
+        }
+
+        *target = CopyString(s);
+      }
+
+  
+      void Apply(char** target,
+                 bool isJson,
+                 const void* dicom,
+                 size_t dicomSize) 
+      {
+        ParsedDicomFile parsed(dicom, dicomSize);
+        Apply(target, isJson, parsed);
+      }
     };
   }
 
@@ -827,6 +890,7 @@
     typedef std::list<OrthancPluginIncomingHttpRequestFilter2>  IncomingHttpRequestFilters2;
     typedef std::list<OrthancPluginIncomingDicomInstanceFilter>  IncomingDicomInstanceFilters;
     typedef std::list<OrthancPluginDecodeImageCallback>  DecodeImageCallbacks;
+    typedef std::list<OrthancPluginTranscoderCallback>  TranscoderCallbacks;
     typedef std::list<OrthancPluginJobsUnserializer>  JobsUnserializers;
     typedef std::list<OrthancPluginRefreshMetricsCallback>  RefreshMetricsCallbacks;
     typedef std::list<StorageCommitmentScp*>  StorageCommitmentScpCallbacks;
@@ -841,6 +905,7 @@
     OrthancPluginFindCallback  findCallback_;
     OrthancPluginWorklistCallback  worklistCallback_;
     DecodeImageCallbacks  decodeImageCallbacks_;
+    TranscoderCallbacks  transcoderCallbacks_;
     JobsUnserializers  jobsUnserializers_;
     _OrthancPluginMoveCallback moveCallbacks_;
     IncomingHttpRequestFilters  incomingHttpRequestFilters_;
@@ -855,7 +920,7 @@
     boost::recursive_mutex changeCallbackMutex_;
     boost::mutex findCallbackMutex_;
     boost::mutex worklistCallbackMutex_;
-    boost::mutex decodeImageCallbackMutex_;
+    boost::shared_mutex decoderTranscoderMutex_;  // Changed from "boost::mutex" in Orthanc 1.7.0
     boost::mutex jobsUnserializersMutex_;
     boost::mutex refreshMetricsMutex_;
     boost::mutex storageCommitmentScpMutex_;
@@ -1831,8 +1896,8 @@
     DicomInstanceToStore              instance_;
 
   public:
-    DicomInstanceFromTranscoded(IDicomTranscoder::TranscodedDicom& transcoded) :
-      parsed_(ParsedDicomFile::AcquireDcmtkObject(transcoded.ReleaseDicom()))
+    DicomInstanceFromTranscoded(IDicomTranscoder::DicomImage& transcoded) :
+      parsed_(transcoded.ReleaseAsParsedDicomFile())
     {
       instance_.SetParsedDicomFile(*parsed_);
       instance_.SetOrigin(DicomInstanceOrigin::FromPlugins());
@@ -2046,7 +2111,7 @@
     const _OrthancPluginDecodeImageCallback& p = 
       *reinterpret_cast<const _OrthancPluginDecodeImageCallback*>(parameters);
 
-    boost::mutex::scoped_lock lock(pimpl_->decodeImageCallbackMutex_);
+    boost::unique_lock<boost::shared_mutex> lock(pimpl_->decoderTranscoderMutex_);
 
     pimpl_->decodeImageCallbacks_.push_back(p.callback);
     LOG(INFO) << "Plugin has registered a callback to decode DICOM images (" 
@@ -2054,6 +2119,19 @@
   }
 
 
+  void OrthancPlugins::RegisterTranscoderCallback(const void* parameters)
+  {
+    const _OrthancPluginTranscoderCallback& p = 
+      *reinterpret_cast<const _OrthancPluginTranscoderCallback*>(parameters);
+
+    boost::unique_lock<boost::shared_mutex> lock(pimpl_->decoderTranscoderMutex_);
+
+    pimpl_->transcoderCallbacks_.push_back(p.callback);
+    LOG(INFO) << "Plugin has registered a callback to transcode DICOM images (" 
+              << pimpl_->transcoderCallbacks_.size() << " transcoder(s) now active)";
+  }
+
+
   void OrthancPlugins::RegisterJobsUnserializer(const void* parameters)
   {
     const _OrthancPluginJobsUnserializer& p = 
@@ -2693,6 +2771,11 @@
     // Images returned to plugins are assumed to be writeable. If the
     // input image is read-only, we return a copy so that it can be modified.
 
+    if (image.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    
     if (image->IsReadOnly())
     {
       std::unique_ptr<Image> copy(new Image(image->GetFormat(), image->GetWidth(), image->GetHeight(), false));
@@ -2746,30 +2829,17 @@
         
       case _OrthancPluginService_GetInstanceDecodedFrame:
       {
-        bool hasDecoderPlugin;
-
-        {
-          boost::mutex::scoped_lock lock(pimpl_->decodeImageCallbackMutex_);
-          hasDecoderPlugin = !pimpl_->decodeImageCallbacks_.empty();
-        }
-
-        std::unique_ptr<ImageAccessor> decoded;
         if (p.targetImage == NULL)
         {
           throw OrthancException(ErrorCode_NullPointer);
         }
-        else if (hasDecoderPlugin)
+
+        std::unique_ptr<ImageAccessor> decoded;
         {
-          // TODO - This call could be speeded up the future, if a
-          // "decoding context" gets introduced in the decoder plugins
-          
-          decoded.reset(Decode(instance.GetBufferData(), instance.GetBufferSize(), p.frameIndex));
+          PImpl::ServerContextLock lock(*pimpl_);
+          decoded.reset(lock.GetContext().DecodeDicomFrame(instance, p.frameIndex));
         }
-        else
-        {
-          decoded.reset(DicomImageDecoder::Decode(instance.GetParsedDicomFile(), p.frameIndex));
-        }
-
+        
         *(p.targetImage) = ReturnImage(decoded);
         return;
       }
@@ -2789,7 +2859,39 @@
         CopyToMemoryBuffer(*p.targetBuffer, serialized);
         return;
       }
+
+      case _OrthancPluginService_GetInstanceAdvancedJson:
+      {
+        if (p.targetStringToFree == NULL)
+        {
+          throw OrthancException(ErrorCode_NullPointer);
+        }
         
+        Json::Value json;
+        instance.GetParsedDicomFile().DatasetToJson(
+          json, Plugins::Convert(p.format), 
+          static_cast<DicomToJsonFlags>(p.flags), p.maxStringLength);
+
+        Json::FastWriter writer;
+        *p.targetStringToFree = CopyString(writer.write(json));        
+        return;
+      }
+      
+      case _OrthancPluginService_GetInstanceDicomWebJson:
+      case _OrthancPluginService_GetInstanceDicomWebXml:
+      {
+        if (p.targetStringToFree == NULL)
+        {
+          throw OrthancException(ErrorCode_NullPointer);
+        }
+
+        DicomWebBinaryFormatter formatter(p.dicomWebCallback, p.dicomWebPayload);
+        formatter.Apply(p.targetStringToFree,
+                        (service == _OrthancPluginService_GetInstanceDicomWebJson),
+                        instance.GetParsedDicomFile());
+        return;
+      }
+
       default:
         throw OrthancException(ErrorCode_InternalError);
     }
@@ -3675,6 +3777,9 @@
       case _OrthancPluginService_GetInstanceRawFrame:
       case _OrthancPluginService_GetInstanceDecodedFrame:
       case _OrthancPluginService_SerializeDicomInstance:
+      case _OrthancPluginService_GetInstanceAdvancedJson:
+      case _OrthancPluginService_GetInstanceDicomWebJson:
+      case _OrthancPluginService_GetInstanceDicomWebXml:
         AccessDicomInstance2(service, parameters);
         return true;
 
@@ -4195,28 +4300,23 @@
         const _OrthancPluginEncodeDicomWeb& p =
           *reinterpret_cast<const _OrthancPluginEncodeDicomWeb*>(parameters);
 
-        DicomWebBinaryFormatter formatter(p);
-        
-        DicomWebJsonVisitor visitor;
-        visitor.SetFormatter(formatter);
-
-        {
-          ParsedDicomFile dicom(p.dicom, p.dicomSize);
-          dicom.Apply(visitor);
-        }
-
-        std::string s;
-
-        if (service == _OrthancPluginService_EncodeDicomWebJson)
-        {
-          s = visitor.GetResult().toStyledString();
-        }
-        else
-        {
-          visitor.FormatXml(s);
-        }
-
-        *p.target = CopyString(s);
+        DicomWebBinaryFormatter formatter(p.callback);
+        formatter.Apply(p.target,
+                        (service == _OrthancPluginService_EncodeDicomWebJson),
+                        p.dicom, p.dicomSize);
+        return true;
+      }
+
+      case _OrthancPluginService_EncodeDicomWebJson2:
+      case _OrthancPluginService_EncodeDicomWebXml2:
+      {
+        const _OrthancPluginEncodeDicomWeb2& p =
+          *reinterpret_cast<const _OrthancPluginEncodeDicomWeb2*>(parameters);
+
+        DicomWebBinaryFormatter formatter(p.callback, p.payload);
+        formatter.Apply(p.target,
+                        (service == _OrthancPluginService_EncodeDicomWebJson2),
+                        p.dicom, p.dicomSize);
         return true;
       }
 
@@ -4269,32 +4369,52 @@
         }
         else
         {
-          ParsedDicomFile dicom(p.buffer, p.size);
-
           std::set<DicomTransferSyntax> syntaxes;
           syntaxes.insert(transferSyntax);
-          
-          std::unique_ptr<IDicomTranscoder::TranscodedDicom> transcoded;
+
+          IDicomTranscoder::DicomImage source;
+          source.SetExternalBuffer(p.buffer, p.size);
+
+          IDicomTranscoder::DicomImage transcoded;
+          bool success;
           
           {
             PImpl::ServerContextLock lock(*pimpl_);
-            transcoded.reset(lock.GetContext().GetTranscoder().TranscodeToParsed(
-                               dicom.GetDcmtkObject(), p.buffer, p.size,
-                               syntaxes, true /* allow new sop */));
+            success = lock.GetContext().Transcode(
+              transcoded, source, syntaxes, true /* allow new sop */);
           }
 
-          if (transcoded.get() == NULL)
+          if (success)
+          {
+            *(p.target) = reinterpret_cast<OrthancPluginDicomInstance*>(
+              new DicomInstanceFromTranscoded(transcoded));
+            return true;
+          }
+          else
           {
             throw OrthancException(ErrorCode_NotImplemented, "Cannot transcode image");
           }
-          else
-          {
-            *(p.target) = reinterpret_cast<OrthancPluginDicomInstance*>(
-              new DicomInstanceFromTranscoded(*transcoded));
-            return true;
-          }
         }
       }
+
+      case _OrthancPluginService_CreateMemoryBuffer:
+      {
+        const _OrthancPluginCreateMemoryBuffer& p =
+          *reinterpret_cast<const _OrthancPluginCreateMemoryBuffer*>(parameters);
+
+        p.target->size = p.size;
+        
+        if (p.size == 0)
+        {
+          p.target->data = NULL;
+        }
+        else
+        {
+          p.target->data = malloc(p.size);
+        }          
+        
+        return true;
+      }
         
       default:
         return false;
@@ -4349,6 +4469,10 @@
         RegisterDecodeImageCallback(parameters);
         return true;
 
+      case _OrthancPluginService_RegisterTranscoderCallback:
+        RegisterTranscoderCallback(parameters);
+        return true;
+
       case _OrthancPluginService_RegisterJobsUnserializer:
         RegisterJobsUnserializer(parameters);
         return true;
@@ -4733,16 +4857,23 @@
 
   bool OrthancPlugins::HasCustomImageDecoder()
   {
-    boost::mutex::scoped_lock lock(pimpl_->decodeImageCallbackMutex_);
+    boost::shared_lock<boost::shared_mutex> lock(pimpl_->decoderTranscoderMutex_);
     return !pimpl_->decodeImageCallbacks_.empty();
   }
 
 
-  ImageAccessor*  OrthancPlugins::DecodeUnsafe(const void* dicom,
-                                               size_t size,
-                                               unsigned int frame)
+  bool OrthancPlugins::HasCustomTranscoder()
   {
-    boost::mutex::scoped_lock lock(pimpl_->decodeImageCallbackMutex_);
+    boost::shared_lock<boost::shared_mutex> lock(pimpl_->decoderTranscoderMutex_);
+    return !pimpl_->transcoderCallbacks_.empty();
+  }
+
+
+  ImageAccessor* OrthancPlugins::Decode(const void* dicom,
+                                        size_t size,
+                                        unsigned int frame)
+  {
+    boost::shared_lock<boost::shared_mutex> lock(pimpl_->decoderTranscoderMutex_);
 
     for (PImpl::DecodeImageCallbacks::const_iterator
            decoder = pimpl_->decodeImageCallbacks_.begin();
@@ -4759,25 +4890,6 @@
     return NULL;
   }
 
-
-  ImageAccessor* OrthancPlugins::Decode(const void* dicom,
-                                        size_t size,
-                                        unsigned int frame)
-  {
-    ImageAccessor* result = DecodeUnsafe(dicom, size, frame);
-
-    if (result != NULL)
-    {
-      return result;
-    }
-    else
-    {
-      LOG(INFO) << "The installed image decoding plugins cannot handle an image, fallback to the built-in decoder";
-      DefaultDicomImageDecoder defaultDecoder;
-      return defaultDecoder.Decode(dicom, size, frame); 
-    }
-  }
-
   
   bool OrthancPlugins::IsAllowed(HttpMethod method,
                                  const char* uri,
@@ -5064,16 +5176,79 @@
   }
 
 
-  bool OrthancPlugins::Transcode(std::string& target,
-                                 DicomTransferSyntax& sourceSyntax /* out */,
-                                 DicomTransferSyntax& targetSyntax /* out */,
-                                 bool& hasSopInstanceUidChanged /* out */,
-                                 const void* buffer,
-                                 size_t size,
-                                 const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                                 bool allowNewSopInstanceUid)
+  class MemoryBufferRaii : public boost::noncopyable
   {
-    // TODO
+  private:
+    OrthancPluginMemoryBuffer  buffer_;
+
+  public:
+    MemoryBufferRaii()
+    {
+      buffer_.size = 0;
+      buffer_.data = NULL;
+    }
+
+    ~MemoryBufferRaii()
+    {
+      if (buffer_.size != 0)
+      {
+        free(buffer_.data);
+      }
+    }
+
+    OrthancPluginMemoryBuffer* GetObject()
+    {
+      return &buffer_;
+    }
+
+    void ToString(std::string& target) const
+    {
+      target.resize(buffer_.size);
+
+      if (buffer_.size != 0)
+      {
+        memcpy(&target[0], buffer_.data, buffer_.size);
+      }
+    }
+  };
+  
+
+  bool OrthancPlugins::TranscodeBuffer(std::string& target,
+                                       const void* buffer,
+                                       size_t size,
+                                       const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                                       bool allowNewSopInstanceUid)
+  {
+    boost::shared_lock<boost::shared_mutex> lock(pimpl_->decoderTranscoderMutex_);
+
+    if (pimpl_->transcoderCallbacks_.empty())
+    {
+      return NULL;
+    }
+
+    std::vector<const char*> uids;
+    uids.reserve(allowedSyntaxes.size());
+    for (std::set<DicomTransferSyntax>::const_iterator
+           it = allowedSyntaxes.begin(); it != allowedSyntaxes.end(); ++it)
+    {
+      uids.push_back(GetTransferSyntaxUid(*it));
+    }
+    
+    for (PImpl::TranscoderCallbacks::const_iterator
+           transcoder = pimpl_->transcoderCallbacks_.begin();
+         transcoder != pimpl_->transcoderCallbacks_.end(); ++transcoder)
+    {
+      MemoryBufferRaii a;
+
+      if ((*transcoder) (a.GetObject(), buffer, size, uids.empty() ? NULL : &uids[0],
+                         static_cast<uint32_t>(uids.size()), allowNewSopInstanceUid) ==
+          OrthancPluginErrorCode_Success)
+      {
+        a.ToString(target);
+        return true;
+      }
+    }
+
     return false;
   }
 }
--- a/Plugins/Engine/OrthancPlugins.h	Wed May 20 16:38:33 2020 +0200
+++ b/Plugins/Engine/OrthancPlugins.h	Wed May 20 17:03:24 2020 +0200
@@ -124,6 +124,8 @@
 
     void RegisterDecodeImageCallback(const void* parameters);
 
+    void RegisterTranscoderCallback(const void* parameters);
+
     void RegisterJobsUnserializer(const void* parameters);
 
     void RegisterIncomingHttpRequestFilter(const void* parameters);
@@ -237,14 +239,11 @@
 
   protected:
     // From "MemoryBufferTranscoder"
-    virtual bool Transcode(std::string& target,
-                           DicomTransferSyntax& sourceSyntax /* out */,
-                           DicomTransferSyntax& targetSyntax /* out */,
-                           bool& hasSopInstanceUidChanged /* out */,
-                           const void* buffer,
-                           size_t size,
-                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                           bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
+    virtual bool TranscodeBuffer(std::string& target,
+                                 const void* buffer,
+                                 size_t size,
+                                 const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                                 bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
     
   public:
     OrthancPlugins();
@@ -328,12 +327,7 @@
 
     bool HasCustomImageDecoder();
 
-    // Contrarily to "Decode()", this method does not fallback to the
-    // builtin image decoder, if no installed custom decoder can
-    // handle the image (it returns NULL in this case).
-    ImageAccessor* DecodeUnsafe(const void* dicom,
-                                size_t size,
-                                unsigned int frame);
+    bool HasCustomTranscoder();
 
     virtual ImageAccessor* Decode(const void* dicom,
                                   size_t size,
--- a/Plugins/Include/orthanc/OrthancCPlugin.h	Wed May 20 16:38:33 2020 +0200
+++ b/Plugins/Include/orthanc/OrthancCPlugin.h	Wed May 20 17:03:24 2020 +0200
@@ -28,6 +28,7 @@
  *    - Possibly register a callback to answer chunked HTTP transfers using ::OrthancPluginRegisterChunkedRestCallback().
  *    - Possibly register a callback for Storage Commitment SCP using ::OrthancPluginRegisterStorageCommitmentScpCallback().
  *    - Possibly register a callback to filter incoming DICOM instance using OrthancPluginRegisterIncomingDicomInstanceFilter().
+ *    - Possibly register a custom transcoder for DICOM images using OrthancPluginRegisterTranscoderCallback().
  * -# <tt>void OrthancPluginFinalize()</tt>:
  *    This function is invoked by Orthanc during its shutdown. The plugin
  *    must free all its memory.
@@ -64,6 +65,9 @@
  *
  * @defgroup Orthanc Orthanc
  * @brief Functions to access the content of the Orthanc server.
+ *
+ * @defgroup DicomInstance DicomInstance
+ * @brief Functions to access DICOM images that are managed by the Orthanc core.
  **/
 
 
@@ -438,8 +442,11 @@
     _OrthancPluginService_SetMetricsValue = 31,
     _OrthancPluginService_EncodeDicomWebJson = 32,
     _OrthancPluginService_EncodeDicomWebXml = 33,
-    _OrthancPluginService_ChunkedHttpClient = 34,   /* New in Orthanc 1.5.7 */
-    _OrthancPluginService_GetTagName = 35,   /* New in Orthanc 1.5.7 */
+    _OrthancPluginService_ChunkedHttpClient = 34,    /* New in Orthanc 1.5.7 */
+    _OrthancPluginService_GetTagName = 35,           /* New in Orthanc 1.5.7 */
+    _OrthancPluginService_EncodeDicomWebJson2 = 36,  /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_EncodeDicomWebXml2 = 37,   /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_CreateMemoryBuffer = 38,   /* New in Orthanc 1.7.0 */
     
     /* Registration of callbacks */
     _OrthancPluginService_RegisterRestCallback = 1000,
@@ -457,7 +464,8 @@
     _OrthancPluginService_RegisterChunkedRestCallback = 1012,  /* New in Orthanc 1.5.7 */
     _OrthancPluginService_RegisterStorageCommitmentScpCallback = 1013,
     _OrthancPluginService_RegisterIncomingDicomInstanceFilter = 1014,
-
+    _OrthancPluginService_RegisterTranscoderCallback = 1015,   /* New in Orthanc 1.7.0 */
+    
     /* Sending answers to REST calls */
     _OrthancPluginService_AnswerBuffer = 2000,
     _OrthancPluginService_CompressAndAnswerPngImage = 2001,  /* Unused as of Orthanc 0.9.4 */
@@ -510,6 +518,9 @@
     _OrthancPluginService_GetInstanceDecodedFrame = 4014,  /* New in Orthanc 1.7.0 */
     _OrthancPluginService_TranscodeDicomInstance = 4015,   /* New in Orthanc 1.7.0 */
     _OrthancPluginService_SerializeDicomInstance = 4016,   /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_GetInstanceAdvancedJson = 4017,  /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_GetInstanceDicomWebJson = 4018,  /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_GetInstanceDicomWebXml = 4019,   /* New in Orthanc 1.7.0 */
     
     /* Services for plugins implementing a database back-end */
     _OrthancPluginService_RegisterDatabaseBackend = 5000,
@@ -1017,7 +1028,8 @@
 
 
   /**
-   * @brief Opaque structure that represents a DICOM instance received by Orthanc.
+   * @brief Opaque structure that represents a DICOM instance that is managed by the Orthanc core.
+   * @ingroup DicomInstance
    **/
   typedef struct _OrthancPluginDicomInstance_t OrthancPluginDicomInstance;
 
@@ -1617,6 +1629,45 @@
 
 
   /**
+   * @brief Callback executed to encode a binary tag in DICOMweb.
+   * 
+   * Signature of a callback function that is called by Orthanc
+   * whenever a DICOM tag that contains a binary value must be written
+   * to a JSON or XML node, while a DICOMweb document is being
+   * generated. The value representation (VR) of the DICOM tag can be
+   * OB, OD, OF, OL, OW, or UN.
+   * 
+   * @see OrthancPluginEncodeDicomWebJson() and OrthancPluginEncodeDicomWebXml()
+   * @param node The node being generated, as provided by Orthanc.
+   * @param setter The setter to be used to encode the content of the node. If
+   * the setter is not called, the binary tag is not written to the output document.
+   * @param levelDepth The depth of the node in the DICOM hierarchy of sequences.
+   * This parameter gives the number of elements in the "levelTagGroup", 
+   * "levelTagElement", and "levelIndex" arrays.
+   * @param levelTagGroup The group of the parent DICOM tags in the hierarchy.
+   * @param levelTagElement The element of the parent DICOM tags in the hierarchy.
+   * @param levelIndex The index of the node in the parent sequences of the hierarchy.
+   * @param tagGroup The group of the DICOM tag of interest.
+   * @param tagElement The element of the DICOM tag of interest.
+   * @param vr The value representation of the binary DICOM node.
+   * @param payload The user payload.
+   * @ingroup Callbacks
+   **/
+  typedef void (*OrthancPluginDicomWebBinaryCallback2) (
+    OrthancPluginDicomWebNode*          node,
+    OrthancPluginDicomWebSetBinaryNode  setter,
+    uint32_t                            levelDepth,
+    const uint16_t*                     levelTagGroup,
+    const uint16_t*                     levelTagElement,
+    const uint32_t*                     levelIndex,
+    uint16_t                            tagGroup,
+    uint16_t                            tagElement,
+    OrthancPluginValueRepresentation    vr,
+    void*                               payload);
+
+
+
+  /**
    * @brief Data structure that contains information about the Orthanc core.
    **/
   typedef struct _OrthancPluginContext_t
@@ -2723,7 +2774,7 @@
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param instance The instance of interest.
    * @return The AET if success, NULL if error.
-   * @ingroup Callbacks
+   * @ingroup DicomInstance
    **/
   ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceRemoteAet(
     OrthancPluginContext*              context,
@@ -2756,7 +2807,7 @@
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param instance The instance of interest.
    * @return The size of the file, -1 in case of error.
-   * @ingroup Callbacks
+   * @ingroup DicomInstance
    **/
   ORTHANC_PLUGIN_INLINE int64_t OrthancPluginGetInstanceSize(
     OrthancPluginContext*             context,
@@ -2789,7 +2840,7 @@
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param instance The instance of interest.
    * @return The pointer to the DICOM data, NULL in case of error.
-   * @ingroup Callbacks
+   * @ingroup DicomInstance
    **/
   ORTHANC_PLUGIN_INLINE const void* OrthancPluginGetInstanceData(
     OrthancPluginContext*              context,
@@ -2825,7 +2876,7 @@
    * @param instance The instance of interest.
    * @return The NULL value in case of error, or a string containing the JSON file.
    * This string must be freed by OrthancPluginFreeString().
-   * @ingroup Callbacks
+   * @ingroup DicomInstance
    **/
   ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceJson(
     OrthancPluginContext*              context,
@@ -2863,7 +2914,7 @@
    * @param instance The instance of interest.
    * @return The NULL value in case of error, or a string containing the JSON file.
    * This string must be freed by OrthancPluginFreeString().
-   * @ingroup Callbacks
+   * @ingroup DicomInstance
    **/
   ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceSimplifiedJson(
     OrthancPluginContext*              context,
@@ -2902,7 +2953,7 @@
    * @param instance The instance of interest.
    * @param metadata The metadata of interest.
    * @return 1 if the metadata is present, 0 if it is absent, -1 in case of error.
-   * @ingroup Callbacks
+   * @ingroup DicomInstance
    **/
   ORTHANC_PLUGIN_INLINE int  OrthancPluginHasInstanceMetadata(
     OrthancPluginContext*              context,
@@ -2943,7 +2994,7 @@
    *         returned string belongs to the instance object and must NOT be 
    *         deallocated. Please make a copy of the string if you wish to access 
    *         it later.
-   * @ingroup Callbacks
+   * @ingroup DicomInstance
    **/
   ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceMetadata(
     OrthancPluginContext*              context,
@@ -5115,7 +5166,7 @@
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param instance The instance of interest.
    * @return The origin of the instance.
-   * @ingroup Callbacks
+   * @ingroup DicomInstance
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginInstanceOrigin OrthancPluginGetInstanceOrigin(
     OrthancPluginContext*             context,
@@ -5191,8 +5242,11 @@
   /**
    * @brief Register a callback to handle the decoding of DICOM images.
    *
-   * This function registers a custom callback to the decoding of
-   * DICOM images, replacing the built-in decoder of Orthanc.
+   * This function registers a custom callback to decode DICOM images,
+   * extending the built-in decoder of Orthanc that uses
+   * DCMTK. Starting with Orthanc 1.7.0, the exact behavior is
+   * affected by the configuration option
+   * "BuiltinDecoderTranscoderOrder" of Orthanc.
    *
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param callback The callback.
@@ -5324,6 +5378,7 @@
    * @param frameIndex The index of the frame of interest in a multi-frame image.
    * @return The uncompressed image. It must be freed with OrthancPluginFreeImage().
    * @ingroup Images
+   * @see OrthancPluginGetInstanceDecodedFrame()
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginImage* OrthancPluginDecodeDicomImage(
     OrthancPluginContext*  context,
@@ -6799,6 +6854,7 @@
    * @see OrthancPluginCreateDicom()
    * @return The NULL value in case of error, or the JSON document. This string must
    * be freed by OrthancPluginFreeString().
+   * @deprecated OrthancPluginEncodeDicomWebJson2()
    * @ingroup Toolbox
    **/
   ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebJson(
@@ -6837,9 +6893,10 @@
    * @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.
-   * @return The NULL value in case of error, or the JSON document. This string must
+   * @return The NULL value in case of error, or the XML document. This string must
    * be freed by OrthancPluginFreeString().
    * @see OrthancPluginCreateDicom()
+   * @deprecated OrthancPluginEncodeDicomWebXml2()
    * @ingroup Toolbox
    **/
   ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebXml(
@@ -6869,6 +6926,104 @@
   
 
 
+  typedef struct
+  {
+    char**                                target;
+    const void*                           dicom;
+    uint32_t                              dicomSize;
+    OrthancPluginDicomWebBinaryCallback2  callback;
+    void*                                 payload;
+  } _OrthancPluginEncodeDicomWeb2;
+
+  /**
+   * @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* OrthancPluginEncodeDicomWebJson2(
+    OrthancPluginContext*                 context,
+    const void*                           dicom,
+    uint32_t                              dicomSize,
+    OrthancPluginDicomWebBinaryCallback2  callback,
+    void*                                 payload)
+  {
+    char* target = NULL;
+    
+    _OrthancPluginEncodeDicomWeb2 params;
+    params.target = &target;
+    params.dicom = dicom;
+    params.dicomSize = dicomSize;
+    params.callback = callback;
+    params.payload = payload;
+
+    if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebJson2, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  /**
+   * @brief Convert a DICOM instance to DICOMweb XML.
+   *
+   * This function converts a memory buffer containing a DICOM instance,
+   * into its DICOMweb XML 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 XML document. This string must
+   * be freed by OrthancPluginFreeString().
+   * @see OrthancPluginCreateDicom()
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebXml2(
+    OrthancPluginContext*                 context,
+    const void*                           dicom,
+    uint32_t                              dicomSize,
+    OrthancPluginDicomWebBinaryCallback2  callback,
+    void*                                 payload)
+  {
+    char* target = NULL;
+    
+    _OrthancPluginEncodeDicomWeb2 params;
+    params.target = &target;
+    params.dicom = dicom;
+    params.dicomSize = dicomSize;
+    params.callback = callback;
+    params.payload = payload;
+
+    if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebXml2, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+  
+
+
   /**
    * @brief Callback executed when a HTTP header is received during a chunked transfer.
    *
@@ -7484,7 +7639,7 @@
    * @param instance The instance of interest.
    * @return The NULL value in case of error, or a string containing the
    * transfer syntax UID. This string must be freed by OrthancPluginFreeString().
-   * @ingroup Callbacks
+   * @ingroup DicomInstance
    **/
   ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceTransferSyntaxUid(
     OrthancPluginContext*              context,
@@ -7519,7 +7674,7 @@
    * @param instance The instance of interest.
    * @return "1" if the DICOM instance contains pixel data, or "0" if
    * the tag is missing, or "-1" in the case of an error.
-   * @ingroup Callbacks
+   * @ingroup DicomInstance
    **/
   ORTHANC_PLUGIN_INLINE int32_t OrthancPluginHasInstancePixelData(
     OrthancPluginContext*             context,
@@ -7558,6 +7713,19 @@
     const char*                   transferSyntax;
   } _OrthancPluginCreateDicomInstance;
 
+  /**
+   * @brief Parse a DICOM instance.
+   *
+   * This function parses a memory buffer that contains a DICOM
+   * file. The function returns a new pointer to a data structure that
+   * is managed by the Orthanc core.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param buffer The memory buffer containing the DICOM instance.
+   * @param size The size of the memory buffer.
+   * @return The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance().
+   * @ingroup DicomInstance
+   **/
   ORTHANC_PLUGIN_INLINE OrthancPluginDicomInstance* OrthancPluginCreateDicomInstance(
     OrthancPluginContext*  context,
     const void*            buffer,
@@ -7586,6 +7754,16 @@
     OrthancPluginDicomInstance*   dicom;
   } _OrthancPluginFreeDicomInstance;
 
+  /**
+   * @brief Free a DICOM instance.
+   *
+   * This function frees a DICOM instance that was parsed using
+   * OrthancPluginCreateDicomInstance().
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param dicom The DICOM instance.
+   * @ingroup DicomInstance
+   **/
   ORTHANC_PLUGIN_INLINE void  OrthancPluginFreeDicomInstance(
     OrthancPluginContext*        context, 
     OrthancPluginDicomInstance*  dicom)
@@ -7599,13 +7777,30 @@
 
   typedef struct
   {
-    uint32_t*                          targetUint32;
-    OrthancPluginMemoryBuffer*         targetBuffer;
-    OrthancPluginImage**               targetImage;
-    const OrthancPluginDicomInstance*  instance;
-    uint32_t                           frameIndex;
+    uint32_t*                             targetUint32;
+    OrthancPluginMemoryBuffer*            targetBuffer;
+    OrthancPluginImage**                  targetImage;
+    char**                                targetStringToFree;
+    const OrthancPluginDicomInstance*     instance;
+    uint32_t                              frameIndex;
+    OrthancPluginDicomToJsonFormat        format;
+    OrthancPluginDicomToJsonFlags         flags;
+    uint32_t                              maxStringLength;
+    OrthancPluginDicomWebBinaryCallback2  dicomWebCallback;
+    void*                                 dicomWebPayload;
   } _OrthancPluginAccessDicomInstance2;
 
+  /**
+   * @brief Get the number of frames in a DICOM instance.
+   *
+   * This function returns the number of frames that are part of a
+   * DICOM image managed by the Orthanc core.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return The number of frames (will be zero in the case of an error).
+   * @ingroup DicomInstance
+   **/
   ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetInstanceFramesCount(
     OrthancPluginContext*             context,
     const OrthancPluginDicomInstance* instance)
@@ -7628,6 +7823,24 @@
     }
   }
 
+
+  /**
+   * @brief Get the raw content of a frame in a DICOM instance.
+   *
+   * This function returns a memory buffer containing the raw content
+   * of a frame in a DICOM instance that is managed by the Orthanc
+   * core. This is notably useful for compressed transfer syntaxes, as
+   * it gives access to the embedded files (such as JPEG, JPEG-LS or
+   * JPEG2k). The Orthanc core transparently reassembles the fragments
+   * to extract the raw frame.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param instance The instance of interest.
+   * @param frameIndex The index of the frame of interest.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup DicomInstance
+   **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetInstanceRawFrame(
     OrthancPluginContext*             context,
     OrthancPluginMemoryBuffer*        target,
@@ -7643,6 +7856,19 @@
     return context->InvokeService(context, _OrthancPluginService_GetInstanceRawFrame, &params);
   }
 
+
+  /**
+   * @brief Decode one frame from a DICOM instance.
+   *
+   * This function decodes one frame of a DICOM image that is managed
+   * by the Orthanc core.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @param frameIndex The index of the frame of interest.
+   * @return The uncompressed image. It must be freed with OrthancPluginFreeImage().
+   * @ingroup DicomInstance
+   **/
   ORTHANC_PLUGIN_INLINE OrthancPluginImage* OrthancPluginGetInstanceDecodedFrame(
     OrthancPluginContext*             context,
     const OrthancPluginDicomInstance* instance,
@@ -7666,6 +7892,22 @@
     }
   }
 
+  
+  /**
+   * @brief Parse and transcode a DICOM instance.
+   *
+   * This function parses a memory buffer that contains a DICOM file,
+   * then transcodes it to the given transfer syntax. The function
+   * returns a new pointer to a data structure that is managed by the
+   * Orthanc core.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param buffer The memory buffer containing the DICOM instance.
+   * @param size The size of the memory buffer.
+   * @param transferSyntax The transfer syntax UID for the transcoding.
+   * @return The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance().
+   * @ingroup DicomInstance
+   **/
   ORTHANC_PLUGIN_INLINE OrthancPluginDicomInstance* OrthancPluginTranscodeDicomInstance(
     OrthancPluginContext*  context,
     const void*            buffer,
@@ -7691,6 +7933,19 @@
     }
   }
 
+  /**
+   * @brief Writes a DICOM instance to a memory buffer.
+   *
+   * This function returns a memory buffer containing the
+   * serialization of a DICOM instance that is managed by the Orthanc
+   * core.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param instance The instance of interest.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup DicomInstance
+   **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSerializeDicomInstance(
     OrthancPluginContext*             context,
     OrthancPluginMemoryBuffer*        target,
@@ -7703,6 +7958,228 @@
 
     return context->InvokeService(context, _OrthancPluginService_SerializeDicomInstance, &params);
   }
+  
+
+  /**
+   * @brief Format a DICOM memory buffer as a JSON string.
+   *
+   * This function takes as DICOM instance managed by the Orthanc
+   * core, and outputs a JSON string representing the tags of this
+   * DICOM file.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The DICOM instance of interest.
+   * @param format The output format.
+   * @param flags Flags governing the output.
+   * @param maxStringLength The maximum length of a field. Too long fields will
+   * be output as "null". The 0 value means no maximum length.
+   * @return The NULL value if the case of an error, or the JSON
+   * string. This string must be freed by OrthancPluginFreeString().
+   * @ingroup DicomInstance
+   * @see OrthancPluginDicomBufferToJson
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceAdvancedJson(
+    OrthancPluginContext*              context,
+    const OrthancPluginDicomInstance*  instance,
+    OrthancPluginDicomToJsonFormat     format,
+    OrthancPluginDicomToJsonFlags      flags, 
+    uint32_t                           maxStringLength)
+  {
+    char* result = NULL;
+
+    _OrthancPluginAccessDicomInstance2 params;
+    memset(&params, 0, sizeof(params));
+    params.targetStringToFree = &result;
+    params.instance = instance;
+    params.format = format;
+    params.flags = flags;
+    params.maxStringLength = maxStringLength;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceAdvancedJson, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Convert a DICOM instance to DICOMweb JSON.
+   *
+   * This function converts a DICOM instance that is managed by the
+   * Orthanc core, into its DICOMweb JSON representation.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The DICOM instance of interest.
+   * @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().
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceDicomWebJson(
+    OrthancPluginContext*                 context,
+    const OrthancPluginDicomInstance*     instance,
+    OrthancPluginDicomWebBinaryCallback2  callback,
+    void*                                 payload)
+  {
+    char* target = NULL;
+    
+    _OrthancPluginAccessDicomInstance2 params;
+    params.targetStringToFree = &target;
+    params.instance = instance;
+    params.dicomWebCallback = callback;
+    params.dicomWebPayload = payload;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceDicomWebJson, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+  
+
+  /**
+   * @brief Convert a DICOM instance to DICOMweb XML.
+   *
+   * This function converts a DICOM instance that is managed by the
+   * Orthanc core, into its DICOMweb XML representation.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The DICOM instance of interest.
+   * @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 XML document. This string must
+   * be freed by OrthancPluginFreeString().
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceDicomWebXml(
+    OrthancPluginContext*                 context,
+    const OrthancPluginDicomInstance*     instance,
+    OrthancPluginDicomWebBinaryCallback2  callback,
+    void*                                 payload)
+  {
+    char* target = NULL;
+    
+    _OrthancPluginAccessDicomInstance2 params;
+    params.targetStringToFree = &target;
+    params.instance = instance;
+    params.dicomWebCallback = callback;
+    params.dicomWebPayload = payload;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceDicomWebXml, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+
+  /**
+   * @brief Signature of a callback function to transcode a DICOM instance.
+   * @param transcoded Target memory buffer. It must be allocated by the
+   * plugin using OrthancPluginCreateMemoryBuffer().
+   * @param buffer Memory buffer containing the source DICOM instance.
+   * @param size Size of the source memory buffer.
+   * @param allowedSyntaxes A C array of possible transfer syntaxes UIDs for the
+   * result of the transcoding. The plugin must choose by itself the 
+   * transfer syntax that will be used for the resulting DICOM image.
+   * @param countSyntaxes The number of transfer syntaxes that are contained
+   * in the "allowedSyntaxes" array.
+   * @param allowNewSopInstanceUid Whether the transcoding plugin can select
+   * a transfer syntax that will change the SOP instance UID (or, in other 
+   * terms, whether the plugin can transcode using lossy compression).
+   * @return 0 if success (i.e. image successfully transcoded and stored into
+   * "transcoded"), or the error code if failure.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginTranscoderCallback) (
+    OrthancPluginMemoryBuffer* transcoded /* out */,
+    const void*                buffer,
+    uint64_t                   size,
+    const char* const*         allowedSyntaxes,
+    uint32_t                   countSyntaxes,
+    uint8_t                    allowNewSopInstanceUid);
+
+
+  typedef struct
+  {
+    OrthancPluginTranscoderCallback callback;
+  } _OrthancPluginTranscoderCallback;
+
+  /**
+   * @brief Register a callback to handle the transcoding of DICOM images.
+   *
+   * This function registers a custom callback to transcode DICOM
+   * images, extending the built-in transcoder of Orthanc that uses
+   * DCMTK. The exact behavior is affected by the configuration option
+   * "BuiltinDecoderTranscoderOrder" of Orthanc.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterTranscoderCallback(
+    OrthancPluginContext*            context,
+    OrthancPluginTranscoderCallback  callback)
+  {
+    _OrthancPluginTranscoderCallback params;
+    params.callback = callback;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterTranscoderCallback, &params);
+  }
+  
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  target;
+    uint32_t                    size;
+  } _OrthancPluginCreateMemoryBuffer;
+
+  /**
+   * @brief Create a memory buffer.
+   *
+   * This function creates a memory buffer that is managed by the
+   * Orthanc core. The main use case of this function is for plugins
+   * that act as DICOM transcoders.
+   * 
+   * Your plugin should never call "free()" on the resulting memory
+   * buffer, as the C library that is used by the plugin is in general
+   * not the same as the one used by the Orthanc core.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param size Size of the memory buffer to be created.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCreateMemoryBuffer(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    uint32_t                    size)
+  {
+    _OrthancPluginCreateMemoryBuffer params;
+    params.target = target;
+    params.size = size;
+
+    return context->InvokeService(context, _OrthancPluginService_CreateMemoryBuffer, &params);
+  }
+  
 
 #ifdef  __cplusplus
 }
--- a/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Wed May 20 16:38:33 2020 +0200
+++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Wed May 20 17:03:24 2020 +0200
@@ -130,6 +130,28 @@
   }
 
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+  MemoryBuffer::MemoryBuffer(const void* buffer,
+                             size_t size)
+  {
+    uint32_t s = static_cast<uint32_t>(size);
+    if (static_cast<size_t>(s) != size)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory);
+    }
+    else if (OrthancPluginCreateMemoryBuffer(GetGlobalContext(), &buffer_, s) !=
+             OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory);
+    }
+    else
+    {
+      memcpy(buffer_.data, buffer, size);
+    }
+  }
+#endif
+
+
   void MemoryBuffer::Clear()
   {
     if (buffer_.data != NULL)
@@ -3364,7 +3386,9 @@
     }
     else
     {
-      return new DicomInstance(instance);
+      boost::movelib::unique_ptr<DicomInstance> result(new DicomInstance(instance));
+      result->toFree_ = true;
+      return result.release();
     }
   }
 #endif
--- a/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Wed May 20 16:38:33 2020 +0200
+++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Wed May 20 17:03:24 2020 +0200
@@ -146,6 +146,13 @@
   public:
     MemoryBuffer();
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+    // This constructor makes a copy of the given buffer in the memory
+    // handled by the Orthanc core
+    MemoryBuffer(const void* buffer,
+                 size_t size);
+#endif
+
     ~MemoryBuffer()
     {
       Clear();
--- a/Plugins/Samples/GdcmDecoder/CMakeLists.txt	Wed May 20 16:38:33 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,83 +0,0 @@
-# Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
-# Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the GNU Affero General Public License
-# as published by the Free Software Foundation, either version 3 of
-# the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-
-cmake_minimum_required(VERSION 2.8)
-
-project(GdcmDecoder)
-
-SET(PLUGIN_VERSION "mainline" CACHE STRING "Version of the plugin")
-
-
-# Parameters of the build
-set(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)")
-set(STANDALONE_BUILD ON CACHE BOOL "Standalone build (all the resources are embedded, necessary for releases)")
-set(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages")
-
-# Advanced parameters to fine-tune linking against system libraries
-set(USE_SYSTEM_GDCM ON CACHE BOOL "Use the system version of Grassroot DICOM (GDCM)")
-set(USE_SYSTEM_ORTHANC_SDK ON CACHE BOOL "Use the system version of the Orthanc plugin SDK")
-
-# Setup the Orthanc framework
-set(ORTHANC_ROOT ${CMAKE_SOURCE_DIR}/../../..)
-
-set(ORTHANC_FRAMEWORK_PLUGIN ON)
-include(${ORTHANC_ROOT}/Resources/CMake/OrthancFrameworkParameters.cmake)
-
-set(ENABLE_LOCALE OFF CACHE INTERNAL "")      # Disable support for locales (notably in Boost)
-set(ENABLE_MODULE_IMAGES OFF CACHE INTERNAL "")
-set(ENABLE_MODULE_JOBS OFF CACHE INTERNAL "")
-
-include(${ORTHANC_ROOT}/Resources/CMake/OrthancFrameworkConfiguration.cmake)
-
-include(GdcmConfiguration.cmake)
-
-
-# Check that the Orthanc SDK headers are available
-if (STATIC_BUILD OR NOT USE_SYSTEM_ORTHANC_SDK)
-  #include_directories(${CMAKE_SOURCE_DIR}/Resources/Orthanc/Sdk-0.9.5)
-  include_directories(${CMAKE_SOURCE_DIR}/../../Include) # TODO => SYNC 0.9.5
-else ()
-  CHECK_INCLUDE_FILE_CXX(orthanc/OrthancCPlugin.h HAVE_ORTHANC_H)
-  if (NOT HAVE_ORTHANC_H)
-    message(FATAL_ERROR "Please install the headers of the Orthanc plugins SDK")
-  endif()
-endif()
-
-
-include_directories(${ORTHANC_ROOT})
-
-add_definitions(
-  -DPLUGIN_VERSION="${PLUGIN_VERSION}"
-  -DHAS_ORTHANC_EXCEPTION=1
-  -DORTHANC_ENABLE_LOGGING_PLUGIN=1
-  )
-
-add_library(GdcmDecoder SHARED
-  GdcmDecoderCache.cpp
-  GdcmImageDecoder.cpp
-  Plugin.cpp
-  ${CMAKE_SOURCE_DIR}/../Common/OrthancPluginCppWrapper.cpp
-  ${ORTHANC_CORE_SOURCES}
-  )
-
-target_link_libraries(GdcmDecoder ${GDCM_LIBRARIES})
-
-if (STATIC_BUILD OR NOT USE_SYSTEM_GDCM)
-  add_dependencies(GdcmDecoder GDCM)
-endif()
--- a/Plugins/Samples/GdcmDecoder/GdcmConfiguration.cmake	Wed May 20 16:38:33 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,141 +0,0 @@
-# Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
-# Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the GNU Affero General Public License
-# as published by the Free Software Foundation, either version 3 of
-# the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-
-if (STATIC_BUILD OR NOT USE_SYSTEM_GDCM)
-  if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
-      ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
-      ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD")
-    # If using gcc, build GDCM with the "-fPIC" argument to allow its
-    # embedding into the shared library containing the Orthanc plugin
-    set(AdditionalCFlags "-fPIC")
-    set(AdditionalCxxFlags ${AdditionalCFlags})
-  elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows" AND
-      CMAKE_COMPILER_IS_GNUCXX)
-    # Prevents error: "jump to label ‘err’ crosses initialization" of some variable
-    # within "Source/Common/gdcmCAPICryptographicMessageSyntax.cxx" if using MinGW
-    set(AdditionalCxxFlags "-fpermissive")
-  elseif (${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
-    # This definition is necessary to compile
-    # "Source/MediaStorageAndFileFormat/gdcmFileStreamer.cxx"
-    set(AdditionalCFlags "-Doff64_t=off_t") 
-    set(AdditionalCxxFlags ${AdditionalCFlags})
-  endif()
-  
-  set(Flags
-    "-DCMAKE_C_FLAGS=${CMAKE_C_FLAGS} ${AdditionalCFlags}"
-    "-DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS} ${AdditionalCxxFlags}"
-    -DCMAKE_C_FLAGS_DEBUG=${CMAKE_C_FLAGS_DEBUG}
-    -DCMAKE_CXX_FLAGS_DEBUG=${CMAKE_CXX_FLAGS_DEBUG}
-    -DCMAKE_C_FLAGS_RELEASE=${CMAKE_C_FLAGS_RELEASE}
-    -DCMAKE_CXX_FLAGS_RELEASE=${CMAKE_CXX_FLAGS_RELEASE}
-    -DCMAKE_C_FLAGS_MINSIZEREL=${CMAKE_C_FLAGS_MINSIZEREL}
-    -DCMAKE_CXX_FLAGS_MINSIZEREL=${CMAKE_CXX_FLAGS_MINSIZEREL} 
-    -DCMAKE_C_FLAGS_RELWITHDEBINFO=${CMAKE_C_FLAGS_RELWITHDEBINFO} 
-    -DCMAKE_CXX_FLAGS_RELWITHDEBINFO=${CMAKE_CXX_FLAGS_RELWITHDEBINFO}
-    )
-
-  if (CMAKE_TOOLCHAIN_FILE)
-    # Take absolute path to the toolchain
-    get_filename_component(TMP ${CMAKE_TOOLCHAIN_FILE} REALPATH BASE ${CMAKE_SOURCE_DIR})
-    list(APPEND Flags -DCMAKE_TOOLCHAIN_FILE=${TMP})
-  endif()
-
-  # Don't build manpages (since gdcm 2.8.4)
-  list(APPEND Flags -DGDCM_BUILD_DOCBOOK_MANPAGES=OFF)
-
-  if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
-    # Trick to disable the compilation of socket++ by gdcm, which is
-    # incompatible with LSB, but fortunately only required for DICOM
-    # Networking
-    list(APPEND Flags -DGDCM_USE_SYSTEM_SOCKETXX=ON)
-
-    # Detect the number of CPU cores to run "make" with as much
-    # parallelism as possible
-    include(ProcessorCount)
-    ProcessorCount(N)
-    if (NOT N EQUAL 0)
-      set(MAKE_PARALLEL -j${N})
-    endif()
-      
-    # For Linux Standard Base, avoid building incompatible target gdcmMEXD (*)
-    set(BUILD_COMMAND BUILD_COMMAND
-      ${CMAKE_MAKE_PROGRAM} ${MAKE_PARALLEL}
-      gdcmMSFF gdcmcharls gdcmDICT gdcmDSED gdcmIOD gdcmjpeg8
-      gdcmjpeg12 gdcmjpeg16 gdcmopenjp2 gdcmzlib gdcmCommon gdcmexpat)
-  endif()
-
-  include(ExternalProject)
-  externalproject_add(GDCM
-    URL "http://orthanc.osimis.io/ThirdPartyDownloads/gdcm-3.0.4.tar.gz"
-    URL_MD5 "f12dbded708356d5fa0b5ed37ccdb66e"
-    TIMEOUT 60
-    CMAKE_ARGS -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} ${Flags}
-    ${BUILD_COMMAND}    # Customize "make", only for Linux Standard Base (*)
-    INSTALL_COMMAND ""  # Skip the install step
-    )
-
-  if(MSVC)
-    set(Suffix ".lib")
-    set(Prefix "")
-  else()
-    set(Suffix ".a")
-    list(GET CMAKE_FIND_LIBRARY_PREFIXES 0 Prefix)
-  endif()
-
-  set(GDCM_LIBRARIES
-    # WARNING: The order of the libraries below *is* important!
-    ${Prefix}gdcmMSFF${Suffix}
-    ${Prefix}gdcmcharls${Suffix}
-    ${Prefix}gdcmDICT${Suffix}
-    ${Prefix}gdcmDSED${Suffix}
-    ${Prefix}gdcmIOD${Suffix}
-    ${Prefix}gdcmjpeg8${Suffix}
-    ${Prefix}gdcmjpeg12${Suffix}
-    ${Prefix}gdcmjpeg16${Suffix}
-    ${Prefix}gdcmopenjp2${Suffix}
-    ${Prefix}gdcmzlib${Suffix}
-    ${Prefix}gdcmCommon${Suffix}
-    ${Prefix}gdcmexpat${Suffix}
-
-    #${Prefix}socketxx${Suffix}
-    #${Prefix}gdcmMEXD${Suffix}  # DICOM Networking, unneeded by Orthanc plugins
-    #${Prefix}gdcmgetopt${Suffix}
-    #${Prefix}gdcmuuid${Suffix}
-    )
-
-  ExternalProject_Get_Property(GDCM binary_dir)
-  include_directories(${binary_dir}/Source/Common)
-  link_directories(${binary_dir}/bin)
-
-  ExternalProject_Get_Property(GDCM source_dir)
-  include_directories(
-    ${source_dir}/Source/Common
-    ${source_dir}/Source/MediaStorageAndFileFormat
-    ${source_dir}/Source/DataStructureAndEncodingDefinition
-    )
-
-else()
-  find_package(GDCM REQUIRED)
-  if (GDCM_FOUND)
-    include(${GDCM_USE_FILE})
-    set(GDCM_LIBRARIES gdcmCommon gdcmMSFF)
-  else(GDCM_FOUND)
-    message(FATAL_ERROR "Cannot find GDCM, did you set GDCM_DIR?")
-  endif(GDCM_FOUND)
-endif()
--- a/Plugins/Samples/GdcmDecoder/GdcmDecoderCache.cpp	Wed May 20 16:38:33 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,97 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "GdcmDecoderCache.h"
-
-#include "../../../Core/Compatibility.h"
-
-namespace OrthancPlugins
-{
-  std::string GdcmDecoderCache::ComputeMd5(const void* dicom,
-                                           size_t size)
-  {
-    std::string result;
-
-    char* md5 = OrthancPluginComputeMd5(OrthancPlugins::GetGlobalContext(), dicom, size);
-
-    if (md5 == NULL)
-    {
-      throw std::runtime_error("Cannot compute MD5 hash");
-    }
-
-    bool ok = false;
-    try
-    {
-      result.assign(md5);
-      ok = true;
-    }
-    catch (...)
-    {
-    }
-
-    OrthancPluginFreeString(OrthancPlugins::GetGlobalContext(), md5);
-
-    if (!ok)
-    {
-      throw std::runtime_error("Not enough memory");
-    }
-    else
-    {    
-      return result;
-    }
-  }
-
-
-  OrthancImage* GdcmDecoderCache::Decode(const void* dicom,
-                                         const uint32_t size,
-                                         uint32_t frameIndex)
-  {
-    std::string md5 = ComputeMd5(dicom, size);
-
-    // First check whether the previously decoded image is the same
-    // as this one
-    {
-      boost::mutex::scoped_lock lock(mutex_);
-
-      if (decoder_.get() != NULL &&
-          size_ == size &&
-          md5_ == md5)
-      {
-        // This is the same image: Reuse the previous decoding
-        return new OrthancImage(decoder_->Decode(frameIndex));
-      }
-    }
-
-    // This is not the same image
-    std::unique_ptr<GdcmImageDecoder> decoder(new GdcmImageDecoder(dicom, size));
-    std::unique_ptr<OrthancImage> image(new OrthancImage(decoder->Decode(frameIndex)));
-
-    {
-      // Cache the newly created decoder for further use
-      boost::mutex::scoped_lock lock(mutex_);
-      decoder_.reset(decoder.release());
-      size_ = size;
-      md5_ = md5;
-    }
-
-    return image.release();
-  }
-}
--- a/Plugins/Samples/GdcmDecoder/GdcmDecoderCache.h	Wed May 20 16:38:33 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../../../Core/Compatibility.h"
-#include "GdcmImageDecoder.h"
-#include "../Common/OrthancPluginCppWrapper.h"
-
-#include <boost/thread.hpp>
-
-
-namespace OrthancPlugins
-{
-  class GdcmDecoderCache : public boost::noncopyable
-  {
-  private:
-    boost::mutex   mutex_;
-    std::unique_ptr<OrthancPlugins::GdcmImageDecoder>  decoder_;
-    size_t       size_;
-    std::string  md5_;
-
-    static std::string ComputeMd5(const void* dicom,
-                                  size_t size);
-
-  public:
-    GdcmDecoderCache() : size_(0)
-    {
-    }
-
-    OrthancImage* Decode(const void* dicom,
-                         const uint32_t size,
-                         uint32_t frameIndex);
-  };
-}
--- a/Plugins/Samples/GdcmDecoder/GdcmImageDecoder.cpp	Wed May 20 16:38:33 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,408 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "GdcmImageDecoder.h"
-
-#include "../../../Core/Compatibility.h"
-
-#include <gdcmImageReader.h>
-#include <gdcmImageApplyLookupTable.h>
-#include <gdcmImageChangePlanarConfiguration.h>
-#include <gdcmImageChangePhotometricInterpretation.h>
-#include <stdexcept>
-#include <boost/iostreams/stream.hpp>
-#include <boost/iostreams/device/array.hpp>
-
-
-namespace OrthancPlugins
-{
-  struct GdcmImageDecoder::PImpl
-  {
-    const void*           dicom_;
-    size_t                size_;
-
-    gdcm::ImageReader reader_;
-    std::unique_ptr<gdcm::ImageApplyLookupTable> lut_;
-    std::unique_ptr<gdcm::ImageChangePhotometricInterpretation> photometric_;
-    std::unique_ptr<gdcm::ImageChangePlanarConfiguration> interleaved_;
-    std::string decoded_;
-
-    PImpl(const void* dicom,
-          size_t size) :
-      dicom_(dicom),
-      size_(size)
-    {
-    }
-
-
-    const gdcm::DataSet& GetDataSet() const
-    {
-      return reader_.GetFile().GetDataSet();
-    }
-
-
-    const gdcm::Image& GetImage() const
-    {
-      if (interleaved_.get() != NULL)
-      {
-        return interleaved_->GetOutput();
-      }
-
-      if (lut_.get() != NULL)
-      {
-        return lut_->GetOutput();
-      }
-
-      if (photometric_.get() != NULL)
-      {
-        return photometric_->GetOutput();
-      }
-
-      return reader_.GetImage();
-    }
-
-
-    void Decode()
-    {
-      // Change photometric interpretation or apply LUT, if required
-      {
-        const gdcm::Image& image = GetImage();
-        if (image.GetPixelFormat().GetSamplesPerPixel() == 1 &&
-            image.GetPhotometricInterpretation() == gdcm::PhotometricInterpretation::PALETTE_COLOR)
-        {
-          lut_.reset(new gdcm::ImageApplyLookupTable());
-          lut_->SetInput(image);
-          if (!lut_->Apply())
-          {
-            throw std::runtime_error( "GDCM cannot apply the lookup table");
-          }
-        }
-        else if (image.GetPixelFormat().GetSamplesPerPixel() == 1)
-        {
-          if (image.GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::MONOCHROME1 &&
-              image.GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::MONOCHROME2)
-          {
-            photometric_.reset(new gdcm::ImageChangePhotometricInterpretation());
-            photometric_->SetInput(image);
-            photometric_->SetPhotometricInterpretation(gdcm::PhotometricInterpretation::MONOCHROME2);
-            if (!photometric_->Change() ||
-                GetImage().GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::MONOCHROME2)
-            {
-              throw std::runtime_error("GDCM cannot change the photometric interpretation");
-            }
-          }      
-        }
-        else 
-        {
-          if (image.GetPixelFormat().GetSamplesPerPixel() == 3 &&
-              image.GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::RGB &&
-              image.GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::YBR_FULL &&
-              (image.GetTransferSyntax() != gdcm::TransferSyntax::JPEG2000Lossless ||
-               image.GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::YBR_RCT))
-          {
-            photometric_.reset(new gdcm::ImageChangePhotometricInterpretation());
-            photometric_->SetInput(image);
-            photometric_->SetPhotometricInterpretation(gdcm::PhotometricInterpretation::RGB);
-            if (!photometric_->Change() ||
-                GetImage().GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::RGB)
-            {
-              throw std::runtime_error("GDCM cannot change the photometric interpretation");
-            }
-          }
-        }
-      }
-
-      // Possibly convert planar configuration to interleaved
-      {
-        const gdcm::Image& image = GetImage();
-        if (image.GetPlanarConfiguration() != 0 && 
-            image.GetPixelFormat().GetSamplesPerPixel() != 1)
-        {
-          interleaved_.reset(new gdcm::ImageChangePlanarConfiguration());
-          interleaved_->SetInput(image);
-          if (!interleaved_->Change() ||
-              GetImage().GetPlanarConfiguration() != 0)
-          {
-            throw std::runtime_error("GDCM cannot change the planar configuration to interleaved");
-          }
-        }
-      }
-    }
-  };
-
-  GdcmImageDecoder::GdcmImageDecoder(const void* dicom,
-                                     size_t size) :
-    pimpl_(new PImpl(dicom, size))
-  {
-    // Setup a stream to the memory buffer
-    using namespace boost::iostreams;
-    basic_array_source<char> source(reinterpret_cast<const char*>(dicom), size);
-    stream<basic_array_source<char> > stream(source);
-
-    // Parse the DICOM instance using GDCM
-    pimpl_->reader_.SetStream(stream);
-    if (!pimpl_->reader_.Read())
-    {
-      throw std::runtime_error("Bad file format");
-    }
-
-    pimpl_->Decode();
-  }
-
-
-  OrthancPluginPixelFormat GdcmImageDecoder::GetFormat() const
-  {
-    const gdcm::Image& image = pimpl_->GetImage();
-
-    if (image.GetPixelFormat().GetSamplesPerPixel() == 1 &&
-        (image.GetPhotometricInterpretation() == gdcm::PhotometricInterpretation::MONOCHROME1 ||
-         image.GetPhotometricInterpretation() == gdcm::PhotometricInterpretation::MONOCHROME2))
-    {
-      switch (image.GetPixelFormat())
-      {
-        case gdcm::PixelFormat::UINT16:
-          return OrthancPluginPixelFormat_Grayscale16;
-
-        case gdcm::PixelFormat::INT16:
-          return OrthancPluginPixelFormat_SignedGrayscale16;
-
-        case gdcm::PixelFormat::UINT8:
-          return OrthancPluginPixelFormat_Grayscale8;
-
-        default:
-          throw std::runtime_error("Unsupported pixel format");
-      }
-    }
-    else if (image.GetPixelFormat().GetSamplesPerPixel() == 3 &&
-             (image.GetPhotometricInterpretation() == gdcm::PhotometricInterpretation::RGB ||
-              image.GetPhotometricInterpretation() == gdcm::PhotometricInterpretation::YBR_FULL ||
-              image.GetPhotometricInterpretation() == gdcm::PhotometricInterpretation::YBR_RCT))
-    {
-      switch (image.GetPixelFormat())
-      {
-        case gdcm::PixelFormat::UINT8:
-          return OrthancPluginPixelFormat_RGB24;
-
-        case gdcm::PixelFormat::UINT16:
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 3, 1)
-          return OrthancPluginPixelFormat_RGB48;
-#else
-          throw std::runtime_error("RGB48 pixel format is only supported if compiled against Orthanc SDK >= 1.3.1");
-#endif
-          
-        default:
-          break;
-      }      
-    }
-
-    throw std::runtime_error("Unsupported pixel format");
-  }
-
-
-  unsigned int GdcmImageDecoder::GetWidth() const
-  {
-    return pimpl_->GetImage().GetColumns();
-  }
-
-
-  unsigned int GdcmImageDecoder::GetHeight() const
-  {
-    return pimpl_->GetImage().GetRows();
-  }
-
-  
-  unsigned int GdcmImageDecoder::GetFramesCount() const
-  {
-    return pimpl_->GetImage().GetDimension(2);
-  }
-
-
-  size_t GdcmImageDecoder::GetBytesPerPixel(OrthancPluginPixelFormat format)
-  {
-    switch (format)
-    {
-      case OrthancPluginPixelFormat_Grayscale8:
-        return 1;
-
-      case OrthancPluginPixelFormat_Grayscale16:
-      case OrthancPluginPixelFormat_SignedGrayscale16:
-        return 2;
-
-      case OrthancPluginPixelFormat_RGB24:
-        return 3;
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 3, 1)
-      case OrthancPluginPixelFormat_RGB48:
-        return 6;
-#endif
-
-      default:
-        throw std::runtime_error("Unsupport pixel format");
-    }
-  }
-
-  static void ConvertYbrToRgb(uint8_t rgb[3],
-                              const uint8_t ybr[3])
-  {
-    // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.3.html#sect_C.7.6.3.1.2
-    // https://en.wikipedia.org/wiki/YCbCr#JPEG_conversion
-    
-    // TODO - Check out the outcome of Mathieu's discussion about
-    // truncation of YCbCr-to-RGB conversion:
-    // https://groups.google.com/forum/#!msg/comp.protocols.dicom/JHuGeyWbTz8/ARoTWrJzAQAJ
-
-    const float Y  = ybr[0];
-    const float Cb = ybr[1];
-    const float Cr = ybr[2];
-
-    const float result[3] = {
-      Y                             + 1.402f    * (Cr - 128.0f),
-      Y - 0.344136f * (Cb - 128.0f) - 0.714136f * (Cr - 128.0f),
-      Y + 1.772f    * (Cb - 128.0f)
-    };
-
-    for (uint8_t i = 0; i < 3 ; i++)
-    {
-      if (result[i] < 0)
-      {
-        rgb[i] = 0;
-      }
-      else if (result[i] > 255)
-      {
-        rgb[i] = 255;
-      }
-      else
-      {
-        rgb[i] = static_cast<uint8_t>(result[i]);
-      }
-    }    
-  }
-
-  
-  static void FixPhotometricInterpretation(OrthancImage& image,
-                                           gdcm::PhotometricInterpretation interpretation)
-  {
-    switch (interpretation)
-    {
-      case gdcm::PhotometricInterpretation::MONOCHROME1:
-      case gdcm::PhotometricInterpretation::MONOCHROME2:
-      case gdcm::PhotometricInterpretation::RGB:
-        return;
-
-      case gdcm::PhotometricInterpretation::YBR_FULL:
-      {
-        // Fix for Osimis issue WVB-319: Some images are not loading in US_MF
-
-        uint32_t width = image.GetWidth();
-        uint32_t height = image.GetHeight();
-        uint32_t pitch = image.GetPitch();
-        uint8_t* buffer = reinterpret_cast<uint8_t*>(image.GetBuffer());
-        
-        if (image.GetPixelFormat() != OrthancPluginPixelFormat_RGB24 ||
-            pitch < 3 * width)
-        {
-          throw std::runtime_error("Internal error");
-        }
-
-        for (uint32_t y = 0; y < height; y++)
-        {
-          uint8_t* p = buffer + y * pitch;
-          for (uint32_t x = 0; x < width; x++, p += 3)
-          {
-            const uint8_t ybr[3] = { p[0], p[1], p[2] };
-            uint8_t rgb[3];
-            ConvertYbrToRgb(rgb, ybr);
-            p[0] = rgb[0];
-            p[1] = rgb[1];
-            p[2] = rgb[2];
-          }
-        }
-
-        return;
-      }
-
-      default:
-        throw std::runtime_error("Unsupported output photometric interpretation");
-    }    
-  }
-
-
-  OrthancPluginImage* GdcmImageDecoder::Decode(unsigned int frameIndex) const
-  {
-    unsigned int frames = GetFramesCount();
-    unsigned int width = GetWidth();
-    unsigned int height = GetHeight();
-    OrthancPluginPixelFormat format = GetFormat();
-    size_t bpp = GetBytesPerPixel(format);
-
-    if (frameIndex >= frames)
-    {
-      throw std::runtime_error("Inexistent frame index");
-    }
-
-    std::string& decoded = pimpl_->decoded_;
-    OrthancImage target(format, width, height);
-
-    if (width == 0 ||
-        height == 0)
-    {
-      return target.Release();
-    }
-
-    if (decoded.empty())
-    {
-      decoded.resize(pimpl_->GetImage().GetBufferLength());
-      if (!pimpl_->GetImage().GetBuffer(&decoded[0]))
-      {
-        throw std::runtime_error("Image not properly decoded to a memory buffer");
-      }
-    }
-
-    const void* sourceBuffer = &decoded[0];
-
-    if (target.GetPitch() == bpp * width &&
-        frames == 1)
-    {
-      assert(decoded.size() == target.GetPitch() * target.GetHeight());      
-      memcpy(target.GetBuffer(), sourceBuffer, decoded.size());
-    }
-    else 
-    {
-      size_t targetPitch = target.GetPitch();
-      size_t sourcePitch = width * bpp;
-
-      const uint8_t* a = (reinterpret_cast<const uint8_t*>(decoded.c_str()) +
-                          sourcePitch * height * frameIndex);
-      uint8_t* b = reinterpret_cast<uint8_t*>(target.GetBuffer());
-
-      for (uint32_t y = 0; y < height; y++)
-      {
-        memcpy(b, a, sourcePitch);
-        a += sourcePitch;
-        b += targetPitch;
-      }
-    }
-    
-    FixPhotometricInterpretation(target, pimpl_->GetImage().GetPhotometricInterpretation());
-                                 
-    return target.Release();
-  }
-}
--- a/Plugins/Samples/GdcmDecoder/GdcmImageDecoder.h	Wed May 20 16:38:33 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,55 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../Common/OrthancPluginCppWrapper.h"
-
-#include <stdint.h>
-#include <boost/noncopyable.hpp>
-#include <boost/shared_ptr.hpp>
-
-
-namespace OrthancPlugins
-{
-  class GdcmImageDecoder : public boost::noncopyable
-  {
-  private:
-    struct PImpl;
-    boost::shared_ptr<PImpl> pimpl_;
-  
-  public:
-    GdcmImageDecoder(const void* dicom,
-                     size_t size);
-
-    OrthancPluginPixelFormat GetFormat() const;
-
-    unsigned int GetWidth() const;
-
-    unsigned int GetHeight() const;
-
-    unsigned int GetFramesCount() const;
-
-    static size_t GetBytesPerPixel(OrthancPluginPixelFormat format);
-
-    OrthancPluginImage* Decode(unsigned int frameIndex) const;
-  };
-}
--- a/Plugins/Samples/GdcmDecoder/Plugin.cpp	Wed May 20 16:38:33 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,249 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../../../Core/Compatibility.h"
-#include "../../../Core/DicomFormat/DicomMap.h"
-#include "../../../Core/Toolbox.h"
-#include "GdcmDecoderCache.h"
-
-static OrthancPlugins::GdcmDecoderCache  cache_;
-static bool restrictTransferSyntaxes_ = false;
-static std::set<std::string> enabledTransferSyntaxes_;
-
-
-static bool ExtractTransferSyntax(std::string& transferSyntax,
-                                  const void* dicom,
-                                  const uint32_t size)
-{
-  Orthanc::DicomMap header;
-  if (!Orthanc::DicomMap::ParseDicomMetaInformation(header, reinterpret_cast<const char*>(dicom), size))
-  {
-    return false;
-  }
-
-  const Orthanc::DicomValue* tag = header.TestAndGetValue(0x0002, 0x0010);
-  if (tag == NULL ||
-      tag->IsNull() ||
-      tag->IsBinary())
-  {
-    return false;
-  }
-  else
-  {
-    // Stripping spaces should not be required, as this is a UI value
-    // representation whose stripping is supported by the Orthanc
-    // core, but let's be careful...
-    transferSyntax = Orthanc::Toolbox::StripSpaces(tag->GetContent());
-    return true;
-  }
-}
-
-
-static bool IsTransferSyntaxEnabled(const void* dicom,
-                                    const uint32_t size)
-{
-  std::string formattedSize;
-
-  {
-    char tmp[16];
-    sprintf(tmp, "%0.1fMB", static_cast<float>(size) / (1024.0f * 1024.0f));
-    formattedSize.assign(tmp);
-  }
-
-  if (!restrictTransferSyntaxes_)
-  {
-    LOG(INFO) << "Decoding one DICOM instance of " << formattedSize << " using GDCM";
-    return true;
-  }
-
-  std::string transferSyntax;
-  if (!ExtractTransferSyntax(transferSyntax, dicom, size))
-  {
-    LOG(INFO) << "Cannot extract the transfer syntax of this instance of "
-              << formattedSize << ", will use GDCM to decode it";
-    return true;
-  }
-  else if (enabledTransferSyntaxes_.find(transferSyntax) != enabledTransferSyntaxes_.end())
-  {
-    // Decoding for this transfer syntax is enabled
-    LOG(INFO) << "Using GDCM to decode this instance of " << formattedSize
-              << " with transfer syntax " << transferSyntax;
-    return true;
-  }
-  else
-  {
-    LOG(INFO) << "Won't use GDCM to decode this instance of " << formattedSize
-              << ", as its transfer syntax " << transferSyntax << " is disabled";
-    return false;
-  }
-}
-
-
-static OrthancPluginErrorCode DecodeImageCallback(OrthancPluginImage** target,
-                                                  const void* dicom,
-                                                  const uint32_t size,
-                                                  uint32_t frameIndex)
-{
-  try
-  {
-    if (!IsTransferSyntaxEnabled(dicom, size))
-    {
-      *target = NULL;
-      return OrthancPluginErrorCode_Success;
-    }
-
-    std::unique_ptr<OrthancPlugins::OrthancImage> image;
-
-#if 0
-    // Do not use the cache
-    OrthancPlugins::GdcmImageDecoder decoder(dicom, size);
-    image.reset(new OrthancPlugins::OrthancImage(decoder.Decode(frameIndex)));
-#else
-    image.reset(cache_.Decode(dicom, size, frameIndex));
-#endif
-
-    *target = image->Release();
-
-    return OrthancPluginErrorCode_Success;
-  }
-  catch (Orthanc::OrthancException& e)
-  {
-    *target = NULL;
-
-    LOG(WARNING) << "Cannot decode image using GDCM: " << e.What();
-    return OrthancPluginErrorCode_Plugin;
-  }
-  catch (std::runtime_error& e)
-  {
-    *target = NULL;
-
-    LOG(WARNING) << "Cannot decode image using GDCM: " << e.what();
-    return OrthancPluginErrorCode_Plugin;
-  }
-  catch (...)
-  {
-    *target = NULL;
-
-    LOG(WARNING) << "Native exception while decoding image using GDCM";
-    return OrthancPluginErrorCode_Plugin;
-  }
-}
-
-
-
-/**
- * We force the redefinition of the "ORTHANC_PLUGINS_API" macro, that
- * was left empty with gcc until Orthanc SDK 1.5.7 (no "default"
- * visibility). This causes the version script, if run from "Holy
- * Build Box", to make private the 4 global functions of the plugin.
- **/
-
-#undef ORTHANC_PLUGINS_API
-
-#ifdef WIN32
-#  define ORTHANC_PLUGINS_API __declspec(dllexport)
-#elif __GNUC__ >= 4
-#  define ORTHANC_PLUGINS_API __attribute__ ((visibility ("default")))
-#else
-#  define ORTHANC_PLUGINS_API
-#endif
-
-
-extern "C"
-{
-  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
-  {
-    static const char* const KEY_GDCM = "Gdcm";
-    static const char* const KEY_ENABLE_GDCM = "EnableGdcm";
-    static const char* const KEY_RESTRICT_TRANSFER_SYNTAXES = "RestrictTransferSyntaxes";
-
-    OrthancPlugins::SetGlobalContext(context);
-    LOG(INFO) << "Initializing the advanced decoder of medical images using GDCM";
-
-
-    /* Check the version of the Orthanc core */
-    if (OrthancPluginCheckVersion(context) == 0)
-    {
-      char info[1024];
-      sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin",
-              context->orthancVersion,
-              ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
-              ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
-              ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
-      OrthancPluginLogError(context, info);
-      return -1;
-    }
-
-    OrthancPluginSetDescription(context, "Advanced decoder of medical images using GDCM.");
-
-    OrthancPlugins::OrthancConfiguration global;
-
-    bool enabled = true;
-    
-    if (global.IsSection(KEY_GDCM))
-    {
-      OrthancPlugins::OrthancConfiguration config;
-      global.GetSection(config, KEY_GDCM);
-
-      enabled = config.GetBooleanValue(KEY_ENABLE_GDCM, true);
-
-      if (config.LookupSetOfStrings(enabledTransferSyntaxes_, KEY_RESTRICT_TRANSFER_SYNTAXES, false))
-      {
-        restrictTransferSyntaxes_ = true;
-        
-        for (std::set<std::string>::const_iterator it = enabledTransferSyntaxes_.begin();
-             it != enabledTransferSyntaxes_.end(); ++it)
-        {
-          LOG(WARNING) << "Orthanc will use GDCM to decode transfer syntax: " << *it;
-        }
-      }
-    }
-
-    if (enabled)
-    {
-      OrthancPluginRegisterDecodeImageCallback(context, DecodeImageCallback);
-    }
-    else
-    {
-      LOG(WARNING) << "The advanced decoder of medical images using GDCM is disabled";
-    }
-    
-    return 0;
-  }
-
-
-  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
-  {
-    LOG(INFO) << "Finalizing the advanced decoder of medical images using GDCM";
-  }
-
-
-  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
-  {
-    return "gdcm-decoder";
-  }
-
-
-  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
-  {
-    return PLUGIN_VERSION;
-  }
-}
--- a/Plugins/Samples/GdcmDecoder/README	Wed May 20 16:38:33 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-This sample shows how to replace the decoder of DICOM images that is
-built in Orthanc, by the GDCM library.
-
-A production-ready version of this sample, is available in the
-offical Web viewer plugin:
-http://www.orthanc-server.com/static.php?page=web-viewer
--- a/Resources/Configuration.json	Wed May 20 16:38:33 2020 +0200
+++ b/Resources/Configuration.json	Wed May 20 17:03:24 2020 +0200
@@ -547,5 +547,30 @@
   // Whether Orthanc transcodes DICOM files to an uncompressed
   // transfer syntax over the DICOM protocol, if the remote modality
   // does not support compressed transfer syntaxes (new in Orthanc 1.7.0).
-  "TranscodeDicomProtocol" : true
+  "TranscodeDicomProtocol" : true,
+
+  // If some plugin to decode/transcode DICOM instances is installed,
+  // this option specifies whether the built-in decoder/transcoder of
+  // Orthanc (that uses DCMTK) is applied before or after the plugins,
+  // or is not applied at all (new in Orthanc 1.7.0). The allowed
+  // values for this option are "After" (default value, corresponding
+  // to the behavior of Orthanc <= 1.6.1), "Before", or "Disabled".
+  "BuiltinDecoderTranscoderOrder" : "After",
+
+  // If this option is set, Orthanc will transparently transcode any
+  // incoming DICOM instance to the given transfer syntax before
+  // storing it into its database. Beware that this might result in
+  // high CPU usage (if transcoding to some compressed transfer
+  // syntax), or in higher disk consumption (if transcoding to an
+  // uncompressed syntax). Also, beware that transcoding to a transfer
+  // syntax with lossy compression (notably JPEG) will change the
+  // "SOPInstanceUID" DICOM tag, and thus the Orthanc identifier at
+  // the instance level, which might break external workflow.
+  /**
+     "IngestTranscoding" : "1.2.840.10008.1.2",
+  **/
+
+  // The compression level that is used when transcoding to one of the
+  // lossy/JPEG transfer syntaxes (integer between 1 and 100).
+  "DicomLossyTranscodingQuality" : 90
 }
--- a/Resources/Orthanc.doxygen	Wed May 20 16:38:33 2020 +0200
+++ b/Resources/Orthanc.doxygen	Wed May 20 17:03:24 2020 +0200
@@ -545,7 +545,7 @@
 # this will also influence the order of the classes in the class list.
 # The default value is: NO.
 
-SORT_BRIEF_DOCS        = NO
+SORT_BRIEF_DOCS        = YES
 
 # If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the
 # (brief and detailed) documentation of class members so that constructors and
--- a/Resources/OrthancPlugin.doxygen	Wed May 20 16:38:33 2020 +0200
+++ b/Resources/OrthancPlugin.doxygen	Wed May 20 17:03:24 2020 +0200
@@ -545,7 +545,7 @@
 # this will also influence the order of the classes in the class list.
 # The default value is: NO.
 
-SORT_BRIEF_DOCS        = NO
+SORT_BRIEF_DOCS        = YES
 
 # If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the
 # (brief and detailed) documentation of class members so that constructors and
--- a/UnitTestsSources/FromDcmtkTests.cpp	Wed May 20 16:38:33 2020 +0200
+++ b/UnitTestsSources/FromDcmtkTests.cpp	Wed May 20 17:03:24 2020 +0200
@@ -513,7 +513,8 @@
   f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "Tata");  // (**)
 
   std::string s;
-  ASSERT_FALSE(f.LookupTransferSyntax(s));
+  ASSERT_TRUE(f.LookupTransferSyntax(s));
+  ASSERT_EQ(s, GetTransferSyntaxUid(DicomTransferSyntax_LittleEndianExplicit));
 
   ASSERT_THROW(f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"),
                          false, DicomReplaceMode_ThrowIfAbsent, ""), OrthancException);
@@ -1979,6 +1980,8 @@
   Orthanc::SystemToolbox::ReadFile(source, "/home/jodogne/Subversion/orthanc-tests/Database/KarstenHilbertRF.dcm");
 
   std::unique_ptr<DcmFileFormat> toto(FromDcmtkBridge::LoadFromMemoryBuffer(source.c_str(), source.size()));
+  const std::string sourceUid = IDicomTranscoder::GetSopInstanceUid(*toto);
+  
   DicomTransferSyntax sourceSyntax;
   ASSERT_TRUE(FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, *toto));
 
@@ -1987,14 +1990,16 @@
   for (int i = 0; i <= DicomTransferSyntax_XML; i++)
   {
     DicomTransferSyntax a = (DicomTransferSyntax) i;
+    
+    std::set<DicomTransferSyntax> s;
+    s.insert(a);
 
     std::string t;
 
-    bool hasSopInstanceUidChanged;
-    DicomTransferSyntax sourceSyntax2;
+    IDicomTranscoder::DicomImage source, target;
+    source.AcquireParsed(dynamic_cast<DcmFileFormat*>(toto->clone()));
 
-    std::unique_ptr<DcmFileFormat> cloned(dynamic_cast<DcmFileFormat*>(toto->clone()));
-    if (!transcoder.TranscodeParsedToBuffer(t, sourceSyntax2, hasSopInstanceUidChanged, *cloned, a, true))
+    if (!transcoder.Transcode(target, source, s, true))
     {
       printf("**************** CANNOT: [%s] => [%s]\n",
              GetTransferSyntaxUid(sourceSyntax), GetTransferSyntaxUid(a));
@@ -2002,22 +2007,21 @@
     else
     {
       DicomTransferSyntax targetSyntax;
-      ASSERT_TRUE(FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax, *cloned));
+      ASSERT_TRUE(FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax, target.GetParsed()));
       
       ASSERT_EQ(targetSyntax, a);
-      ASSERT_EQ(sourceSyntax, sourceSyntax2);
       bool lossy = (a == DicomTransferSyntax_JPEGProcess1 ||
                     a == DicomTransferSyntax_JPEGProcess2_4 ||
                     a == DicomTransferSyntax_JPEGLSLossy);
       
       printf("SIZE: %lu\n", t.size());
-      if (hasSopInstanceUidChanged)
+      if (sourceUid == IDicomTranscoder::GetSopInstanceUid(target.GetParsed()))
       {
-        ASSERT_TRUE(lossy);
+        ASSERT_FALSE(lossy);
       }
       else
       {
-        ASSERT_FALSE(lossy);
+        ASSERT_TRUE(lossy);
       }
     }
   }
--- a/UnitTestsSources/MultiThreadingTests.cpp	Wed May 20 16:38:33 2020 +0200
+++ b/UnitTestsSources/MultiThreadingTests.cpp	Wed May 20 17:03:24 2020 +0200
@@ -1549,6 +1549,30 @@
     ASSERT_EQ("username", tmp.GetPeer().GetUsername());
     ASSERT_EQ("password", tmp.GetPeer().GetPassword());
     ASSERT_TRUE(tmp.GetPeer().IsPkcs11Enabled());
+    ASSERT_FALSE(tmp.IsTranscode());
+    ASSERT_THROW(tmp.GetTransferSyntax(), OrthancException);
+  }
+
+  {
+    OrthancPeerStoreJob job(GetContext());
+    ASSERT_THROW(job.SetTranscode("nope"), OrthancException);
+    job.SetTranscode("1.2.840.10008.1.2.4.50");
+    
+    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    std::unique_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    OrthancPeerStoreJob& tmp = dynamic_cast<OrthancPeerStoreJob&>(*job);
+    ASSERT_EQ("http://127.0.0.1:8042/", tmp.GetPeer().GetUrl());
+    ASSERT_EQ("", tmp.GetPeer().GetUsername());
+    ASSERT_EQ("", tmp.GetPeer().GetPassword());
+    ASSERT_FALSE(tmp.GetPeer().IsPkcs11Enabled());
+    ASSERT_TRUE(tmp.IsTranscode());
+    ASSERT_EQ(DicomTransferSyntax_JPEGProcess1, tmp.GetTransferSyntax());
   }
 
   // ResourceModificationJob
@@ -1560,7 +1584,8 @@
     ResourceModificationJob job(GetContext());
     job.SetModification(modification.release(), ResourceType_Patient, true);
     job.SetOrigin(DicomInstanceOrigin::FromLua());
-    
+
+    job.AddTrailingStep();  // Necessary since 1.7.0
     ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
     ASSERT_TRUE(job.Serialize(s));
   }
@@ -1571,10 +1596,33 @@
 
     ResourceModificationJob& tmp = dynamic_cast<ResourceModificationJob&>(*job);
     ASSERT_TRUE(tmp.IsAnonymization());
+    ASSERT_FALSE(tmp.IsTranscode());
+    ASSERT_THROW(tmp.GetTransferSyntax(), OrthancException);
     ASSERT_EQ(RequestOrigin_Lua, tmp.GetOrigin().GetRequestOrigin());
     ASSERT_TRUE(tmp.GetModification().IsRemoved(DICOM_TAG_STUDY_DESCRIPTION));
   }
 
+  {
+    ResourceModificationJob job(GetContext());
+    ASSERT_THROW(job.SetTranscode("nope"), OrthancException);
+    job.SetTranscode(DicomTransferSyntax_JPEGProcess1);
+
+    job.AddTrailingStep();  // Necessary since 1.7.0
+    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    std::unique_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    ResourceModificationJob& tmp = dynamic_cast<ResourceModificationJob&>(*job);
+    ASSERT_FALSE(tmp.IsAnonymization());
+    ASSERT_TRUE(tmp.IsTranscode());
+    ASSERT_EQ(DicomTransferSyntax_JPEGProcess1, tmp.GetTransferSyntax());
+    ASSERT_EQ(RequestOrigin_Unknown, tmp.GetOrigin().GetRequestOrigin());
+  }
+
   // SplitStudyJob
 
   std::string instance;