changeset 7025:a998c3b34bf4 streaming

integration mainline -> streaming
author Alain Mazy <am@orthanc.team>
date Wed, 15 Jul 2026 11:42:31 +0200
parents e9cebd8402c5 (diff) a412fcd67ae6 (current diff)
children d251cd0ec90a
files NEWS OrthancFramework/Sources/DicomNetworking/DicomServer.cpp OrthancFramework/Sources/JobsEngine/JobsEngine.cpp OrthancFramework/Sources/Logging.cpp OrthancFramework/Sources/Logging.h OrthancFramework/Sources/MultiThreading/ThreadPool.cpp OrthancFramework/Sources/Toolbox.cpp OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp OrthancFramework/UnitTestsSources/ToolboxTests.cpp OrthancServer/Plugins/Engine/OrthancPlugins.cpp OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h OrthancServer/Resources/Configuration.json OrthancServer/Sources/LuaScripting.cpp OrthancServer/Sources/OrthancConfiguration.cpp OrthancServer/Sources/OrthancConfiguration.h OrthancServer/Sources/OrthancGetRequestHandler.cpp OrthancServer/Sources/OrthancMoveRequestHandler.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp OrthancServer/Sources/OrthancWebDav.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h OrthancServer/Sources/ServerIndex.cpp OrthancServer/Sources/ServerTranscoder.cpp OrthancServer/Sources/main.cpp
diffstat 117 files changed, 4594 insertions(+), 2297 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Wed Jul 08 11:45:33 2026 +0200
+++ b/.hgignore	Wed Jul 15 11:42:31 2026 +0200
@@ -2,8 +2,11 @@
 ThirdPartyDownloads/
 CMakeLists.txt.user
 *.cpp.orig
+*.cpp.rej
 *.h.orig
+*.h.rej
 *.patch.orig
+*.patch.rej
 .vs/
 .vscode/
 *~
--- a/NEWS	Wed Jul 08 11:45:33 2026 +0200
+++ b/NEWS	Wed Jul 15 11:42:31 2026 +0200
@@ -4,6 +4,22 @@
 General
 -------
 
+* Full rewrite of access to storage area and of DICOM parsing/transcoding
+  to improve caching and to limit memory usage.  
+  This change brings updated configurations (TODO: review):
+  - "StorageLoaderThreads"
+  - "StorageMemoryCapacity"
+  - "DicomParserThreads"
+  - "DicomParserMemoryCapacity"
+  - "DicomParserCacheSize"
+  - "TranscoderThreads"
+  - "TranscoderMemoryCapacity"
+  - "TranscoderCacheSize"
+  - "SequentialDicomReaderThreads"
+  - "SequentialDicomReaderWindowSize"
+  - "SequentialDicomReaderWindowCapacity"
+  And new metrics:
+  - TODO list
 * Orthanc will now refuse to start if you have "AuthenticationEnabled" and "RemoteAccessAllowed"
   set to true and have not defined any users in "RegisteredUsers".
 * New values for "OverwriteInstances" configuration. In previous versions, this configuration
@@ -21,12 +37,16 @@
   - StoreKeyValue(storeId, key, value)
   - value = GetKeyValue(storeId, key)
   - DeleteKeyValue(storeId, key)
-* C-Find SCP: 
-  - When Orthanc was requested a sequence in a C-Find query, Orthanc was actually not returning 
+* Logs:
+  - Some line of log now contain additional contextual informations like the job id they relate to.
+    A new "--logs-no-context" command line option can be used to get back to the previous behavior to
+    keep backward compatibility and reduce the lines length.
+* C-Find SCP:
+  - When Orthanc was requested a sequence in a C-Find query, Orthanc was actually not returning
     any sequences in the C-Find answer if the matched resource did contain the requested sequence and
     was returning an empty sequence if the matched resource did not contain the requested sequence.
     Orthanc now returns the full sequence content when the matched resource contains the requested sequence.
-  - When Orthanc was requested to return a private tag in a C-Find query, Orthanc was actually not returning 
+  - When Orthanc was requested to return a private tag in a C-Find query, Orthanc was actually not returning
     the resources that did not contain the private tag although it was not used as a filter in the query.
 * Fix mismatches between the configuration default values and the documentation:
   - "HttpTimeout" was documented as 60 while its default value is 0 (no timeout).
@@ -35,7 +55,7 @@
 REST API
 --------
 
-* API version upgraded to 31
+* API version upgraded to 32
 * New fields reported in the "/system" route:
   - "OverwriteInstancesMode" (Boolean field "OverwriteInstances" is kept for backward
     compatibility)
@@ -49,10 +69,10 @@
   SeriesNumber at the series level and now using " - " instead of " " to separate DICOM tags used
   in folder names.
 * In "/tools/metrics-prometheus":
-  - Removed "orthanc_jobs_completed", "orthanc_jobs_success" and "orthanc_jobs_failed" that were 
+  - Removed "orthanc_jobs_completed", "orthanc_jobs_success" and "orthanc_jobs_failed" that were
     only considering the jobs currently stored in the JobsHistory and that were not suitable for
-    monitoring.  These metrics have been replaced by "orthanc_jobs_total_completed", 
-    "orthanc_jobs_total_success" and "orthanc_jobs_total_failed" that are now considering all the 
+    monitoring.  These metrics have been replaced by "orthanc_jobs_total_completed",
+    "orthanc_jobs_total_success" and "orthanc_jobs_total_failed" that are now considering all the
     jobs that have run since Orthanc started.
 
 Plugin SDK
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake	Wed Jul 15 11:42:31 2026 +0200
@@ -404,7 +404,6 @@
       ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Compression/HierarchicalZipWriter.cpp
       ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Compression/ZipWriter.cpp
       ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/StorageAccessor.cpp
-      ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/StorageCache.cpp
       )
   endif()
 endif()
@@ -602,7 +601,6 @@
     add_definitions(-DORTHANC_ENABLE_DCMTK_TRANSCODING=1)
     list(APPEND ORTHANC_DICOM_SOURCES_INTERNAL
       ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/DcmtkTranscoder.cpp
-      ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/IDicomTranscoder.cpp
       ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/Internals/SopInstanceUidFixer.cpp
       ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/MemoryBufferTranscoder.cpp
       )
@@ -657,10 +655,12 @@
   list(APPEND ORTHANC_CORE_SOURCES_INTERNAL
     ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Cache/MemoryStringCache.cpp
     ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Cache/SharedArchive.cpp
+    ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DataSource/BaseDataIdentifier.cpp
     ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DataSource/DataSourceAnswer.cpp
     ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DataSource/DataSourceMemoryBudget.cpp
     ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DataSource/DataSourceReader.cpp
     ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DataSource/DataSourceRequest.cpp
+    ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DataSource/DataSourceSequentialReader.cpp
     ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DataSource/StorageAreaDataSource.cpp
     ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileBuffer.cpp
     ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/FilesystemStorage.cpp
@@ -681,6 +681,9 @@
   if (ENABLE_DCMTK)
     list(APPEND ORTHANC_CORE_SOURCES_INTERNAL
       ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DataSource/DicomDataSource.cpp
+      ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DataSource/DicomSequentialReader.cpp
+      ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DataSource/TranscoderDataSource.cpp
+      ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/IDicomTranscoder.cpp
       )
   endif()
 
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Wed Jul 15 11:42:31 2026 +0200
@@ -39,7 +39,7 @@
 # Version of the Orthanc API, can be retrieved from "/system" URI in
 # order to check whether new URI endpoints are available even if using
 # the mainline version of Orthanc
-set(ORTHANC_API_VERSION "31")
+set(ORTHANC_API_VERSION "32")
 
 
 #####################################################################
--- a/OrthancFramework/Sources/Cache/ICachePageProvider.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/Cache/ICachePageProvider.h	Wed Jul 15 11:42:31 2026 +0200
@@ -24,14 +24,16 @@
 
 #pragma once
 
+#include "../Compatibility.h"
+#include "../IDynamicObject.h"
+
 #include <string>
-#include "../IDynamicObject.h"
 
 namespace Orthanc
 {
   namespace Deprecated
   {
-    class ICachePageProvider
+    class ORTHANC_DEPRECATED_CLASS(ICachePageProvider)
     {
     public:
       virtual ~ICachePageProvider()
--- a/OrthancFramework/Sources/Cache/ICacheable.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/Cache/ICacheable.h	Wed Jul 15 11:42:31 2026 +0200
@@ -24,11 +24,13 @@
 
 #pragma once
 
+#include "../Compatibility.h"
+
 #include <boost/noncopyable.hpp>
 
 namespace Orthanc
 {
-  class ICacheable : public boost::noncopyable
+  class ORTHANC_DEPRECATED_CLASS(ICacheable) : public boost::noncopyable
   {
   public:
     virtual ~ICacheable()
--- a/OrthancFramework/Sources/Cache/MemoryCache.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryCache.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -22,11 +22,16 @@
  **/
 
 
+#if defined(__GNUC__) || defined(__clang__)
+#  pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+#endif
+
 #include "../PrecompiledHeaders.h"
 #include "MemoryCache.h"
 
 #include "../Logging.h"
 
+
 namespace Orthanc
 {
   namespace Deprecated
--- a/OrthancFramework/Sources/Cache/MemoryCache.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryCache.h	Wed Jul 15 11:42:31 2026 +0200
@@ -37,7 +37,7 @@
     /**
      * WARNING: This class is NOT thread-safe.
      **/
-    class ORTHANC_PUBLIC MemoryCache : public boost::noncopyable
+    class ORTHANC_PUBLIC ORTHANC_DEPRECATED_CLASS(MemoryCache) : public boost::noncopyable
     {
     private:
       struct Page
--- a/OrthancFramework/Sources/Cache/MemoryObjectCache.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryObjectCache.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -22,6 +22,10 @@
  **/
 
 
+#if defined(__GNUC__) || defined(__clang__)
+#  pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+#endif
+
 #include "../PrecompiledHeaders.h"
 #include "MemoryObjectCache.h"
 
--- a/OrthancFramework/Sources/Cache/MemoryObjectCache.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryObjectCache.h	Wed Jul 15 11:42:31 2026 +0200
@@ -42,7 +42,7 @@
   /**
    *  Note: this class is thread safe
    **/
-  class ORTHANC_PUBLIC MemoryObjectCache : public boost::noncopyable
+  class ORTHANC_PUBLIC ORTHANC_DEPRECATED_CLASS(MemoryObjectCache) : public boost::noncopyable
   {
   private:
     class Item;
@@ -82,7 +82,7 @@
 
     void Invalidate(const std::string& key);
 
-    class Accessor : public boost::noncopyable
+    class ORTHANC_PUBLIC Accessor : public boost::noncopyable
     {
     private:
 #if !defined(__EMSCRIPTEN__)
--- a/OrthancFramework/Sources/Cache/MemoryStringCache.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryStringCache.h	Wed Jul 15 11:42:31 2026 +0200
@@ -46,7 +46,7 @@
    * 
    * Note: this class is thread safe
    **/
-  class ORTHANC_PUBLIC MemoryStringCache : public boost::noncopyable
+  class ORTHANC_PUBLIC ORTHANC_DEPRECATED_CLASS(MemoryStringCache) : public boost::noncopyable
   {
   public:
     class ORTHANC_PUBLIC Accessor : public boost::noncopyable
--- a/OrthancFramework/Sources/Cache/SharedObjectCache.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/Cache/SharedObjectCache.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -189,4 +189,13 @@
 
     assert(!lru_.Contains(id));
   }
+
+
+  void SharedObjectCache::GetStatistics(size_t& currentCount,
+                                        size_t& currentSize)
+  {
+    Mutex::ScopedLock lock(mutex_);
+    currentCount = lru_.GetSize();
+    currentSize = currentSize_;
+  }
 }
--- a/OrthancFramework/Sources/Cache/SharedObjectCache.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/Cache/SharedObjectCache.h	Wed Jul 15 11:42:31 2026 +0200
@@ -43,7 +43,7 @@
     Mutex                                mutex_;
     LeastRecentlyUsedIndex<std::string>  lru_;
     Content                              content_;
-    size_t                               capacity_;
+    const size_t                         capacity_;
     size_t                               currentSize_;
 
     void MakeRoom(size_t newObjectSize);
@@ -69,5 +69,13 @@
                size_t size);
 
     void Invalidate(const std::string& id);
+
+    size_t GetCapacity() const
+    {
+      return capacity_;  // This is a constant value, it can be read without the mutex
+    }
+
+    void GetStatistics(size_t& currentCount,
+                       size_t& currentSize);
   };
 }
--- a/OrthancFramework/Sources/Compatibility.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/Compatibility.h	Wed Jul 15 11:42:31 2026 +0200
@@ -47,6 +47,18 @@
 #endif
 
 
+// Macro "ORTHANC_DEPRECATED_CLASS" tags a class as having been deprecated
+#if (__cplusplus >= 201402L)  // C++14
+#  define ORTHANC_DEPRECATED_CLASS(f) [[deprecated]] f
+#elif defined(__GNUC__) || defined(__clang__)
+#  define ORTHANC_DEPRECATED_CLASS(f) __attribute__((deprecated)) f
+#elif defined(_MSC_VER)
+#  define ORTHANC_DEPRECATED_CLASS(f) __declspec(deprecated) f
+#else
+#  define ORTHANC_DEPRECATED
+#endif
+
+
 // Macros "ORTHANC_OVERRIDE" and "ORTHANC_FINAL" wrap the "override"
 // and "final" keywords introduced in C++11, to do compile-time
 // checking of virtual methods
--- a/OrthancFramework/Sources/Constants.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/Constants.h	Wed Jul 15 11:42:31 2026 +0200
@@ -33,6 +33,16 @@
   static const uint64_t MEGABYTE = 1024ull * 1024ull;
   static const uint64_t GIGABYTE = 1024ull * 1024ull * 1024ull;
 
+  inline float BytesToFloatMegabytes(uint64_t size)
+  {
+    return static_cast<float>(size) / static_cast<float>(MEGABYTE);
+  }
+
+  inline unsigned int BytesToMegabytes(uint64_t size)
+  {
+    return size / MEGABYTE;
+  }
+
   static const uint64_t MAX_IMAGE_FRAME_SIZE = (sizeof(void*) == 4
                                                 ? 1 * Orthanc::GIGABYTE   // 1 GB on 32 bits system
                                                 : 4 * Orthanc::GIGABYTE); // 4 GB on 64 bits system
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DataSource/BaseDataIdentifier.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -0,0 +1,78 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "BaseDataIdentifier.h"
+
+#include "../OrthancException.h"
+
+
+namespace Orthanc
+{
+  void BaseDataIdentifier::SetUserData(IDynamicObject* userData)
+  {
+    std::unique_ptr<IDynamicObject> protection(userData);
+
+    if (userData == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+      userData_.reset(protection.release());
+    }
+  }
+
+
+  bool BaseDataIdentifier::HasUserData() const
+  {
+    return userData_.get() != NULL;
+  }
+
+
+  const IDynamicObject& BaseDataIdentifier::GetUserData() const
+  {
+    if (userData_.get() != NULL)
+    {
+      return *userData_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  IDynamicObject* BaseDataIdentifier::ReleaseUserData()
+  {
+    if (userData_.get() != NULL)
+    {
+      return userData_.release();
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DataSource/BaseDataIdentifier.h	Wed Jul 15 11:42:31 2026 +0200
@@ -0,0 +1,47 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IDataIdentifier.h"
+#include "../Compatibility.h"
+
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC BaseDataIdentifier : public IDataIdentifier
+  {
+  private:
+    std::unique_ptr<IDynamicObject> userData_;
+
+  public:
+    void SetUserData(IDynamicObject* userData);
+
+    virtual bool HasUserData() const ORTHANC_OVERRIDE;
+
+    virtual const IDynamicObject& GetUserData() const ORTHANC_OVERRIDE;
+
+    virtual IDynamicObject* ReleaseUserData() ORTHANC_OVERRIDE;
+  };
+}
--- a/OrthancFramework/Sources/DataSource/DataSourceAnswer.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/DataSource/DataSourceAnswer.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -75,10 +75,17 @@
 
   const IDataIdentifier& DataSourceAnswer::Item::GetId() const
   {
-    assert(id_.get() != NULL);
-    return *id_;
+    if (id_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *id_;
+    }
   }
 
+
   const boost::shared_ptr<IDynamicObject>& DataSourceAnswer::Item::GetValue() const
   {
     if (error_)
--- a/OrthancFramework/Sources/DataSource/DataSourceAnswer.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/DataSource/DataSourceAnswer.h	Wed Jul 15 11:42:31 2026 +0200
@@ -75,6 +75,16 @@
       const IDataIdentifier& GetId() const;
 
       const boost::shared_ptr<IDynamicObject>& GetValue() const;
+
+      bool HasUserData() const
+      {
+        return id_->HasUserData();
+      }
+
+      IDynamicObject* ReleaseUserData()
+      {
+        return id_->ReleaseUserData();
+      }
     };
 
   private:
--- a/OrthancFramework/Sources/DataSource/DataSourceMemoryBudget.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/DataSource/DataSourceMemoryBudget.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -25,11 +25,65 @@
 #include "../PrecompiledHeaders.h"
 #include "DataSourceMemoryBudget.h"
 
+#include "../Constants.h"
+#include "../MetricsRegistry.h"
+#include "../OrthancException.h"
+
+#include <math.h>
 
 namespace Orthanc
 {
   namespace Internals
   {
+    DataSourceMemoryBudget::MetricsConfiguration::MetricsConfiguration(const boost::shared_ptr<MetricsRegistry>& metrics,
+                                                                       const std::string& capacityMaxSizeMegabytesName,
+                                                                       const std::string& capacityCurrentSizeMegabytesName,
+                                                                       const std::string& capacityCountName,
+                                                                       const std::string& capacityMaxUsageSinceStartMegabytesName) :
+      maxMemoryUsageSinceStart_(0)
+    {
+      if (metrics.get() == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+      else if (capacityMaxSizeMegabytesName.empty() ||
+               capacityCurrentSizeMegabytesName.empty() ||
+               capacityCountName.empty() ||
+               capacityMaxUsageSinceStartMegabytesName.empty())
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+      else
+      {
+        metrics_ = metrics;
+        capacityMaxSizeMegabytesName_ = capacityMaxSizeMegabytesName;
+        capacityCurrentSizeMegabytesName_ = capacityCurrentSizeMegabytesName;
+        capacityCountName_ = capacityCountName;
+        capacityMaxUsageSinceStartMegabytesName_ = capacityMaxUsageSinceStartMegabytesName;
+
+        Update(0, 0, 0);
+      }
+    }
+
+    void DataSourceMemoryBudget::MetricsConfiguration::Update(uint64_t maximumMemorySize, uint64_t currentMemorySize, unsigned int currentReservationCount)
+    {
+      maxMemoryUsageSinceStart_ = std::max(currentMemorySize, maxMemoryUsageSinceStart_);
+
+      metrics_->SetFloatValue(capacityMaxSizeMegabytesName_, BytesToFloatMegabytes(maximumMemorySize)); // will be updated when we set the capacity
+      metrics_->SetFloatValue(capacityCurrentSizeMegabytesName_, BytesToFloatMegabytes(currentMemorySize));
+      metrics_->SetIntegerValue(capacityCountName_, currentReservationCount);
+      metrics_->SetFloatValue(capacityMaxUsageSinceStartMegabytesName_, BytesToFloatMegabytes(maxMemoryUsageSinceStart_));
+    }
+
+
+    void DataSourceMemoryBudget::SetMetricsConfiguration(const MetricsConfiguration& configuration)
+    {
+      metricsConfiguration_ = configuration;
+      
+      boost::mutex::scoped_lock lock(mutex_);
+      metricsConfiguration_.Update(maximumMemory_, currentMemory_, reservations_);
+    }
+
     DataSourceMemoryBudget::DataSourceMemoryBudget(uint64_t maximumMemory) :
       maximumMemory_(maximumMemory),
       currentMemory_(0),
@@ -50,6 +104,8 @@
 
       currentMemory_ += memory;
       reservations_++;
+      
+      metricsConfiguration_.Update(maximumMemory_, currentMemory_, reservations_);
     }
 
 
@@ -62,7 +118,9 @@
 
       assert(reservations_ > 0);
       reservations_--;
-
+      
+      metricsConfiguration_.Update(maximumMemory_, currentMemory_, reservations_);
+      
       cond_.notify_all();
     }
 
@@ -72,5 +130,17 @@
       boost::mutex::scoped_lock lock(mutex_);
       return currentMemory_;
     }
+
+
+    void DataSourceMemoryBudget::GetStatistics(uint64_t& maximumMemory,
+                                               uint64_t& currentMemory,
+                                               unsigned int& countReservations)
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      maximumMemory = maximumMemory_;
+      currentMemory = currentMemory_;
+      countReservations = reservations_;
+    }
   }
 }
--- a/OrthancFramework/Sources/DataSource/DataSourceMemoryBudget.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/DataSource/DataSourceMemoryBudget.h	Wed Jul 15 11:42:31 2026 +0200
@@ -29,6 +29,7 @@
 #endif
 
 #include "../Compatibility.h"
+#include "../OrthancFramework.h"
 
 #include <boost/noncopyable.hpp>
 #include <boost/thread/condition_variable.hpp>
@@ -38,28 +39,59 @@
 
 namespace Orthanc
 {
+  class MetricsRegistry;
+
   namespace Internals
   {
-    // This is basically a semaphore with the uint64_t data type
-    class DataSourceMemoryBudget : public boost::noncopyable
+    // This is basically a semaphore with the uint64_t data type (with metrics !)
+    class ORTHANC_PUBLIC DataSourceMemoryBudget : public boost::noncopyable
     {
+    public:
+      class MetricsConfiguration
+      {
+      private:
+        boost::shared_ptr<MetricsRegistry>  metrics_;
+        std::string                         capacityMaxSizeMegabytesName_;
+        std::string                         capacityCurrentSizeMegabytesName_;
+        std::string                         capacityCountName_;
+        std::string                         capacityMaxUsageSinceStartMegabytesName_;
+        uint64_t                            maxMemoryUsageSinceStart_;
+
+      public:
+        MetricsConfiguration()
+        {
+        }
+
+        MetricsConfiguration(const boost::shared_ptr<MetricsRegistry>& metrics,
+                             const std::string& capacityMaxSizeMegabytesName,
+                             const std::string& capacityCurrentSizeMegabytesName,
+                             const std::string& capacityCountName,
+                             const std::string& capacityMaxUsageSinceStartMegabytesName);
+
+        void Update(uint64_t maximumMemorySize, uint64_t currentMemorySize, unsigned int currentReservationCount);
+      };
+
+
     private:
       boost::mutex               mutex_;
       boost::condition_variable  cond_;
       const uint64_t             maximumMemory_;
       uint64_t                   currentMemory_;
       unsigned int               reservations_;
+      MetricsConfiguration       metricsConfiguration_;
 
     public:
-      explicit DataSourceMemoryBudget(uint64_t maximumMemory);
+      explicit DataSourceMemoryBudget(uint64_t maximumMemory); //, const MetricsConfiguration& metricsConfiguration);
 
       void Acquire(size_t memory) ORTHANC_NOEXCEPT;
 
       void Release(size_t memory) ORTHANC_NOEXCEPT;
 
+      void SetMetricsConfiguration(const MetricsConfiguration& configuration);
+
       uint64_t GetCurrentMemory() ORTHANC_NOEXCEPT;
 
-      class Lock : public boost::noncopyable
+      class ORTHANC_PUBLIC Lock : public boost::noncopyable
       {
       private:
         DataSourceMemoryBudget&  that_;
@@ -79,6 +111,10 @@
           that_.Release(memory_);
         }
       };
+
+      void GetStatistics(uint64_t& maximumMemory,
+                         uint64_t& currentMemory,
+                         unsigned int& countReservations);
     };
   }
 }
--- a/OrthancFramework/Sources/DataSource/DataSourceReader.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/DataSource/DataSourceReader.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -26,6 +26,8 @@
 #include "DataSourceReader.h"
 
 #include "../Cache/SharedObjectCache.h"
+#include "../Constants.h"
+#include "../MetricsRegistry.h"
 #include "../OrthancException.h"
 #include "DataSourceMemoryBudget.h"
 
@@ -36,6 +38,86 @@
 
 namespace Orthanc
 {
+  DataSourceReader::MetricsConfiguration::MetricsConfiguration(const boost::shared_ptr<MetricsRegistry>& metrics,
+                                                               const std::string& cacheSizeMegabytesName,
+                                                               const std::string& cacheCountName,
+                                                               const std::string& cacheHitCountName,
+                                                               const std::string& cacheMissCountName,
+                                                               const std::string& capacityMaxSizeMegabytesName,
+                                                               const std::string& capacityCurrentSizeMegabytesName,
+                                                               const std::string& capacityCountName,
+                                                               const std::string& capacityMaxUsageSinceStartMegabytesName)
+  {
+    if (metrics.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else if (cacheSizeMegabytesName.empty() ||
+             cacheCountName.empty() ||
+             cacheHitCountName.empty() ||
+             cacheMissCountName.empty() || 
+             capacityMaxSizeMegabytesName.empty() ||
+             capacityCurrentSizeMegabytesName.empty() ||
+             capacityCountName.empty() ||
+             capacityMaxUsageSinceStartMegabytesName.empty())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      metrics_ = metrics;
+      cacheSizeMegabytesName_ = cacheSizeMegabytesName;
+      cacheCountName_ = cacheCountName;
+      cacheHitCountName_ = cacheHitCountName;
+      cacheMissCountName_ = cacheMissCountName;
+      capacityMaxSizeMegabytesName_ = capacityMaxSizeMegabytesName;
+      capacityCurrentSizeMegabytesName_ = capacityCurrentSizeMegabytesName;
+      capacityCountName_ = capacityCountName;
+      capacityMaxUsageSinceStartMegabytesName_ = capacityMaxUsageSinceStartMegabytesName;
+
+      metrics_->SetFloatValue(cacheSizeMegabytesName_, 0);
+      metrics_->SetIntegerValue(cacheCountName_, 0);
+      metrics_->SetIntegerValue(cacheHitCountName_, 0);
+      metrics_->SetIntegerValue(cacheMissCountName_, 0);
+      metrics_->SetFloatValue(capacityMaxSizeMegabytesName_, 0); // will be updated when we set the capacity
+      metrics_->SetFloatValue(capacityCurrentSizeMegabytesName_, 0);
+      metrics_->SetIntegerValue(capacityCountName_, 0);
+      metrics_->SetFloatValue(capacityMaxUsageSinceStartMegabytesName_, 0);
+    }
+  }
+
+
+  void DataSourceReader::MetricsConfiguration::SetCacheStatistics(SharedObjectCache& cache)
+  {
+    if (metrics_)
+    {
+      size_t count, size;
+      cache.GetStatistics(count, size);
+
+      metrics_->SetFloatValue(cacheSizeMegabytesName_, BytesToFloatMegabytes(size));
+      metrics_->SetIntegerValue(cacheCountName_, count);
+    }
+  }
+
+
+  void DataSourceReader::MetricsConfiguration::IncrementCacheHitCount()
+  {
+    if (metrics_)
+    {
+      metrics_->IncrementIntegerValue(cacheHitCountName_, 1);
+    }
+  }
+
+
+  void DataSourceReader::MetricsConfiguration::IncrementCacheMissCount()
+  {
+    if (metrics_)
+    {
+      metrics_->IncrementIntegerValue(cacheMissCountName_, 1);
+    }
+  }
+
+
   class DataSourceReader::DataSourceRunnable : public IRunnable
   {
   private:
@@ -44,18 +126,21 @@
     std::unique_ptr<IDataIdentifier>                      id_;
     boost::shared_ptr<SharedObjectCache>                  cache_;
     boost::shared_ptr<Internals::DataSourceMemoryBudget>  budget_;
+    MetricsConfiguration                                  metricsConfiguration_;
 
   public:
-    DataSourceRunnable(boost::shared_ptr<DataSourceAnswer>& answer,
+    DataSourceRunnable(const boost::shared_ptr<DataSourceAnswer>& answer,
                        IDataSource& source,
                        IDataIdentifier* id,
                        boost::shared_ptr<SharedObjectCache>& cache,
-                       boost::shared_ptr<Internals::DataSourceMemoryBudget>& budget) :
+                       boost::shared_ptr<Internals::DataSourceMemoryBudget>& budget,
+                       const MetricsConfiguration& metrics) :
       answer_(answer),
       source_(source),
       id_(id),
       cache_(cache),
-      budget_(budget)
+      budget_(budget),
+      metricsConfiguration_(metrics)
     {
       if (id == NULL ||
           answer.get() == NULL)
@@ -66,7 +151,17 @@
 
     virtual void Run() ORTHANC_OVERRIDE
     {
-      // Phase 1: Do all the work WITHOUT holding a strong reference to "DataSourceAnswer".
+      // Phase 1: Make sure the target answer is still alive before doing unnecessary work.
+      {
+        boost::shared_ptr<DataSourceAnswer> lock = answer_.lock();
+        if (!lock)
+        {
+          // The answer was abandonned, give up the request
+          return;
+        }
+      }
+
+      // Phase 2: Do all the work WITHOUT holding a strong reference to "DataSourceAnswer".
       boost::shared_ptr<IDynamicObject> value;
       std::unique_ptr<OrthancException> error;
 
@@ -80,6 +175,15 @@
         if (cache_ && hasCacheKey)
         {
           value = cache_->GetCachedValue(cacheKey);
+
+          if (value)
+          {
+            metricsConfiguration_.IncrementCacheHitCount();
+          }
+          else
+          {
+            metricsConfiguration_.IncrementCacheMissCount();
+          }
         }
 
         if (!value)
@@ -92,9 +196,9 @@
             preReservation.reset(new Internals::DataSourceMemoryBudget::Lock(*budget_, estimatedSize));
           }
 
-          value.reset(source_.Load(*id_));
+          value.reset(source_.Load(*id_, cache_));
 
-          if (!error && !value)
+          if (!value)
           {
             error.reset(new OrthancException(ErrorCode_NullPointer));
           }
@@ -102,6 +206,7 @@
           if (!error && cache_ && hasCacheKey)
           {
             cache_->Store(cacheKey, value, source_.GetValueSize(*value));
+            metricsConfiguration_.SetCacheStatistics(*cache_);
           }
         }
 
@@ -119,7 +224,7 @@
         error.reset(new OrthancException(ErrorCode_InternalError, "Unknown exception in Datasource::Run"));
       }
 
-      // Phase 2: Acquire budget WITHOUT holding a strong reference to "DataSourceAnswer".
+      // Phase 3: Acquire budget WITHOUT holding a strong reference to "DataSourceAnswer".
       // If the "DataSourceAnswer" has been dropped, "answer_.lock()" below will return NULL
       // and we will immediately "Release()" to revert this.
       if (!error)
@@ -127,7 +232,7 @@
         budget_->Acquire(size);   // may block; "DataSourceAnswer" CAN be destroyed here
       }
 
-      // Phase 3: Only now take a strong reference to "DataSourceAnswer".
+      // Phase 4: Only now take a strong reference to "DataSourceAnswer".
       {
         boost::shared_ptr<DataSourceAnswer> lock = answer_.lock();
 
@@ -156,13 +261,13 @@
   };
 
 
-  DataSourceReader::DataSourceReader(IExecutorService* executor /* takes ownership */,
+  DataSourceReader::DataSourceReader(const boost::shared_ptr<IExecutorService>& executor /* takes ownership */,
                                      IDataSource* source /* takes ownership */) :
     executor_(executor),
     source_(source),
     budget_(new Internals::DataSourceMemoryBudget(0))
   {
-    if (executor == NULL ||
+    if (executor.get() == NULL ||
         source == NULL)
     {
       throw OrthancException(ErrorCode_NullPointer);
@@ -184,10 +289,38 @@
     cache_ = boost::make_shared<SharedObjectCache>(capacity);
   }
 
+  void DataSourceReader::SetMetricsConfiguration(const MetricsConfiguration& configuration)
+  {
+    metricsConfiguration_ = configuration;
+    if (budget_.get())
+    {
+      budget_->SetMetricsConfiguration(Internals::DataSourceMemoryBudget::MetricsConfiguration(configuration.metrics_,
+                                                                                               configuration.capacityMaxSizeMegabytesName_,
+                                                                                               configuration.capacityCurrentSizeMegabytesName_,
+                                                                                               configuration.capacityCountName_,
+                                                                                               configuration.capacityMaxUsageSinceStartMegabytesName_));
+    }
+  }
 
-  void DataSourceReader::SetMaximumMemory(uint64_t maximumMemory)
+
+  void DataSourceReader::SetCapacity(uint64_t maximumMemory)
   {
     budget_ = boost::make_shared<Internals::DataSourceMemoryBudget>(maximumMemory);
+    budget_->SetMetricsConfiguration(Internals::DataSourceMemoryBudget::MetricsConfiguration(metricsConfiguration_.metrics_,
+                                                                                             metricsConfiguration_.capacityMaxSizeMegabytesName_,
+                                                                                             metricsConfiguration_.capacityCurrentSizeMegabytesName_,
+                                                                                             metricsConfiguration_.capacityCountName_,
+                                                                                             metricsConfiguration_.capacityMaxUsageSinceStartMegabytesName_));
+  }
+
+  uint64_t DataSourceReader::GetCapacity() const
+  {
+    uint64_t maximumMemory, currentMemory;
+    unsigned int currentReservations;
+
+    GetStatistics(maximumMemory, currentMemory, currentReservations);
+    
+    return maximumMemory;
   }
 
 
@@ -200,14 +333,14 @@
     while (!protection->IsEmpty())
     {
       std::unique_ptr<IDataIdentifier> identifier(protection->Dequeue());
-      executor_->Submit(new DataSourceRunnable(answer, *source_, identifier.release(), cache_, budget_));
+      executor_->Submit(new DataSourceRunnable(answer, *source_, identifier.release(), cache_, budget_, metricsConfiguration_));
     }
 
     return answer;
   }
 
 
-  boost::shared_ptr<IDynamicObject> DataSourceReader::ReadSingle(IDataIdentifier* id)
+  DataSourceAnswer::Item* DataSourceReader::ReadSingle(IDataIdentifier* id /* takes ownership */)
   {
     std::unique_ptr<IDataIdentifier> protection(id);
 
@@ -217,8 +350,52 @@
     boost::shared_ptr<DataSourceAnswer> answer(Submit(request.release()));
 
     std::unique_ptr<DataSourceAnswer::Item> item(answer->Dequeue());
+    assert(item != NULL);
     assert(answer->Dequeue() == NULL);
 
-    return item->GetValue();
+    return item.release();
+  }
+
+
+  void DataSourceReader::GetStatistics(uint64_t& tasksMaximumMemory,
+                                       uint64_t& tasksCurrentMemory,
+                                       unsigned int& tasksReservations) const
+  {
+    boost::shared_ptr<Internals::DataSourceMemoryBudget> budgetCopy(budget_);  // increment shared_ptr ref count to make sure the budget is not deleted while we use it.
+    budgetCopy->GetStatistics(tasksMaximumMemory, tasksCurrentMemory, tasksReservations);
+  }
+
+
+  size_t DataSourceReader::GetCacheCapacity() const
+  {
+    boost::shared_ptr<SharedObjectCache> lock(cache_);
+
+    if (lock)
+    {
+      return lock->GetCapacity();
+    }
+    else
+    {
+      return 0;
+    }
+  }
+
+
+  void DataSourceReader::StoreIntoCache(const std::string& key,
+                                        IDynamicObject* value /* takes ownership */)
+  {
+    boost::shared_ptr<IDynamicObject> protection(value);
+
+    if (value == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+    if (cache_)
+    {
+      size_t size = source_->GetValueSize(*value);
+      cache_->Store(key, protection, size);
+      metricsConfiguration_.SetCacheStatistics(*cache_);
+    }
   }
 }
--- a/OrthancFramework/Sources/DataSource/DataSourceReader.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/DataSource/DataSourceReader.h	Wed Jul 15 11:42:31 2026 +0200
@@ -34,6 +34,7 @@
 
 namespace Orthanc
 {
+  class MetricsRegistry;
   class SharedObjectCache;
 
   namespace Internals
@@ -43,36 +44,101 @@
 
   class ORTHANC_PUBLIC DataSourceReader : public boost::noncopyable
   {
+  public:
+    class MetricsConfiguration
+    {
+    protected:
+      boost::shared_ptr<MetricsRegistry>  metrics_;
+      std::string                         cacheSizeMegabytesName_;
+      std::string                         cacheCountName_;
+      std::string                         cacheHitCountName_;
+      std::string                         cacheMissCountName_;
+      std::string                         capacityMaxSizeMegabytesName_;
+      std::string                         capacityCurrentSizeMegabytesName_;
+      std::string                         capacityCountName_;
+      std::string                         capacityMaxUsageSinceStartMegabytesName_;
+
+    public:
+      MetricsConfiguration()
+      {
+      }
+
+      MetricsConfiguration(const boost::shared_ptr<MetricsRegistry>& metrics,
+                           const std::string& cacheSizeMegabytesName,
+                           const std::string& cacheCountName,
+                           const std::string& cacheHitCountName,
+                           const std::string& cacheMissCountName,
+                           const std::string& capacityMaxSizeMegabytesName,
+                           const std::string& capacityCurrentSizeMegabytesName,
+                           const std::string& capacityCountName,
+                           const std::string& capacityMaxUsageSinceStartMegabytesName);
+
+      void SetCacheStatistics(SharedObjectCache& cache);
+
+      void IncrementCacheHitCount();
+
+      void IncrementCacheMissCount();
+      
+      friend DataSourceReader;
+    };
+
   private:
     class DataSourceRunnable;
 
-    std::unique_ptr<IExecutorService>                     executor_;
+    boost::shared_ptr<IExecutorService>                   executor_;
     std::unique_ptr<IDataSource>                          source_;
+    size_t                                                cacheSize_;
     boost::shared_ptr<SharedObjectCache>                  cache_;
     boost::shared_ptr<Internals::DataSourceMemoryBudget>  budget_;
+    MetricsConfiguration                                  metricsConfiguration_;
 
   public:
-    DataSourceReader(IExecutorService* executor /* takes ownership */,
+    DataSourceReader(const boost::shared_ptr<IExecutorService>& executor,
                      IDataSource* source /* takes ownership */);
 
     ~DataSourceReader();
 
-    IDataSource& GetSource() const
+    const boost::shared_ptr<IExecutorService>& GetExecutorService() const
     {
-      return *source_;
+      return executor_;
     }
 
+    void SetMetricsConfiguration(const MetricsConfiguration& configuration);
+
     void CreateCache(size_t capacity);
 
-    void SetMaximumMemory(uint64_t maximumMemory);
+    /**
+     * Apply backpressure by limiting memory for pending read
+     * tasks. Further reads block until memory is released.
+     **/
+    void SetCapacity(uint64_t maximumMemory);
+
+    uint64_t GetCapacity() const;
 
+    /**
+     * Request the data source to load a set of items. The values will
+     * be read in parallel by a thread pool (cf. "executor_") and will
+     * be answered in no specific order through a message queue
+     * (cf. class "DataSourceAnswer"). The user data stored in
+     * "IDataIdentifier" can be used to identify items. If order is
+     * important, use the class "DataSourceSequentialReader" instead.
+     **/
     boost::shared_ptr<DataSourceAnswer> Submit(DataSourceRequest* request /* takes ownership */);
 
-    boost::shared_ptr<IDynamicObject> ReadSingle(IDataIdentifier* id /* takes ownership */);
+    DataSourceAnswer::Item* ReadSingle(IDataIdentifier* id /* takes ownership */);
 
     void Stop()
     {
       executor_->Stop();
     }
+
+    void GetStatistics(uint64_t& tasksMaximumMemory,
+                       uint64_t& tasksCurrentMemory,
+                       unsigned int& tasksReservations) const;
+
+    size_t GetCacheCapacity() const;
+
+    void StoreIntoCache(const std::string& key,
+                        IDynamicObject* value /* takes ownership */);
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DataSource/DataSourceSequentialReader.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -0,0 +1,336 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DataSourceSequentialReader.h"
+
+#include "DataSourceReader.h"
+#include "../OrthancException.h"
+
+namespace Orthanc
+{
+  DataSourceSequentialReader::Item::Item(IDynamicObject* value,
+                                         size_t estimatedSize) :
+    value_(value),
+    estimatedSize_(estimatedSize)
+  {
+    if (value == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+  }
+
+
+  DataSourceSequentialReader::Item::Item(const OrthancException& error) :
+    error_(new OrthancException(error)),
+    estimatedSize_(0)
+  {
+  }
+
+
+  const IDynamicObject& DataSourceSequentialReader::Item::GetValue() const
+  {
+    if (value_.get() != NULL)
+    {
+      assert(error_.get() == NULL);
+      return *value_;
+    }
+    else if (error_.get() != NULL)
+    {
+      throw *error_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void DataSourceSequentialReader::Item::SetUserData(IDynamicObject* userData)
+  {
+    if (userData == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+      userData_.reset(userData);
+    }
+  }
+
+
+  const IDynamicObject& DataSourceSequentialReader::Item::GetUserData() const
+  {
+    if (userData_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *userData_;
+    }
+  }
+
+
+  IDynamicObject* DataSourceSequentialReader::Item::ReleaseValue()
+  {
+    if (value_.get() != NULL)
+    {
+      assert(error_.get() == NULL);
+      return value_.release();
+    }
+    else if (error_.get() != NULL)
+    {
+      throw *error_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  IDynamicObject* DataSourceSequentialReader::Item::ReleaseUserData()
+  {
+    if (userData_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return userData_.release();
+    }
+  }
+
+
+  class DataSourceSequentialReader::Callable : public ICallable
+  {
+  private:
+    boost::shared_ptr<DataSourceReader>    reader_;
+    boost::shared_ptr<IValueDisconnector>  disconnector_;
+    std::unique_ptr<IDataIdentifier>       request_;
+    size_t                                 estimatedSize_;
+
+  public:
+    Callable(const boost::shared_ptr<DataSourceReader>& reader,
+             const boost::shared_ptr<IValueDisconnector>& disconnector,
+             IDataIdentifier* request,
+             size_t estimatedSize) :
+      reader_(reader),
+      disconnector_(disconnector),
+      request_(request),
+      estimatedSize_(estimatedSize)
+    {
+      if (reader.get() == NULL ||
+          disconnector.get() == NULL ||
+          request_ == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+    }
+
+    virtual IDynamicObject* Call() ORTHANC_OVERRIDE
+    {
+      std::unique_ptr<DataSourceAnswer::Item> answer(reader_->ReadSingle(request_.release()));
+
+      std::unique_ptr<IDynamicObject> userData;
+
+      if (answer->HasUserData())
+      {
+        userData.reset(answer->ReleaseUserData());
+      }
+
+      std::unique_ptr<Item> item;
+
+      try
+      {
+        std::unique_ptr<IDynamicObject> detached(disconnector_->Apply(answer.release()));
+        if (detached.get() == NULL)
+        {
+          throw OrthancException(ErrorCode_NullPointer);
+        }
+
+        item.reset(new Item(detached.release(), estimatedSize_));
+      }
+      catch (OrthancException& e)
+      {
+        item.reset(new Item(e));
+      }
+      catch (...)
+      {
+        item.reset(new Item(OrthancException(ErrorCode_InternalError)));
+      }
+
+      if (userData.get() != NULL)
+      {
+        item->SetUserData(userData.release());
+      }
+
+      return item.release();
+    }
+  };
+
+
+  void DataSourceSequentialReader::FillWindow()
+  {
+    while (!pendingRequests_.empty() &&
+           runningRequests_.size() < windowSize_)
+    {
+      size_t estimatedSize;
+      if (pendingRequests_.front()->EstimateValueSize(estimatedSize))
+      {
+        // Ensure that at least 1 callable is running even if too large for capacity
+        if (!runningRequests_.empty() &&
+            windowCapacity_ != 0 &&
+            runningSize_ + estimatedSize > windowCapacity_)
+        {
+          return;
+        }
+      }
+      else
+      {
+        estimatedSize = 0;
+      }
+
+      std::unique_ptr<IDataIdentifier> front(pendingRequests_.front());
+      pendingRequests_.pop_front();
+
+      runningSize_ += estimatedSize;
+
+      std::unique_ptr<ICallable> callable(new Callable(reader_, disconnector_, front.release(), estimatedSize));
+      runningRequests_.push_back(executor_->Submit(callable.release()));
+    }
+  }
+
+
+  DataSourceSequentialReader::DataSourceSequentialReader(const boost::shared_ptr<IExecutorService>& executor,
+                                                         const boost::shared_ptr<DataSourceReader>& reader,
+                                                         IValueDisconnector* disconnector /* takes ownership */,
+                                                         unsigned int windowSize,
+                                                         uint64_t windowCapacity) :
+    executor_(executor),
+    reader_(reader),
+    disconnector_(disconnector),
+    windowSize_(windowSize),
+    windowCapacity_(windowCapacity),
+    started_(false),
+    runningSize_(0)
+  {
+    if (executor.get() == NULL ||
+        reader.get() == NULL ||
+        disconnector == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+    if (windowSize == 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  DataSourceSequentialReader::~DataSourceSequentialReader()
+  {
+    for (PendingRequests::iterator it = pendingRequests_.begin(); it != pendingRequests_.end(); ++it)
+    {
+      assert(*it != NULL);
+      delete *it;
+    }
+
+    for (RunningRequests::iterator it = runningRequests_.begin(); it != runningRequests_.end(); ++it)
+    {
+      assert(*it != NULL);
+      delete *it;
+    }
+  }
+
+
+  void DataSourceSequentialReader::Submit(IDataIdentifier* request /* takes ownership */)
+  {
+    std::unique_ptr<IDataIdentifier> protection(request);
+
+    if (request == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else if (started_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      pendingRequests_.push_back(protection.release());
+    }
+  }
+
+
+  void DataSourceSequentialReader::Start()
+  {
+    if (started_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      started_ = true;
+      FillWindow();
+    }
+  }
+
+
+  bool DataSourceSequentialReader::HasNext() const
+  {
+    if (!started_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return !runningRequests_.empty();
+    }
+  }
+
+
+  DataSourceSequentialReader::Item* DataSourceSequentialReader::Next()
+  {
+    if (!started_ ||
+        !HasNext())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      std::unique_ptr<Future> future(runningRequests_.front());
+      runningRequests_.pop_front();
+
+      std::unique_ptr<Item> item(dynamic_cast<Item*>(future->ReleaseResult()));
+      runningSize_ -= item->GetEstimatedSize();
+
+      FillWindow();
+
+      return item.release();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DataSource/DataSourceSequentialReader.h	Wed Jul 15 11:42:31 2026 +0200
@@ -0,0 +1,143 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../MultiThreading/IExecutorService.h"
+#include "DataSourceAnswer.h"
+
+
+namespace Orthanc
+{
+  class DataSourceReader;
+  class OrthancException;
+
+  /**
+   * This class retrieves items from a data source in a deterministic
+   * order while still using parallel execution through a thread pool.
+   *
+   * Internally, it uses a sliding-window approach. Since items may
+   * complete out of order, the class buffers them until earlier items
+   * become available. This additional buffering is necessary because
+   * applying backpressure (as done in "DataSourceReader::Submit()")
+   * could otherwise lead to deadlock. If item ordering is not
+   * required, prefer "DataSourceReader::Submit()", which supports
+   * backpressure and avoids the extra memory overhead.
+   *
+   * Note that this class is not thread safe, it should be invoked
+   * from a single thread.
+   **/
+  class ORTHANC_PUBLIC DataSourceSequentialReader : public boost::noncopyable
+  {
+  public:
+    class ORTHANC_PUBLIC Item : public IDynamicObject
+    {
+    private:
+      std::unique_ptr<IDynamicObject>    value_;
+      std::unique_ptr<OrthancException>  error_;
+      std::unique_ptr<IDynamicObject>    userData_;
+      size_t                             estimatedSize_;
+
+    public:
+      Item(IDynamicObject* value,
+           size_t estimatedSize);
+
+      explicit Item(const OrthancException& error);
+
+      const IDynamicObject& GetValue() const;
+
+      void SetUserData(IDynamicObject* userData);
+
+      bool HasUserData() const
+      {
+        return userData_.get() != NULL;
+      }
+
+      const IDynamicObject& GetUserData() const;
+
+      size_t GetEstimatedSize() const
+      {
+        return estimatedSize_;
+      }
+
+      IDynamicObject* ReleaseValue();
+
+      IDynamicObject* ReleaseUserData();
+    };
+
+
+    /**
+     * Extracts the value from an answer item so that it can be
+     * buffered independently and is no longer subject to
+     * backpressure.
+     **/
+    class ORTHANC_PUBLIC IValueDisconnector : public boost::noncopyable
+    {
+    public:
+      virtual ~IValueDisconnector()
+      {
+      }
+
+      // Note that "source" will never contain user data, those are handled at the level above
+      virtual IDynamicObject* Apply(DataSourceAnswer::Item* source) = 0;
+    };
+
+
+  private:
+    class Callable;
+
+    typedef std::list<IDataIdentifier*>  PendingRequests;
+    typedef std::list<Future*>           RunningRequests;
+
+    boost::shared_ptr<IExecutorService>    executor_;
+    boost::shared_ptr<DataSourceReader>    reader_;
+    boost::shared_ptr<IValueDisconnector>  disconnector_;
+    unsigned int                           windowSize_;
+    uint64_t                               windowCapacity_;
+
+    bool             started_;
+    PendingRequests  pendingRequests_;
+    RunningRequests  runningRequests_;
+    uint64_t         runningSize_;
+
+    void FillWindow();
+
+  public:
+    DataSourceSequentialReader(const boost::shared_ptr<IExecutorService>& executor,
+                               const boost::shared_ptr<DataSourceReader>& reader,
+                               IValueDisconnector* disconnector /* takes ownership */,
+                               unsigned int windowSize /* number of elements in the sliding window */,
+                               uint64_t windowCapacity /* if 0, memory usage is only controlled by the size of the sliding window */);
+
+    ~DataSourceSequentialReader();
+
+    void Submit(IDataIdentifier* request /* takes ownership */);
+
+    void Start();
+
+    bool HasNext() const;
+
+    Item* Next();
+  };
+}
--- a/OrthancFramework/Sources/DataSource/DicomDataSource.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/DataSource/DicomDataSource.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -26,7 +26,9 @@
 #include "DicomDataSource.h"
 
 #include "../DicomParsing/ParsedDicomFile.h"
+#include "../Logging.h"
 #include "../OrthancException.h"
+#include "BaseDataIdentifier.h"
 #include "DataSourceReader.h"
 #include "StorageAreaDataSource.h"
 
@@ -35,81 +37,60 @@
 
 namespace Orthanc
 {
-  static size_t ComputeValueSize(size_t size,
-                                 bool isKeepRawBuffer)
-  {
-    if (isKeepRawBuffer)
-    {
-      uint64_t s = 2 * static_cast<uint64_t>(size);
-      if (s != static_cast<uint64_t>(static_cast<size_t>(s)))
-      {
-        return std::numeric_limits<size_t>::max();
-      }
-      else
-      {
-        return static_cast<size_t>(s);
-      }
-    }
-    else
-    {
-      return size;
-    }
-  }
-
-
-  class DicomDataSource::BaseIdentifier : public IDataIdentifier
+  class DicomDataSource::Identifier : public BaseDataIdentifier
   {
   public:
     virtual StorageAreaDataSource::Range* ReadRange(DataSourceReader& reader) const = 0;
 
-    virtual bool IsKeepRawBuffer() const = 0;
+    virtual const FileInfo& GetAttachment() const = 0;
+
+    virtual bool IsUntilPixelData() const = 0;
   };
 
 
-  class DicomDataSource::WholeIdentifier : public DicomDataSource::BaseIdentifier
+  class DicomDataSource::WholeIdentifier : public DicomDataSource::Identifier
   {
   private:
     FileInfo  attachment_;
-    bool      keepRawBuffer_;
-    bool      checkMD5_;
 
   public:
-    explicit WholeIdentifier(const FileInfo& attachment,
-                             bool keepRawBuffer,
-                             bool checkMD5) :
-      attachment_(attachment),
-      keepRawBuffer_(keepRawBuffer),
-      checkMD5_(checkMD5)
+    explicit WholeIdentifier(const FileInfo& attachment) :
+      attachment_(attachment)
     {
     }
 
     virtual bool GetCacheKey(std::string& key) const ORTHANC_OVERRIDE
     {
       key = (attachment_.GetUuid() + "|" +
-             boost::lexical_cast<std::string>(attachment_.GetContentType()) + "|" +
-             boost::lexical_cast<std::string>(keepRawBuffer_));
+             boost::lexical_cast<std::string>(attachment_.GetContentType()));
       return true;
     }
 
     virtual bool EstimateValueSize(size_t& target) const ORTHANC_OVERRIDE
     {
-      target = ComputeValueSize(attachment_.GetUncompressedSize(), keepRawBuffer_);
+      target = attachment_.GetUncompressedSize();
       return true;
     }
 
     virtual StorageAreaDataSource::Range* ReadRange(DataSourceReader& reader) const ORTHANC_OVERRIDE
     {
-      return StorageAreaDataSource::ReadAttachment(reader, attachment_, true /* uncompress */, checkMD5_);
+      return StorageAreaDataSource::Execute(
+        reader, StorageAreaDataSource::CreateAttachmentRequest(attachment_, true /* uncompress */));
     }
 
-    virtual bool IsKeepRawBuffer() const ORTHANC_OVERRIDE
+    virtual const FileInfo& GetAttachment() const ORTHANC_OVERRIDE
     {
-      return keepRawBuffer_;
+      return attachment_;
+    }
+
+    virtual bool IsUntilPixelData() const ORTHANC_OVERRIDE
+    {
+      return false;
     }
   };
 
 
-  class DicomDataSource::BeginningIdentifier : public DicomDataSource::BaseIdentifier
+  class DicomDataSource::BeginningIdentifier : public DicomDataSource::Identifier
   {
   private:
     FileInfo  attachment_;
@@ -141,12 +122,17 @@
 
     virtual StorageAreaDataSource::Range* ReadRange(DataSourceReader& reader) const ORTHANC_OVERRIDE
     {
-      return StorageAreaDataSource::ReadBeginning(reader, attachment_, pixelDataOffset_);
+      return StorageAreaDataSource::Execute(reader, StorageAreaDataSource::CreateBeginningRequest(attachment_, pixelDataOffset_));
     }
 
-    virtual bool IsKeepRawBuffer() const ORTHANC_OVERRIDE
+    virtual const FileInfo& GetAttachment() const ORTHANC_OVERRIDE
     {
-      return false;
+      return attachment_;
+    }
+
+    virtual bool IsUntilPixelData() const ORTHANC_OVERRIDE
+    {
+      return true;
     }
   };
 
@@ -154,17 +140,15 @@
   class DicomDataSource::Value : public IDynamicObject
   {
   private:
-    std::unique_ptr<ParsedDicomFile>   dicom_;
-    size_t                             size_;
-    bool                               hasRawBuffer_;
-    std::string                        rawBuffer_;
+    Mutex                             mutex_;
+    std::unique_ptr<ParsedDicomFile>  dicom_;
+    size_t                            size_;
 
   public:
     Value(ParsedDicomFile* dicom,
           size_t size) :
       dicom_(dicom),
-      size_(size),
-      hasRawBuffer_(false)
+      size_(size)
     {
       if (dicom == NULL)
       {
@@ -172,50 +156,19 @@
       }
     }
 
-    ParsedDicomFile& GetContent() const
-    {
-      return *dicom_;
-    }
-
-    size_t GetRawBufferSize() const
+    size_t GetSize() const
     {
       return size_;
     }
 
-    void SetRawBuffer(const void* data,
-                      size_t size)
+    Mutex::ScopedLock* Lock()
     {
-      if (hasRawBuffer_)
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-      else if (size != size_)
-      {
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-      else
-      {
-        hasRawBuffer_ = true;
-        rawBuffer_.assign(reinterpret_cast<const char*>(data), size);
-      }
+      return new Mutex::ScopedLock(mutex_);
     }
 
-    bool HasRawBuffer() const
-    {
-      return hasRawBuffer_;
-    }
-
-    const void* GetRawBufferData() const
+    ParsedDicomFile& GetContent() const
     {
-      if (hasRawBuffer_)
-      {
-        assert(rawBuffer_.size() == size_);
-        return rawBuffer_.empty() ? NULL : rawBuffer_.c_str();
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
+      return *dicom_;
     }
   };
 
@@ -230,9 +183,10 @@
   }
 
 
-  IDynamicObject* DicomDataSource::Load(const IDataIdentifier& obj)
+  IDynamicObject* DicomDataSource::Load(const IDataIdentifier& obj,
+                                        const boost::shared_ptr<SharedObjectCache>& readerCache)
   {
-    const BaseIdentifier& id = dynamic_cast<const BaseIdentifier&>(obj);
+    const Identifier& id = dynamic_cast<const Identifier&>(obj);
 
     std::unique_ptr<StorageAreaDataSource::Range> range(id.ReadRange(*storageAreaReader_));
 
@@ -240,30 +194,19 @@
 
     {
       // Sanity check
-      bool ok = false;
       size_t estimatedSize;
-      if (obj.EstimateValueSize(estimatedSize))
-      {
-        ok = (estimatedSize == ComputeValueSize(size, id.IsKeepRawBuffer()));
-      }
-
-      if (!ok)
+      if (!obj.EstimateValueSize(estimatedSize) ||
+          estimatedSize != size)
       {
         THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_InternalError);
       }
     }
 
+    LOG(INFO) << "Parsing DICOM attachment"
+              << (id.IsUntilPixelData() ? " (until pixel data): " : ": ")
+              << id.GetAttachment().GetUuid();
     std::unique_ptr<Value> value(new Value(new ParsedDicomFile(range->GetData(), size), size));
-
-    if (id.IsKeepRawBuffer())
-    {
-      value->SetRawBuffer(range->GetData(), size);
-      assert(GetValueSize(*value) == 2 * size);
-    }
-    else
-    {
-      assert(GetValueSize(*value) == size);
-    }
+    assert(GetValueSize(*value) == size);
 
     return value.release();
   }
@@ -272,66 +215,77 @@
   size_t DicomDataSource::GetValueSize(const IDynamicObject& obj) const
   {
     const Value& value = dynamic_cast<const Value&>(obj);
-    return ComputeValueSize(value.GetRawBufferSize(), value.HasRawBuffer());
+    return value.GetSize();
   }
 
 
-  DicomDataSource::Dicom::Dicom(const boost::shared_ptr<IDynamicObject>& value) :
-    value_(value)
+  DicomDataSource::Dicom::Dicom(DataSourceAnswer::Item* item) :
+    item_(item)
   {
-    if (value.get() == NULL)
+    if (item == NULL)
     {
       throw OrthancException(ErrorCode_NullPointer);
     }
   }
 
 
-  ParsedDicomFile& DicomDataSource::Dicom::Lock::GetContent() const
+  ParsedDicomFile* DicomDataSource::Dicom::Clone()
   {
-    return dynamic_cast<const Value&>(*that_.value_).GetContent();
+    Lock lock(*this);
+    return lock.GetContent().Clone(true);
   }
 
 
-  bool DicomDataSource::Dicom::Lock::HasRawBuffer() const
+  DicomDataSource::Dicom::Lock::Lock(Dicom& that):
+    that_(that)
   {
-    return dynamic_cast<const Value&>(*that_.value_).HasRawBuffer();
-  }
-
-
-  const void* DicomDataSource::Dicom::Lock::GetRawBufferData() const
-  {
-    return dynamic_cast<const Value&>(*that_.value_).GetRawBufferData();
+    Value& value = dynamic_cast<Value&>(*that_.item_->GetValue());
+    lock_.reset(value.Lock());
   }
 
 
-  size_t DicomDataSource::Dicom::Lock::GetRawBufferSize() const
+  ParsedDicomFile& DicomDataSource::Dicom::Lock::GetContent() const
   {
-    return dynamic_cast<const Value&>(*that_.value_).GetRawBufferSize();
+    const Value& value = dynamic_cast<const Value&>(*that_.item_->GetValue());
+    return value.GetContent();
   }
 
 
-  DicomDataSource::Dicom* DicomDataSource::ReadWhole(DataSourceReader& reader,
-                                                     const FileInfo& attachment,
-                                                     bool keepRawBuffer,
-                                                     bool checkMD5)
+  IDataIdentifier* DicomDataSource::CreateWholeRequest(const FileInfo& attachment)
   {
-    std::unique_ptr<IDataIdentifier> id(new WholeIdentifier(attachment, keepRawBuffer, checkMD5));
-    boost::shared_ptr<IDynamicObject> value = reader.ReadSingle(id.release());
-    return new Dicom(value);
+    return new WholeIdentifier(attachment);
   }
 
 
-  DicomDataSource::Dicom* DicomDataSource::ReadUntilPixelData(DataSourceReader& reader,
-                                                              const FileInfo& attachment,
+  IDataIdentifier* DicomDataSource::CreateUntilPixelDataRequest(const FileInfo& attachment,
                                                               uint64_t pixelDataOffset)
   {
     if (static_cast<uint64_t>(static_cast<size_t>(pixelDataOffset)) != pixelDataOffset)
     {
       throw OrthancException(ErrorCode_NotEnoughMemory);
     }
+    else if (attachment.GetCompressionType() != CompressionType_None)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+    else
+    {
+      return new BeginningIdentifier(attachment, static_cast<size_t>(pixelDataOffset));
+    }
+  }
 
-    std::unique_ptr<IDataIdentifier> id(new BeginningIdentifier(attachment, static_cast<size_t>(pixelDataOffset)));
-    boost::shared_ptr<IDynamicObject> value = reader.ReadSingle(id.release());
-    return new Dicom(value);
+
+  DicomDataSource::Dicom* DicomDataSource::Execute(DataSourceReader& reader,
+                                                   IDataIdentifier* request)
+  {
+    if (request == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+      std::unique_ptr<DataSourceAnswer::Item> item(reader.ReadSingle(request));
+      return new Dicom(item.release());
+    }
   }
 }
--- a/OrthancFramework/Sources/DataSource/DicomDataSource.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/DataSource/DicomDataSource.h	Wed Jul 15 11:42:31 2026 +0200
@@ -35,6 +35,7 @@
 #include "../Compatibility.h"
 #include "../FileStorage/FileInfo.h"
 #include "../MultiThreading/Mutex.h"
+#include "DataSourceAnswer.h"
 #include "IDataIdentifier.h"
 #include "IDataSource.h"
 
@@ -46,10 +47,10 @@
   class DataSourceReader;
   class ParsedDicomFile;
 
-  class DicomDataSource : public IDataSource
+  class ORTHANC_PUBLIC DicomDataSource : public IDataSource
   {
   private:
-    class BaseIdentifier;
+    class Identifier;
     class WholeIdentifier;
     class BeginningIdentifier;
     class Value;
@@ -59,54 +60,45 @@
   public:
     explicit DicomDataSource(const boost::shared_ptr<DataSourceReader>& storageAreaReader);
 
-    virtual IDynamicObject* Load(const IDataIdentifier& obj) ORTHANC_OVERRIDE;
+    virtual IDynamicObject* Load(const IDataIdentifier& obj,
+                                 const boost::shared_ptr<SharedObjectCache>& readerCache /* could be NULL */) ORTHANC_OVERRIDE;
 
     virtual size_t GetValueSize(const IDynamicObject& obj) const ORTHANC_OVERRIDE;
 
-    class Dicom : public boost::noncopyable
+    class ORTHANC_PUBLIC Dicom : public boost::noncopyable
     {
     private:
-      Mutex                              mutex_;
-      boost::shared_ptr<IDynamicObject>  value_;
+      std::unique_ptr<DataSourceAnswer::Item>  item_;   // Holding item puts backpressure on the data source
 
     public:
-      explicit Dicom(const boost::shared_ptr<IDynamicObject>& value);
+      explicit Dicom(DataSourceAnswer::Item* item /* takes ownership */);
+
+      ParsedDicomFile* Clone();
 
       /**
        * Access to the DICOM value must be protected by a mutex, as it
        * could be shared by multiple threads if caching is enabled in
        * the DataSourceReader.
        **/
-      class Lock : public boost::noncopyable
+      class ORTHANC_PUBLIC Lock : public boost::noncopyable
       {
       private:
-        Dicom&             that_;
-        Mutex::ScopedLock  lock_;
+        Dicom&                              that_;
+        std::unique_ptr<Mutex::ScopedLock>  lock_;
 
       public:
-        explicit Lock(Dicom& that) :
-          that_(that),
-          lock_(that.mutex_)
-        {
-        }
+        explicit Lock(Dicom& that);
 
         ParsedDicomFile& GetContent() const;
-
-        bool HasRawBuffer() const;
-
-        const void* GetRawBufferData() const;
-
-        size_t GetRawBufferSize() const;
       };
     };
 
-    static Dicom* ReadWhole(DataSourceReader& reader,
-                            const FileInfo& attachment,
-                            bool keepRawBuffer,  // Must be set to "true" for transcoding
-                            bool checkMD5);
+    static IDataIdentifier* CreateWholeRequest(const FileInfo& attachment);
 
-    static Dicom* ReadUntilPixelData(DataSourceReader& reader,
-                                     const FileInfo& attachment,
-                                     uint64_t pixelDataOffset);
+    static IDataIdentifier* CreateUntilPixelDataRequest(const FileInfo& attachment,
+                                                        uint64_t pixelDataOffset);
+
+    static Dicom* Execute(DataSourceReader& reader,
+                          IDataIdentifier* request /* takes ownership */);
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DataSource/DicomSequentialReader.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -0,0 +1,345 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DicomSequentialReader.h"
+
+#include "../DicomParsing/ParsedDicomFile.h"
+#include "../OrthancException.h"
+#include "../StringMemoryBuffer.h"
+#include "BaseDataIdentifier.h"
+#include "DataSourceReader.h"
+#include "DataSourceSequentialReader.h"
+#include "DicomDataSource.h"
+#include "StorageAreaDataSource.h"
+#include "TranscoderDataSource.h"
+
+
+namespace Orthanc
+{
+  DicomSequentialReader::Item::Item(ParsedDicomFile* parsed) :
+    parsed_(parsed)
+  {
+    if (parsed == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+  }
+
+
+  DicomSequentialReader::Item::Item(const boost::shared_ptr<IMemoryBuffer>& raw) :
+    raw_(raw)
+  {
+    if (raw.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+  }
+
+
+  DicomSequentialReader::Item::Item(ParsedDicomFile* parsed /* takes ownership */,
+                                    IMemoryBuffer* raw /* takes ownership */) :
+    parsed_(parsed),
+    raw_(raw)
+  {
+    if (parsed == NULL ||
+        raw == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+  }
+
+
+  ParsedDicomFile& DicomSequentialReader::Item::GetParsedDicomFile()
+  {
+    if (parsed_.get() != NULL)
+    {
+      return *parsed_;
+    }
+    else if (raw_.get() != NULL)
+    {
+      parsed_.reset(new ParsedDicomFile(raw_->GetData(), raw_->GetSize()));
+      return *parsed_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  const IMemoryBuffer& DicomSequentialReader::Item::GetRawMemoryBuffer()
+  {
+    if (raw_.get() != NULL)
+    {
+      return *raw_;
+    }
+    else if (parsed_.get() != NULL)
+    {
+      std::string s;
+      parsed_->SaveToMemoryBuffer(s);
+
+      raw_.reset(StringMemoryBuffer::CreateFromSwap(s));
+      return *raw_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  void DicomSequentialReader::Item::SetUserData(IDynamicObject* userData /* takes ownership */)
+  {
+    if (userData == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+      userData_.reset(userData);
+    }
+  }
+
+
+  const IDynamicObject& DicomSequentialReader::Item::GetUserData() const
+  {
+    if (userData_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *userData_;
+    }
+  }
+
+
+  class DicomSequentialReader::Disconnector : public DataSourceSequentialReader::IValueDisconnector
+  {
+  private:
+    SourceType  sourceType_;
+
+  public:
+    explicit Disconnector(SourceType sourceType) :
+      sourceType_(sourceType)
+    {
+    }
+
+    virtual IDynamicObject* Apply(DataSourceAnswer::Item* source) ORTHANC_OVERRIDE
+    {
+      std::unique_ptr<DataSourceAnswer::Item> protection(source);
+
+      switch (sourceType_)
+      {
+        case SourceType_StorageArea:
+        {
+          std::unique_ptr<StorageAreaDataSource::Range> range(new StorageAreaDataSource::Range(protection.release()));
+          return new Item(range->GetBuffer());  // The buffer is shared through the cache
+        }
+
+        case SourceType_Dicom:
+        {
+          std::unique_ptr<DicomDataSource::Dicom> dicom(new DicomDataSource::Dicom(protection.release()));
+          return new Item(dicom->Clone());
+        }
+
+        case SourceType_Transcoder:
+        {
+          std::unique_ptr<TranscoderDataSource::Transcoded> dicom(new TranscoderDataSource::Transcoded(protection.release()));
+          TranscoderDataSource::Transcoded::LockAsParsed lock(*dicom);
+          return new Item(lock.GetContent().Clone(true));
+        }
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+  };
+
+
+  DicomSequentialReader::DicomSequentialReader(SourceType sourceType,
+                                               const boost::shared_ptr<IExecutorService>& executor,
+                                               DicomTransferSyntax targetSyntax,
+                                               TranscodingSopInstanceUidMode mode,
+                                               bool hasLossyQuality,
+                                               unsigned int lossyQuality,
+                                               const boost::shared_ptr<DataSourceReader>& reader,
+                                               unsigned int windowSize,
+                                               uint64_t windowCapacity) :
+    sourceType_(sourceType),
+    targetSyntax_(targetSyntax),
+    mode_(mode),
+    hasLossyQuality_(hasLossyQuality),
+    lossyQuality_(lossyQuality),
+    reader_(new DataSourceSequentialReader(executor, reader, new Disconnector(sourceType), windowSize, windowCapacity))
+  {
+  }
+
+
+  void DicomSequentialReader::SubmitInternal(const FileInfo& attachment,
+                                             IDynamicObject* userData)
+  {
+    std::unique_ptr<IDynamicObject> protection(userData);
+
+    std::unique_ptr<IDataIdentifier> id;
+
+    switch (sourceType_)
+    {
+      case SourceType_StorageArea:
+        id.reset(StorageAreaDataSource::CreateAttachmentRequest(attachment, true /* uncompress */));
+        break;
+
+      case SourceType_Dicom:
+        id.reset(DicomDataSource::CreateWholeRequest(attachment));
+        break;
+
+      case SourceType_Transcoder:
+        id.reset(TranscoderDataSource::CreateRequest(attachment, targetSyntax_, mode_, hasLossyQuality_, lossyQuality_));
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    if (protection.get() != NULL)
+    {
+      dynamic_cast<BaseDataIdentifier&>(*id).SetUserData(protection.release());
+    }
+
+    reader_->Submit(id.release());
+  }
+
+
+  void DicomSequentialReader::Submit(const FileInfo& attachment)
+  {
+    SubmitInternal(attachment, NULL);
+  }
+
+
+  void DicomSequentialReader::Submit(const FileInfo& attachment,
+                                     IDynamicObject* userData)
+  {
+    if (userData == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+    SubmitInternal(attachment, userData);
+  }
+
+
+  void DicomSequentialReader::Start()
+  {
+    reader_->Start();
+  }
+
+
+  bool DicomSequentialReader::HasNext()
+  {
+    return reader_->HasNext();
+  }
+
+
+  DicomSequentialReader::Item* DicomSequentialReader::Next()
+  {
+    std::unique_ptr<DataSourceSequentialReader::Item> source(reader_->Next());
+
+    std::unique_ptr<Item> target(dynamic_cast<Item*>(source->ReleaseValue()));
+
+    if (source->HasUserData())
+    {
+      target->SetUserData(source->ReleaseUserData());
+    }
+
+    return target.release();
+  }
+
+
+  DicomSequentialReader::Factory::Factory(const boost::shared_ptr<IExecutorService>& executor,
+                                          const boost::shared_ptr<DataSourceReader>& storageAreaReader,
+                                          const boost::shared_ptr<DataSourceReader>& dicomReader,
+                                          const boost::shared_ptr<DataSourceReader>& transcoderReader,
+                                          unsigned int windowSize,
+                                          uint64_t windowCapacity) :
+    executor_(executor),
+    storageAreaReader_(storageAreaReader),
+    dicomReader_(dicomReader),
+    transcoderReader_(transcoderReader),
+    windowSize_(windowSize),
+    windowCapacity_(windowCapacity)
+  {
+    if (!executor)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+    if (windowSize == 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  DicomSequentialReader* DicomSequentialReader::Factory::CreateForOriginalRawMemoryBuffer() const
+  {
+    return new DicomSequentialReader(
+      DicomSequentialReader::SourceType_StorageArea, executor_,
+      DicomTransferSyntax_LittleEndianImplicit, TranscodingSopInstanceUidMode_AllowNew,  /* dummy values */
+      false /* no lossy */, 0 /* unused lossy quality */,
+      storageAreaReader_, windowSize_, windowCapacity_);
+  }
+
+
+  DicomSequentialReader* DicomSequentialReader::Factory::CreateForTranscodedRawMemoryBuffer(DicomTransferSyntax targetSyntax,
+                                                                                            TranscodingSopInstanceUidMode mode,
+                                                                                            bool hasLossyQuality,
+                                                                                            unsigned int lossyQuality) const
+  {
+    // No optimization seems possible, we need to go through the transcoder anyway
+    return CreateForTranscodedParsedDicomFile(targetSyntax, mode, hasLossyQuality, lossyQuality);
+  }
+
+
+  DicomSequentialReader* DicomSequentialReader::Factory::CreateForOriginalParsedDicomFile() const
+  {
+    return new DicomSequentialReader(
+      DicomSequentialReader::SourceType_Dicom, executor_,
+      DicomTransferSyntax_LittleEndianImplicit, TranscodingSopInstanceUidMode_AllowNew,  /* dummy values */
+      false /* no lossy */, 0 /* unused lossy quality */,
+      dicomReader_, windowSize_, windowCapacity_);
+  }
+
+
+  DicomSequentialReader* DicomSequentialReader::Factory::CreateForTranscodedParsedDicomFile(DicomTransferSyntax targetSyntax,
+                                                                                            TranscodingSopInstanceUidMode mode,
+                                                                                            bool hasLossyQuality,
+                                                                                            unsigned int lossyQuality) const
+  {
+    return new DicomSequentialReader(
+      DicomSequentialReader::SourceType_Transcoder, executor_,
+      targetSyntax, mode, hasLossyQuality, lossyQuality,
+      transcoderReader_, windowSize_, windowCapacity_);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DataSource/DicomSequentialReader.h	Wed Jul 15 11:42:31 2026 +0200
@@ -0,0 +1,159 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Compatibility.h"
+#include "../FileStorage/FileInfo.h"
+#include "../IMemoryBuffer.h"
+#include "../MultiThreading/IExecutorService.h"
+
+
+namespace Orthanc
+{
+  class DataSourceReader;
+  class DataSourceSequentialReader;
+  class ParsedDicomFile;
+
+  class ORTHANC_PUBLIC DicomSequentialReader : public boost::noncopyable
+  {
+  public:
+    class ORTHANC_PUBLIC Item : public IDynamicObject
+    {
+    private:
+      std::unique_ptr<ParsedDicomFile>  parsed_;
+      boost::shared_ptr<IMemoryBuffer>  raw_;
+      std::unique_ptr<IDynamicObject>   userData_;
+
+    public:
+      explicit Item(ParsedDicomFile* parsed /* takes ownership */);
+
+      explicit Item(const boost::shared_ptr<IMemoryBuffer>& raw);
+
+      Item(ParsedDicomFile* parsed /* takes ownership */,
+           IMemoryBuffer* raw /* takes ownership */);
+
+      ParsedDicomFile& GetParsedDicomFile();
+
+      // There is a "const" here, as the value could be shared through the cache, so it cannot be modified
+      const IMemoryBuffer& GetRawMemoryBuffer();
+
+      void SetUserData(IDynamicObject* userData /* takes ownership */);
+
+      bool HasUserData() const
+      {
+        return userData_.get() != NULL;
+      }
+
+      const IDynamicObject& GetUserData() const;
+    };
+
+  private:
+    class Disconnector;
+
+    enum SourceType
+    {
+      SourceType_StorageArea,
+      SourceType_Dicom,
+      SourceType_Transcoder
+    };
+
+    SourceType                                     sourceType_;
+    DicomTransferSyntax                            targetSyntax_;
+    TranscodingSopInstanceUidMode                  mode_;
+    bool                                           hasLossyQuality_;
+    unsigned int                                   lossyQuality_;
+    boost::shared_ptr<DataSourceSequentialReader>  reader_;  // boost::shared_ptr<> for PImpl
+
+    DicomSequentialReader(SourceType sourceType,
+                          const boost::shared_ptr<IExecutorService>& executor,
+                          DicomTransferSyntax targetSyntax,
+                          TranscodingSopInstanceUidMode mode,
+                          bool hasLossyQuality,
+                          unsigned int lossyQuality,
+                          const boost::shared_ptr<DataSourceReader>& reader,
+                          unsigned int windowSize,
+                          uint64_t windowCapacity);
+
+    void SubmitInternal(const FileInfo& attachment,
+                        IDynamicObject* userData);
+
+  public:
+    // The factory can be called from multiple threads
+    class ORTHANC_PUBLIC Factory : public boost::noncopyable
+    {
+    private:
+      boost::shared_ptr<IExecutorService>  executor_;
+      boost::shared_ptr<DataSourceReader>  storageAreaReader_;
+      boost::shared_ptr<DataSourceReader>  dicomReader_;
+      boost::shared_ptr<DataSourceReader>  transcoderReader_;
+      unsigned int                         windowSize_;
+      uint64_t                             windowCapacity_;
+
+    public:
+      // The readers are allowed to be NULL, for unit tests
+      Factory(const boost::shared_ptr<IExecutorService>& executor,
+              const boost::shared_ptr<DataSourceReader>& storageAreaReader,
+              const boost::shared_ptr<DataSourceReader>& dicomReader,
+              const boost::shared_ptr<DataSourceReader>& transcoderReader,
+              unsigned int windowSize,
+              uint64_t windowCapacity);
+
+      /**
+       * Methods below prioritize the downloading of the raw DICOM
+       * files (and avoid DICOM parsing if possible).
+       **/
+
+      DicomSequentialReader* CreateForOriginalRawMemoryBuffer() const;
+
+      DicomSequentialReader* CreateForTranscodedRawMemoryBuffer(DicomTransferSyntax targetSyntax,
+                                                                TranscodingSopInstanceUidMode mode,
+                                                                bool hasLossyQuality,
+                                                                unsigned int lossyQuality) const;
+
+      /**
+       * Methods below prioritize the parsing of the DICOM files (and
+       * exploit the DICOM cache if possible).
+       **/
+
+      DicomSequentialReader* CreateForOriginalParsedDicomFile() const;
+
+      DicomSequentialReader* CreateForTranscodedParsedDicomFile(DicomTransferSyntax targetSyntax,
+                                                                TranscodingSopInstanceUidMode mode,
+                                                                bool hasLossyQuality,
+                                                                unsigned int lossyQuality) const;
+    };
+
+    void Submit(const FileInfo& attachment);
+
+    void Submit(const FileInfo& attachment,
+                IDynamicObject* userData /* takes ownership */);
+
+    void Start();
+
+    bool HasNext();
+
+    Item* Next();
+  };
+}
--- a/OrthancFramework/Sources/DataSource/IDataIdentifier.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/DataSource/IDataIdentifier.h	Wed Jul 15 11:42:31 2026 +0200
@@ -24,7 +24,7 @@
 
 #pragma once
 
-#include "../OrthancFramework.h"
+#include "../IDynamicObject.h"
 
 #include <boost/noncopyable.hpp>
 
@@ -42,5 +42,11 @@
     virtual bool GetCacheKey(std::string& key) const = 0;
 
     virtual bool EstimateValueSize(size_t& target) const = 0;
+
+    virtual bool HasUserData() const = 0;
+
+    virtual const IDynamicObject& GetUserData() const = 0;
+
+    virtual IDynamicObject* ReleaseUserData() = 0;
   };
 }
--- a/OrthancFramework/Sources/DataSource/IDataSource.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/DataSource/IDataSource.h	Wed Jul 15 11:42:31 2026 +0200
@@ -27,8 +27,13 @@
 #include "../IDynamicObject.h"
 #include "IDataIdentifier.h"
 
+#include <boost/shared_ptr.hpp>
+
+
 namespace Orthanc
 {
+  class SharedObjectCache;
+
   class ORTHANC_PUBLIC IDataSource : public boost::noncopyable
   {
   public:
@@ -36,7 +41,8 @@
     {
     }
 
-    virtual IDynamicObject* Load(const IDataIdentifier& identifier) = 0;
+    virtual IDynamicObject* Load(const IDataIdentifier& identifier,
+                                 const boost::shared_ptr<SharedObjectCache>& readerCache /* could be NULL */) = 0;
 
     virtual size_t GetValueSize(const IDynamicObject& value) const = 0;
   };
--- a/OrthancFramework/Sources/DataSource/StorageAreaDataSource.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/DataSource/StorageAreaDataSource.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -25,9 +25,13 @@
 #include "../PrecompiledHeaders.h"
 #include "StorageAreaDataSource.h"
 
+#include "../Cache/SharedObjectCache.h"
+#include "../Logging.h"
+#include "../MetricsRegistry.h"
 #include "../OrthancException.h"
 #include "../StringMemoryBuffer.h"
 #include "../Toolbox.h"
+#include "BaseDataIdentifier.h"
 #include "DataSourceReader.h"
 
 #if ORTHANC_ENABLE_ZLIB == 1
@@ -37,16 +41,40 @@
 #include <boost/lexical_cast.hpp>
 #include <cassert>
 
+
 namespace Orthanc
 {
+  static std::string ComputeCacheKey(std::string uuid,
+                                     FileContentType type,
+                                     uint64_t start,
+                                     uint64_t end)
+  {
+    // custom data is not part of the cache key, as it is for internal use by the plugin
+    // (it is opaque to the Orthanc core)
+    return (uuid + "|" +
+            boost::lexical_cast<std::string>(type) + "|" +
+            boost::lexical_cast<std::string>(start) + "|" +
+            boost::lexical_cast<std::string>(end));
+  }
+
+
+  static std::string ComputeCacheKeyForWholeAttachment(const FileInfo& attachment)
+  {
+    return ComputeCacheKey(attachment.GetUuid(), attachment.GetContentType(), 0, attachment.GetCompressedSize());
+  }
+
+
   class StorageAreaDataSource::Value : public IDynamicObject
   {
   private:
-    std::unique_ptr<IMemoryBuffer>  buffer_;
+    boost::shared_ptr<IMemoryBuffer>  buffer_;
+    bool                              checkMD5_;
 
   public:
-    explicit Value(IMemoryBuffer* buffer) :
-      buffer_(buffer)
+    explicit Value(IMemoryBuffer* buffer,
+                   bool checkMD5) :
+      buffer_(buffer),
+      checkMD5_(checkMD5)
     {
       if (buffer == NULL)
       {
@@ -54,22 +82,57 @@
       }
     }
 
-    const IMemoryBuffer& GetBuffer() const
+    const boost::shared_ptr<IMemoryBuffer>& GetBuffer() const
     {
       assert(buffer_.get() != NULL);
-      return *buffer_;
+      return buffer_;
+    }
+
+    bool IsCheckMD5() const
+    {
+      return checkMD5_;
+    }
+
+    void ExtractRange(std::string& target,
+                      uint64_t start,
+                      uint64_t end) const
+    {
+      if (start >= buffer_->GetSize() ||
+          end > buffer_->GetSize())
+      {
+        throw OrthancException(ErrorCode_BadRange);
+      }
+      else
+      {
+        target.assign(reinterpret_cast<const char*>(buffer_->GetData()) + start, end - start);
+      }
     }
   };
 
 
-  class StorageAreaDataSource::Identifier : public IDataIdentifier
+  class StorageAreaDataSource::IPostProcessing : public boost::noncopyable
+  {
+  public:
+    virtual ~IPostProcessing()
+    {
+    }
+
+    // This method can return NULL if no post-processing is required
+    virtual IMemoryBuffer* Apply(const Value& value) const = 0;
+  };
+
+
+  class StorageAreaDataSource::Identifier : public BaseDataIdentifier
   {
   private:
-    std::string      uuid_;
-    FileContentType  type_;
-    uint64_t         start_;
-    uint64_t         end_;
-    std::string      customData_;
+    std::string                       uuid_;
+    FileContentType                   type_;
+    uint64_t                          start_;
+    uint64_t                          end_;
+    std::string                       customData_;
+    std::unique_ptr<IPostProcessing>  postProcessing_;
+    bool                              hasWholeKey_;
+    std::string                       wholeKey_;
 
   public:
     Identifier(const std::string& uuid,
@@ -81,7 +144,8 @@
       type_(type),
       start_(start),
       end_(end),
-      customData_(customData)
+      customData_(customData),
+      hasWholeKey_(false)
     {
       if (start > end)
       {
@@ -95,176 +159,361 @@
       }
     }
 
-    Value* Read(IPluginStorageArea& area) const
+    uint64_t GetStart() const
+    {
+      return start_;
+    }
+
+    uint64_t GetEnd() const
+    {
+      return end_;
+    }
+
+    /**
+     * WARNING: SetWholeKey() must only be used to retrieve compressed
+     * date from the storage area (or if there is no compression).
+     **/
+    void SetWholeKey(const std::string& key)
     {
-      return new Value(area.ReadRange(uuid_, type_, start_, end_, customData_));
+      if (hasWholeKey_)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else if (key.empty())
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+      else
+      {
+        hasWholeKey_ = true;
+        wholeKey_ = key;
+      }
+    }
+
+    bool HasWholeKey() const
+    {
+      return hasWholeKey_;
+    }
+
+    const std::string GetWholeKey() const
+    {
+      if (hasWholeKey_)
+      {
+        return wholeKey_;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    IMemoryBuffer* Read(IPluginStorageArea& area) const
+    {
+      return area.ReadRange(uuid_, type_, start_, end_, customData_);
     }
 
     virtual bool GetCacheKey(std::string& key) const ORTHANC_OVERRIDE
     {
-      // custom data is not part of the cache key, as it is for internal use by the plugin
-      // (it is opaque to the Orthanc core)
-      key = (uuid_ + "|" +
-             boost::lexical_cast<std::string>(type_) + "|" +
-             boost::lexical_cast<std::string>(start_) + "|" +
-             boost::lexical_cast<std::string>(end_));
+      key = ComputeCacheKey(uuid_, type_, start_, end_);
       return true;
     }
 
     virtual bool EstimateValueSize(size_t& target) const ORTHANC_OVERRIDE
     {
       assert(start_ <= end_);
-      return end_ - start_;
+      target = end_ - start_;
+      return true;
+    }
+
+    void SetPostProcessing(IPostProcessing* postProcessing)
+    {
+      std::unique_ptr<IPostProcessing> protection(postProcessing);
+
+      if (postProcessing == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+      else if (postProcessing_.get() != NULL)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        postProcessing_.reset(protection.release());
+      }
+    }
+
+    bool HasPostProcessing() const
+    {
+      return postProcessing_.get() != NULL;
+    }
+
+    const IPostProcessing& GetPostProcessing() const
+    {
+      if (postProcessing_.get() == NULL)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return *postProcessing_;
+      }
     }
   };
 
 
+  class StorageAreaDataSource::AttachmentPostProcessing : public StorageAreaDataSource::IPostProcessing
+  {
+  private:
+    FileInfo  attachment_;
+    bool      uncompress_;
+
+  public:
+    AttachmentPostProcessing(const FileInfo& attachment,
+                             bool uncompress) :
+      attachment_(attachment),
+      uncompress_(uncompress)
+    {
+      if (attachment.GetCompressionType() == CompressionType_None)
+      {
+        if (attachment.GetCompressedMD5() != attachment.GetUncompressedMD5() ||
+            attachment.GetCompressedSize() != attachment.GetUncompressedSize())
+        {
+          throw OrthancException(ErrorCode_CorruptedFile);
+        }
+      }
+    }
+
+    const FileInfo& GetAttachment() const
+    {
+      return attachment_;
+    }
+
+    virtual IMemoryBuffer* Apply(const Value& value) const ORTHANC_OVERRIDE
+    {
+      if (value.IsCheckMD5())
+      {
+        std::string md5;
+        Toolbox::ComputeMD5(md5, value.GetBuffer()->GetData(), value.GetBuffer()->GetSize());
+
+        if (md5 != attachment_.GetCompressedMD5())
+        {
+          throw OrthancException(ErrorCode_CorruptedFile);
+        }
+      }
+
+      if (uncompress_)
+      {
+        switch (attachment_.GetCompressionType())
+        {
+          case CompressionType_None:
+            if (value.IsCheckMD5() &&
+                attachment_.GetCompressedMD5() != attachment_.GetUncompressedMD5())
+            {
+              throw OrthancException(ErrorCode_CorruptedFile);
+            }
+            else
+            {
+              return NULL;
+            }
+
+          case CompressionType_ZlibWithSize:
+          {
+#if ORTHANC_ENABLE_ZLIB == 1
+            ZlibCompressor zlib;
+
+            std::string content;
+            zlib.Uncompress(content, value.GetBuffer()->GetData(), value.GetBuffer()->GetSize());
+
+            if (value.IsCheckMD5())
+            {
+              std::string md5;
+              Toolbox::ComputeMD5(md5, content);
+
+              if (md5 != attachment_.GetUncompressedMD5())
+              {
+                throw OrthancException(ErrorCode_CorruptedFile);
+              }
+            }
+
+            return StringMemoryBuffer::CreateFromSwap(content);
+#else
+            throw OrthancException(ErrorCode_InternalError, "Support for zlib is disabled, cannot uncompress attachment");
+#endif
+          }
+
+          default:
+            throw OrthancException(ErrorCode_NotImplemented);
+        }
+      }
+      else
+      {
+        return NULL;
+      }
+    }
+  };
+
+
+  StorageAreaDataSource::StorageAreaDataSource(IPluginStorageArea& area,
+                                               bool checkMD5) :
+    area_(area),
+    checkMD5_(checkMD5)
+  {
+  }
+
+
+  void StorageAreaDataSource::SetMetricsRegistry(const boost::shared_ptr<MetricsRegistry>& metrics,
+                                                 const std::string& metricsReadBytesName,
+                                                 const std::string& metricsReadDurationName)
+  {
+    if (metrics.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+      metrics_ = metrics;
+      metricsReadBytesName_ = metricsReadBytesName;
+      metricsReadDurationName_ = metricsReadDurationName;
+
+      metrics_->SetIntegerValue(metricsReadBytesName_, 0);
+      metrics_->SetIntegerValue(metricsReadDurationName_, 0);
+    }
+  }
+
+
   size_t StorageAreaDataSource::GetValueSize(const IDynamicObject& value) const
   {
     const Value& tmp = dynamic_cast<const Value&>(value);
-    return tmp.GetBuffer().GetSize();
-  }
-
-  IDynamicObject* StorageAreaDataSource::Load(const IDataIdentifier& identifier)
-  {
-    const Identifier& id = dynamic_cast<const Identifier&>(identifier);
-    return id.Read(area_);
+    return tmp.GetBuffer()->GetSize();
   }
 
 
-  const StorageAreaDataSource::Value& StorageAreaDataSource::Range::GetValue() const
+  IDynamicObject* StorageAreaDataSource::Load(const IDataIdentifier& identifier,
+                                              const boost::shared_ptr<SharedObjectCache>& readerCache)
   {
-    assert(value_.get() != NULL);
-    return dynamic_cast<const StorageAreaDataSource::Value&>(*value_);
+    const Identifier& id = dynamic_cast<const Identifier&>(identifier);
+
+    if (id.HasWholeKey() &&
+        readerCache)
+    {
+      // Extract the request range from the whole compressed attachment if the latter is already present in the cache
+      boost::shared_ptr<IDynamicObject> wholeCached = readerCache->GetCachedValue(id.GetWholeKey());
+      if (wholeCached)
+      {
+        const Value& wholeRange = dynamic_cast<const Value&>(*wholeCached);
+
+        std::string content;
+        wholeRange.ExtractRange(content, id.GetStart(), id.GetEnd());
+
+        return new Value(StringMemoryBuffer::CreateFromSwap(content), false);
+      }
+    }
+
+    std::unique_ptr<IMemoryBuffer> buffer;
+
+    {
+      std::unique_ptr<MetricsRegistry::Timer> timer;
+      if (metrics_)
+      {
+        timer.reset(new MetricsRegistry::Timer(*metrics_, metricsReadDurationName_));
+      }
+
+      buffer.reset(id.Read(area_));
+    }
+
+    if (metrics_)
+    {
+      metrics_->IncrementIntegerValue(metricsReadBytesName_, static_cast<int64_t>(buffer->GetSize()));
+    }
+
+    return new Value(buffer.release(), checkMD5_);
   }
 
 
-  StorageAreaDataSource::Range::Range(const boost::shared_ptr<IDynamicObject>& value) :
-    value_(value)
+  const boost::shared_ptr<IMemoryBuffer>& StorageAreaDataSource::Range::GetBuffer() const
   {
-    if (value_.get() == NULL)
+    if (item_.get() != NULL)
+    {
+      assert(buffer_.get() == NULL);
+      return dynamic_cast<const Value&>(*item_->GetValue()).GetBuffer();
+    }
+    else if (buffer_.get() != NULL)
+    {
+      assert(item_.get() == NULL);
+      return buffer_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  StorageAreaDataSource::Range::Range(IMemoryBuffer* buffer) :
+    buffer_(buffer)
+  {
+    if (buffer == NULL)
     {
       throw OrthancException(ErrorCode_NullPointer);
     }
   }
 
 
-  const void* StorageAreaDataSource::Range::GetData() const
-  {
-    return GetValue().GetBuffer().GetData();
-  }
-
-
-  size_t StorageAreaDataSource::Range::GetSize() const
-  {
-    return GetValue().GetBuffer().GetSize();
-  }
-
-
-  void StorageAreaDataSource::Range::Copy(std::string& to) const
-  {
-    const IMemoryBuffer& buffer = GetValue().GetBuffer();
-    to.assign(reinterpret_cast<const char*>(buffer.GetData()), buffer.GetSize());
-  }
-
-
-  StorageAreaDataSource::Range* StorageAreaDataSource::Range::CreateFromSwap(std::string& content)
+  StorageAreaDataSource::Range::Range(DataSourceAnswer::Item* item) :
+    item_(item)
   {
-    boost::shared_ptr<Value> value(new Value(StringMemoryBuffer::CreateFromSwap(content)));
-    return new Range(value);
-  }
-
-
-  StorageAreaDataSource::Range* StorageAreaDataSource::ReadRange(DataSourceReader& reader,
-                                                                 const std::string& uuid,
-                                                                 FileContentType type,
-                                                                 uint64_t start,
-                                                                 uint64_t end,
-                                                                 const std::string& customData)
-  {
-    std::unique_ptr<IDataIdentifier> id(new Identifier(uuid, type, start, end, customData));
-    boost::shared_ptr<IDynamicObject> value = reader.ReadSingle(id.release());
-    return new Range(value);
-  }
-
-
-  StorageAreaDataSource::Range* StorageAreaDataSource::ReadAttachment(DataSourceReader& reader,
-                                                                      const FileInfo& attachment,
-                                                                      bool uncompress,
-                                                                      bool checkMD5)
-  {
-    if (attachment.GetCompressionType() == CompressionType_None)
+    if (item == NULL)
     {
-      if (attachment.GetCompressedMD5() != attachment.GetUncompressedMD5() ||
-          attachment.GetCompressedSize() != attachment.GetUncompressedSize())
-      {
-        throw OrthancException(ErrorCode_CorruptedFile);
-      }
+      throw OrthancException(ErrorCode_NullPointer);
     }
 
-    std::unique_ptr<StorageAreaDataSource::Range> range(ReadRange(reader,
-                                                                  attachment.GetUuid(),
-                                                                  attachment.GetContentType(),
-                                                                  0, attachment.GetCompressedSize(),
-                                                                  attachment.GetCustomData()));
-
-    if (checkMD5)
-    {
-      std::string md5;
-      Toolbox::ComputeMD5(md5, range->GetData(), range->GetSize());
+    const Identifier& id = dynamic_cast<const Identifier&>(item->GetId());
 
-      if (md5 != attachment.GetCompressedMD5())
-      {
-        throw OrthancException(ErrorCode_CorruptedFile);
-      }
-    }
-
-    if (uncompress)
+    if (id.HasPostProcessing())
     {
-      switch (attachment.GetCompressionType())
-      {
-        case CompressionType_None:
-          return range.release();
+      const Value& value = dynamic_cast<const Value&>(*item->GetValue());
+      std::unique_ptr<IMemoryBuffer> postProcessed(id.GetPostProcessing().Apply(value));
 
-        case CompressionType_ZlibWithSize:
-        {
-#if ORTHANC_ENABLE_ZLIB == 1
-          ZlibCompressor zlib;
-
-          std::string content;
-          zlib.Uncompress(content, range->GetData(), range->GetSize());
-
-          if (checkMD5)
-          {
-            std::string md5;
-            Toolbox::ComputeMD5(md5, content);
-
-            if (md5 != attachment.GetUncompressedMD5())
-            {
-              throw OrthancException(ErrorCode_CorruptedFile);
-            }
-          }
-
-          return Range::CreateFromSwap(content);
-#else
-          throw OrthancException(ErrorCode_InternalError, "Support for zlib is disabled, cannot uncompress attachment");
-#endif
-        }
-
-        default:
-          THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_NotImplemented);
+      if (postProcessed.get() != NULL)
+      {
+        // In this case, the backpressure on the source "item" is released
+        item_.reset(NULL);
+        buffer_.reset(postProcessed.release());
       }
-    }
-    else
-    {
-      return range.release();
+      else
+      {
+        // No postprocessing was applied, keep backpressure on "item"
+      }
     }
   }
 
 
-  StorageAreaDataSource::Range* StorageAreaDataSource::ReadBeginning(DataSourceReader& reader,
-                                                                     const FileInfo& attachment,
-                                                                     uint64_t untilPosition)
+  IDataIdentifier* StorageAreaDataSource::CreateRangeRequest(const std::string& uuid,
+                                                             FileContentType type,
+                                                             uint64_t start,
+                                                             uint64_t end,
+                                                             const std::string& pluginCustomData)
+  {
+    return new Identifier(uuid, type, start, end, pluginCustomData);
+  }
+
+
+  IDataIdentifier* StorageAreaDataSource::CreateAttachmentRequest(const FileInfo& attachment,
+                                                                  bool uncompress)
+  {
+    std::unique_ptr<Identifier> id(new Identifier(attachment.GetUuid(),
+                                                  attachment.GetContentType(),
+                                                  0, attachment.GetCompressedSize(),
+                                                  attachment.GetCustomData()));
+    id->SetPostProcessing(new AttachmentPostProcessing(attachment, uncompress));
+
+    return id.release();
+  }
+
+
+  IDataIdentifier* StorageAreaDataSource::CreateBeginningRequest(const FileInfo& attachment,
+                                                                 uint64_t untilPosition)
   {
     if (attachment.GetCompressionType() != CompressionType_None)
     {
@@ -282,16 +531,21 @@
       throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
 
-    return ReadRange(reader, attachment.GetUuid(), attachment.GetContentType(),
-                     0, untilPosition, attachment.GetCustomData());
+    std::unique_ptr<Identifier> id(new Identifier(attachment.GetUuid(), attachment.GetContentType(),
+                                                  0, untilPosition, attachment.GetCustomData()));
+
+    // Using "SetWholeKey()" allows to extract a range if the whole attachment is already in the reader cache
+    assert(attachment.GetCompressionType() == CompressionType_None);
+    id->SetWholeKey(ComputeCacheKeyForWholeAttachment(attachment));
+
+    return id.release();
   }
 
 
   StorageAreaDataSource::Range* StorageAreaDataSource::ReadRange(DataSourceReader& reader,
                                                                  const FileInfo& attachment,
                                                                  const StorageRange& range,
-                                                                 bool uncompress,
-                                                                 bool checkMD5)
+                                                                 bool uncompress)
   {
     // This mimics "StorageAccessor::ReadRange()"
 
@@ -299,13 +553,13 @@
         attachment.GetCompressionType() != CompressionType_None)
     {
       // An uncompression is needed in this case
-      std::unique_ptr<StorageAreaDataSource::Range> uncompressed(
-        ReadAttachment(reader, attachment, true /* uncompress */, checkMD5));
+      std::unique_ptr<Range> uncompressed(
+        Execute(reader, CreateAttachmentRequest(attachment, true /* uncompress */)));
 
       std::string content;
       range.Extract(content, uncompressed->GetData(), uncompressed->GetSize());
 
-      return Range::CreateFromSwap(content);
+      return new Range(StringMemoryBuffer::CreateFromSwap(content));
     }
     else
     {
@@ -321,9 +575,52 @@
       }
       else
       {
-        return ReadRange(reader, attachment.GetUuid(), attachment.GetContentType(),
-                         start, endExclusive, attachment.GetCustomData());
+        std::unique_ptr<Identifier> id(new Identifier(attachment.GetUuid(), attachment.GetContentType(),
+                                                      start, endExclusive, attachment.GetCustomData()));
+
+        // Using "SetWholeKey()" allows to extract a range if the whole compressed attachment is already in the reader cache
+        assert(!uncompress || attachment.GetCompressionType() == CompressionType_None);
+        id->SetWholeKey(ComputeCacheKeyForWholeAttachment(attachment));
+
+        return Execute(reader, id.release());
       }
     }
   }
+
+
+  StorageAreaDataSource::Range* StorageAreaDataSource::Execute(DataSourceReader& reader,
+                                                               IDataIdentifier* request /* takes ownership */)
+  {
+    if (request == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+      return new Range(reader.ReadSingle(request));
+    }
+  }
+
+
+  void StorageAreaDataSource::StoreIntoCache(DataSourceReader& reader,
+                                             const FileInfo& attachment,
+                                             const void* data,
+                                             size_t size)
+  {
+    if (size != attachment.GetCompressedSize())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    const size_t capacity = reader.GetCacheCapacity();
+
+    if (capacity != 0 &&
+        size <= capacity)
+    {
+      const std::string key = ComputeCacheKeyForWholeAttachment(attachment);
+      std::unique_ptr<Value> value(new Value(StringMemoryBuffer::CreateFromBuffer(data, size), false /* no MD5 */));
+
+      reader.StoreIntoCache(key, value.release());
+    }
+  }
 }
--- a/OrthancFramework/Sources/DataSource/StorageAreaDataSource.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/DataSource/StorageAreaDataSource.h	Wed Jul 15 11:42:31 2026 +0200
@@ -32,6 +32,8 @@
 #include "../FileStorage/FileInfo.h"
 #include "../FileStorage/IStorageArea.h"
 #include "../FileStorage/StorageRange.h"
+#include "../IMemoryBuffer.h"
+#include "DataSourceAnswer.h"
 #include "IDataSource.h"
 
 #include <boost/shared_ptr.hpp>
@@ -40,65 +42,88 @@
 namespace Orthanc
 {
   class DataSourceReader;
+  class MetricsRegistry;
 
   class ORTHANC_PUBLIC StorageAreaDataSource : public IDataSource
   {
   private:
     class Value;
     class Identifier;
+    class IPostProcessing;
+    class AttachmentPostProcessing;
 
   private:
-    IPluginStorageArea&  area_;
+    IPluginStorageArea&                 area_;
+    bool                                checkMD5_;
+    boost::shared_ptr<MetricsRegistry>  metrics_;
+    std::string                         metricsReadBytesName_;
+    std::string                         metricsReadDurationName_;
 
   public:
-    explicit StorageAreaDataSource(IPluginStorageArea& area) :
-      area_(area)
-    {
-    }
+    explicit StorageAreaDataSource(IPluginStorageArea& area,
+                                   bool checkMD5);
+
+    void SetMetricsRegistry(const boost::shared_ptr<MetricsRegistry>& metrics,
+                            const std::string& metricsReadBytesName,
+                            const std::string& metricsReadDurationName);
 
     virtual size_t GetValueSize(const IDynamicObject& value) const ORTHANC_OVERRIDE;
 
-    virtual IDynamicObject* Load(const IDataIdentifier& identifier) ORTHANC_OVERRIDE;
+    virtual IDynamicObject* Load(const IDataIdentifier& identifier,
+                                 const boost::shared_ptr<SharedObjectCache>& readerCache /* could be NULL */) ORTHANC_OVERRIDE;
 
-    class Range : public boost::noncopyable
+    class ORTHANC_PUBLIC Range : public boost::noncopyable
     {
     private:
-      boost::shared_ptr<IDynamicObject>  value_;
-
-      const Value& GetValue() const;
+      std::unique_ptr<DataSourceAnswer::Item>  item_;   // Holding item puts backpressure on the data source (can be NULL)
+      boost::shared_ptr<IMemoryBuffer>         buffer_;  // To be used if "item_ == NULL"
 
     public:
-      explicit Range(const boost::shared_ptr<IDynamicObject>& value);
+      explicit Range(DataSourceAnswer::Item* item /* takes ownership */);
 
-      const void* GetData() const;
+      explicit Range(IMemoryBuffer* buffer /* takes ownership */);
+
+      const boost::shared_ptr<IMemoryBuffer>& GetBuffer() const;
 
-      size_t GetSize() const;
+      const void* GetData() const
+      {
+        return GetBuffer()->GetData();
+      }
 
-      void Copy(std::string& to) const;
+      const size_t GetSize() const
+      {
+        return GetBuffer()->GetSize();
+      }
 
-      static Range* CreateFromSwap(std::string& content);
+      void Copy(std::string& target) const
+      {
+        GetBuffer()->CopyToString(target);
+      }
     };
 
-    static Range* ReadRange(DataSourceReader& reader,
-                            const std::string& uuid,
-                            FileContentType type,
-                            uint64_t start /* inclusive */,
-                            uint64_t end /* exclusive */,
-                            const std::string& customData);
+    static IDataIdentifier* CreateRangeRequest(const std::string& uuid,
+                                               FileContentType type,
+                                               uint64_t start /* inclusive */,
+                                               uint64_t end /* exclusive */,
+                                               const std::string& pluginCustomData);
 
-    static Range* ReadAttachment(DataSourceReader& reader,
-                                 const FileInfo& attachment,
-                                 bool uncompress,
-                                 bool checkMD5);
+    static IDataIdentifier* CreateAttachmentRequest(const FileInfo& attachment,
+                                                    bool uncompress);
 
-    static Range* ReadBeginning(DataSourceReader& reader,
-                                const FileInfo& attachment,
-                                uint64_t untilPosition /* exclusive */);
+    static IDataIdentifier* CreateBeginningRequest(const FileInfo& attachment,
+                                                   uint64_t untilPosition /* exclusive */);
+
+    static Range* Execute(DataSourceReader& reader,
+                          IDataIdentifier* request /* takes ownership */);
 
     static Range* ReadRange(DataSourceReader& reader,
                             const FileInfo& attachment,
                             const StorageRange& range,
-                            bool uncompress,
-                            bool checkMD5);
+                            bool uncompress);
+
+    static void StoreIntoCache(DataSourceReader& reader,
+                               const FileInfo& attachment,
+                               const void* data,
+                               size_t size);
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DataSource/TranscoderDataSource.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -0,0 +1,344 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, 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 "../PrecompiledHeaders.h"
+#include "TranscoderDataSource.h"
+
+#include "../DicomParsing/ParsedDicomFile.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "BaseDataIdentifier.h"
+#include "DataSourceReader.h"
+#include "StorageAreaDataSource.h"
+
+#include <boost/lexical_cast.hpp>
+#include <dcmtk/dcmdata/dcfilefo.h>
+
+
+namespace Orthanc
+{
+  static bool IsLosslessTransferSyntax(DicomTransferSyntax syntax)
+  {
+    return (syntax == DicomTransferSyntax_LittleEndianImplicit ||
+            syntax == DicomTransferSyntax_LittleEndianExplicit ||
+            syntax == DicomTransferSyntax_DeflatedLittleEndianExplicit ||
+            syntax == DicomTransferSyntax_BigEndianExplicit ||
+            syntax == DicomTransferSyntax_JPEGLSLossless ||
+            syntax == DicomTransferSyntax_JPEG2000LosslessOnly ||
+            syntax == DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly ||
+            syntax == DicomTransferSyntax_RLELossless ||
+            syntax == DicomTransferSyntax_JPEGXLLossless ||
+            syntax == DicomTransferSyntax_HighThroughputJPEG2000LosslessOnly);
+  }
+
+
+  class TranscoderDataSource::Value : public IDynamicObject
+  {
+  private:
+    Mutex                             parsedMutex_;
+    std::unique_ptr<ParsedDicomFile>  parsed_;
+    std::unique_ptr<std::string>      raw_;
+    size_t                            size_;
+
+  public:
+    explicit Value(IDicomTranscoder::DicomImage* transcoded)
+    {
+      std::unique_ptr<IDicomTranscoder::DicomImage> protection(transcoded);
+
+      if (transcoded == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+
+      if (!transcoded->HasParsed() &&
+          !transcoded->HasInternalBuffer())
+      {
+        throw OrthancException(ErrorCode_InternalError, "No transcoded image is available");
+      }
+
+      /**
+       * Note that both "transcoded->HasParsed()" and
+       * "transcoded->HasInternalBuffer()" could be true in debug
+       * mode, because of "IDicomTranscoder::CheckTranscoding()".
+       **/
+
+      if (transcoded->HasParsed())
+      {
+        parsed_.reset(transcoded->ReleaseAsParsedDicomFile());
+      }
+
+      if (transcoded->HasInternalBuffer())
+      {
+        raw_.reset(transcoded->ReleaseInternalBuffer());
+        size_ = raw_->size();  // This is more accurate than "f.calcElementLength()" below
+      }
+      else
+      {
+        assert(parsed_.get() != NULL);
+        DcmFileFormat& f = parsed_->GetDcmtkObject();
+        size_ = f.calcElementLength(f.getDataset()->getOriginalXfer(), EET_ExplicitLength);
+      }
+    }
+
+    size_t GetSize() const
+    {
+      return size_;
+    }
+
+    bool HasParsed() const
+    {
+      return parsed_.get() != NULL;
+    }
+
+    bool HasRawBuffer() const
+    {
+      return raw_.get() != NULL;
+    }
+
+    Mutex::ScopedLock* LockParsed()
+    {
+      return new Mutex::ScopedLock(parsedMutex_);
+    }
+
+    ParsedDicomFile& GetParsed() const
+    {
+      if (parsed_.get() == NULL)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return *parsed_;
+      }
+    }
+
+    const std::string& GetRawBuffer() const
+    {
+      if (raw_.get() == NULL)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return *raw_;
+      }
+    }
+  };
+
+
+  class TranscoderDataSource::Identifier : public BaseDataIdentifier
+  {
+  private:
+    FileInfo                       attachment_;
+    DicomTransferSyntax            targetSyntax_;
+    TranscodingSopInstanceUidMode  mode_;
+    bool                           hasLossyQuality_;
+    unsigned int                   lossyQuality_;
+
+  public:
+    Identifier(const FileInfo& attachment,
+               DicomTransferSyntax targetSyntax,
+               TranscodingSopInstanceUidMode mode) :
+      attachment_(attachment),
+      targetSyntax_(targetSyntax),
+      mode_(mode),
+      hasLossyQuality_(false),
+      lossyQuality_(0)
+    {
+    }
+
+    void SetLossyQuality(unsigned int quality)
+    {
+      hasLossyQuality_ = true;
+      lossyQuality_ = quality;
+    }
+
+    virtual bool GetCacheKey(std::string& key) const ORTHANC_OVERRIDE
+    {
+      key = (attachment_.GetUuid() + "|" +
+             GetTransferSyntaxUid(targetSyntax_) + "|" +
+             boost::lexical_cast<std::string>(mode_));
+
+      if (hasLossyQuality_)
+      {
+        key += "|" + boost::lexical_cast<std::string>(lossyQuality_);
+      }
+
+      return true;
+    }
+
+    virtual bool EstimateValueSize(size_t& target) const ORTHANC_OVERRIDE
+    {
+      target = attachment_.GetUncompressedSize();
+      return true;
+    }
+
+    Value* Load(IDicomTranscoder& transcoder,
+                DataSourceReader& storageAreaReader) const
+    {
+      std::unique_ptr<StorageAreaDataSource::Range> range(
+        StorageAreaDataSource::Execute(
+          storageAreaReader, StorageAreaDataSource::CreateAttachmentRequest(attachment_, true /* uncompress */)));
+
+      IDicomTranscoder::DicomImage source;
+      source.SetExternalBuffer(range->GetData(), range->GetSize());
+
+      std::unique_ptr<IDicomTranscoder::DicomImage> target(new IDicomTranscoder::DicomImage);
+
+      std::set<DicomTransferSyntax> allowedSyntaxes;
+      allowedSyntaxes.insert(targetSyntax_);
+
+      LOG(INFO) << "Transcoding DICOM attachment to " << GetTransferSyntaxUid(targetSyntax_) << ": " << attachment_.GetUuid();
+
+      bool success;
+      if (hasLossyQuality_)
+      {
+        success = transcoder.Transcode(*target, source, allowedSyntaxes, mode_, lossyQuality_);
+      }
+      else
+      {
+        success = transcoder.Transcode(*target, source, allowedSyntaxes, mode_);
+      }
+
+      if (success)
+      {
+        return new Value(target.release());
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_NotImplemented);
+      }
+    }
+  };
+
+
+  TranscoderDataSource::TranscoderDataSource(const boost::shared_ptr<IDicomTranscoder>& transcoder,
+                                             const boost::shared_ptr<DataSourceReader>& storageAreaReader) :
+    transcoder_(transcoder),
+    storageAreaReader_(storageAreaReader)
+  {
+    if (!transcoder_ ||
+        !storageAreaReader_)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+  }
+
+
+  IDynamicObject* TranscoderDataSource::Load(const IDataIdentifier& id,
+                                             const boost::shared_ptr<SharedObjectCache>& readerCache)
+  {
+    return dynamic_cast<const Identifier&>(id).Load(*transcoder_, *storageAreaReader_);
+  }
+
+
+  size_t TranscoderDataSource::GetValueSize(const IDynamicObject& value) const
+  {
+    return dynamic_cast<const Value&>(value).GetSize();
+  }
+
+
+  TranscoderDataSource::Transcoded::Transcoded(DataSourceAnswer::Item* item) :
+    item_(item)
+  {
+    if (!item)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+  }
+
+
+  TranscoderDataSource::Transcoded::LockAsParsed::LockAsParsed(Transcoded& that)
+  {
+    Value& value = dynamic_cast<Value&>(*that.item_->GetValue());
+
+    if (value.HasParsed())
+    {
+      lock_.reset(value.LockParsed());
+      content_ = &value.GetParsed();
+    }
+    else
+    {
+      assert(value.HasRawBuffer());
+      parsed_.reset(new ParsedDicomFile(value.GetRawBuffer()));
+
+      content_ = parsed_.get();
+    }
+
+    assert(content_ != NULL);
+  }
+
+
+  TranscoderDataSource::Transcoded::LockAsBuffer::LockAsBuffer(Transcoded& that)
+  {
+    Value& value = dynamic_cast<Value&>(*that.item_->GetValue());
+
+    if (value.HasParsed())
+    {
+      std::unique_ptr<Mutex::ScopedLock> lock(value.LockParsed());
+      serialized_.reset(new std::string);
+      value.GetParsed().SaveToMemoryBuffer(*serialized_);
+      content_ = serialized_.get();
+    }
+    else
+    {
+      assert(value.HasRawBuffer());
+      content_ = &value.GetRawBuffer();
+    }
+
+    assert(content_ != NULL);
+  }
+
+
+  IDataIdentifier* TranscoderDataSource::CreateRequest(const FileInfo& attachment,
+                                                       DicomTransferSyntax targetSyntax,
+                                                       TranscodingSopInstanceUidMode mode,
+                                                       bool hasLossyQuality,
+                                                       unsigned int lossyQuality)
+  {
+    std::unique_ptr<Identifier> id(new Identifier(attachment, targetSyntax, mode));
+
+    if (hasLossyQuality &&
+        !IsLosslessTransferSyntax(targetSyntax))
+    {
+      id->SetLossyQuality(lossyQuality);
+    }
+
+    return id.release();
+  }
+
+
+  TranscoderDataSource::Transcoded* TranscoderDataSource::Execute(DataSourceReader& reader,
+                                                                  IDataIdentifier* request)
+  {
+    if (request == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+      std::unique_ptr<DataSourceAnswer::Item> item(reader.ReadSingle(request));
+      return new Transcoded(item.release());
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DataSource/TranscoderDataSource.h	Wed Jul 15 11:42:31 2026 +0200
@@ -0,0 +1,117 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, 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 "../DicomParsing/IDicomTranscoder.h"
+#include "../FileStorage/FileInfo.h"
+#include "../MultiThreading/Mutex.h"
+#include "DataSourceAnswer.h"
+#include "IDataSource.h"
+
+#include <boost/shared_ptr.hpp>
+
+
+namespace Orthanc
+{
+  class DataSourceReader;
+
+  class ORTHANC_PUBLIC TranscoderDataSource : public IDataSource
+  {
+  private:
+    class Value;
+    class Identifier;
+
+    boost::shared_ptr<IDicomTranscoder>  transcoder_;
+    boost::shared_ptr<DataSourceReader>  storageAreaReader_;
+
+  public:
+    TranscoderDataSource(const boost::shared_ptr<IDicomTranscoder>& transcoder,
+                         const boost::shared_ptr<DataSourceReader>& storageAreaReader);
+
+    virtual IDynamicObject* Load(const IDataIdentifier& id,
+                                 const boost::shared_ptr<SharedObjectCache>& readerCache /* could be NULL */) ORTHANC_OVERRIDE;
+
+    virtual size_t GetValueSize(const IDynamicObject& value) const ORTHANC_OVERRIDE;
+
+    class ORTHANC_PUBLIC Transcoded : public boost::noncopyable
+    {
+    private:
+      std::unique_ptr<DataSourceAnswer::Item>  item_;   // Holding item puts backpressure on the data source
+
+    public:
+      explicit Transcoded(DataSourceAnswer::Item* item /* takes ownership */);
+
+      class ORTHANC_PUBLIC LockAsParsed : public boost::noncopyable
+      {
+      private:
+        /**
+         * Access to the transcoded DICOM must be protected by a
+         * mutex, as it could be shared by multiple threads if caching
+         * is enabled in the DataSourceReader.
+         **/
+
+        std::unique_ptr<Mutex::ScopedLock>  lock_;
+        std::unique_ptr<ParsedDicomFile>    parsed_;
+        const ParsedDicomFile*              content_;
+
+      public:
+        explicit LockAsParsed(Transcoded& that);
+
+        const ParsedDicomFile& GetContent() const
+        {
+          return *content_;
+        }
+      };
+
+      class ORTHANC_PUBLIC LockAsBuffer : public boost::noncopyable
+      {
+      private:
+        /**
+         * Access to the transcoded DICOM must only be protected in
+         * the constructor, if serialization to the buffer is required.
+         **/
+
+        std::unique_ptr<std::string>  serialized_;
+        const std::string*            content_;
+
+      public:
+        explicit LockAsBuffer(Transcoded& that);
+
+        const std::string& GetContent() const
+        {
+          return *content_;
+        }
+      };
+    };
+
+    static IDataIdentifier* CreateRequest(const FileInfo& attachment,
+                                          DicomTransferSyntax targetSyntax,
+                                          TranscodingSopInstanceUidMode mode,
+                                          bool hasLossyQuality,
+                                          unsigned int lossyQuality);
+
+    static Transcoded* Execute(DataSourceReader& reader,
+                               IDataIdentifier* request /* takes ownership */);
+  };
+}
--- a/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -426,4 +426,17 @@
       return buffer_->size();
     }
   }
+
+
+  std::string* IDicomTranscoder::DicomImage::ReleaseInternalBuffer()
+  {
+    if (buffer_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return buffer_.release();
+    }
+  }
 }
--- a/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h	Wed Jul 15 11:42:31 2026 +0200
@@ -24,8 +24,13 @@
 
 #pragma once
 
+#if ORTHANC_ENABLE_DCMTK != 1
+#  error The macro ORTHANC_ENABLE_DCMTK must be set to 1
+#endif
+
 #include "../Compatibility.h"
 #include "../Enumerations.h"
+#include "../IDynamicObject.h"
 
 #include <boost/noncopyable.hpp>
 #include <set>
@@ -43,7 +48,7 @@
   class ORTHANC_PUBLIC IDicomTranscoder : public boost::noncopyable
   {
   public:
-    class ORTHANC_PUBLIC DicomImage : public boost::noncopyable
+    class ORTHANC_PUBLIC DicomImage : public IDynamicObject
     {
     private:
       std::unique_ptr<DcmFileFormat>  parsed_;
@@ -86,6 +91,18 @@
       const void* GetBufferData();
 
       size_t GetBufferSize();
+
+      bool HasParsed() const
+      {
+        return parsed_.get() != NULL;
+      }
+
+      bool HasInternalBuffer() const
+      {
+        return buffer_.get() != NULL;
+      }
+
+      std::string* ReleaseInternalBuffer();
     };
 
 
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -22,6 +22,10 @@
  **/
 
 
+#if defined(__GNUC__) || defined(__clang__)
+#  pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+#endif
+
 #include "../PrecompiledHeaders.h"
 #include "ParsedDicomCache.h"
 
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.h	Wed Jul 15 11:42:31 2026 +0200
@@ -29,7 +29,7 @@
 
 namespace Orthanc
 {
-  class ORTHANC_PUBLIC ParsedDicomCache : public boost::noncopyable
+  class ORTHANC_PUBLIC ORTHANC_DEPRECATED_CLASS(ParsedDicomCache) : public boost::noncopyable
   {
   private:
     class Item;
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -24,7 +24,6 @@
 
 #include "../PrecompiledHeaders.h"
 #include "StorageAccessor.h"
-#include "StorageCache.h"
 
 #include "../Logging.h"
 #include "../StringMemoryBuffer.h"
@@ -47,8 +46,6 @@
 static const std::string METRICS_REMOVE_DURATION = "orthanc_storage_remove_duration_ms";
 static const std::string METRICS_READ_BYTES = "orthanc_storage_read_bytes";
 static const std::string METRICS_WRITTEN_BYTES = "orthanc_storage_written_bytes";
-static const std::string METRICS_CACHE_HIT_COUNT = "orthanc_storage_cache_hit_count";
-static const std::string METRICS_CACHE_MISS_COUNT = "orthanc_storage_cache_miss_count";
 
 
 namespace Orthanc
@@ -72,34 +69,14 @@
 
   StorageAccessor::StorageAccessor(IPluginStorageArea& area) :
     area_(area),
-    cache_(NULL),
     metrics_(NULL)
   {
   }
   
 
   StorageAccessor::StorageAccessor(IPluginStorageArea& area,
-                                   StorageCache& cache) :
-    area_(area),
-    cache_(&cache),
-    metrics_(NULL)
-  {
-  }
-
-
-  StorageAccessor::StorageAccessor(IPluginStorageArea& area,
                                    MetricsRegistry& metrics) :
     area_(area),
-    cache_(NULL),
-    metrics_(&metrics)
-  {
-  }
-
-  StorageAccessor::StorageAccessor(IPluginStorageArea& area,
-                                   StorageCache& cache,
-                                   MetricsRegistry& metrics) :
-    area_(area),
-    cache_(&cache),
     metrics_(&metrics)
   {
   }
@@ -151,12 +128,6 @@
           metrics_->IncrementIntegerValue(METRICS_WRITTEN_BYTES, static_cast<int64_t>(size));
         }
         
-        if (cache_ != NULL)
-        {
-          StorageCache::Accessor cacheAccessor(*cache_);
-          cacheAccessor.Add(uuid, type, data, size);
-        }
-
         info = FileInfo(uuid, type, size, md5);
         info.SetCustomData(customData);
         return;
@@ -194,12 +165,6 @@
           metrics_->IncrementIntegerValue(METRICS_WRITTEN_BYTES, static_cast<int64_t>(compressed.size()));
         }
 
-        if (cache_ != NULL)
-        {
-          StorageCache::Accessor cacheAccessor(*cache_);
-          cacheAccessor.Add(uuid, type, data, size);    // always add uncompressed data to cache
-        }
-
         info = FileInfo(uuid, type, size, md5,
                         CompressionType_ZlibWithSize, compressed.size(), compressedMD5);
         info.SetCustomData(customData);
@@ -211,39 +176,10 @@
     }
   }
 
+
   void StorageAccessor::Read(std::string& content,
                              const FileInfo& info)
   {
-    if (cache_ == NULL)
-    {
-      ReadWholeInternal(content, info);
-    }
-    else
-    {
-      StorageCache::Accessor cacheAccessor(*cache_);
-
-      if (!cacheAccessor.Fetch(content, info.GetUuid(), info.GetContentType()))
-      {
-        if (metrics_ != NULL)
-        {
-          metrics_->IncrementIntegerValue(METRICS_CACHE_MISS_COUNT, 1);
-        }
-
-        ReadWholeInternal(content, info);
-
-        // always store the uncompressed data in cache
-        cacheAccessor.Add(info.GetUuid(), info.GetContentType(), content);
-      } 
-      else if (metrics_ != NULL)
-      {
-        metrics_->IncrementIntegerValue(METRICS_CACHE_HIT_COUNT, 1);
-      }
-    }
-  }
-
-  void StorageAccessor::ReadWholeInternal(std::string& content,
-                                          const FileInfo& info)
-  {
     switch (info.GetCompressionType())
     {
       case CompressionType_None:
@@ -299,35 +235,6 @@
   void StorageAccessor::ReadRaw(std::string& content,
                                 const FileInfo& info)
   {
-    if (cache_ == NULL || info.GetCompressionType() != CompressionType_None)
-    {
-      ReadRawInternal(content, info);
-    }
-    else
-    {// use the cache only if the data is uncompressed.
-      StorageCache::Accessor cacheAccessor(*cache_);
-
-      if (!cacheAccessor.Fetch(content, info.GetUuid(), info.GetContentType()))
-      {
-        if (metrics_ != NULL)
-        {
-          metrics_->IncrementIntegerValue(METRICS_CACHE_MISS_COUNT, 1);
-        }
-
-        ReadRawInternal(content, info);
-
-        cacheAccessor.Add(info.GetUuid(), info.GetContentType(), content);
-      }
-      else if (metrics_ != NULL)
-      {
-        metrics_->IncrementIntegerValue(METRICS_CACHE_HIT_COUNT, 1);
-      }
-    }
-  }
-
-  void StorageAccessor::ReadRawInternal(std::string& content,
-                                        const FileInfo& info)
-  {
     std::unique_ptr<IMemoryBuffer> buffer;
 
     {
@@ -348,11 +255,6 @@
                                FileContentType type,
                                const std::string& customData)
   {
-    if (cache_ != NULL)
-    {
-      cache_->Invalidate(fileUuid, type);
-    }
-
     {
       MetricsTimer timer(*this, METRICS_REMOVE_DURATION);
       area_.Remove(fileUuid, type, customData);
@@ -370,55 +272,6 @@
                                        const FileInfo& info,
                                        uint64_t end /* exclusive */)
   {
-    if (cache_ == NULL)
-    {
-      ReadStartRangeInternal(target, info, end);
-    }
-    else
-    {
-      StorageCache::Accessor accessorStartRange(*cache_);
-      if (!accessorStartRange.FetchStartRange(target, info.GetUuid(), info.GetContentType(), end))
-      {
-        // the start range is not in cache, let's check if the whole file is
-        StorageCache::Accessor accessorWhole(*cache_);
-        if (!accessorWhole.Fetch(target, info.GetUuid(), info.GetContentType()))
-        {
-          if (metrics_ != NULL)
-          {
-            metrics_->IncrementIntegerValue(METRICS_CACHE_MISS_COUNT, 1);
-          }
-
-          // if nothing is in the cache, let's read and cache only the start
-          ReadStartRangeInternal(target, info, end);
-          accessorStartRange.AddStartRange(info.GetUuid(), info.GetContentType(), target);
-        }
-        else
-        {
-          if (metrics_ != NULL)
-          {
-            metrics_->IncrementIntegerValue(METRICS_CACHE_HIT_COUNT, 1);
-          }
-
-          // we have read the whole file, check size and resize if needed
-          if (target.size() < end)
-          {
-            throw OrthancException(ErrorCode_CorruptedFile);
-          }
-
-          target.resize(end);
-        }
-      }
-      else if (metrics_ != NULL)
-      {
-        metrics_->IncrementIntegerValue(METRICS_CACHE_HIT_COUNT, 1);
-      }
-    }
-  }
-
-  void StorageAccessor::ReadStartRangeInternal(std::string& target,
-                                                const FileInfo& info,
-                                                uint64_t end /* exclusive */)
-  {
     std::unique_ptr<IMemoryBuffer> buffer;
 
     {
@@ -445,41 +298,14 @@
         info.GetCompressionType() != CompressionType_None)
     {
       // An uncompression is needed in this case
-      if (cache_ != NULL)
-      {
-        StorageCache::Accessor cacheAccessor(*cache_);
-
-        std::string content;
-        if (cacheAccessor.Fetch(content, info.GetUuid(), info.GetContentType()))
-        {
-          range.Extract(target, content);
-          return;
-        }
-      }
-
       std::string content;
       Read(content, info);
       range.Extract(target, content);
     }
     else
     {
-      // Access to the raw attachment is sufficient in this case
-      if (info.GetCompressionType() == CompressionType_None &&
-          cache_ != NULL)
-      {
-        // Check out whether the raw attachment is already present in the cache, by chance
-        StorageCache::Accessor cacheAccessor(*cache_);
-
-        std::string content;
-        if (cacheAccessor.Fetch(content, info.GetUuid(), info.GetContentType()))
-        {
-          range.Extract(target, content);
-          return;
-        }
-      }
-
       if (range.HasEnd() &&
-        range.GetEndInclusive() >= info.GetCompressedSize())
+          range.GetEndInclusive() >= info.GetCompressedSize())
       {
         throw OrthancException(ErrorCode_BadRange);
       }
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h	Wed Jul 15 11:42:31 2026 +0200
@@ -55,21 +55,19 @@
 namespace Orthanc
 {
   class MetricsRegistry;
-  class StorageCache;
 
   /**
    * This class handles the compression/decompression of the raw files
    * contained in the storage area, and monitors timing metrics (if
    * enabled).
    **/
-  class ORTHANC_PUBLIC StorageAccessor : boost::noncopyable
+  class ORTHANC_PUBLIC ORTHANC_DEPRECATED_CLASS(StorageAccessor) : boost::noncopyable
   {
   private:
     class MetricsTimer;
 
-    IPluginStorageArea&     area_;
-    StorageCache*     cache_;
-    MetricsRegistry*  metrics_;
+    IPluginStorageArea&  area_;
+    MetricsRegistry*     metrics_;
 
 #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
     void SetupSender(BufferHttpSender& sender,
@@ -81,13 +79,6 @@
     explicit StorageAccessor(IPluginStorageArea& area);
 
     StorageAccessor(IPluginStorageArea& area,
-                    StorageCache& cache);
-
-    StorageAccessor(IPluginStorageArea& area,
-                    MetricsRegistry& metrics);
-
-    StorageAccessor(IPluginStorageArea& area,
-                    StorageCache& cache,
                     MetricsRegistry& metrics);
 
     void Write(FileInfo& info /* out */,
@@ -149,17 +140,5 @@
                     const std::string& mime,
                     const std::string& contentFilename);
 #endif
-
-  private:
-    void ReadStartRangeInternal(std::string& target,
-                                const FileInfo& info,
-                                uint64_t end /* exclusive */);
-
-    void ReadWholeInternal(std::string& content,
-                           const FileInfo& info);
-
-    void ReadRawInternal(std::string& content,
-                         const FileInfo& info);
-
   };
 }
--- a/OrthancFramework/Sources/FileStorage/StorageCache.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,201 +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-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeaders.h"
-#include "StorageCache.h"
-
-#include "../Compatibility.h"
-#include "../Logging.h"
-#include "../OrthancException.h"
-
-#include <boost/lexical_cast.hpp>
-
-
-namespace Orthanc
-{
-  static std::string GetCacheKeyFullFile(const std::string& uuid,
-                                         FileContentType contentType)
-  {
-    return uuid + ":" + boost::lexical_cast<std::string>(contentType) + ":1";
-  }
-
-
-  static std::string GetCacheKeyStartRange(const std::string& uuid,
-                                           FileContentType contentType)
-  {
-    return uuid + ":" + boost::lexical_cast<std::string>(contentType) + ":0";
-  }
-
-
-  static std::string GetCacheKeyTranscodedInstance(const std::string& uuid,
-                                                   DicomTransferSyntax transferSyntax)
-  {
-    return uuid + ":ts:" + GetTransferSyntaxUid(transferSyntax);
-  }
-
-
-  void StorageCache::SetMaximumSize(size_t size)
-  {
-    cache_.SetMaximumSize(size);
-  }
-  
-
-  void StorageCache::Invalidate(const std::string& uuid,
-                                FileContentType contentType)
-  {
-    std::set<DicomTransferSyntax> transferSyntaxes;
-
-    {
-      boost::mutex::scoped_lock lock(subKeysMutex_);
-      transferSyntaxes = subKeysTransferSyntax_;
-    }
-
-    // invalidate full file, start range file and possible transcoded instances
-    const std::string keyFullFile = GetCacheKeyFullFile(uuid, contentType);
-    cache_.Invalidate(keyFullFile);
-
-    const std::string keyPartialFile = GetCacheKeyStartRange(uuid, contentType);
-    cache_.Invalidate(keyPartialFile);
-    
-    for (std::set<DicomTransferSyntax>::const_iterator it = transferSyntaxes.begin(); it != transferSyntaxes.end(); ++it)
-    {
-      const std::string keyTransferSyntax = GetCacheKeyTranscodedInstance(uuid, *it);
-      cache_.Invalidate(keyTransferSyntax);
-    }
-  }
-
-
-  StorageCache::Accessor::Accessor(StorageCache& cache)
-  : MemoryStringCache::Accessor(cache.cache_),
-    storageCache_(cache)
-  {
-  }
-
-  void StorageCache::Accessor::Add(const std::string& uuid, 
-                                   FileContentType contentType,
-                                   const std::string& value)
-  {
-
-    std::string key = GetCacheKeyFullFile(uuid, contentType);
-    MemoryStringCache::Accessor::Add(key, value);
-  }
-
-  void StorageCache::Accessor::AddStartRange(const std::string& uuid, 
-                                             FileContentType contentType,
-                                             const std::string& value)
-  {
-    const std::string key = GetCacheKeyStartRange(uuid, contentType);
-    MemoryStringCache::Accessor::Add(key, value);
-  }
-
-  void StorageCache::Accessor::Add(const std::string& uuid, 
-                                   FileContentType contentType,
-                                   const void* buffer,
-                                   size_t size)
-  {
-    const std::string key = GetCacheKeyFullFile(uuid, contentType);
-    MemoryStringCache::Accessor::Add(key, reinterpret_cast<const char*>(buffer), size);
-  }                                   
-
-  bool StorageCache::Accessor::Fetch(std::string& value, 
-                                     const std::string& uuid,
-                                     FileContentType contentType)
-  {
-    const std::string key = GetCacheKeyFullFile(uuid, contentType);
-    if (MemoryStringCache::Accessor::Fetch(value, key))
-    {
-      LOG(INFO) << "Read attachment \"" << uuid << "\" with content type "
-                << boost::lexical_cast<std::string>(contentType) << " from cache";
-      return true;
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-  bool StorageCache::Accessor::FetchTranscodedInstance(std::string& value, 
-                                                       const std::string& uuid,
-                                                       DicomTransferSyntax targetSyntax)
-  {
-    const std::string key = GetCacheKeyTranscodedInstance(uuid, targetSyntax);
-    if (MemoryStringCache::Accessor::Fetch(value, key))
-    {
-      LOG(INFO) << "Read instance \"" << uuid << "\" transcoded to "
-                << GetTransferSyntaxUid(targetSyntax) << " from cache";
-      return true;
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-  void StorageCache::Accessor::AddTranscodedInstance(const std::string& uuid,
-                                                     DicomTransferSyntax targetSyntax,
-                                                     const void* buffer,
-                                                     size_t size)
-  {
-    {
-      boost::mutex::scoped_lock lock(storageCache_.subKeysMutex_);
-      storageCache_.subKeysTransferSyntax_.insert(targetSyntax);
-    }
-
-    const std::string key = GetCacheKeyTranscodedInstance(uuid, targetSyntax);
-    MemoryStringCache::Accessor::Add(key, reinterpret_cast<const char*>(buffer), size);
-  }
-
-  bool StorageCache::Accessor::FetchStartRange(std::string& value, 
-                                               const std::string& uuid,
-                                               FileContentType contentType,
-                                               uint64_t end /* exclusive */)
-  {
-    const std::string keyPartialFile = GetCacheKeyStartRange(uuid, contentType);
-    if (MemoryStringCache::Accessor::Fetch(value, keyPartialFile) && value.size() >= end)
-    {
-      if (value.size() > end)  // the start range that has been cached is larger than the requested value
-      {
-        value.resize(end);
-      }
-
-      LOG(INFO) << "Read start of attachment \"" << uuid << "\" with content type "
-                << boost::lexical_cast<std::string>(contentType) << " from cache";
-      return true;
-    }
-
-    return false;
-  }
-
-
-  size_t StorageCache::GetCurrentSize() const
-  {
-    return cache_.GetCurrentSize();
-  }
-  
-  size_t StorageCache::GetNumberOfItems() const
-  {
-    return cache_.GetNumberOfItems();
-  }
-
-}
--- a/OrthancFramework/Sources/FileStorage/StorageCache.h	Wed Jul 08 11:45:33 2026 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,125 +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-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../Cache/MemoryStringCache.h"
-
-#include "../Compatibility.h"  // For ORTHANC_OVERRIDE
-
-#include <boost/thread/mutex.hpp>
-#include <map>
-
-namespace Orthanc
-{
-   /**
-   *  Note: this class is thread safe
-   **/
-   class ORTHANC_PUBLIC StorageCache : public boost::noncopyable
-    {
-    public:
-
-      // The StorageCache is only accessible through this accessor.
-      // It will make sure that only one user will fill load data and fill
-      // the cache if multiple users try to access the same item at the same time.
-      // This scenario happens a lot when multiple workers from a viewer access 
-      // the same file.
-      class Accessor : public MemoryStringCache::Accessor
-      {
-        StorageCache& storageCache_;
-      public:
-        explicit Accessor(StorageCache& cache);
-
-        void Add(const std::string& uuid, 
-                 FileContentType contentType,
-                 const std::string& value);
-
-        void AddStartRange(const std::string& uuid, 
-                           FileContentType contentType,
-                           const std::string& value);
-
-        void Add(const std::string& uuid, 
-                 FileContentType contentType,
-                 const void* buffer,
-                 size_t size);
-
-        bool Fetch(std::string& value, 
-                   const std::string& uuid,
-                   FileContentType contentType);
-
-        bool FetchStartRange(std::string& value, 
-                             const std::string& uuid,
-                             FileContentType contentType,
-                             uint64_t end /* exclusive */);
-
-        bool FetchTranscodedInstance(std::string& value, 
-                                     const std::string& uuid,
-                                     DicomTransferSyntax targetSyntax);
-
-        void AddTranscodedInstance(const std::string& uuid,
-                                   DicomTransferSyntax targetSyntax,
-                                   const void* buffer,
-                                   size_t size);
-      };
-
-    private:
-      MemoryStringCache             cache_;
-      std::set<DicomTransferSyntax> subKeysTransferSyntax_;
-      boost::mutex                  subKeysMutex_;
-
-    public:
-      void SetMaximumSize(size_t size);
-
-      void Invalidate(const std::string& uuid,
-                      FileContentType contentType);
-
-      size_t GetCurrentSize() const;
-      
-      size_t GetNumberOfItems() const;
-
-    private:
-      void Add(const std::string& uuid, 
-               FileContentType contentType,
-               const std::string& value);
-
-      void AddStartRange(const std::string& uuid, 
-                         FileContentType contentType,
-                         const std::string& value);
-
-      void Add(const std::string& uuid, 
-               FileContentType contentType,
-               const void* buffer,
-               size_t size);
-
-      bool Fetch(std::string& value, 
-                 const std::string& uuid,
-                 FileContentType contentType);
-
-      bool FetchStartRange(std::string& value, 
-                           const std::string& uuid,
-                           FileContentType contentType,
-                           uint64_t end /* exclusive */);
-
-    };
-}
--- a/OrthancFramework/Sources/FileStorage/StorageRange.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageRange.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -245,6 +245,10 @@
       {
         range.SetStartInclusive(tmp);
       }
+      else
+      {
+        throw OrthancException(ErrorCode_BadRange);
+      }
     }
 
     if (!tokens[1].empty())
@@ -253,6 +257,10 @@
       {
         range.SetEndInclusive(tmp);
       }
+      else
+      {
+        throw OrthancException(ErrorCode_BadRange);
+      }
     }
 
     range.SanityCheck();
--- a/OrthancFramework/Sources/HttpServer/HttpOutput.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpOutput.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -377,7 +377,7 @@
 
 
   void HttpOutput::SendStatus(HttpStatus status,
-                              const char* message,
+                              const void* message,
                               size_t messageSize)
   {
     if (status == HttpStatus_301_MovedPermanently ||
--- a/OrthancFramework/Sources/HttpServer/HttpOutput.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpOutput.h	Wed Jul 15 11:42:31 2026 +0200
@@ -158,7 +158,7 @@
     }
 
     void SendStatus(HttpStatus status,
-		    const char* message,
+		    const void* message,
 		    size_t messageSize);
 
     void SendStatus(HttpStatus status);
--- a/OrthancFramework/Sources/IMemoryBuffer.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/IMemoryBuffer.h	Wed Jul 15 11:42:31 2026 +0200
@@ -40,6 +40,8 @@
     {
     }
 
+    virtual void CopyToString(std::string& target) const = 0;
+
     // The content of the memory buffer will emptied after this call
     virtual void MoveToString(std::string& target) = 0;
 
--- a/OrthancFramework/Sources/Images/ImageBuffer.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/Images/ImageBuffer.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -26,6 +26,7 @@
 #include "ImageBuffer.h"
 
 #include "../OrthancException.h"
+#include "../Constants.h"
 
 #include <boost/lexical_cast.hpp>
 #include <stdio.h>
@@ -49,6 +50,11 @@
       const uint64_t tmpPitch = static_cast<uint64_t>(GetBytesPerPixel()) * static_cast<uint64_t>(width_);
       const uint64_t size = tmpPitch * static_cast<uint64_t>(height_);
 
+      if (size > MAX_IMAGE_FRAME_SIZE) {
+        throw OrthancException(ErrorCode_BadFileFormat, std::string("Trying to create an ImageBuffer whose size (") + boost::lexical_cast<std::string>(size) + " bytes) is larger than the limit (" + boost::lexical_cast<std::string>(MAX_IMAGE_FRAME_SIZE) + " bytes)");
+      }
+
+
       if (static_cast<uint64_t>(static_cast<unsigned int>(tmpPitch)) != tmpPitch ||
           static_cast<uint64_t>(static_cast<size_t>(size)) != size)
       {
--- a/OrthancFramework/Sources/JobsEngine/JobsEngine.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobsEngine.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -140,6 +140,8 @@
         CLOG(INFO, JOBS) << "Executing " << jobType << " job with priority " << running.GetPriority()
                          << " in worker thread " << workerIndex << ": " << running.GetId();
 
+        Logging::ScopedCurrentThreadContextSetter logContext(std::string("job ") + running.GetId());
+
         while (engine->IsRunning())
         {
           if (!engine->ExecuteStep(running, workerIndex))
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -114,6 +114,8 @@
       pauseScheduled_(false),
       cancelScheduled_(false)
     {
+      Logging::ScopedCurrentThreadContextSetter logContext(std::string("job ") + id_);
+      
       if (job == NULL)
       {
         throw OrthancException(ErrorCode_NullPointer);
--- a/OrthancFramework/Sources/Logging.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/Logging.h	Wed Jul 15 11:42:31 2026 +0200
@@ -205,7 +205,6 @@
       ~ScopedCurrentThreadContextSetter();
     };
 
-
     struct ORTHANC_LOCAL NullStream : public std::ostream
     {
       NullStream() : 
--- a/OrthancFramework/Sources/MallocMemoryBuffer.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/MallocMemoryBuffer.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -102,6 +102,17 @@
   }
 
     
+  void MallocMemoryBuffer::CopyToString(std::string& target) const
+  {
+    target.resize(size_);
+
+    if (size_ != 0)
+    {
+      memcpy(&target[0], buffer_, size_);
+    }
+  }
+
+
   void MallocMemoryBuffer::MoveToString(std::string& target)
   {
     target.resize(size_);
--- a/OrthancFramework/Sources/MallocMemoryBuffer.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/MallocMemoryBuffer.h	Wed Jul 15 11:42:31 2026 +0200
@@ -53,6 +53,8 @@
                 uint64_t size,
                 FreeFunction freeFunction);
     
+    virtual void CopyToString(std::string& target) const ORTHANC_OVERRIDE;
+
     virtual void MoveToString(std::string& target) ORTHANC_OVERRIDE;
 
     virtual const void* GetData() const ORTHANC_OVERRIDE
--- a/OrthancFramework/Sources/MultiThreading/Mutex.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/MultiThreading/Mutex.h	Wed Jul 15 11:42:31 2026 +0200
@@ -47,6 +47,10 @@
       explicit ScopedLock(Mutex& mutex)
       {
       }
+
+      void Unlock()
+      {
+      }
     };
   };
 
@@ -68,6 +72,11 @@
         lock_(mutex.mutex_)
       {
       }
+
+      void Unlock()
+      {
+        lock_.unlock();
+      }
     };
   };
 #endif
--- a/OrthancFramework/Sources/MultiThreading/ThreadPool.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/MultiThreading/ThreadPool.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -27,6 +27,8 @@
 
 #include "../Logging.h"
 #include "../OrthancException.h"
+#include "../MetricsRegistry.h"
+#include "../Toolbox.h"
 #include "FutureState.h"
 
 #include <boost/lexical_cast.hpp>
@@ -38,6 +40,15 @@
 
 namespace Orthanc
 {
+  void ThreadPool::SetMetricsConfiguration(MetricsRegistry& metrics,
+                                           const std::string& availableThreadsMetricName)
+  {
+    availableThreadsMetrics_.reset(new MetricsRegistry::SharedMetrics(metrics,
+                                                                      availableThreadsMetricName,
+                                                                      MetricsUpdatePolicy_MinOver10Seconds));
+    availableThreadsMetrics_->SetInitialValue(countThreads_);
+  }
+
   class ThreadPool::ITask : public IDynamicObject
   {
   public:
@@ -54,24 +65,28 @@
   class ThreadPool::CallableTask : public ITask
   {
   private:
-    std::unique_ptr<ICallable>               callable_;
-    boost::weak_ptr<Internals::FutureState>  state_;
+    std::unique_ptr<ICallable>                      callable_;
+    boost::weak_ptr<Internals::FutureState>         state_;
+    std::unique_ptr<Logging::ThreadContextMemento>  callerMemento_;
 
   public:
     CallableTask(ICallable* callable,
                  boost::shared_ptr<Internals::FutureState>& state) :
       callable_(callable),
-      state_(state)
+      state_(state),
+      callerMemento_(Logging::CreateCurrentThreadContextMemento())
     {
       assert(callable != NULL);
     }
 
-    void Execute() ORTHANC_OVERRIDE
+    virtual void Execute() ORTHANC_OVERRIDE
     {
       boost::shared_ptr<Internals::FutureState> locked = state_.lock();
 
       if (locked)
       {
+        Logging::ThreadContextMemento::ScopedSetter setter(*callerMemento_);
+
         try
         {
           locked->AcquireResult(callable_->Call());
@@ -91,7 +106,7 @@
       }
     }
 
-    void Cancel() ORTHANC_OVERRIDE
+    virtual void Cancel() ORTHANC_OVERRIDE
     {
       boost::shared_ptr<Internals::FutureState> locked = state_.lock();
 
@@ -110,21 +125,24 @@
   class ThreadPool::RunnableTask : public ITask
   {
   private:
-    std::unique_ptr<IRunnable>  runnable_;
+    std::unique_ptr<IRunnable>                      runnable_;
+    std::unique_ptr<Logging::ThreadContextMemento>  callerMemento_;
 
   public:
     explicit RunnableTask(IRunnable* runnable) :
-      runnable_(runnable)
+      runnable_(runnable),
+      callerMemento_(Logging::CreateCurrentThreadContextMemento())
     {
       assert(runnable != NULL);
     }
 
-    void Execute() ORTHANC_OVERRIDE
+    virtual void Execute() ORTHANC_OVERRIDE
     {
+      Logging::ThreadContextMemento::ScopedSetter setter(*callerMemento_);
       runnable_->Run();
     }
 
-    void Cancel() ORTHANC_OVERRIDE
+    virtual void Cancel() ORTHANC_OVERRIDE
     {
     }
   };
@@ -233,9 +251,16 @@
 
       if (task.get() != NULL)
       {
+        std::unique_ptr<MetricsRegistry::AvailableResourcesDecounter> decounter; // mark this thread as being used in the metrics
+        if (availableThreadsMetrics_.get())
+        {
+          decounter.reset(new MetricsRegistry::AvailableResourcesDecounter(*availableThreadsMetrics_));
+        }
+
         try
         {
-          dynamic_cast<ITask&>(*task).Execute();
+          ITask& iTask = dynamic_cast<ITask&>(*task);
+          iTask.Execute();
         }
         catch (const OrthancException& e)
         {
@@ -280,7 +305,7 @@
   }
 
 
-  void ThreadPool::SetCountThreads(unsigned int count)
+  void ThreadPool::SetThreadsCount(unsigned int count)
   {
     if (count < 1)
     {
@@ -302,7 +327,7 @@
   }
 
 
-  unsigned int ThreadPool::GetCountThreads()
+  unsigned int ThreadPool::GetThreadsCount()
   {
     boost::mutex::scoped_lock lock(mutex_);
     return countThreads_;
--- a/OrthancFramework/Sources/MultiThreading/ThreadPool.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/MultiThreading/ThreadPool.h	Wed Jul 15 11:42:31 2026 +0200
@@ -27,6 +27,7 @@
 #include "../Compatibility.h"
 #include "IExecutorService.h"
 #include "SharedMessageQueue.h"
+#include "../MetricsRegistry.h"
 
 #include <boost/thread.hpp>
 
@@ -54,6 +55,8 @@
     State                                 state_;
     unsigned int                          dequeueTimeoutMilliseconds_;
 
+    std::unique_ptr<MetricsRegistry::SharedMetrics> availableThreadsMetrics_;
+
     template <bool throws>
     void StopInternal();
 
@@ -66,14 +69,17 @@
 
     void SetLoggingThreadName(const std::string& name);
 
-    void SetCountThreads(unsigned int count);
+    void SetThreadsCount(unsigned int count);
 
-    unsigned int GetCountThreads();
+    unsigned int GetThreadsCount();
 
     void SetDequeueTimeout(unsigned int milliseconds);
 
     unsigned int GetDequeueTimeout();
 
+    void SetMetricsConfiguration(MetricsRegistry& metrics,
+                                 const std::string& availableThreadsMetricName);
+
     void Start();
 
     virtual Future* Submit(ICallable* callable /* takes ownership */) ORTHANC_OVERRIDE;
--- a/OrthancFramework/Sources/StringMemoryBuffer.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/StringMemoryBuffer.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -25,9 +25,17 @@
 #include "PrecompiledHeaders.h"
 #include "StringMemoryBuffer.h"
 
+#include "OrthancException.h"
+
 
 namespace Orthanc
 {
+  void StringMemoryBuffer::CopyToString(std::string& target) const
+  {
+    target = buffer_;
+  }
+
+
   void StringMemoryBuffer::MoveToString(std::string& target)
   {
     buffer_.swap(target);
@@ -59,4 +67,21 @@
     result->Copy(buffer, start, end);
     return result.release();
   }
+
+
+  IMemoryBuffer* StringMemoryBuffer::CreateFromBuffer(const void* data,
+                                                      size_t size)
+  {
+    if (size != 0 &&
+        data == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+      std::string s;
+      s.assign(reinterpret_cast<const char*>(data), size);
+      return CreateFromSwap(s);
+    }
+  }
 }
--- a/OrthancFramework/Sources/StringMemoryBuffer.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/StringMemoryBuffer.h	Wed Jul 15 11:42:31 2026 +0200
@@ -50,6 +50,8 @@
       buffer_.swap(buffer);
     }
 
+    virtual void CopyToString(std::string& target) const ORTHANC_OVERRIDE;
+
     virtual void MoveToString(std::string& target) ORTHANC_OVERRIDE;
 
     virtual const void* GetData() const ORTHANC_OVERRIDE
@@ -67,5 +69,8 @@
     static IMemoryBuffer* CreateFromCopy(const std::string& buffer);
 
     static IMemoryBuffer* CreateFromCopy(const std::string& buffer, size_t start /* inclusive */, size_t end /* exclusive */);
+
+    static IMemoryBuffer* CreateFromBuffer(const void* data,
+                                           size_t size);
   };
 }
--- a/OrthancFramework/Sources/Toolbox.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/Sources/Toolbox.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -3118,7 +3118,7 @@
     if (sizeInBytes < 1024)
     {
       std::ostringstream oss;
-      oss << sizeInBytes << "bytes";
+      oss << sizeInBytes << " bytes";
       return oss.str();
     }
     else
--- a/OrthancFramework/UnitTestsSources/FileStorageTests.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -29,10 +29,13 @@
 
 #include <gtest/gtest.h>
 
+#if defined(__GNUC__) || defined(__clang__)
+#  pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+#endif
+
 #include "../Sources/FileStorage/FilesystemStorage.h"
 #include "../Sources/FileStorage/PluginStorageAreaAdapter.h"
 #include "../Sources/FileStorage/StorageAccessor.h"
-#include "../Sources/FileStorage/StorageCache.h"
 #include "../Sources/Logging.h"
 #include "../Sources/OrthancException.h"
 #include "../Sources/Toolbox.h"
@@ -196,8 +199,7 @@
 TEST(StorageAccessor, NoCompression)
 {
   PluginStorageAreaAdapter s(new FilesystemStorage("UnitTestsStorage"));
-  StorageCache cache;
-  StorageAccessor accessor(s, cache);
+  StorageAccessor accessor(s);
 
   const std::string data = "Hello world";
   FileInfo info;
@@ -219,8 +221,7 @@
 TEST(StorageAccessor, Compression)
 {
   PluginStorageAreaAdapter s(new FilesystemStorage("UnitTestsStorage"));
-  StorageCache cache;
-  StorageAccessor accessor(s, cache);
+  StorageAccessor accessor(s);
 
   const std::string data = "Hello world";
   FileInfo info;
@@ -241,8 +242,7 @@
 TEST(StorageAccessor, Mix)
 {
   PluginStorageAreaAdapter s(new FilesystemStorage("UnitTestsStorage"));
-  StorageCache cache;
-  StorageAccessor accessor(s, cache);
+  StorageAccessor accessor(s);
 
   const std::string compressedData = "Hello";
   const std::string uncompressedData = "HelloWorld";
--- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -37,6 +37,10 @@
 
 #include <gtest/gtest.h>
 
+#if defined(__GNUC__) || defined(__clang__)
+#  pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+#endif
+
 #include "../Sources/Compatibility.h"
 #include "../Sources/DicomFormat/DicomImageInformation.h"
 #include "../Sources/DicomFormat/DicomPath.h"
--- a/OrthancFramework/UnitTestsSources/JobsTests.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/UnitTestsSources/JobsTests.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -1843,10 +1843,10 @@
 TEST(ThreadPool, Basic)
 {
   ThreadPool pool;
-  ASSERT_THROW(pool.SetCountThreads(0), OrthancException);
+  ASSERT_THROW(pool.SetThreadsCount(0), OrthancException);
   ASSERT_THROW(pool.SetDequeueTimeout(0), OrthancException);
   pool.SetDequeueTimeout(1);  // Ensure fast shutdown in unit tests
-  ASSERT_EQ(1u, pool.GetCountThreads());
+  ASSERT_EQ(1u, pool.GetThreadsCount());
   ASSERT_EQ(1u, pool.GetDequeueTimeout());
 
   pool.Start();
--- a/OrthancFramework/UnitTestsSources/MemoryCacheTests.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/UnitTestsSources/MemoryCacheTests.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -29,6 +29,10 @@
 
 #include <gtest/gtest.h>
 
+#if defined(__GNUC__) || defined(__clang__)
+#  pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+#endif
+
 #include "../Sources/Cache/MemoryCache.h"
 #include "../Sources/Cache/MemoryStringCache.h"
 #include "../Sources/Cache/SharedArchive.h"
@@ -399,6 +403,8 @@
 
 void ThreadingScenarioHappyThread1()
 {
+  Orthanc::Logging::ScopedCurrentThreadNameSetter setter("MEM-TEST-1");
+
   // the first thread to call Fetch (will be in charge of adding)
   Orthanc::MemoryStringCache::Accessor a(ThreadingScenarioHappyCache);
   std::string v;
@@ -426,6 +432,8 @@
 
 void ThreadingScenarioHappyThread2()
 {
+  Orthanc::Logging::ScopedCurrentThreadNameSetter setter("MEM-TEST-2");
+
   Orthanc::MemoryStringCache::Accessor a(ThreadingScenarioHappyCache);
   std::string v;
 
@@ -468,6 +476,8 @@
 
 void ThreadingScenarioFailureThread1()
 {
+  Orthanc::Logging::ScopedCurrentThreadNameSetter setter("MEM-TEST-1");
+
   // the first thread to call Fetch (will be in charge of adding)
   Orthanc::MemoryStringCache::Accessor a(ThreadingScenarioFailureCache);
   std::string v;
@@ -491,6 +501,8 @@
 
 void ThreadingScenarioFailureThread2()
 {
+  Orthanc::Logging::ScopedCurrentThreadNameSetter setter("MEM-TEST-2");
+
   Orthanc::MemoryStringCache::Accessor a(ThreadingScenarioFailureCache);
   std::string v;
 
@@ -527,6 +539,8 @@
 
 void ThreadingScenarioInvalidateThread1()
 {
+  Orthanc::Logging::ScopedCurrentThreadNameSetter setter("MEM-TEST-1");
+
   // the first thread to call Fetch (will be in charge of adding)
   Orthanc::MemoryStringCache::Accessor a(ThreadingScenarioInvalidateCache);
   std::string v;
@@ -551,6 +565,8 @@
 
 void ThreadingScenarioInvalidateThread2()
 {
+  Orthanc::Logging::ScopedCurrentThreadNameSetter setter("MEM-TEST-2");
+
   Orthanc::MemoryStringCache::Accessor a(ThreadingScenarioInvalidateCache);
   std::string v;
 
--- a/OrthancFramework/UnitTestsSources/ToolboxTests.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancFramework/UnitTestsSources/ToolboxTests.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -380,7 +380,7 @@
 
 TEST(Toolbox, GetHumanFileSize)
 {
-  ASSERT_EQ("234bytes", Toolbox::GetHumanFileSize(234));
+  ASSERT_EQ("234 bytes", Toolbox::GetHumanFileSize(234));
   ASSERT_EQ("2.29KB", Toolbox::GetHumanFileSize(2345));
   ASSERT_EQ("22.91KB", Toolbox::GetHumanFileSize(23456));
   ASSERT_EQ("229.07KB", Toolbox::GetHumanFileSize(234567));
@@ -491,13 +491,6 @@
   ASSERT_EQ(MEGABYTE * static_cast<int16_t>(16 * 1024), 16ull * 1024ull * 1024ull * 1024ull);
   ASSERT_EQ(static_cast<int32_t>(16 * 1024 * 1024) * KILOBYTE, 16ull * 1024ull * 1024ull * 1024ull);
   ASSERT_EQ(KILOBYTE * static_cast<int32_t>(16 * 1024 * 1024), 16ull * 1024ull * 1024ull * 1024ull);
-
-  ASSERT_EQ(8, sizeof(static_cast<int16_t>(16) * GIGABYTE));
-  ASSERT_EQ(8, sizeof(GIGABYTE * static_cast<int16_t>(16)));
-  ASSERT_EQ(8, sizeof(static_cast<int16_t>(16 * 1024) * MEGABYTE));
-  ASSERT_EQ(8, sizeof(MEGABYTE * static_cast<int16_t>(16 * 1024)));
-  ASSERT_EQ(8, sizeof(static_cast<int32_t>(16 * 1024 * 1024) * KILOBYTE));
-  ASSERT_EQ(8, sizeof(KILOBYTE * static_cast<int32_t>(16 * 1024 * 1024)));
 }
 
 
--- a/OrthancServer/CMakeLists.txt	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/CMakeLists.txt	Wed Jul 15 11:42:31 2026 +0200
@@ -163,7 +163,6 @@
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/SplitStudyJob.cpp
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/StorageCommitmentScpJob.cpp
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/StoreJob.cpp
-  ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/ThreadedInstancesLoader.cpp
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp  
   ${CMAKE_SOURCE_DIR}/Sources/ServerToolbox.cpp
   ${CMAKE_SOURCE_DIR}/Sources/ServerTranscoder.cpp
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -35,6 +35,8 @@
 
 #include "../../../OrthancFramework/Sources/Compression/GzipCompressor.h"
 #include "../../../OrthancFramework/Sources/Compression/ZlibCompressor.h"
+#include "../../../OrthancFramework/Sources/DataSource/DicomDataSource.h"
+#include "../../../OrthancFramework/Sources/DataSource/StorageAreaDataSource.h"
 #include "../../../OrthancFramework/Sources/DicomFormat/DicomArray.h"
 #include "../../../OrthancFramework/Sources/DicomNetworking/DicomConnectionInfo.h"
 #include "../../../OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h"
@@ -65,6 +67,7 @@
 #include "../../Sources/Search/HierarchicalMatcher.h"
 #include "../../Sources/ServerContext.h"
 #include "../../Sources/ServerToolbox.h"
+#include "../../Sources/ServerTranscoder.h"
 #include "OrthancPluginDatabase.h"
 #include "OrthancPluginDatabaseV3.h"
 #include "OrthancPluginDatabaseV4.h"
@@ -3546,14 +3549,14 @@
     const _OrthancPluginGetDicomForInstance& p = 
       *reinterpret_cast<const _OrthancPluginGetDicomForInstance*>(parameters);
 
-    std::string dicom;
+    std::unique_ptr<StorageAreaDataSource::Range> raw;
 
     {
       PImpl::ServerContextReference lock(*pimpl_);
-      lock.GetContext().ReadDicom(dicom, p.instanceId);
-    }
-
-    CopyToMemoryBuffer(p.target, dicom);
+      raw.reset(lock.GetContext().ReadRawDicom(p.instanceId));
+    }
+
+    CopyToMemoryBuffer(p.target, raw->GetData(), raw->GetSize());
   }
 
   static void ThrowOnHttpError(HttpStatus httpStatus)
@@ -4040,7 +4043,7 @@
         std::unique_ptr<ImageAccessor> decoded;
         {
           PImpl::ServerContextReference lock(*pimpl_);
-          decoded.reset(lock.GetContext().DecodeDicomFrame(instance, p.frameIndex));
+          decoded.reset(lock.GetContext().GetTranscoder()->DecodeFrame(instance, p.frameIndex));
         }
         
         *(p.targetImage) = ReturnImage(decoded);
@@ -4154,8 +4157,14 @@
 
       case OrthancPluginImageFormat_Dicom:
       {
-        PImpl::ServerContextReference lock(*pimpl_);
-        image.reset(lock.GetContext().DecodeDicomFrame(p.data, p.size, 0));
+        std::unique_ptr<DicomInstanceToStore> instance(DicomInstanceToStore::CreateFromBuffer(p.data, p.size));
+
+        {
+          PImpl::ServerContextReference lock(*pimpl_);
+          image.reset(lock.GetContext().GetTranscoder()->DecodeFrame(
+                        *instance, 0 /* frame index is not available in _OrthancPluginUncompressImage */));
+        }
+
         break;
       }
 
@@ -4700,14 +4709,11 @@
         throw OrthancException(ErrorCode_NullPointer);
       }
 
-      std::string content;
-
       {
         PImpl::ServerContextReference lock(*pimpl_);
-        lock.GetContext().ReadDicom(content, p.instanceId);
-      }
-
-      dicom.reset(new ParsedDicomFile(content));
+        std::unique_ptr<DicomDataSource::Dicom> loaded(lock.GetContext().ReadParsedDicom(p.instanceId));
+        dicom.reset(loaded->Clone());
+      }
     }
 
     Json::Value json;
@@ -4832,8 +4838,13 @@
 
       case _OrthancPluginService_DecodeDicomImage:
       {
-        PImpl::ServerContextReference lock(*pimpl_);
-        result.reset(lock.GetContext().DecodeDicomFrame(p.constBuffer, p.bufferSize, p.frameIndex));
+        std::unique_ptr<DicomInstanceToStore> instance(DicomInstanceToStore::CreateFromBuffer(p.constBuffer, p.bufferSize));
+
+        {
+          PImpl::ServerContextReference lock(*pimpl_);
+          result.reset(lock.GetContext().GetTranscoder()->DecodeFrame(*instance, p.frameIndex));
+        }
+
         break;
       }
 
@@ -5128,14 +5139,14 @@
     {
       case OrthancPluginLoadDicomInstanceMode_WholeDicom:
       {
-        std::string buffer;
+        std::unique_ptr<StorageAreaDataSource::Range> raw;
 
         {
           PImpl::ServerContextReference lock(*pimpl_);
-          lock.GetContext().ReadDicom(buffer, params.instanceId);
-        }
-
-        target.reset(new DicomInstanceFromBuffer(buffer));
+          raw.reset(lock.GetContext().ReadRawDicom(params.instanceId));
+        }
+
+        target.reset(new DicomInstanceFromBuffer(raw->GetData(), raw->GetSize()));
         break;
       }
         
@@ -5145,17 +5156,9 @@
         std::unique_ptr<ParsedDicomFile> parsed;
 
         {
-          std::string buffer;
-        
-          {
-            PImpl::ServerContextReference lock(*pimpl_);
-            if (!lock.GetContext().ReadDicomUntilPixelData(buffer, params.instanceId))
-            {
-              lock.GetContext().ReadDicom(buffer, params.instanceId);
-            }
-          }
-
-          parsed.reset(new ParsedDicomFile(buffer));
+          PImpl::ServerContextReference lock(*pimpl_);
+          std::unique_ptr<DicomDataSource::Dicom> loaded(lock.GetContext().ReadParsedDicom(params.instanceId));
+          parsed.reset(loaded->Clone());
         }
 
         parsed->RemoveFromPixelData();
@@ -6085,7 +6088,7 @@
 
           {
             PImpl::ServerContextReference lock(*pimpl_);
-            success = lock.GetContext().GetTranscoder().Transcode(transcoded, source, syntaxes, TranscodingSopInstanceUidMode_AllowNew);
+            success = lock.GetContext().GetTranscoder()->Transcode(transcoded, source, syntaxes, TranscodingSopInstanceUidMode_AllowNew);
           }
 
           if (success)
@@ -6893,7 +6896,7 @@
 
   ImageAccessor* OrthancPlugins::Decode(const void* dicom,
                                         size_t size,
-                                        unsigned int frame)
+                                        unsigned int frame) const
   {
     boost::shared_lock<boost::shared_mutex> lock(pimpl_->decoderTranscoderMutex_);
 
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h	Wed Jul 15 11:42:31 2026 +0200
@@ -53,7 +53,6 @@
 #include "../../../OrthancFramework/Sources/JobsEngine/IJob.h"
 #include "../../../OrthancFramework/Sources/MallocMemoryBuffer.h"
 #include "../../Sources/Database/IDatabaseWrapper.h"
-#include "../../Sources/IDicomImageDecoder.h"
 #include "../../Sources/IServerListener.h"
 #include "../../Sources/ServerJobs/IStorageCommitmentFactory.h"
 #include "PluginMemoryBuffer64.h"
@@ -73,7 +72,6 @@
     public IPluginServiceProvider, 
     public IServerListener,
     public IWorklistRequestHandlerFactory,
-    public IDicomImageDecoder,
     public IFindRequestHandlerFactory,
     public IMoveRequestHandlerFactory,
     public IStorageCommitmentFactory,
@@ -394,9 +392,9 @@
 
     bool HasCustomTranscoder();
 
-    virtual ImageAccessor* Decode(const void* dicom,
-                                  size_t size,
-                                  unsigned int frame) ORTHANC_OVERRIDE;
+    ImageAccessor* Decode(const void* dicom,
+                          size_t size,
+                          unsigned int frame) const;
 
     bool IsAllowed(HttpMethod method,
                    const char* uri,
--- a/OrthancServer/Plugins/Engine/PluginMemoryBuffer32.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer32.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -61,6 +61,19 @@
   }
 
 
+  void PluginMemoryBuffer32::CopyToString(std::string& target) const
+  {
+    SanityCheck();
+
+    target.resize(buffer_.size);
+
+    if (buffer_.size != 0)
+    {
+      memcpy(&target[0], buffer_.data, buffer_.size);
+    }
+  }
+
+
   void PluginMemoryBuffer32::MoveToString(std::string& target)
   {
     SanityCheck();
--- a/OrthancServer/Plugins/Engine/PluginMemoryBuffer32.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer32.h	Wed Jul 15 11:42:31 2026 +0200
@@ -49,6 +49,8 @@
       Clear();
     }
 
+    virtual void CopyToString(std::string& target) const ORTHANC_OVERRIDE;
+
     virtual void MoveToString(std::string& target) ORTHANC_OVERRIDE;
 
     virtual const void* GetData() const ORTHANC_OVERRIDE;
--- a/OrthancServer/Plugins/Engine/PluginMemoryBuffer64.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer64.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -58,6 +58,19 @@
   }
 
 
+  void PluginMemoryBuffer64::CopyToString(std::string& target) const
+  {
+    SanityCheck();
+
+    target.resize(buffer_.size);
+
+    if (buffer_.size != 0)
+    {
+      memcpy(&target[0], buffer_.data, buffer_.size);
+    }
+  }
+
+
   void PluginMemoryBuffer64::MoveToString(std::string& target)
   {
     SanityCheck();
--- a/OrthancServer/Plugins/Engine/PluginMemoryBuffer64.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer64.h	Wed Jul 15 11:42:31 2026 +0200
@@ -49,6 +49,8 @@
       Clear();
     }
 
+    virtual void CopyToString(std::string& target) const ORTHANC_OVERRIDE;
+
     virtual void MoveToString(std::string& target) ORTHANC_OVERRIDE;
 
     virtual const void* GetData() const ORTHANC_OVERRIDE;
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Wed Jul 15 11:42:31 2026 +0200
@@ -9997,10 +9997,15 @@
    * must only be called from threads that the plugin has created
    * itself.
    *
+   * At the end of the lifetime of the thread,
+   * OrthancPluginClearCurrentThreadName() must be called to release
+   * the allocated resources.
+   *
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param threadName The name of the current thread. A thread name cannot be longer than 16 characters.
    * @return 0 if success, other value if error.
    * @ingroup Toolbox
+   * @see OrthancPluginClearCurrentThreadName()
    **/
   ORTHANC_PLUGIN_SINCE_SDK("1.12.2")
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSetCurrentThreadName(
@@ -11201,13 +11206,16 @@
   /**
    * @brief Clear the name of the current thread.
    *
-   * This function releases the resources allocated to store the threa
-   * name.  This function must only be called from threads that the plugin 
-   * has created itself when the thread is reaching its end of life.
+   * This function releases the resources allocated to store the name
+   * of the current thread, after a call to
+   * "OrthancPluginSetCurrentThreadName()". This function must only be
+   * called from threads that the plugin has created itself when the
+   * thread is reaching its end of life.
    *
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @return 0 if success, other value if error.
    * @ingroup Toolbox
+   * @see OrthancPluginSetCurrentThreadName()
    **/
   ORTHANC_PLUGIN_SINCE_SDK("1.12.12")
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginClearCurrentThreadName(
--- a/OrthancServer/Resources/Configuration.json	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Resources/Configuration.json	Wed Jul 15 11:42:31 2026 +0200
@@ -64,6 +64,24 @@
   // is disabled.  (new in Orthanc 1.10.0)
   "MaximumStorageCacheSize" : 128,
 
+  // Amount of RAM (in MB) that are allocated to the storage loader threads.
+  // Note that this limit can be overpassed when a single file 
+  // exceeds this size.
+  // (new in Orthanc 1.12.11+).
+  // You should monitor the "orthanc_storage_memory_usage_mb" and
+  // "orthanc_storage_memory_max_usage_mb" metrics to determine the 
+  // optimal value for your setup. 
+  "StorageMemoryCapacity": 512,
+
+  // Number of threads that are used to read files from the storage.
+  // (new in Orthanc 1.12.11+)
+  // A value > 4 is meaningful only if the storage is a distributed 
+  // network storage (e.g., object storage plugin).  In this case,
+  // you can typically increase it to 20 and you should then monitor 
+  // the "orthanc_storage_available_threads" metrics to determine 
+  // the optimal value for your setup.
+  "StorageLoaderThreads": 4,
+
   // List of paths to the custom Lua scripts that are to be loaded
   // into this instance of Orthanc
   "LuaScripts" : [
@@ -1136,5 +1154,86 @@
   // might be assigned to different patients because there is no
   // centralized patient ID management. (new in Orthanc 1.12.11)
   // Warning: As of 1.12.11, this is an experimental configuration.
-  "PatientLevelEnabled": true
+  "PatientLevelEnabled": true,
+
+
+
+  // -> TODO-Streaming: move this into AdvancedConfiguration.json
+
+  // add a page in the book: performance: fine tuning: limiting memory + increasing the number of threads blah blah blah
+
+
+  // Number of threads that are used to parse DICOM files (when you need
+  // to access the value of DICOM tags that are not store in DB).
+  // (new in Orthanc 1.12.11+)
+  // You should monitor the "orthanc_dicom_parser_available_threads" 
+  // metrics to determine the optimal value for your setup and usage.
+  "DicomParserThreads": 2,
+
+  // Amount of RAM (in MB) that are allocated to the DICOM parser threads.
+  // Note that this limit can be overpassed when a single file 
+  // exceeds this size.
+  // (new in Orthanc 1.12.11+).
+  // You should monitor the "orthanc_dicom_parser_memory_usage_mb" and
+  // "orthanc_dicom_parser_memory_max_usage_mb" metrics to determine the 
+  // optimal value for your setup and usage. 
+  "DicomParserMemoryCapacity": 256,
+
+  // Maximum size of the DICOM parser cache in MB.  This cache
+  // is stored in RAM and contains a parsed version of recently read
+  // DIOCM files.  A value of "0" indicates the cache
+  // is disabled.  (new in Orthanc 1.12.11+)
+  "DicomParserCacheSize" : 128,
+
+  // Number of threads that are used to parse transcode DICOM files.
+  // (new in Orthanc 1.12.11+)
+  // You should monitor the "orthanc_transcoder_available_threads" 
+  // metrics to determine the optimal value for your setup and usage.
+  "TranscoderThreads": 4,
+
+  // Amount of RAM (in MB) that are allocated to the transcoder threads.
+  // Note that this limit can be overpassed when a single file 
+  // exceeds this size.
+  // (new in Orthanc 1.12.11+).
+  // You should monitor the "orthanc_transcoder_memory_usage_mb" and
+  // "orthanc_transcoder_memory_max_usage_mb" metrics to determine the 
+  // optimal value for your setup and usage. 
+  "TranscoderMemoryCapacity": 256,
+
+  // Maximum size of the transcoder cache in MB.  This cache
+  // is stored in RAM and contains transcoded versions of recently read
+  // DIOCM files.  A value of "0" indicates the cache
+  // is disabled.  (new in Orthanc 1.12.11+)
+  "TranscoderCacheSize" : 128,
+
+  // Number of threads that are used to provide DICOM files sequentially
+  // (currently used in Archive jobs, C-Store and C-Get).
+  // Each time such a job needs to get a DICOM file content, it requests it from
+  // a sequential DICOM reader that will itself use a s torage loader thread.
+  // Therefore, by default, the value of this configuration is identical to
+  // "StorageLoaderThreads".
+  // (new in Orthanc 1.13.0)
+  // You should monitor the "orthanc_seq_reader_available_threads" 
+  // metrics to determine the optimal value for your setup and usage.
+  // "SequentialDicomReaderThreads": 4,
+
+  // Number of files each sequential DICOM reader is allowed to buffer
+  // in advance before it is consumed by the related job.
+  // This is kind of equivalent to the older "LoaderThreads" configuration
+  // but, furthermore, you must make sure that there are enough "StorageLoaderThreads"
+  // and "SequentialDicomReaderThreads" for each job -> 
+  // "StorageLoaderThreads" should be larger than "ConcurrentJobs" * "SequentialDicomReaderWindowCapacity"
+  // Together with "SequentialDicomReaderWindowCapacity", this also defines the amount of memory that is
+  // used by each job that required a SequentialDicomReader.
+  "SequentialDicomReaderWindowSize": 4,
+  
+  // Amount of RAM (in MB) that are allocated to each sequential DICOM
+  // Reader.  The sequential DICOM reader stops pauses buffering as soon
+  // as one of the "SequentialDicomReaderWindowSize" or 
+  // "SequentialDicomReaderWindowCapacity" limit is reached.
+  // Note that this limit can be overpassed when a single file 
+  // exceeds this size.
+  // (new in Orthanc 1.13.0).
+  "SequentialDicomReaderWindowCapacity": 64
+
 }
--- a/OrthancServer/Resources/ImplementationNotes/JobsEngineClasses.txt	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Resources/ImplementationNotes/JobsEngineClasses.txt	Wed Jul 15 11:42:31 2026 +0200
@@ -22,6 +22,6 @@
     virtual bool HandleTrailingStep() = 0;
 
 
-class StoreJob : SetOfInstancesJob   (contains a ThreadedInstancesLoader)
+class StoreJob : SetOfInstancesJob   (contains a DicomSequentialReader)
 
-Class ModalityStoreJob : StoreJob
\ No newline at end of file
+Class ModalityStoreJob : StoreJob
--- a/OrthancServer/Resources/RunCppCheck-2.1.sh	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Resources/RunCppCheck-2.1.sh	Wed Jul 15 11:42:31 2026 +0200
@@ -15,8 +15,8 @@
 assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:279
 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:292
 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:391
-assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3078
-assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:287
+assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3110
+assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:293
 constParameter:../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp
 knownArgument:../../OrthancFramework/UnitTestsSources/ImageTests.cpp
 knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp
@@ -26,18 +26,18 @@
 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:74
 stlFindInsert:../../OrthancServer/Sources/Database/MainDicomTagsRegistry.cpp:65
 stlFindInsert:../../OrthancServer/Sources/Database/ResourcesContent.h:141
-stlFindInsert:../../OrthancServer/Sources/OrthancWebDav.cpp:329
+stlFindInsert:../../OrthancServer/Sources/OrthancWebDav.cpp:333
 stlFindInsert:../../OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp:42
-stlFindInsert:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:362
+stlFindInsert:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:368
 stlFindInsert:../../OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp:192
 syntaxError:../../OrthancFramework/Sources/SQLite/FunctionContext.h:53
 syntaxError:../../OrthancFramework/UnitTestsSources/DicomMapTests.cpp:74
 syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:325
 unreadVariable:../../OrthancFramework/Sources/FileStorage/StorageAccessor.cpp
-unreadVariable:../../OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp:1173
-useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:92
+unreadVariable:../../OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp:1174
+useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:100
 useInitializationList:../../OrthancFramework/Sources/Images/PngWriter.cpp:99
-useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:275
+useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:277
 EOF
 
 CPPCHECK_BUILD_DIR=/tmp/cppcheck-build-dir-2.1/
--- a/OrthancServer/Resources/RunCppCheck-2.17.1.sh	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Resources/RunCppCheck-2.17.1.sh	Wed Jul 15 11:42:31 2026 +0200
@@ -12,24 +12,20 @@
 cat <<EOF > /tmp/cppcheck-suppressions.txt
 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:292
 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:391
-assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:287
+assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:293
 constParameterCallback:../../OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp:50
 constParameterCallback:../../OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp:112
 constParameterCallback:../../OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp:113
 constParameterCallback:../../OrthancFramework/Sources/Pkcs11.cpp:125
 constParameterCallback:../../OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp:3553
-constParameterCallback:../../OrthancServer/Sources/OrthancGetRequestHandler.cpp:48
+constParameterCallback:../../OrthancServer/Sources/OrthancGetRequestHandler.cpp:66
 constParameterPointer:../../OrthancFramework/Sources/Logging.cpp:450
 constParameterPointer:../../OrthancFramework/Sources/Logging.cpp:454
 constParameterPointer:../../OrthancFramework/Sources/Toolbox.cpp:3351
 knownConditionTrueFalse:../../OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp:112
-knownConditionTrueFalse:../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp:440
-knownConditionTrueFalse:../../OrthancFramework/Sources/Images/PamReader.cpp:204
-knownConditionTrueFalse:../../OrthancFramework/Sources/Images/PamReader.cpp:207
+knownConditionTrueFalse:../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp:441
+knownConditionTrueFalse:../../OrthancFramework/Sources/Images/PamReader.cpp:208
 knownConditionTrueFalse:../../OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp:345
-knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2444
-knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2445
-knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2446
 knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2447
 knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2448
 knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2449
@@ -37,6 +33,9 @@
 knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2451
 knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2452
 knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2453
+knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2454
+knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2455
+knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2456
 nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:321
 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:166
 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:74
@@ -45,11 +44,11 @@
 syntaxError:../../OrthancFramework/UnitTestsSources/DicomMapTests.cpp:74
 syntaxError:../../OrthancFramework/UnitTestsSources/ZipTests.cpp:133
 syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:325
-useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:99
+useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:100
 useInitializationList:../../OrthancFramework/Sources/Images/PngWriter.cpp:99
-useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:280
+useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:277
 variableScope:../../OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp:230
-variableScope:../../OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp:97
+variableScope:../../OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp:98
 EOF
 
 CPPCHECK_BUILD_DIR=/tmp/cppcheck-build-dir-2.17.1/
--- a/OrthancServer/Resources/RunCppCheck-2.20.0.sh	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Resources/RunCppCheck-2.20.0.sh	Wed Jul 15 11:42:31 2026 +0200
@@ -10,10 +10,10 @@
 fi
 
 cat <<EOF > /tmp/cppcheck-suppressions.txt
-assertWithSideEffect:../../OrthancFramework/Sources/DataSource/DataSourceReader.cpp:220
+assertWithSideEffect:../../OrthancFramework/Sources/DataSource/DataSourceReader.cpp:310
 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:292
 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:391
-assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:287
+assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:293
 constParameterPointer:../../OrthancFramework/Sources/Logging.cpp:450
 constParameterPointer:../../OrthancFramework/Sources/Logging.cpp:454
 constParameterPointer:../../OrthancFramework/Sources/Toolbox.cpp:3351
@@ -25,11 +25,11 @@
 syntaxError:../../OrthancFramework/Sources/SQLite/FunctionContext.h
 syntaxError:../../OrthancFramework/UnitTestsSources/DicomMapTests.cpp:74
 syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:325
-useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:99
+useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:100
 useInitializationList:../../OrthancFramework/Sources/Images/PngWriter.cpp:99
-useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:280
+useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:277
 variableScope:../../OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp:230
-variableScope:../../OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp:97
+variableScope:../../OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp:98
 EOF
 
 CPPCHECK_BUILD_DIR=/tmp/cppcheck-build-dir-2.20.0/
--- a/OrthancServer/Sources/IDicomImageDecoder.h	Wed Jul 08 11:45:33 2026 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,43 +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-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, 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 "../../OrthancFramework/Sources/Images/ImageAccessor.h"
-
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  class IDicomImageDecoder : public boost::noncopyable
-  {
-  public:
-    virtual ~IDicomImageDecoder()
-    {
-    }
-
-    virtual ImageAccessor* Decode(const void* dicom,
-                                  size_t size,
-                                  unsigned int frame) = 0;
-  };
-}
--- a/OrthancServer/Sources/OrthancConfiguration.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/OrthancConfiguration.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -1384,4 +1384,19 @@
       return loaderThreads;
     }
   }
+
+  unsigned int OrthancConfiguration::GetConcurrentJobs() const
+  {
+    return GetUnsignedIntegerParameter(ORTHANC_CONFIG_CONCURRENT_JOBS);
+  }
+
+  unsigned int OrthancConfiguration::GetHttpThreadsCount() const
+  {
+    return GetUnsignedIntegerParameter(ORTHANC_CONFIG_HTTP_THREADS_COUNT);
+  }
+
+  unsigned int OrthancConfiguration::GetDicomThreadsCount() const
+  {
+    return GetUnsignedIntegerParameter(ORTHANC_CONFIG_DICOM_THREADS_COUNT);
+  }
 }
--- a/OrthancServer/Sources/OrthancConfiguration.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/OrthancConfiguration.h	Wed Jul 15 11:42:31 2026 +0200
@@ -41,7 +41,6 @@
 #define ORTHANC_CONFIG_DICOM_AET "DicomAet"
 #define ORTHANC_CONFIG_DICOM_PORT "DicomPort"
 #define ORTHANC_CONFIG_HTTP_PORT "HttpPort"
-#define ORTHANC_CONFIG_MAXIMUM_STORAGE_CACHE_SIZE "MaximumStorageCacheSize"
 #define ORTHANC_CONFIG_MAXIMUM_STORAGE_SIZE "MaximumStorageSize"
 #define ORTHANC_CONFIG_MAXIMUM_STORAGE_MODE "MaximumStorageMode"
 #define ORTHANC_CONFIG_MAXIMUM_PATIENT_COUNT "MaximumPatientCount"
@@ -54,8 +53,25 @@
 #define ORTHANC_CONFIG_DICOM_DEFAULT_RETRIEVE_METHOD "DicomDefaultRetrieveMethod"
 #define ORTHANC_CONFIG_PATIENT_LEVEL_ENABLED "PatientLevelEnabled"
 #define ORTHANC_CONFIG_READ_ONLY "ReadOnly"
+#define ORTHANC_CONFIG_CONCURRENT_JOBS "ConcurrentJobs"
+#define ORTHANC_CONFIG_HTTP_THREADS_COUNT "HttpThreadsCount"
+#define ORTHANC_CONFIG_DICOM_THREADS_COUNT "DicomThreadsCount"
 #define ORTHANC_CONFIG_STORAGE_DIRECTORY "StorageDirectory"
 
+#define ORTHANC_CONFIG_STORAGE_LOADER_THREADS "StorageLoaderThreads"
+#define ORTHANC_CONFIG_STORAGE_MEMORY_CAPACITY "StorageMemoryCapacity"
+#define ORTHANC_CONFIG_MAXIMUM_STORAGE_CACHE_SIZE "MaximumStorageCacheSize"
+#define ORTHANC_CONFIG_DICOM_PARSER_SOURCE_THREADS "DicomParserThreads"
+#define ORTHANC_CONFIG_DICOM_PARSER_MEMORY_CAPACITY "DicomParserMemoryCapacity"
+#define ORTHANC_CONFIG_DICOM_PARSER_CACHE_SIZE "DicomParserCacheSize"
+#define ORTHANC_CONFIG_TRANSCODER_THREADS "TranscoderThreads"
+#define ORTHANC_CONFIG_TRANSCODER_MEMORY_CAPACITY "TranscoderMemoryCapacity"
+#define ORTHANC_CONFIG_TRANSCODER_CACHE_SIZE "TranscoderCacheSize"
+#define ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_THREADS "SequentialDicomReaderThreads"
+#define ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_WINDOW_SIZE "SequentialDicomReaderWindowSize"
+#define ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_WINDOW_CAPACITY "SequentialDicomReaderWindowCapacity"
+
+
 
 namespace Orthanc
 {
@@ -264,6 +280,12 @@
 
     unsigned int GetLoaderThreads() const;
 
+    unsigned int GetConcurrentJobs() const;
+
+    unsigned int GetHttpThreadsCount() const;
+
+    unsigned int GetDicomThreadsCount() const;
+
     void Format(std::string& result) const;
     
     void SetDefaultEncoding(Encoding encoding);
--- a/OrthancServer/Sources/OrthancGetRequestHandler.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/OrthancGetRequestHandler.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -30,7 +30,7 @@
 #include "OrthancConfiguration.h"
 #include "ServerContext.h"
 #include "ServerJobs/DicomModalityStoreJob.h"
-#include "ServerJobs/ThreadedInstancesLoader.h"
+#include "ServerTranscoder.h"
 
 #include <dcmtk/dcmdata/dcdeftag.h>
 #include <dcmtk/dcmdata/dcfilefo.h>
@@ -44,6 +44,24 @@
 
 namespace Orthanc
 {
+  class OrthancGetRequestHandler::ReaderUserData : public IDynamicObject
+  {
+  private:
+    std::string instanceId_;
+
+  public:
+    explicit ReaderUserData(const std::string& instanceId) :
+      instanceId_(instanceId)
+    {
+    }
+
+    const std::string& GetInstanceId() const
+    {
+      return instanceId_;
+    }
+  };
+
+
   static void ProgressCallback(void *callbackData,
                                T_DIMSE_StoreProgress *progress,
                                T_DIMSE_C_StoreRQ *req)
@@ -65,45 +83,55 @@
       throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
     
-    if (instancesLoader_.get() == NULL)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    const std::string& id = instancesIds_[position_++];
-
-    std::string dicom;
-    instancesLoader_->WaitDicomInstance(dicom, id);
-    
-    if (dicom.empty())
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-
-    std::unique_ptr<DcmFileFormat> parsed(
-      FromDcmtkBridge::LoadFromMemoryBuffer(dicom.c_str(), dicom.size()));
-
-    if (parsed.get() == NULL ||
-        parsed->getDataset() == NULL)
+    if (instancesLoader_.get() == NULL ||
+        !instancesLoader_->HasNext())
     {
       THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_InternalError);
     }
-    
-    DcmDataset& dataset = *parsed->getDataset();
-    
-    OFString a, b;
-    if (!dataset.findAndGetOFString(DCM_SOPClassUID, a).good() ||
-        !dataset.findAndGetOFString(DCM_SOPInstanceUID, b).good())
+
     {
-      throw OrthancException(ErrorCode_NoSopClassOrInstance,
-                             "Unable to determine the SOP class/instance for C-STORE with AET " +
-                             originatorAet_);
-    }
+      /**
+       * It is OK to modify the content of "dicom", as
+       * "DicomSequentialReader" returns copies of the cached objects
+       * (cf. "DataSourceSequentialReader::IValueDisconnector"). (*)
+       **/
+      std::unique_ptr<DicomSequentialReader::Item> next(instancesLoader_->Next());
+      assert(next.get() != NULL &&
+             next->HasUserData());
+
+      {
+        const std::string& expectedId = instancesIds_[position_];
+        position_++;
+
+        // This is just a sanity check, the class "ReaderUserData" could be removed
+        const ReaderUserData& userData = dynamic_cast<const ReaderUserData&>(next->GetUserData());
+        if (userData.GetInstanceId() != expectedId)
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+      }
 
-    std::string sopClassUid(a.c_str());
-    std::string sopInstanceUid(b.c_str());
+      ParsedDicomFile& dicom = next->GetParsedDicomFile();
+
+      if (dicom.GetDcmtkObject().getDataset() == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
     
-    return PerformGetSubOp(assoc, sopClassUid, sopInstanceUid, parsed.release());
+      OFString a, b;
+      if (!dicom.GetDcmtkObject().getDataset()->findAndGetOFString(DCM_SOPClassUID, a).good() ||
+          !dicom.GetDcmtkObject().getDataset()->findAndGetOFString(DCM_SOPInstanceUID, b).good())
+      {
+        throw OrthancException(ErrorCode_NoSopClassOrInstance,
+                               "Unable to determine the SOP class/instance for C-STORE with AET " +
+                               originatorAet_);
+      }
+
+      std::string sopClassUid(a.c_str());
+      std::string sopInstanceUid(b.c_str());
+
+      return PerformGetSubOp(assoc, sopClassUid, sopInstanceUid, dicom);
+    }
   }
 
   
@@ -233,13 +261,10 @@
   bool OrthancGetRequestHandler::PerformGetSubOp(T_ASC_Association* assoc,
                                                  const std::string& sopClassUid,
                                                  const std::string& sopInstanceUid,
-                                                 DcmFileFormat* dicomRaw)
+                                                 ParsedDicomFile& dicom)
   {
-    assert(dicomRaw != NULL);
-    std::unique_ptr<DcmFileFormat> dicom(dicomRaw);
-    
     DicomTransferSyntax sourceSyntax;
-    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, *dicom))
+    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, dicom.GetDcmtkObject()))
     {
       failedCount_++;
       AddFailedUIDInstance(sopInstanceUid);
@@ -316,7 +341,7 @@
       // No transcoding is required
       DcmDataset *stDetailTmp = NULL;
       cond = DIMSE_storeUser(
-        assoc, presId, &req, NULL /* imageFileName */, dicom->getDataset(),
+        assoc, presId, &req, NULL /* imageFileName */, dicom.GetDcmtkObject().getDataset(),
         ProgressCallback, NULL /* callbackData */,
         (timeout_ > 0 ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), static_cast<int>(timeout_),
         &rsp, &stDetailTmp, &cancelParameters);
@@ -326,12 +351,19 @@
     {
       // Transcoding to the selected uncompressed transfer syntax
       IDicomTranscoder::DicomImage source, transcoded;
-      source.AcquireParsed(dicom.release());
+      source.AcquireParsed(dicom);  // This invalidates the "dicom" object, cf. (*)
+
+      /**
+       * It doesn't seem possible to uniformly trancode upfront
+       * using "DicomSequentialReader::CreateForTranscoded()", as the
+       * selected transfer syntax is not constant, but varies upon
+       * each individual instance given the presentation context.
+       **/
 
       std::set<DicomTransferSyntax> ts;
       ts.insert(selectedSyntax);
       
-      if (context_.GetTranscoder().Transcode(transcoded, source, ts, TranscodingSopInstanceUidMode_AllowNew))
+      if (context_.GetTranscoder()->Transcode(transcoded, source, ts, TranscodingSopInstanceUidMode_AllowNew))
       {
         // Transcoding has succeeded
         DcmDataset *stDetailTmp = NULL;
@@ -550,7 +582,6 @@
     localAet_ = context_.GetDefaultLocalApplicationEntityTitle();
     position_ = 0;
     originatorAet_ = originatorAet;
-    unsigned int loaderThreads = 1;
 
     {
       OrthancConfiguration::ReaderLock lock;
@@ -574,11 +605,8 @@
         throw OrthancException(ErrorCode_InexistentItem,
                                "C-GET: Rejecting SCU request from unknown modality with AET: " + originatorAet);
       }
-
-      loaderThreads = lock.GetConfiguration().GetLoaderThreads();
     }
 
-    instancesLoader_.reset(new ThreadedInstancesLoader(context_, loaderThreads, false, DicomTransferSyntax_BigEndianExplicit /* dummy unused value */, 0, "CGET"));
     std::vector<FileInfo> filesInfo;
 
     for (std::list<std::string>::const_iterator
@@ -590,11 +618,16 @@
       context_.GetOrderedChildInstances(instancesIds_, filesInfo, *resourceId, level);
     }
 
+
+    instancesLoader_.reset(context_.GetDicomSequentialReaderFactory().CreateForOriginalParsedDicomFile());
+
     for (size_t i = 0; i < instancesIds_.size(); ++i)
     {
-      instancesLoader_->PreloadDicomInstance(instancesIds_[i], filesInfo[i]);
+      instancesLoader_->Submit(filesInfo[i], new ReaderUserData(instancesIds_[i]));
     }
 
+    instancesLoader_->Start();
+
     failedUIDs_.clear();
 
     completedCount_ = 0;
--- a/OrthancServer/Sources/OrthancGetRequestHandler.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/OrthancGetRequestHandler.h	Wed Jul 15 11:42:31 2026 +0200
@@ -23,29 +23,28 @@
 #pragma once
 
 #include "../../OrthancFramework/Sources/Compatibility.h"  // For ORTHANC_OVERRIDE
+#include "../../OrthancFramework/Sources/DataSource/DicomSequentialReader.h"
 #include "../../OrthancFramework/Sources/DicomNetworking/IGetRequestHandler.h"
 #include "../../OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h"
 
-#include <dcmtk/dcmnet/dimse.h>
-
 #include <list>
 
-class DcmFileFormat;
-
 namespace Orthanc
 {
+  class ParsedDicomFile;
   class ServerContext;
-  class ThreadedInstancesLoader;
   
   class OrthancGetRequestHandler : public IGetRequestHandler
   {
   private:
+    class ReaderUserData;
+
     ServerContext& context_;
     std::string localAet_;
     std::vector<std::string> instancesIds_;
     size_t position_;
     std::string originatorAet_;
-    std::unique_ptr<ThreadedInstancesLoader> instancesLoader_;
+    std::unique_ptr<DicomSequentialReader> instancesLoader_;
 
     unsigned int completedCount_;
     unsigned int warningCount_;
@@ -63,7 +62,7 @@
     bool PerformGetSubOp(T_ASC_Association *assoc,
                          const std::string& sopClassUid,
                          const std::string& sopInstanceUid,
-                         DcmFileFormat* datasetRaw);
+                         ParsedDicomFile& dicom);
     
     void AddFailedUIDInstance(const std::string& sopInstance);
 
--- a/OrthancServer/Sources/OrthancMoveRequestHandler.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/OrthancMoveRequestHandler.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -27,13 +27,13 @@
 #include "../../OrthancFramework/Sources/DicomFormat/DicomArray.h"
 #include "../../OrthancFramework/Sources/DicomNetworking/DicomConnectionInfo.h"
 #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
+#include "../../OrthancFramework/Sources/DataSource/DicomSequentialReader.h"
 #include "../../OrthancFramework/Sources/Logging.h"
 #include "../../OrthancFramework/Sources/MetricsRegistry.h"
 
 #include "OrthancConfiguration.h"
 #include "ServerContext.h"
 #include "ServerJobs/DicomModalityStoreJob.h"
-#include "ServerJobs/ThreadedInstancesLoader.h"
 
 
 namespace Orthanc
@@ -42,14 +42,31 @@
   {
     // Anonymous namespace to avoid clashes between compilation modules
 
+    class MoveReaderUserData : public IDynamicObject
+    {
+    private:
+      std::string instanceId_;
+
+    public:
+      explicit MoveReaderUserData(const std::string& instanceId) :
+        instanceId_(instanceId)
+      {
+      }
+
+      const std::string& GetInstanceId() const
+      {
+        return instanceId_;
+      }
+    };
+
+
     class SynchronousMove : public IMoveRequestIterator
     {
     private:
       ServerContext& context_;
       std::string localAet_;
       std::vector<std::string> instancesIds_;
-      // std::vector<FileInfo> filesInfo_;
-      std::unique_ptr<ThreadedInstancesLoader> instancesLoader_;
+      std::unique_ptr<DicomSequentialReader> instancesLoader_;
       size_t position_;
       RemoteModalityParameters remote_;
       std::string originatorAet_;
@@ -69,12 +86,9 @@
         originatorAet_(originatorAet),
         originatorId_(originatorId)
       {
-        unsigned int loaderThreads;
-
         {
           OrthancConfiguration::ReaderLock lock;
           remote_ = lock.GetConfiguration().GetModalityUsingAet(targetAet);
-          loaderThreads = lock.GetConfiguration().GetLoaderThreads();
         }
 
         if (remote_.HasLocalAet())
@@ -82,8 +96,6 @@
           localAet_ = remote_.GetLocalAet();  // from the DicomModalities config
         }
 
-        instancesLoader_.reset(new ThreadedInstancesLoader(context_, loaderThreads, false, DicomTransferSyntax_BigEndianExplicit /* dummy value*/, 0, "CSTO"));
-
         std::vector<FileInfo> filesInfo;
         for (size_t i = 0; i < publicIds.size(); i++)
         {
@@ -95,10 +107,14 @@
           context.GetOrderedChildInstances(instancesIds_, filesInfo, resourceId, resourceType);
         }
 
+        instancesLoader_.reset(context_.GetDicomSequentialReaderFactory().CreateForOriginalRawMemoryBuffer());
+
         for (size_t i = 0; i < instancesIds_.size(); ++i)
         {
-          instancesLoader_->PreloadDicomInstance(instancesIds_[i], filesInfo[i]);
+          instancesLoader_->Submit(filesInfo[i], new MoveReaderUserData(instancesIds_[i]));
         }
+
+        instancesLoader_->Start();
       }
 
       virtual unsigned int GetSubOperationCount() const ORTHANC_OVERRIDE
@@ -113,10 +129,26 @@
           return Status_Failure;
         }
 
+        if (instancesLoader_.get() == NULL ||
+            !instancesLoader_->HasNext())
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+
         const std::string& id = instancesIds_[position_++];
 
-        std::string dicom;
-        instancesLoader_->WaitDicomInstance(dicom, id);
+        std::unique_ptr<DicomSequentialReader::Item> next(instancesLoader_->Next());
+        assert(next.get() != NULL &&
+               next->HasUserData());
+
+        {
+          // This is just a sanity check, the class "MoveReaderUserData" could be removed
+          const MoveReaderUserData& userData = dynamic_cast<const MoveReaderUserData&>(next->GetUserData());
+          if (userData.GetInstanceId() != id)
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+        }
 
         if (connection_.get() == NULL)
         {
@@ -124,8 +156,11 @@
           connection_.reset(new DicomStoreUserConnection(params));
         }
 
+        const IMemoryBuffer& dicom = next->GetRawMemoryBuffer();
+
         std::string sopClassUid, sopInstanceUid;  // Unused
-        context_.PerformCStoreWithTranscoding(sopClassUid, sopInstanceUid, *connection_, dicom,
+        context_.PerformCStoreWithTranscoding(sopClassUid, sopInstanceUid, *connection_,
+                                              dicom.GetData(), dicom.GetSize(),
                                               true, originatorAet_, originatorId_);
 
         return Status_Success;
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -37,6 +37,7 @@
 #include <boost/lexical_cast.hpp>
 #include <boost/algorithm/string/predicate.hpp>
 
+
 #define INFO_SUBSEQUENCES \
   "Starting with Orthanc 1.9.4, paths to subsequences can be provided using the "\
   "same syntax as the `dcmodify` command-line tool (wildcards are supported as well)."
@@ -216,39 +217,22 @@
 
     std::unique_ptr<ParsedDicomFile> modified;
 
+    if (transcode)
     {
-      ServerContext::DicomCacheLocker locker(context, id);
-      modified.reset(locker.GetDicom().Clone(true));
+      std::unique_ptr<TranscoderDataSource::Transcoded> transcoded(
+        context.ReadTranscodedDicom(id, targetSyntax, TranscodingSopInstanceUidMode_AllowNew, true, lossyQuality));
+
+      TranscoderDataSource::Transcoded::LockAsParsed lock(*transcoded);
+      modified.reset(lock.GetContent().Clone(true));
+    }
+    else
+    {
+      std::unique_ptr<DicomDataSource::Dicom> dicom(context.ReadParsedDicom(id));
+      modified.reset(dicom->Clone());
     }
     
     modification.Apply(modified);
-
-    if (transcode)
-    {
-      IDicomTranscoder::DicomImage source;
-      source.AcquireParsed(*modified);  // "modified" is invalid below this point
-      
-      IDicomTranscoder::DicomImage transcoded;
-
-      std::set<DicomTransferSyntax> s;
-      s.insert(targetSyntax);
-      
-      if (context.GetTranscoder().Transcode(transcoded, source, s, TranscodingSopInstanceUidMode_AllowNew, lossyQuality))
-      {      
-        call.GetOutput().AnswerBuffer(transcoded.GetBufferData(),
-                                      transcoded.GetBufferSize(), MimeType_Dicom);
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_InternalError,
-                               "Cannot transcode to transfer syntax: " +
-                               std::string(GetTransferSyntaxUid(targetSyntax)));
-      }
-    }
-    else
-    {
-      modified->Answer(call.GetOutput());
-    }
+    modified->Answer(call.GetOutput());
   }
 
 
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -29,6 +29,7 @@
 #include "../../../OrthancFramework/Sources/CompatibilityMath.h"
 #include "../../../OrthancFramework/Sources/Compression/GzipCompressor.h"
 #include "../../../OrthancFramework/Sources/Constants.h"
+#include "../../../OrthancFramework/Sources/DataSource/StorageAreaDataSource.h"
 #include "../../../OrthancFramework/Sources/DicomFormat/DicomImageInformation.h"
 #include "../../../OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h"
 #include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
@@ -38,7 +39,6 @@
 #include "../../../OrthancFramework/Sources/Images/ImageProcessing.h"
 #include "../../../OrthancFramework/Sources/Images/NumpyWriter.h"
 #include "../../../OrthancFramework/Sources/Logging.h"
-#include "../../../OrthancFramework/Sources/MultiThreading/Semaphore.h"
 #include "../../../OrthancFramework/Sources/SerializationToolbox.h"
 
 #include "../OrthancConfiguration.h"
@@ -51,15 +51,6 @@
 // This "include" is mandatory for Release builds using Linux Standard Base
 #include <boost/shared_ptr.hpp>
 
-#include "../../../OrthancFramework/Sources/FileStorage/StorageAccessor.h"
-
-/**
- * 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?
-
 
 static const char* const IGNORE_LENGTH = "ignore-length";
 static const char* const RECONSTRUCT_FILES = "ReconstructFiles";
@@ -382,8 +373,9 @@
           DicomWebJsonVisitor visitor;
           
           {
-            ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), publicId);
-            locker.GetDicom().Apply(visitor);
+            std::unique_ptr<DicomDataSource::Dicom> dicom(context.ReadParsedDicom(publicId));
+            DicomDataSource::Dicom::Lock lock(*dicom);
+            lock.GetContent().Apply(visitor);
           }
 
           if (mime == MimeType_DicomWebJson)
@@ -411,38 +403,25 @@
 
     if (call.HasArgument(GET_TRANSCODE))
     {
+      const DicomTransferSyntax transferSyntax = GetTransferSyntax(call.GetArgument(GET_TRANSCODE, ""));
+
       unsigned int lossyQuality;
       unsigned int defaultLossyQuality;
+
       {
         OrthancConfiguration::ReaderLock lock;
         defaultLossyQuality = lock.GetConfiguration().GetDicomLossyTranscodingQuality();
       }
+
       lossyQuality = call.GetUnsignedInteger32Argument(GET_LOSSY_QUALITY, defaultLossyQuality);
 
-      std::string source;
-      std::string attachmentId;
-      std::string transcoded;
-      context.ReadDicom(source, attachmentId, publicId);
-
-      if (lossyQuality != defaultLossyQuality) // we can't use the cache if the lossy quality is not the default one
-      {
-        IDicomTranscoder::DicomImage targetImage;
-        IDicomTranscoder::DicomImage sourceImage;
-        sourceImage.SetExternalBuffer(source);
-        std::set<DicomTransferSyntax> allowedSyntaxes;
-        allowedSyntaxes.insert(GetTransferSyntax(call.GetArgument(GET_TRANSCODE, "")));
-
-        if (context.GetTranscoder().Transcode(targetImage, sourceImage, allowedSyntaxes, TranscodingSopInstanceUidMode_AllowNew, lossyQuality))
-        {
-          call.GetOutput().SetContentFilename(filename.c_str());
-          call.GetOutput().AnswerBuffer(targetImage.GetBufferData(), targetImage.GetBufferSize(), MimeType_Dicom);
-        }
-      }
-      else if (context.TranscodeWithCache(transcoded, source, publicId, attachmentId, GetTransferSyntax(call.GetArgument(GET_TRANSCODE, ""))))
-      {
-        call.GetOutput().SetContentFilename(filename.c_str());
-        call.GetOutput().AnswerBuffer(transcoded, MimeType_Dicom);
-      }
+      std::unique_ptr<TranscoderDataSource::Transcoded> transcoded2(
+        context.ReadTranscodedDicom(publicId, transferSyntax, TranscodingSopInstanceUidMode_AllowNew, true, lossyQuality));
+
+      TranscoderDataSource::Transcoded::LockAsBuffer lock(*transcoded2);
+
+      call.GetOutput().SetContentFilename(filename.c_str());
+      call.GetOutput().AnswerBuffer(lock.GetContent(), MimeType_Dicom);
     }
     else
     {
@@ -489,12 +468,14 @@
 
     std::string publicId = call.GetUriComponent("id", "");
 
-    std::string dicom;
-    context.ReadDicom(dicom, publicId);
-
     std::string target;
     call.BodyToString(target);
-    SystemToolbox::WriteFile(dicom, SystemToolbox::PathFromUtf8(target));
+
+    {
+      std::unique_ptr<StorageAreaDataSource::Range> raw(context.ReadRawDicom(publicId));
+      SystemToolbox::WriteFile(raw->GetData(), raw->GetSize(),
+                               SystemToolbox::PathFromUtf8(target), false /* don't automatically call fsync */);
+    }
 
     call.GetOutput().AnswerBuffer("{}", MimeType_Json);
   }
@@ -520,8 +501,9 @@
       Json::Value answer;
 
       {
-        ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), publicId);
-        locker.GetDicom().DatasetToJson(answer, format, flags,
+        std::unique_ptr<DicomDataSource::Dicom> dicom(context.ReadParsedDicom(publicId));
+        DicomDataSource::Dicom::Lock lock(*dicom);
+        lock.GetContent().DatasetToJson(answer, format, flags,
                                         ORTHANC_MAXIMUM_TAG_LENGTH, ignoreTagLength);
       }
 
@@ -634,13 +616,16 @@
       return;
     }
 
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
     std::string publicId = call.GetUriComponent("id", "");
 
     unsigned int numberOfFrames;
       
     {
-      ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), publicId);
-      numberOfFrames = locker.GetDicom().GetFramesCount();
+      std::unique_ptr<DicomDataSource::Dicom> dicom(context.ReadParsedDicom(publicId));
+      DicomDataSource::Dicom::Lock lock(*dicom);
+      numberOfFrames = lock.GetContent().GetFramesCount();
     }
     
     Json::Value result = Json::arrayValue;
@@ -923,9 +908,11 @@
              * 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);
-            handler.Handle(call, decoded, &locker.GetDicom(), frame);
+             **/
+
+            std::unique_ptr<DicomDataSource::Dicom> dicom(context.ReadParsedDicom(publicId));
+            DicomDataSource::Dicom::Lock lock(*dicom);
+            handler.Handle(call, decoded, &lock.GetContent(), frame);
           }
           else
           {
@@ -1252,8 +1239,6 @@
   template <enum ImageExtractionMode mode>
   static void GetImage(RestApiGetCall& call)
   {
-    Semaphore::Locker locker(throttlingSemaphore_);
-        
     GetImageHandler handler(mode);
     IDecodedFrameHandler::Apply(call, handler, mode, false /* not rendered */);
   }
@@ -1261,8 +1246,6 @@
 
   static void GetRenderedFrame(RestApiGetCall& call)
   {
-    Semaphore::Locker locker(throttlingSemaphore_);
-        
     RenderedFrameHandler handler;
     IDecodedFrameHandler::Apply(call, handler, ImageExtractionMode_Preview /* arbitrary value */, true);
   }
@@ -1393,6 +1376,8 @@
     }
     else
     {
+      ServerContext& context = OrthancRestApi::GetContext(call);
+
       const std::string instanceId = call.GetUriComponent("id", "");
       const bool compress = call.GetBooleanArgument("compress", false);
       const bool rescale = call.GetBooleanArgument("rescale", true);
@@ -1406,10 +1391,9 @@
       NumpyVisitor visitor(0 /* no depth, 2D frame */, rescale);
 
       {
-        Semaphore::Locker throttling(throttlingSemaphore_);
-        ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), instanceId);
-        
-        visitor.WriteFrame(locker.GetDicom(), frame);
+        std::unique_ptr<DicomDataSource::Dicom> dicom(context.ReadParsedDicom(instanceId));
+        DicomDataSource::Dicom::Lock lock(*dicom);
+        visitor.WriteFrame(lock.GetContent(), frame);
       }
 
       visitor.Answer(call.GetOutput(), compress);
@@ -1430,15 +1414,17 @@
     }
     else
     {
+      ServerContext& context = OrthancRestApi::GetContext(call);
+
       const std::string instanceId = call.GetUriComponent("id", "");
       const bool compress = call.GetBooleanArgument("compress", false);
       const bool rescale = call.GetBooleanArgument("rescale", true);
 
       {
-        Semaphore::Locker throttling(throttlingSemaphore_);
-        ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), instanceId);
-
-        const unsigned int depth = locker.GetDicom().GetFramesCount();
+        std::unique_ptr<DicomDataSource::Dicom> dicom(context.ReadParsedDicom(instanceId));
+        DicomDataSource::Dicom::Lock lock(*dicom);
+
+        const unsigned int depth = lock.GetContent().GetFramesCount();
         if (depth == 0)
         {
           throw OrthancException(ErrorCode_BadFileFormat, "Empty DICOM instance");
@@ -1448,7 +1434,7 @@
 
         for (unsigned int frame = 0; frame < depth; frame++)
         {
-          visitor.WriteFrame(locker.GetDicom(), frame);
+          visitor.WriteFrame(lock.GetContent(), frame);
         }
 
         visitor.Answer(call.GetOutput(), compress);
@@ -1474,8 +1460,6 @@
       const bool compress = call.GetBooleanArgument("compress", false);
       const bool rescale = call.GetBooleanArgument("rescale", true);
 
-      Semaphore::Locker throttling(throttlingSemaphore_);
-
       ServerIndex& index = OrthancRestApi::GetIndex(call);
       SliceOrdering ordering(index, seriesId);
 
@@ -1495,11 +1479,12 @@
         unsigned int framesCount = ordering.GetFramesCount(i);
 
         {
-          ServerContext::DicomCacheLocker locker(context, instanceId);
+          std::unique_ptr<DicomDataSource::Dicom> dicom(context.ReadParsedDicom(instanceId));
+          DicomDataSource::Dicom::Lock lock(*dicom);
 
           for (unsigned int frame = 0; frame < framesCount; frame++)
           {
-            visitor.WriteFrame(locker.GetDicom(), frame);
+            visitor.WriteFrame(lock.GetContent(), frame);
           }
         }
       }
@@ -1536,8 +1521,6 @@
       return;
     }
 
-    Semaphore::Locker locker(throttlingSemaphore_);
-        
     ServerContext& context = OrthancRestApi::GetContext(call);
 
     std::string frameId = call.GetUriComponent("frame", "0");
@@ -1594,7 +1577,9 @@
       }
       return;
     }
-    
+
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
     std::string frameId = call.GetUriComponent("frame", "0");
 
     unsigned int frame;
@@ -1612,8 +1597,9 @@
     MimeType mime;
 
     {
-      ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), publicId);
-      locker.GetDicom().GetRawFrame(raw, mime, frame);
+      std::unique_ptr<DicomDataSource::Dicom> dicom(context.ReadParsedDicom(publicId));
+      DicomDataSource::Dicom::Lock lock(*dicom);
+      lock.GetContent().GetRawFrame(raw, mime, frame);
     }
 
     if (GzipCompression)
@@ -2341,7 +2327,7 @@
         .AddAnswerType(MimeType_Binary, "The attachment")
         .SetAnswerHeader("ETag", "Revision of the attachment, to be used in further `PUT` or `DELETE` operations")
         .SetHttpHeader("If-None-Match", "Optional revision of the attachment, to check if its content has changed")
-        .SetHttpHeader("Content-Range", "Optional content range to access part of the attachment (new in Orthanc 1.12.5)");
+        .SetHttpHeader("Range", "Optional content range to access part of the attachment (new in Orthanc 1.12.5)");
     
         return;
     }
@@ -2378,13 +2364,12 @@
 
       if (hasRangeHeader)
       {
-        std::string fragment;
-        context.ReadAttachmentRange(fragment, info, range, uncompress);
+        std::unique_ptr<StorageAreaDataSource::Range> fragment(context.ReadAttachment(info, range, uncompress));
 
         uint64_t fullSize = (uncompress ? info.GetUncompressedSize() : info.GetCompressedSize());
         call.GetOutput().GetLowLevelOutput().SetContentType(MimeType_Binary);
         call.GetOutput().GetLowLevelOutput().AddHeader("Content-Range", range.FormatHttpContentRange(fullSize));
-        call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_206_PartialContent, fragment);
+        call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_206_PartialContent, fragment->GetData(), fragment->GetSize());
       }
       else if (uncompress ||
                info.GetCompressionType() == CompressionType_None)
@@ -2394,9 +2379,8 @@
       else
       {
         // Access to the raw attachment (which is compressed)
-        std::string content;
-        context.ReadAttachment(content, info, false /* don't uncompress */, true /* skip cache */);
-        call.GetOutput().AnswerBuffer(content, MimeType_Binary);
+        std::unique_ptr<StorageAreaDataSource::Range> content(context.ReadAttachment(info, false /* don't uncompress */));
+        call.GetOutput().AnswerBuffer(content->GetData(), content->GetSize(), MimeType_Binary);
       }
     }
   }
@@ -2574,13 +2558,14 @@
     bool ok = false;
 
     // First check whether the compressed data is correctly stored in the disk
-    std::string data;
-    context.ReadAttachment(data, info, false, true /* skipCache when you absolutely need the compressed data */);
-
-    std::string actualMD5;
-    Toolbox::ComputeMD5(actualMD5, data);
+    std::string compressedMD5;
+
+    {
+      std::unique_ptr<StorageAreaDataSource::Range> compressed(context.ReadAttachment(info, false /* don't uncompress */));
+      Toolbox::ComputeMD5(compressedMD5, compressed->GetData(), compressed->GetSize());
+    }
     
-    if (actualMD5 == info.GetCompressedMD5())
+    if (compressedMD5 == info.GetCompressedMD5())
     {
       // The compressed data is OK. If a compression algorithm was
       // applied to it, now check the MD5 of the uncompressed data.
@@ -2590,9 +2575,14 @@
       }
       else
       {
-        context.ReadAttachment(data, info, true, true /* skipCache when you absolutely need the compressed data */);
-        Toolbox::ComputeMD5(actualMD5, data);
-        ok = (actualMD5 == info.GetUncompressedMD5());
+        std::string uncompressedMD5;
+
+        {
+          std::unique_ptr<StorageAreaDataSource::Range> uncompressed(context.ReadAttachment(info, true /* uncompress */));
+          Toolbox::ComputeMD5(uncompressedMD5, uncompressed->GetData(), uncompressed->GetSize());
+        }
+
+        ok = (uncompressedMD5 == info.GetUncompressedMD5());
       }
     }
 
@@ -2827,11 +2817,15 @@
       return;
     }
 
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
     std::string id = call.GetUriComponent("id", "");
 
-    ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), id);
-
-    locker.GetDicom().SendPathValue(call.GetOutput(), call.GetTrailingUri());
+    {
+      std::unique_ptr<DicomDataSource::Dicom> dicom(context.ReadParsedDicom(id));
+      DicomDataSource::Dicom::Lock lock(*dicom);
+      lock.GetContent().SendPathValue(call.GetOutput(), call.GetTrailingUri());
+    }
   }
 
 
@@ -3794,14 +3788,20 @@
       return;
     }
 
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
     const std::string id = call.GetUriComponent("id", "");
-    std::string pdf;
-    ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), id);
-
-    if (locker.GetDicom().ExtractPdf(pdf))
+
     {
-      call.GetOutput().AnswerBuffer(pdf, MimeType_Pdf);
-      return;
+      std::unique_ptr<DicomDataSource::Dicom> dicom(context.ReadParsedDicom(id));
+      DicomDataSource::Dicom::Lock lock(*dicom);
+
+      std::string pdf;
+      if (lock.GetContent().ExtractPdf(pdf))
+      {
+        call.GetOutput().AnswerBuffer(pdf, MimeType_Pdf);
+        return;
+      }
     }
   }
 
@@ -3861,16 +3861,16 @@
 
     std::string publicId = call.GetUriComponent("id", "");
 
-    std::string dicomContent;
-    context.ReadDicomForHeader(dicomContent, publicId);
-
-    // TODO Consider using "DicomMap::ParseDicomMetaInformation()" to
-    // speed up things here
-
-    ParsedDicomFile dicom(dicomContent);
-
     Json::Value header;
-    OrthancConfiguration::DefaultDicomHeaderToJson(header, dicom);
+
+    {
+      std::unique_ptr<DicomDataSource::Dicom> dicom(context.ReadDicomUntilPixelData(publicId));
+      DicomDataSource::Dicom::Lock lock(*dicom);
+
+      // TODO Consider using "DicomMap::ParseDicomMetaInformation()" to
+      // speed up things here
+      OrthancConfiguration::DefaultDicomHeaderToJson(header, lock.GetContent());
+    }
 
     AnswerDicomAsJson(call, header, OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Full));
   }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -169,7 +169,6 @@
       result[ORTHANC_CONFIG_DICOM_PORT] = lock.GetConfiguration().GetDicomPort();
       result[ORTHANC_CONFIG_HTTP_PORT] = lock.GetConfiguration().GetHttpPort();
       result[ORTHANC_CONFIG_CHECK_REVISIONS] = lock.GetConfiguration().HasCheckRevisions();  // New in Orthanc 1.9.2
-      result[ORTHANC_CONFIG_MAXIMUM_STORAGE_CACHE_SIZE] = lock.GetConfiguration().GetMaximumStorageCacheSize(); // New in Orthanc 1.12.12
       result[ORTHANC_CONFIG_STORE_MD5_FOR_ATTACHMENTS] = lock.GetConfiguration().HasStoreMD5ForAttachments(); // New in Orthanc 1.12.12
       result[ORTHANC_CONFIG_STORAGE_COMPRESSION] = lock.GetConfiguration().HasStorageCompression(); // New in Orthanc 1.11.0
       result[ORTHANC_CONFIG_DATABASE_SERVER_IDENTIFIER] = lock.GetConfiguration().GetDatabaseServerIdentifier();
@@ -177,6 +176,29 @@
       result[ORTHANC_CONFIG_MAXIMUM_PATIENT_COUNT] = lock.GetConfiguration().GetMaximumPatientCount(); // New in Orthanc 1.12.4
       result[ORTHANC_CONFIG_MAXIMUM_STORAGE_MODE] = lock.GetConfiguration().GetMaximumStorageMode(); // New in Orthanc 1.11.3
       result[ORTHANC_CONFIG_DICOM_DEFAULT_RETRIEVE_METHOD] = lock.GetConfiguration().GetDicomDefaultRetrieveMethod();
+      result[ORTHANC_CONFIG_CONCURRENT_JOBS] = lock.GetConfiguration().GetConcurrentJobs();
+      result[ORTHANC_CONFIG_HTTP_THREADS_COUNT] = lock.GetConfiguration().GetHttpThreadsCount();
+      result[ORTHANC_CONFIG_DICOM_THREADS_COUNT] = lock.GetConfiguration().GetDicomThreadsCount();
+    }
+
+    {
+      uint64_t storageMemoryCapacityMb, transcoderMemoryCapacityMb, dicomParserMemoryCapacityMb;
+      size_t storageMemoryCacheMb, transcoderMemoryCacheMb, dicomParserMemoryCacheMb;
+      unsigned int storageReaderThreadsCount, transcoderReaderThreadsCount, dicomParserThreadsCount;
+
+      context.GetDataSourcesConfigurations(storageMemoryCapacityMb, storageMemoryCacheMb, storageReaderThreadsCount,
+                                          transcoderMemoryCapacityMb, transcoderMemoryCacheMb, transcoderReaderThreadsCount,
+                                          dicomParserMemoryCapacityMb, dicomParserMemoryCacheMb, dicomParserThreadsCount);
+
+      result[ORTHANC_CONFIG_STORAGE_MEMORY_CAPACITY] = BytesToMegabytes(storageMemoryCapacityMb);
+      result[ORTHANC_CONFIG_MAXIMUM_STORAGE_CACHE_SIZE] = BytesToMegabytes(storageMemoryCacheMb);
+      result[ORTHANC_CONFIG_STORAGE_LOADER_THREADS] = storageReaderThreadsCount;
+      result[ORTHANC_CONFIG_TRANSCODER_MEMORY_CAPACITY] = BytesToMegabytes(transcoderMemoryCapacityMb);
+      result[ORTHANC_CONFIG_TRANSCODER_CACHE_SIZE] = BytesToMegabytes(transcoderMemoryCacheMb);
+      result[ORTHANC_CONFIG_TRANSCODER_THREADS] = transcoderReaderThreadsCount;
+      result[ORTHANC_CONFIG_DICOM_PARSER_MEMORY_CAPACITY] = BytesToMegabytes(dicomParserMemoryCapacityMb);
+      result[ORTHANC_CONFIG_DICOM_PARSER_CACHE_SIZE] = BytesToMegabytes(dicomParserMemoryCacheMb);
+      result[ORTHANC_CONFIG_DICOM_PARSER_SOURCE_THREADS] = dicomParserThreadsCount;
     }
 
     DicomTransferSyntax ingestTransferSyntax;
@@ -1011,8 +1033,6 @@
     registry.SetIntegerValue("orthanc_up_time_s", serverUpTime);
     registry.SetIntegerValue("orthanc_last_change", lastChange["Last"].asInt64());
 
-    context.PublishCacheMetrics();
-
     std::string s;
     registry.ExportPrometheusText(s);
 
--- a/OrthancServer/Sources/OrthancWebDav.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/OrthancWebDav.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -226,7 +226,11 @@
           time_ = GetNow();
         }
 
-        context_.ReadDicom(target_, resource.GetIdentifier());
+        {
+          std::unique_ptr<StorageAreaDataSource::Range> raw(context_.ReadRawDicom(resource.GetIdentifier()));
+          raw->Copy(target_);
+        }
+
         success_ = true;
       }
     }
@@ -482,7 +486,12 @@
         try
         {
           mime = MimeType_Dicom;
-          context_.ReadDicom(content, instanceId);
+
+          {
+            std::unique_ptr<StorageAreaDataSource::Range> raw(context_.ReadRawDicom(instanceId));
+            raw->Copy(content);
+          }
+
           LookupTime(time, context_, instanceId, ResourceType_Instance, MetadataType_Instance_ReceptionDate);
           return true;
         }
--- a/OrthancServer/Sources/ServerContext.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -25,32 +25,37 @@
 #include "ServerContext.h"
 
 #include "../../OrthancFramework/Sources/Cache/SharedArchive.h"
+#include "../../OrthancFramework/Sources/Compression/ZlibCompressor.h"
+#include "../../OrthancFramework/Sources/Constants.h"
+#include "../../OrthancFramework/Sources/DataSource/DataSourceReader.h"
+#include "../../OrthancFramework/Sources/DataSource/DicomSequentialReader.h"
 #include "../../OrthancFramework/Sources/DicomFormat/DicomElement.h"
 #include "../../OrthancFramework/Sources/DicomFormat/DicomImageInformation.h"
 #include "../../OrthancFramework/Sources/DicomFormat/DicomStreamReader.h"
 #include "../../OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.h"
 #include "../../OrthancFramework/Sources/DicomParsing/DicomModification.h"
 #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
-#include "../../OrthancFramework/Sources/FileStorage/StorageAccessor.h"
 #include "../../OrthancFramework/Sources/HttpServer/FilesystemHttpSender.h"
 #include "../../OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.h"
 #include "../../OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h"
 #include "../../OrthancFramework/Sources/Logging.h"
 #include "../../OrthancFramework/Sources/MallocMemoryBuffer.h"
 #include "../../OrthancFramework/Sources/MetricsRegistry.h"
+#include "../../OrthancFramework/Sources/MultiThreading/ThreadPool.h"
 #include "../../OrthancFramework/Sources/SerializationToolbox.h"
 #include "../Plugins/Engine/OrthancPlugins.h"
 
 #include "DicomInstanceToStore.h"
 #include "OrthancConfiguration.h"
 #include "OrthancRestApi/OrthancRestApi.h"
+#include "OutgoingDicomInstance.h"
 #include "ResourceFinder.h"
 #include "Search/DatabaseLookup.h"
 #include "ServerJobs/OrthancJobUnserializer.h"
 #include "ServerToolbox.h"
+#include "ServerTranscoder.h"
+#include "SimpleInstanceOrdering.h"
 #include "StorageCommitmentReports.h"
-#include "OutgoingDicomInstance.h"
-#include "SimpleInstanceOrdering.h"
 
 #include <dcmtk/dcmdata/dcfilefo.h>
 #include <dcmtk/dcmnet/dimse.h>
@@ -58,12 +63,53 @@
 #include <dcmtk/dcmdata/dcuid.h>        /* for variable dcmAllStorageSOPClassUIDs */
 
 #include <boost/regex.hpp>
+#include <math.h>
 
 #if HAVE_MALLOC_TRIM == 1
 #  include <malloc.h>
 #endif
 
-static size_t DICOM_CACHE_SIZE = static_cast<size_t>(128) * 1024 * 1024;  // 128 MB
+
+// Those metrics correspond to those found in StorageAccessor in Orthanc <= 1.12.11
+static const char* const METRICS_STORAGE_AREA_CREATE_DURATION = "orthanc_storage_create_duration_ms";
+static const char* const METRICS_STORAGE_AREA_READ_BYTES = "orthanc_storage_read_bytes";
+static const char* const METRICS_STORAGE_AREA_READ_DURATION = "orthanc_storage_read_duration_ms";
+static const char* const METRICS_STORAGE_AREA_REMOVE_DURATION = "orthanc_storage_remove_duration_ms";
+static const char* const METRICS_STORAGE_AREA_WRITTEN_BYTES = "orthanc_storage_written_bytes";
+
+static const char* const METRICS_STORAGE_AREA_CACHE_COUNT = "orthanc_storage_cache_count";
+static const char* const METRICS_STORAGE_AREA_CACHE_HIT_COUNT = "orthanc_storage_cache_hit_count";
+static const char* const METRICS_STORAGE_AREA_CACHE_MISS_COUNT = "orthanc_storage_cache_miss_count";
+static const char* const METRICS_STORAGE_AREA_CACHE_SIZE_MB = "orthanc_storage_cache_size_mb";
+static const char* const METRICS_STORAGE_AREA_MEMORY_CAPACITY_MB = "orthanc_storage_memory_capacity_mb";
+static const char* const METRICS_STORAGE_AREA_MEMORY_USAGE_MB = "orthanc_storage_memory_usage_mb";
+static const char* const METRICS_STORAGE_AREA_MEMORY_MAX_USAGE_MB = "orthanc_storage_memory_max_usage_mb";
+static const char* const METRICS_STORAGE_AREA_MEMORY_COUNT = "orthanc_storage_memory_count";
+static const char* const METRICS_STORAGE_THREAD_POOL_AVAILABLE_THREADS = "orthanc_storage_available_threads";
+
+static const char* const METRICS_DICOM_PARSER_CACHE_COUNT = "orthanc_dicom_parser_cache_count";
+static const char* const METRICS_DICOM_PARSER_CACHE_HIT_COUNT = "orthanc_dicom_parser_cache_hit_count";
+static const char* const METRICS_DICOM_PARSER_CACHE_MISS_COUNT = "orthanc_dicom_parser_cache_miss_count";
+static const char* const METRICS_DICOM_PARSER_CACHE_SIZE_MB = "orthanc_dicom_parser_cache_size_mb";
+static const char* const METRICS_DICOM_PARSER_MEMORY_CAPACITY_MB = "orthanc_dicom_parser_memory_capacity_mb";
+static const char* const METRICS_DICOM_PARSER_MEMORY_USAGE_MB = "orthanc_dicom_parser_memory_usage_mb";
+static const char* const METRICS_DICOM_PARSER_MEMORY_MAX_USAGE_MB = "orthanc_dicom_parser_memory_max_usage_mb";
+static const char* const METRICS_DICOM_PARSER_MEMORY_COUNT = "orthanc_dicom_parser_memory_count";
+static const char* const METRICS_DICOM_PARSER_THREAD_POOL_AVAILABLE_THREADS = "orthanc_dicom_parser_available_threads";
+
+static const char* const METRICS_TRANSCODER_CACHE_COUNT = "orthanc_transcoder_cache_count";
+static const char* const METRICS_TRANSCODER_CACHE_HIT_COUNT = "orthanc_transcoder_cache_hit_count";
+static const char* const METRICS_TRANSCODER_CACHE_MISS_COUNT = "orthanc_transcoder_cache_miss_count";
+static const char* const METRICS_TRANSCODER_CACHE_SIZE_MB = "orthanc_transcoder_cache_size_mb";
+static const char* const METRICS_TRANSCODER_MEMORY_CAPACITY_MB = "orthanc_transcoder_memory_capacity_mb";
+static const char* const METRICS_TRANSCODER_MEMORY_USAGE_MB = "orthanc_transcoder_memory_usage_mb";
+static const char* const METRICS_TRANSCODER_MEMORY_MAX_USAGE_MB = "orthanc_transcoder_memory_max_usage_mb";
+static const char* const METRICS_TRANSCODER_MEMORY_COUNT = "orthanc_transcoder_memory_count";
+static const char* const METRICS_TRANSCODER_THREAD_POOL_AVAILABLE_THREADS = "orthanc_transcoder_available_threads";
+
+static const char* const METRICS_SEQ_READER_THREAD_POOL_AVAILABLE_THREADS = "orthanc_seq_reader_available_threads";
+
+
 
 
 /**
@@ -105,6 +151,97 @@
   }
 
 
+  void ServerContext::CreateFile(FileInfo& info,
+                                 const void* data,
+                                 size_t size,
+                                 FileContentType type,
+                                 CompressionType compression,
+                                 const std::string& precomputedMd5,
+                                 const DicomInstanceToStore* instance)
+  {
+    assert(metricsRegistry_.get() != NULL);
+    const std::string uuid = Toolbox::GenerateUuid();
+
+    std::string md5 = precomputedMd5;
+
+    if (storeMD5_ &&
+        md5.empty()) // if it has not been precomputed, compute it now
+    {
+      Toolbox::ComputeMD5(md5, data, size);
+    }
+
+    std::string customData;
+
+    switch (compression)
+    {
+      case CompressionType_None:
+      {
+        {
+          MetricsRegistry::Timer timer(*metricsRegistry_, METRICS_STORAGE_AREA_CREATE_DURATION);
+          area_.Create(customData, uuid, data, size, type, compression, instance);
+        }
+
+        metricsRegistry_->IncrementIntegerValue(METRICS_STORAGE_AREA_WRITTEN_BYTES, static_cast<int64_t>(size));
+
+        info = FileInfo(uuid, type, size, md5);
+        info.SetCustomData(customData);
+
+        try
+        {
+          StorageAreaDataSource::StoreIntoCache(*storageAreaReader_, info, data, size);
+        }
+        catch (OrthancException& e)
+        {
+          LOG(WARNING) << "Unable to store a new attachment into the cache: " << e.What();
+        }
+
+        return;
+      }
+
+      case CompressionType_ZlibWithSize:
+      {
+        ZlibCompressor zlib;
+
+        std::string compressed;
+        zlib.Compress(compressed, data, size);
+
+        const void* compressedData = compressed.empty() ? NULL : compressed.c_str();
+
+        std::string compressedMD5;
+
+        if (storeMD5_)
+        {
+          Toolbox::ComputeMD5(compressedMD5, compressed);
+        }
+
+        {
+          MetricsRegistry::Timer timer(*metricsRegistry_, METRICS_STORAGE_AREA_CREATE_DURATION);
+          area_.Create(customData, uuid, compressedData, compressed.size(), type, compression, instance);
+        }
+
+        metricsRegistry_->IncrementIntegerValue(METRICS_STORAGE_AREA_WRITTEN_BYTES, static_cast<int64_t>(compressed.size()));
+
+        info = FileInfo(uuid, type, size, md5, CompressionType_ZlibWithSize, compressed.size(), compressedMD5);
+        info.SetCustomData(customData);
+
+        try
+        {
+          StorageAreaDataSource::StoreIntoCache(*storageAreaReader_, info, compressedData, compressed.size());
+        }
+        catch (OrthancException& e)
+        {
+          LOG(WARNING) << "Unable to store a new attachment into the cache: " << e.What();
+        }
+
+        return;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
   ServerContext::StoreResult::StoreResult() :
     status_(StoreStatus_Failure),
     cstoreStatusCode_(0)
@@ -361,21 +498,25 @@
   }
 
 
-  void ServerContext::PublishCacheMetrics()
+  static void GetMemoryConfiguration(unsigned int& resultMb, 
+                                     OrthancConfiguration::ReaderLock& lock,
+                                     const char* configurationName)
   {
-    metricsRegistry_->SetFloatValue("orthanc_dicom_cache_size_mb",
-                                    static_cast<float>(dicomCache_.GetCurrentSize()) / static_cast<float>(1024 * 1024));
-    metricsRegistry_->SetIntegerValue("orthanc_dicom_cache_count", 
-                                    static_cast<int64_t>(dicomCache_.GetNumberOfItems()));
-    metricsRegistry_->SetFloatValue("orthanc_storage_cache_size_mb",
-                                    static_cast<float>(storageCache_.GetCurrentSize()) / static_cast<float>(1024 * 1024));
-    metricsRegistry_->SetIntegerValue("orthanc_storage_cache_count", 
-                                    static_cast<int64_t>(storageCache_.GetNumberOfItems()));
+    if (!lock.GetConfiguration().LookupUnsignedIntegerParameter(resultMb, configurationName))
+    {
+      resultMb = lock.GetConfiguration().GetUnsignedIntegerParameter(configurationName);
+      LOG(WARNING) << "====> '" << configurationName << "' is not defined in your configuration, setting it to " << resultMb << ".  Depending on the available memory on the system, you might want to adapt this value.";
+    }
+    else
+    {
+      LOG(WARNING) << "'" << configurationName << "' is set to " << resultMb;
+    }
   }
 
 
   ServerContext::ServerContext(IDatabaseWrapper& database,
                                IPluginStorageArea& area,
+                               ServerTranscoder* transcoder /* takes ownership */,
                                bool unitTesting,
                                size_t maxCompletedJobs,
                                bool readOnly) :
@@ -384,7 +525,6 @@
     compressionEnabled_(false),
     storeMD5_(true),
     largeDicomThrottler_(1),
-    dicomCache_(DICOM_CACHE_SIZE),
     mainLua_(*this),
     filterLua_(*this),
     luaListener_(*this),
@@ -400,6 +540,8 @@
     isExecuteLuaEnabled_(false),
     isRestApiWriteToFileSystemEnabled_(false),
     overwriteInstances_(OverwriteInstancesMode_Never),
+    transcoder_(transcoder),
+    transcodeDicomProtocol_(true),
     isIngestTranscoding_(false),
     ingestTranscodingOfUncompressed_(true),
     ingestTranscodingOfCompressed_(true),
@@ -419,7 +561,7 @@
         mediaArchive_.reset(
           new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("MediaArchiveSize")));
         defaultLocalAet_ = lock.GetConfiguration().GetOrthancAET();
-        jobsEngine_.SetWorkersCount(lock.GetConfiguration().GetUnsignedIntegerParameter("ConcurrentJobs"));
+        jobsEngine_.SetWorkersCount(lock.GetConfiguration().GetConcurrentJobs());
 
         saveJobs_ = lock.GetConfiguration().GetBooleanParameter("SaveJobs");
         if (readOnly_ && saveJobs_)
@@ -550,6 +692,174 @@
 #else
       LOG(INFO) << "Your platform does not support malloc_trim(), not starting the memory trimming thread";
 #endif
+
+      unsigned int storageLoaderThreads, transcoderThreads, dicomParserThreads;
+      unsigned int storageCacheSizeMb, transcoderCacheSizeMb, dicomParserCacheSizeMb;
+      unsigned int storageMemoryCapacityMb, transcoderMemoryCapacityMb, dicomParserMemoryCapacityMb;
+      unsigned int sequentialReaderThreads, sequentialReaderWindowSize, sequentialReaderWindowCapacityMb;
+      
+      {
+        OrthancConfiguration::ReaderLock lock;
+        unsigned int loaderThreads = lock.GetConfiguration().GetLoaderThreads();
+        unsigned int concurrentJobs = lock.GetConfiguration().GetConcurrentJobs();
+        storageCacheSizeMb = lock.GetConfiguration().GetMaximumStorageCacheSize();
+
+        if (!lock.GetConfiguration().LookupUnsignedIntegerParameter(storageLoaderThreads, ORTHANC_CONFIG_STORAGE_LOADER_THREADS))
+        {
+          storageLoaderThreads = loaderThreads * concurrentJobs;
+          if (storageLoaderThreads < 4)
+          {
+            storageLoaderThreads = 4;
+            LOG(WARNING) << "'" << ORTHANC_CONFIG_STORAGE_LOADER_THREADS << "' is not defined in your configuration, setting it to " << storageLoaderThreads;
+          }
+          else
+          {
+            storageLoaderThreads = std::min(50u, storageLoaderThreads);
+            LOG(WARNING) << "'" << ORTHANC_CONFIG_STORAGE_LOADER_THREADS << "' is not defined in your configuration, setting it to " << storageLoaderThreads << ", based on 'ConcurrentJobs' and the old 'LoaderThreads' configurations.";
+          }
+        }
+        else
+        {
+          LOG(WARNING) << "'" << ORTHANC_CONFIG_STORAGE_LOADER_THREADS << "' is set to " << storageLoaderThreads;
+        }
+
+        // ----> IF you update values here, you must also update them in Configuration.json <-----
+        transcoderThreads = lock.GetConfiguration().GetUnsignedIntegerParameter(ORTHANC_CONFIG_TRANSCODER_THREADS);
+        LOG(WARNING) << "'" << ORTHANC_CONFIG_TRANSCODER_THREADS << "' is set to " << transcoderThreads;
+
+        dicomParserThreads = lock.GetConfiguration().GetUnsignedIntegerParameter(ORTHANC_CONFIG_DICOM_PARSER_SOURCE_THREADS);
+        LOG(WARNING) << "'" << ORTHANC_CONFIG_DICOM_PARSER_SOURCE_THREADS << "' is set to " << dicomParserThreads;
+
+        if (!lock.GetConfiguration().LookupUnsignedIntegerParameter(sequentialReaderThreads, ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_THREADS))
+        {
+          LOG(WARNING) << "'" << ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_THREADS << "' is not defined in your configuration, setting it to the same value as '" << ORTHANC_CONFIG_STORAGE_LOADER_THREADS << "': " << storageLoaderThreads;
+          sequentialReaderThreads = storageLoaderThreads;
+        }
+        else
+        {
+          LOG(WARNING) << "'" << ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_THREADS << "' is set to " << sequentialReaderThreads;
+        }
+
+        // ----> IF you update values here, you must also update them in Configuration.json <-----
+        GetMemoryConfiguration(storageMemoryCapacityMb, lock, ORTHANC_CONFIG_STORAGE_MEMORY_CAPACITY);
+        GetMemoryConfiguration(dicomParserMemoryCapacityMb, lock, ORTHANC_CONFIG_DICOM_PARSER_MEMORY_CAPACITY);
+        GetMemoryConfiguration(dicomParserCacheSizeMb, lock, ORTHANC_CONFIG_DICOM_PARSER_CACHE_SIZE);
+        GetMemoryConfiguration(transcoderMemoryCapacityMb, lock, ORTHANC_CONFIG_TRANSCODER_MEMORY_CAPACITY);
+        GetMemoryConfiguration(transcoderCacheSizeMb, lock, ORTHANC_CONFIG_TRANSCODER_CACHE_SIZE);
+        GetMemoryConfiguration(sequentialReaderWindowCapacityMb, lock, ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_WINDOW_CAPACITY);
+
+        sequentialReaderWindowSize = lock.GetConfiguration().GetUnsignedIntegerParameter(ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_WINDOW_SIZE);
+        LOG(WARNING) << "'" << ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_WINDOW_SIZE << "' is set to " << sequentialReaderWindowSize;
+      }
+
+      // For streaming
+      {
+        boost::shared_ptr<ThreadPool> pool(new ThreadPool);
+        pool->SetThreadsCount(storageLoaderThreads);
+        pool->SetLoggingThreadName("STORAGE-SRC");
+        pool->SetDequeueTimeout(100);
+        pool->SetMetricsConfiguration(*metricsRegistry_, METRICS_STORAGE_THREAD_POOL_AVAILABLE_THREADS);
+        pool->Start();
+
+        std::unique_ptr<StorageAreaDataSource> source(new StorageAreaDataSource(area_, IsStoreMD5ForAttachments()));
+        source->SetMetricsRegistry(metricsRegistry_,
+                                   METRICS_STORAGE_AREA_READ_BYTES, METRICS_STORAGE_AREA_READ_DURATION);
+
+        storageAreaReader_.reset(new DataSourceReader(pool, source.release()));
+        storageAreaReader_->SetMetricsConfiguration(
+          DataSourceReader::MetricsConfiguration(metricsRegistry_,
+                                                 METRICS_STORAGE_AREA_CACHE_SIZE_MB,
+                                                 METRICS_STORAGE_AREA_CACHE_COUNT,
+                                                 METRICS_STORAGE_AREA_CACHE_HIT_COUNT,
+                                                 METRICS_STORAGE_AREA_CACHE_MISS_COUNT,
+                                                 METRICS_STORAGE_AREA_MEMORY_CAPACITY_MB,
+                                                 METRICS_STORAGE_AREA_MEMORY_USAGE_MB,
+                                                 METRICS_STORAGE_AREA_MEMORY_COUNT,
+                                                 METRICS_STORAGE_AREA_MEMORY_MAX_USAGE_MB));
+
+        storageAreaReader_->SetCapacity(storageMemoryCapacityMb * MEGABYTE);
+        storageAreaReader_->CreateCache(storageCacheSizeMb * MEGABYTE);
+        
+        if (storageCacheSizeMb == 0)
+        {
+          LOG(WARNING) << "Storage cache is disabled";
+        }
+        else
+        {
+          LOG(WARNING) << "Storage cache size is " << storageCacheSizeMb << " MB";
+        }
+      }
+
+      {
+        boost::shared_ptr<ThreadPool> pool(new ThreadPool);
+        pool->SetThreadsCount(dicomParserThreads);
+        pool->SetLoggingThreadName("DICOM-SRC");
+        pool->SetDequeueTimeout(100);
+        pool->SetMetricsConfiguration(*metricsRegistry_, METRICS_DICOM_PARSER_THREAD_POOL_AVAILABLE_THREADS);
+        pool->Start();
+
+        dicomReader_.reset(new DataSourceReader(pool, new DicomDataSource(storageAreaReader_)));
+        dicomReader_->SetMetricsConfiguration(
+          DataSourceReader::MetricsConfiguration(metricsRegistry_,
+                                                 METRICS_DICOM_PARSER_CACHE_SIZE_MB,
+                                                 METRICS_DICOM_PARSER_CACHE_COUNT,
+                                                 METRICS_DICOM_PARSER_CACHE_HIT_COUNT,
+                                                 METRICS_DICOM_PARSER_CACHE_MISS_COUNT,
+                                                 METRICS_DICOM_PARSER_MEMORY_CAPACITY_MB,
+                                                 METRICS_DICOM_PARSER_MEMORY_USAGE_MB,
+                                                 METRICS_DICOM_PARSER_MEMORY_COUNT,
+                                                 METRICS_DICOM_PARSER_MEMORY_MAX_USAGE_MB));
+
+        dicomReader_->SetCapacity(dicomParserMemoryCapacityMb * MEGABYTE);
+        dicomReader_->CreateCache(dicomParserCacheSizeMb * MEGABYTE);
+      }
+
+      if (transcoder_.get() != NULL)
+      {
+        boost::shared_ptr<ThreadPool> pool(new ThreadPool);
+        pool->SetThreadsCount(transcoderThreads);
+        pool->SetLoggingThreadName("TRANSCODER");
+        pool->SetDequeueTimeout(100);
+        pool->SetMetricsConfiguration(*metricsRegistry_, METRICS_TRANSCODER_THREAD_POOL_AVAILABLE_THREADS);
+        pool->Start();
+
+        transcoderReader_.reset(new DataSourceReader(pool, new TranscoderDataSource(transcoder_, storageAreaReader_)));
+        transcoderReader_->SetMetricsConfiguration(
+          DataSourceReader::MetricsConfiguration(metricsRegistry_,
+                                                 METRICS_TRANSCODER_CACHE_SIZE_MB,
+                                                 METRICS_TRANSCODER_CACHE_COUNT,
+                                                 METRICS_TRANSCODER_CACHE_HIT_COUNT,
+                                                 METRICS_TRANSCODER_CACHE_MISS_COUNT,
+                                                 METRICS_TRANSCODER_MEMORY_CAPACITY_MB,
+                                                 METRICS_TRANSCODER_MEMORY_USAGE_MB,
+                                                 METRICS_TRANSCODER_MEMORY_COUNT,
+                                                 METRICS_TRANSCODER_MEMORY_MAX_USAGE_MB));
+
+        transcoderReader_->SetCapacity(transcoderMemoryCapacityMb * MEGABYTE);
+        transcoderReader_->CreateCache(transcoderCacheSizeMb * MEGABYTE);
+      }
+
+      {
+        // unsigned int loaderThreads;
+
+        // {
+        //   OrthancConfiguration::ReaderLock lock;
+        //   loaderThreads = lock.GetConfiguration().GetLoaderThreads();
+        // }
+
+        std::unique_ptr<ThreadPool> pool(new ThreadPool);
+        pool->SetThreadsCount(sequentialReaderThreads);  // TODO-Streaming - Check - clarify the difference between SequentialLoader and DataSourceReader
+        pool->SetLoggingThreadName("SEQ-READER");
+        pool->SetDequeueTimeout(100);
+        pool->SetMetricsConfiguration(*metricsRegistry_, METRICS_SEQ_READER_THREAD_POOL_AVAILABLE_THREADS);
+        pool->Start();
+
+        boost::shared_ptr<IExecutorService> executor(pool.release());
+        dicomSequentialReaderFactory_.reset(new DicomSequentialReader::Factory(
+                                              executor, storageAreaReader_, dicomReader_, transcoderReader_,
+                                              sequentialReaderWindowSize,
+                                              sequentialReaderWindowCapacityMb * MEGABYTE));
+      }
     }
     catch (OrthancException&)
     {
@@ -656,8 +966,22 @@
                                  FileContentType type,
                                  const std::string& customData)
   {
-    StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
-    accessor.Remove(fileUuid, type, customData);
+    /**
+     * Note that it is not necessary to explicitly invalidate the
+     * cache after the removal of an attachment, as each attachment
+     * receives a unique UUID in ServerContext::CreateFile(), even if
+     * the DICOM instance is overwritten. The key of the removed
+     * attachment will thus never be seen again as new files are added.
+     **/
+
+    MetricsRegistry::Timer timer(*metricsRegistry_, METRICS_STORAGE_AREA_REMOVE_DURATION);
+    area_.Remove(fileUuid, type, customData);
+  }
+
+
+  void ServerContext::RemoveFile(const FileInfo& attachment)
+  {
+    RemoveFile(attachment.GetUuid(), attachment.GetContentType(), attachment.GetCustomData());
   }
 
 
@@ -718,7 +1042,6 @@
     try
     {
       MetricsRegistry::Timer timer(GetMetricsRegistry(), "orthanc_store_dicom_duration_ms");
-      StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
 
       DicomInstanceHasher hasher(summary);
       resultPublicId = hasher.HashInstance();
@@ -809,12 +1132,6 @@
         return result;
       }
 
-      // Remove the file from the DicomCache
-      // If this is a new instance, this is a no-op
-      // If overwriteMode == OverwriteInstancesMode_IfChanged and the file has not changed, we won't reach this point
-      // If overwriteMode == OverwriteInstancesMode_Always, then, let's always invalidate even if it is meaningful only if the file has changed (note: there is a TODO about cache and MD5)
-      dicomCache_.Invalidate(resultPublicId);
-
       // TODO Should we use "gzip" instead?
       CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None);
 
@@ -823,7 +1140,9 @@
 
       if (!isAdoption)
       {
-        accessor.Write(dicomInfo, dicom.GetBufferData(), dicom.GetBufferSize(), FileContentType_Dicom, compression, dicomMd5, storeMD5_, &dicom);
+        CreateFile(dicomInfo, dicom.GetBufferData(), dicom.GetBufferSize(),
+                   FileContentType_Dicom, compression, dicomMd5, &dicom);
+
         attachments.push_back(dicomInfo);
       }
       else
@@ -836,7 +1155,9 @@
           (!area_.HasEfficientReadRange() ||
            compressionEnabled_))
       {
-        accessor.Write(dicomUntilPixelData, dicom.GetBufferData(), pixelDataOffset, FileContentType_DicomUntilPixelData, compression, storeMD5_, NULL);
+        CreateFile(dicomUntilPixelData, dicom.GetBufferData(), pixelDataOffset, FileContentType_DicomUntilPixelData,
+                   compression, "" /* MD5 will be computed if needed */, NULL);
+
         attachments.push_back(dicomUntilPixelData);
       }
 
@@ -860,11 +1181,11 @@
           LOG(ERROR) << "Unexpected error while storing an instance in DB, cancelling and deleting the attachments: " << ex.GetDetails();
         }
 
-        accessor.Remove(dicomInfo);
+        RemoveFile(dicomInfo);
 
         if (dicomUntilPixelData.IsValid())
         {
-          accessor.Remove(dicomUntilPixelData);
+          RemoveFile(dicomUntilPixelData);
         }
         
         throw;
@@ -883,12 +1204,12 @@
       {
         if (!isAdoption)
         {
-          accessor.Remove(dicomInfo);
+          RemoveFile(dicomInfo);
         }
 
         if (dicomUntilPixelData.IsValid())
         {
-          accessor.Remove(dicomUntilPixelData);
+          RemoveFile(dicomUntilPixelData);
         }
       }
 
@@ -1092,7 +1413,7 @@
           imageInfo.ThrowIfInvalidFrameSize();
         }
 
-        if (GetTranscoder().Transcode(transcoded, source, syntaxes, TranscodingSopInstanceUidMode_AllowNew /* allow new SOP instance UID */))
+        if (GetTranscoder()->Transcode(transcoded, source, syntaxes, TranscodingSopInstanceUidMode_AllowNew /* allow new SOP instance UID */))
         {
           std::unique_ptr<ParsedDicomFile> tmp(transcoded.ReleaseAsParsedDicomFile());
 
@@ -1128,8 +1449,15 @@
                                        const FileInfo& attachment,
                                        const std::string& filename)
   {
-    StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
-    accessor.AnswerFile(output, attachment, GetFileContentMime(attachment.GetContentType()), filename);
+    std::unique_ptr<StorageAreaDataSource::Range> range(ReadAttachment(attachment, true /* uncompress */));
+
+    BufferHttpSender sender;
+    sender.SetBuffer(range->GetData(), range->GetSize());
+    sender.SetContentType(GetFileContentMime(attachment.GetContentType()));
+    sender.SetContentFilename(filename);
+
+    HttpStreamTranscoder transcoder(sender, CompressionType_None);
+    output.AnswerStream(transcoder);
   }
 
 
@@ -1156,13 +1484,15 @@
       return;
     }
 
-    std::string content;
-
-    StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
-    accessor.Read(content, attachment);
 
     FileInfo modified;
-    accessor.Write(modified, content.empty() ? NULL : content.c_str(), content.size(), attachmentType, compression, storeMD5_, NULL);
+
+    {
+      std::unique_ptr<StorageAreaDataSource::Range> range(ReadAttachment(attachment, true /* uncompress */));
+
+      CreateFile(modified, range->GetData(), range->GetSize(), attachmentType, compression,
+                 "" /* MD5 will be computed if needed */, NULL);
+    }
 
     try
     {
@@ -1171,13 +1501,13 @@
                                                 true, revision, modified.GetUncompressedMD5());
       if (status != StoreStatus_Success)
       {
-        accessor.Remove(modified);
+        RemoveFile(modified);
         throw OrthancException(ErrorCode_Database);
       }
     }
     catch (OrthancException&)
     {
-      accessor.Remove(modified);
+      RemoveFile(modified);
       throw;
     }    
   }
@@ -1290,15 +1620,14 @@
 
     if (LookupAttachment(attachment, FileContentType_DicomUntilPixelData, instanceAttachments))
     {
-      std::string dicom;
+      std::unique_ptr<DicomDataSource::Dicom> dicom(
+        DicomDataSource::Execute(*dicomReader_, DicomDataSource::CreateWholeRequest(attachment)));
 
       {
-        StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
-        accessor.Read(dicom, attachment);
+        DicomDataSource::Dicom::Lock lock(*dicom);
+        OrthancConfiguration::DefaultDicomDatasetToJson(result, lock.GetContent(), ignoreTagLength);
       }
 
-      ParsedDicomFile parsed(dicom);
-      OrthancConfiguration::DefaultDicomDatasetToJson(result, parsed, ignoreTagLength);
       InjectEmptyPixelData(result);
     }
     else
@@ -1339,17 +1668,15 @@
          * this case cannot be used if "StorageCompression" option is
          * "true".
          **/
-      
-        std::string dicom;
-        
+
+        std::unique_ptr<DicomDataSource::Dicom> dicom(
+          DicomDataSource::Execute(*dicomReader_, DicomDataSource::CreateUntilPixelDataRequest(attachment, pixelDataOffset)));
+
         {
-          StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
-          accessor.ReadStartRange(dicom, attachment, pixelDataOffset);
+          DicomDataSource::Dicom::Lock lock(*dicom);
+          OrthancConfiguration::DefaultDicomDatasetToJson(result, lock.GetContent(), ignoreTagLength);
         }
         
-        assert(dicom.size() == pixelDataOffset);
-        ParsedDicomFile parsed(dicom);
-        OrthancConfiguration::DefaultDicomDatasetToJson(result, parsed, ignoreTagLength);
         InjectEmptyPixelData(result);
       }
       else if (ignoreTagLength.empty() &&
@@ -1362,15 +1689,10 @@
          * "/tools/invalidate-tags" or to one flavors of
          * "/.../.../reconstruct" will disable this case.
          **/
-      
-        std::string dicomAsJson;
 
-        {
-          StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
-          accessor.Read(dicomAsJson, attachment);
-        }
+        std::unique_ptr<StorageAreaDataSource::Range> dicomAsJson(ReadAttachment(attachment, true /* uncompress */));
 
-        if (!Toolbox::ReadJson(result, dicomAsJson))
+        if (!Toolbox::ReadJson(result, dicomAsJson->GetData(), dicomAsJson->GetSize()))
         {
           throw OrthancException(ErrorCode_CorruptedFile,
                                  "Corrupted DICOM-as-JSON attachment of instance: " + instancePublicId);
@@ -1384,25 +1706,32 @@
          * file from the storage area.
          **/
 
-        std::string dicom;
-        ReadDicom(dicom, instancePublicId);
+        if (!LookupAttachment(attachment, FileContentType_Dicom, instanceAttachments))
+        {
+          throw OrthancException(ErrorCode_UnknownResource);
+        }
 
-        ParsedDicomFile parsed(dicom);
-        OrthancConfiguration::DefaultDicomDatasetToJson(result, parsed, ignoreTagLength);
+        std::unique_ptr<StorageAreaDataSource::Range> range(ReadAttachment(attachment, true /* uncompress */));
+
+        // We don't use the cache, as this would complexify the code for a rare edge case
+        ParsedDicomFile dicom(range->GetData(), range->GetSize());
+        OrthancConfiguration::DefaultDicomDatasetToJson(result, dicom, ignoreTagLength);
 
         if (!hasPixelDataOffset)
         {
           /**
-           * The pixel data offset was never computed for this
+           * (*) The pixel data offset was never computed for this
            * instance, which indicates that it was created using
            * Orthanc <= 1.9.0, or that calls to
            * "LookupPixelDataOffset()" from earlier versions of
            * Orthanc have failed. Try again this precomputation now
            * for future calls.
            **/
+
           ValueRepresentation pixelDataVR;
-          if (DicomStreamReader::LookupPixelDataOffset(pixelDataOffset, pixelDataVR, dicom) &&
-              pixelDataOffset < dicom.size())
+          if (DicomStreamReader::LookupPixelDataOffset(
+                pixelDataOffset, pixelDataVR, range->GetData(), range->GetSize()) &&
+              pixelDataOffset < range->GetSize())
           {
             index_.OverwriteMetadata(instancePublicId, MetadataType_Instance_PixelDataOffset,
                                      boost::lexical_cast<std::string>(pixelDataOffset));
@@ -1412,8 +1741,8 @@
             {
               int64_t newRevision;
               AddAttachment(newRevision, instancePublicId, ResourceType_Instance, FileContentType_DicomUntilPixelData,
-                            dicom.empty() ? NULL: dicom.c_str(), pixelDataOffset,
-                             false /* no old revision */, -1 /* dummy revision */, "" /* dummy MD5 */);
+                            range->GetData(), pixelDataOffset,
+                            false /* no old revision */, -1 /* dummy revision */, "" /* dummy MD5 */);
             }
           }
         }
@@ -1430,227 +1759,103 @@
   }
 
 
-  void ServerContext::ReadDicom(std::string& dicom,
-                                std::string& attachmentId,
-                                const std::string& instancePublicId)
-  {
-    std::unique_ptr<Semaphore::Locker> dummyLargeDicomLocker;  // only the DicomCacheLocker uses a real largeDicomLocker_
-    ReadDicomInternal(dicom, attachmentId, instancePublicId, dummyLargeDicomLocker, 0);
-  }
-
-  void ServerContext::ReadDicomInternal(std::string& dicom,
-                                        std::string& attachmentId,
-                                        const std::string& instancePublicId,
-                                        std::unique_ptr<Semaphore::Locker>& largeDicomLocker,
-                                        std::size_t largeDicomThreshold)
+  FileInfo ServerContext::LookupDicomForInstance(const std::string& instancePublicId)
   {
     FileInfo attachment;
     int64_t revision;
 
-    if (!index_.LookupAttachment(attachment, revision, ResourceType_Instance, instancePublicId, FileContentType_Dicom))
+    if (index_.LookupAttachment(attachment, revision, ResourceType_Instance, instancePublicId, FileContentType_Dicom))
+    {
+      return attachment;
+    }
+    else
     {
       throw OrthancException(ErrorCode_InternalError,
                              "Unable to read attachment " + EnumerationToString(FileContentType_Dicom) +
                              " of instance " + instancePublicId);
     }
+  }
 
-    assert(attachment.GetContentType() == FileContentType_Dicom);
-    attachmentId = attachment.GetUuid();
 
-    if (attachment.GetUncompressedSize() < largeDicomThreshold)  // release ASAP (before the read) if we don't plan to hold the lock (https://discourse.orthanc-server.org/t/patch-release-large-dicom-semaphore-lock-early-for-better-performance/6440)
-    {
-      largeDicomLocker.reset(NULL);
-    }
-
-    ReadAttachment(dicom, attachment, true /* uncompress */);
+  StorageAreaDataSource::Range* ServerContext::ReadAttachment(const FileInfo& attachment,
+                                                              bool uncompress)
+  {
+    return StorageAreaDataSource::Execute(
+      *storageAreaReader_, StorageAreaDataSource::CreateAttachmentRequest(attachment, uncompress));
   }
 
 
-  void ServerContext::ReadDicom(std::string& dicom,
-                                const std::string& instancePublicId)
+  StorageAreaDataSource::Range* ServerContext::ReadAttachment(const FileInfo& attachment,
+                                                              const StorageRange& range,
+                                                              bool uncompress)
   {
-    std::string attachmentId;
-    ReadDicom(dicom, attachmentId, instancePublicId);    
+    return StorageAreaDataSource::ReadRange(*storageAreaReader_, attachment, range, uncompress);
   }
 
-  void ServerContext::ReadDicomInternal(std::string& dicom,
-                                        const std::string& instancePublicId,
-                                        std::unique_ptr<Semaphore::Locker>& largeDicomLocker,
-                                        std::size_t largeDicomThreshold)
+
+  StorageAreaDataSource::Range* ServerContext::ReadRawDicom(const std::string& instancePublicId)
   {
-    std::string attachmentId;
-    ReadDicomInternal(dicom, attachmentId, instancePublicId, largeDicomLocker, largeDicomThreshold);
+    return ReadAttachment(LookupDicomForInstance(instancePublicId), true /* uncompress */);
   }
 
-  void ServerContext::ReadDicomForHeader(std::string& dicom,
-                                         const std::string& instancePublicId)
+
+  DicomDataSource::Dicom* ServerContext::ReadParsedDicom(const std::string& instancePublicId)
   {
-    if (!ReadDicomUntilPixelData(dicom, instancePublicId))
-    {
-      ReadDicom(dicom, instancePublicId);
-    }
+    return DicomDataSource::Execute(*dicomReader_, DicomDataSource::CreateWholeRequest(LookupDicomForInstance(instancePublicId)));
   }
 
-  bool ServerContext::ReadDicomUntilPixelData(std::string& dicom,
-                                              const std::string& instancePublicId)
+
+  DicomDataSource::Dicom* ServerContext::ReadDicomUntilPixelData(const std::string& instancePublicId)
   {
     FileInfo attachment;
     int64_t revision;  // Ignored
-    if (index_.LookupAttachment(attachment, revision, ResourceType_Instance, instancePublicId, FileContentType_DicomUntilPixelData))
-    {
-      StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
 
-      accessor.Read(dicom, attachment);
-      assert(dicom.size() == attachment.GetUncompressedSize());
-
-      return true;
-    }
-
-    if (!area_.HasEfficientReadRange())
-    {
-      return false;
-    }
-    
-    if (!index_.LookupAttachment(attachment, revision, ResourceType_Instance, instancePublicId, FileContentType_Dicom))
-    {
-      throw OrthancException(ErrorCode_InternalError,
-                             "Unable to read the DICOM file of instance " + instancePublicId);
-    }
-
-    std::string s;
-
-    if (attachment.GetCompressionType() == CompressionType_None &&
-        index_.LookupMetadata(s, instancePublicId, ResourceType_Instance,
-                              MetadataType_Instance_PixelDataOffset) &&
-        !s.empty())
+    if (index_.LookupAttachment(attachment, revision, ResourceType_Instance,
+                                instancePublicId, FileContentType_DicomUntilPixelData))
     {
-      uint64_t pixelDataOffset = 0;
-
-      if (SerializationToolbox::ParseUnsignedInteger64(pixelDataOffset, s))
-      {
-        StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
-
-        accessor.ReadStartRange(dicom, attachment, pixelDataOffset);
-        assert(dicom.size() == pixelDataOffset);
-        
-        return true;   // Success
-      }
-      else
-      {
-        LOG(ERROR) << "Metadata \"PixelDataOffset\" is corrupted for instance: " << instancePublicId;
-      }
-    }
-
-    return false;
-  }
-  
-
-  void ServerContext::ReadAttachment(std::string& result,
-                                     const FileInfo& attachment,
-                                     bool uncompressIfNeeded,
-                                     bool skipCache)
-  {
-    std::unique_ptr<StorageAccessor> accessor;
-      
-    if (skipCache)
-    {
-      accessor.reset(new StorageAccessor(area_, GetMetricsRegistry()));
+      return DicomDataSource::Execute(*dicomReader_, DicomDataSource::CreateWholeRequest(attachment));
     }
     else
     {
-      accessor.reset(new StorageAccessor(area_, storageCache_, GetMetricsRegistry()));
-    }
+      attachment = LookupDicomForInstance(instancePublicId);
+
+      std::string metadata;
 
-    if (uncompressIfNeeded)
-    {
-      accessor->Read(result, attachment);
-    }
-    else
-    {
-      // Do not uncompress the content of the storage area, return the
-      // raw data
-      accessor->ReadRaw(result, attachment);
-    }
-  }
-
-  void ServerContext::ReadAttachmentRange(std::string &result,
-                                          const FileInfo &attachment,
-                                          const StorageRange &range,
-                                          bool uncompressIfNeeded)
-  {
-    StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
-    accessor.ReadRange(result, attachment, range, uncompressIfNeeded);
-  }
-
+      if (attachment.GetCompressionType() == CompressionType_None &&
+          index_.LookupMetadata(metadata, instancePublicId, ResourceType_Instance,
+                                MetadataType_Instance_PixelDataOffset) &&
+          !metadata.empty())
+      {
+        uint64_t pixelDataOffset = 0;
 
-  ServerContext::DicomCacheLocker::DicomCacheLocker(ServerContext& context,
-                                                    const std::string& instancePublicId) :
-    context_(context),
-    instancePublicId_(instancePublicId)
-  {
-    accessor_.reset(new ParsedDicomCache::Accessor(context_.dicomCache_, instancePublicId));
-    
-    if (!accessor_->IsValid())
-    {
-      accessor_.reset(NULL);
+        if (SerializationToolbox::ParseUnsignedInteger64(pixelDataOffset, metadata))
+        {
+          return DicomDataSource::Execute(*dicomReader_, DicomDataSource::CreateUntilPixelDataRequest(attachment, pixelDataOffset));
+        }
+        else
+        {
+          LOG(ERROR) << "Metadata \"PixelDataOffset\" is corrupted for instance: " << instancePublicId;
+        }
+      }
 
-      // Throttle to avoid loading several large DICOM files simultaneously (since the ParsedDicomCache is 128MB, loading multiple 50MB files would throw them out directly after loading)
-      largeDicomLocker_.reset(new Semaphore::Locker(context.largeDicomThrottler_));
-      
-      // Release the throttle if loading "small" DICOM files (under
-      // 50MB, which is an arbitrary value)
-      context_.ReadDicomInternal(buffer_, instancePublicId_, largeDicomLocker_, static_cast<size_t>(50) * 1024 * 1024);
-      
-      dicom_.reset(new ParsedDicomFile(buffer_));
-      dicomSize_ = buffer_.size();
-    }
-
-    assert(accessor_.get() != NULL ||
-           dicom_.get() != NULL);
-  }
-
-
-  ServerContext::DicomCacheLocker::~DicomCacheLocker()
-  {
-    if (dicom_.get() != NULL)
-    {
-      try
-      {
-        context_.dicomCache_.Acquire(instancePublicId_, dicom_.release(), dicomSize_);
-      }
-      catch (OrthancException&) // NOLINT(bugprone-empty-catch)
-      {
-      }
+      // Fallback: The pixel data offset is not present or cannot be used, the whole DICOM file must be read
+      return DicomDataSource::Execute(*dicomReader_, DicomDataSource::CreateWholeRequest(attachment));
     }
   }
 
 
-  ParsedDicomFile& ServerContext::DicomCacheLocker::GetDicom() const
+  TranscoderDataSource::Transcoded* ServerContext::ReadTranscodedDicom(const std::string& instancePublicId,
+                                                                       DicomTransferSyntax targetSyntax,
+                                                                       TranscodingSopInstanceUidMode mode,
+                                                                       bool hasLossyQuality,
+                                                                       unsigned int lossyQuality)
   {
-    if (dicom_.get() != NULL)
-    {
-      return *dicom_;
-    }
-    else
-    {
-      assert(accessor_.get() != NULL);
-      return accessor_->GetDicom();
-    }
+    const FileInfo& attachment = LookupDicomForInstance(instancePublicId);
+    return TranscoderDataSource::Execute(*transcoderReader_, TranscoderDataSource::CreateRequest(
+                                           attachment, targetSyntax, mode, hasLossyQuality, lossyQuality));
   }
 
-  const std::string& ServerContext::DicomCacheLocker::GetBuffer()
-  {
-    if (buffer_.size() > 0)
-    {
-      return buffer_;
-    }
-    else
-    {
-      context_.ReadDicom(buffer_, instancePublicId_);
-      return buffer_;
-    }
-  }
-  
+
   void ServerContext::SetStoreMD5ForAttachments(bool storeMD5)
   {
     LOG(INFO) << "Storing MD5 for attachments: " << (storeMD5 ? "yes" : "no");
@@ -1673,12 +1878,10 @@
     // TODO Should we use "gzip" instead?
     CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None);
 
-    StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
-
     assert(attachmentType != FileContentType_Dicom && attachmentType != FileContentType_DicomUntilPixelData); // this method can not be used to store instances
 
     FileInfo attachment;
-    accessor.Write(attachment, data, size, attachmentType, compression, storeMD5_, NULL);
+    CreateFile(attachment, data, size, attachmentType, compression, "" /* MD5 will be computed if needed */, NULL);
 
     try
     {
@@ -1686,7 +1889,7 @@
         newRevision, attachment, resourceId, hasOldRevision, oldRevision, oldMD5);
       if (status != StoreStatus_Success)
       {
-        accessor.Remove(attachment);
+        RemoveFile(attachment);
         return false;
       }
       else
@@ -1697,7 +1900,7 @@
     catch (OrthancException&)
     {
       // Fixed in Orthanc 1.9.6
-      accessor.Remove(attachment);
+      RemoveFile(attachment);
       throw;
     }
   }
@@ -1707,24 +1910,12 @@
                                      const std::string& uuid,
                                      ResourceType expectedType)
   {
-    if (expectedType == ResourceType_Instance)
-    {
-      // remove the file from the DicomCache
-      dicomCache_.Invalidate(uuid);
-    }
-
     return index_.DeleteResource(remainingAncestor, uuid, expectedType);
   }
 
 
   void ServerContext::SignalChange(const ServerIndexChange& change)
   {
-    if (change.GetResourceType() == ResourceType_Instance &&
-        change.GetChangeType() == ChangeType_Deleted)
-    {
-      dicomCache_.Invalidate(change.GetPublicId());
-    }
-    
     pendingChanges_.Enqueue(change.Clone());
   }
 
@@ -1955,65 +2146,56 @@
   }
 
 
-
-
-
-  ImageAccessor* ServerContext::DecodeDicomFrame(const std::string& publicId,
+  ImageAccessor* ServerContext::DecodeDicomFrame(const std::string& instancePublicId,
                                                  unsigned int frameIndex)
   {
-    ServerContext::DicomCacheLocker locker(*this, publicId);
-    std::unique_ptr<ImageAccessor> decoded(GetTranscoder().DecodeFrame(locker.GetDicom(), locker.GetBuffer().c_str(), locker.GetBuffer().size(), frameIndex));
+    FileInfo attachment;
+    int64_t revision;
+
+    if (!index_.LookupAttachment(attachment, revision, ResourceType_Instance, instancePublicId, FileContentType_Dicom))
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Unable to read attachment " + EnumerationToString(FileContentType_Dicom) +
+                             " of instance " + instancePublicId);
+    }
+
+    std::unique_ptr<ImageAccessor> decoded;
+
+    decoded.reset(GetTranscoder()->DecodeFrame(dicomReader_, storageAreaReader_, transcoderReader_, attachment, frameIndex));
 
     if (decoded.get() == NULL)
     {
       OrthancConfiguration::ReaderLock configLock;
       if (configLock.GetConfiguration().IsWarningEnabled(Warnings_003_DecoderFailure))
       {
-        LOG(WARNING) << "W003: Unable to decode frame " << frameIndex << " from instance " << publicId;
+        LOG(WARNING) << "W003: Unable to decode frame " << frameIndex << " from instance " << instancePublicId;
       }
-      return NULL;
+
+      throw OrthancException(ErrorCode_NotImplemented);
     }
-
-    return decoded.release();
+    else
+    {
+      return decoded.release();
+    }
   }
 
 
-  ImageAccessor* ServerContext::DecodeDicomFrame(const DicomInstanceToStore& dicom,
-                                                 unsigned int frameIndex)
-  {
-    return GetTranscoder().DecodeFrame(dicom.GetParsedDicomFile(),
-                                    dicom.GetBufferData(),
-                                    dicom.GetBufferSize(),
-                                    frameIndex);
-
-  }
-
-
-  ImageAccessor* ServerContext::DecodeDicomFrame(const void* dicom,
-                                                 size_t size,
-                                                 unsigned int frameIndex)
-  {
-    std::unique_ptr<ParsedDicomFile> instance(new ParsedDicomFile(dicom, size));
-    return GetTranscoder().DecodeFrame(*instance, dicom, size, frameIndex);
-  }
-  
-
   void ServerContext::PerformCStoreWithTranscoding(std::string& sopClassUid,
                                                    std::string& sopInstanceUid,
                                                    DicomStoreUserConnection& connection,
-                                                   const std::string& dicom,
+                                                   const void* dicomData,
+                                                   size_t dicomSize,
                                                    bool hasMoveOriginator,
                                                    const std::string& moveOriginatorAet,
                                                    uint16_t moveOriginatorId)
   {
-    const void* data = dicom.empty() ? NULL : dicom.c_str();
     const RemoteModalityParameters& modality = connection.GetParameters().GetRemoteModality();
 
     // Filter out outgoing C-Store instances
     {
       boost::shared_lock<boost::shared_mutex> lock(listenersMutex_);
 
-      std::unique_ptr<OutgoingDicomInstance> outgoingInstance(OutgoingDicomInstance::CreateFromBuffer(dicom));
+      std::unique_ptr<OutgoingDicomInstance> outgoingInstance(OutgoingDicomInstance::CreateFromBuffer(dicomData, dicomSize));
       outgoingInstance->SetDestination(DicomInstanceDestination(connection.GetParameters().GetRemoteModality().GetHost(),
                                                                 connection.GetParameters().GetRemoteModality().GetApplicationEntityTitle()));
 
@@ -2042,48 +2224,17 @@
     if (!transcodeDicomProtocol_ ||
         !modality.IsTranscodingAllowed())
     {
-      connection.Store(sopClassUid, sopInstanceUid, data, dicom.size(),
+      connection.Store(sopClassUid, sopInstanceUid, dicomData, dicomSize,
                        hasMoveOriginator, moveOriginatorAet, moveOriginatorId);
     }
     else
     {
-      connection.Transcode(sopClassUid, sopInstanceUid, *transcoder_, data, dicom.size(), preferredTransferSyntax_,
+      connection.Transcode(sopClassUid, sopInstanceUid, *transcoder_, dicomData, dicomSize, preferredTransferSyntax_,
                            hasMoveOriginator, moveOriginatorAet, moveOriginatorId);
     }
   }
 
 
-  bool ServerContext::TranscodeWithCache(std::string& target,
-                                         const std::string& source,
-                                         const std::string& sourceInstanceId,
-                                         const std::string& attachmentId,
-                                         DicomTransferSyntax targetSyntax)
-  {
-    StorageCache::Accessor cacheAccessor(storageCache_);
-
-    if (!cacheAccessor.FetchTranscodedInstance(target, attachmentId, targetSyntax))
-    {
-      IDicomTranscoder::DicomImage sourceDicom;
-      sourceDicom.SetExternalBuffer(source);
-
-      IDicomTranscoder::DicomImage targetDicom;
-      std::set<DicomTransferSyntax> syntaxes;
-      syntaxes.insert(targetSyntax);
-
-      if (GetTranscoder().Transcode(targetDicom, sourceDicom, syntaxes, TranscodingSopInstanceUidMode_AllowNew))
-      {
-        cacheAccessor.AddTranscodedInstance(attachmentId, targetSyntax, reinterpret_cast<const char*>(targetDicom.GetBufferData()), targetDicom.GetBufferSize());
-        target = std::string(reinterpret_cast<const char*>(targetDicom.GetBufferData()), targetDicom.GetBufferSize());
-        return true;
-      }
-
-      return false;
-    }
-
-    return true;
-  }
-
-
   const std::string& ServerContext::GetDeidentifiedContent(const DicomElement &element) const
   {
     static const std::string redactedContent = "*** POTENTIAL PHI ***";
@@ -2272,47 +2423,54 @@
   }
 
 
-  void ServerContext::SetTranscoder(ServerTranscoder* transcoder)
+  const boost::shared_ptr<ServerTranscoder>& ServerContext::GetTranscoder() const
   {
-    std::unique_ptr<ServerTranscoder> protection(transcoder);
-
-    if (transcoder == NULL)
+    if (transcoder_.get() != NULL)
     {
-      throw OrthancException(ErrorCode_NullPointer);
-    }
-    else if (transcoder_.get() != NULL)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      return transcoder_;
     }
     else
     {
-      transcoder_.reset(protection.release());
+      throw OrthancException(ErrorCode_BadSequenceOfCalls, "No transcoder is available");
     }
   }
 
 
-  ServerTranscoder& ServerContext::GetTranscoder() const
+  DicomSequentialReader::Factory& ServerContext::GetDicomSequentialReaderFactory()
   {
-    if (transcoder_.get() != NULL)
-    {
-      return *transcoder_;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
+    return *dicomSequentialReaderFactory_;
   }
 
+  void ServerContext::GetDataSourcesConfigurations(uint64_t& storageMemoryCapacityMb,
+                                                   size_t& storageMemoryCacheMb,
+                                                   unsigned int& storageReaderThreadsCount,
+                                                   uint64_t& transcoderMemoryCapacityMb,
+                                                   size_t& transcoderMemoryCacheMb,
+                                                   unsigned int& transcoderReaderThreadsCount,
+                                                   uint64_t& dicomParserMemoryCapacityMb,
+                                                   size_t& dicomParserMemoryCacheMb,
+                                                   unsigned int& dicomParserThreadsCount)
+  {
+    storageMemoryCapacityMb = storageAreaReader_->GetCapacity();
+    storageMemoryCacheMb = storageAreaReader_->GetCacheCapacity();
+    transcoderMemoryCapacityMb = transcoderReader_->GetCapacity();
+    transcoderMemoryCacheMb = transcoderReader_->GetCacheCapacity();
+    dicomParserMemoryCapacityMb = dicomReader_->GetCapacity();
+    dicomParserMemoryCacheMb = dicomReader_->GetCacheCapacity();
 
-  void ServerContext::ResetTranscoder()
-  {
-    if (transcoder_.get() == NULL)
     {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      boost::shared_ptr<IExecutorService> service = storageAreaReader_->GetExecutorService();
+      storageReaderThreadsCount = dynamic_cast<ThreadPool&>(*service).GetThreadsCount();
     }
-    else
+
     {
-      transcoder_.reset(NULL);
+      boost::shared_ptr<IExecutorService> service = transcoderReader_->GetExecutorService();
+      transcoderReaderThreadsCount = dynamic_cast<ThreadPool&>(*service).GetThreadsCount();
+    }
+
+    {
+      boost::shared_ptr<IExecutorService> service = dicomReader_->GetExecutorService();
+      dicomParserThreadsCount = dynamic_cast<ThreadPool&>(*service).GetThreadsCount();
     }
   }
 }
--- a/OrthancServer/Sources/ServerContext.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Wed Jul 15 11:42:31 2026 +0200
@@ -28,13 +28,15 @@
 #include "OrthancHttpHandler.h"
 #include "ServerIndex.h"
 #include "ServerJobs/IStorageCommitmentFactory.h"
-#include "ServerTranscoder.h"
 
+#include "../../OrthancFramework/Sources/DataSource/DicomDataSource.h"
+#include "../../OrthancFramework/Sources/DataSource/DicomSequentialReader.h"
+#include "../../OrthancFramework/Sources/DataSource/StorageAreaDataSource.h"
+#include "../../OrthancFramework/Sources/DataSource/TranscoderDataSource.h"
 #include "../../OrthancFramework/Sources/DicomParsing/DicomModification.h"
-#include "../../OrthancFramework/Sources/DicomParsing/ParsedDicomCache.h"
-#include "../../OrthancFramework/Sources/FileStorage/StorageAccessor.h"
-#include "../../OrthancFramework/Sources/FileStorage/StorageCache.h"
 #include "../../OrthancFramework/Sources/JobsEngine/JobsEngine.h"
+#include "../../OrthancFramework/Sources/MetricsRegistry.h"
+#include "../../OrthancFramework/Sources/MultiThreading/Future.h"
 #include "../../OrthancFramework/Sources/MultiThreading/Semaphore.h"
 
 
@@ -42,11 +44,13 @@
 {
   class DicomElement;
   class DicomStoreUserConnection;
+  class IExecutorService;
   class OrthancPlugins;
+  class ServerTranscoder;
   class SharedArchive;
   class StorageCommitmentReports;
   
-  
+
   /**
    * This class is responsible for maintaining the storage area on the
    * filesystem (including compression), as well as the index of the
@@ -192,13 +196,11 @@
 
     ServerIndex index_;
     IPluginStorageArea& area_;
-    StorageCache storageCache_;
 
     bool compressionEnabled_;
     bool storeMD5_;
 
     Semaphore largeDicomThrottler_;  // New in Orthanc 1.9.0 (notably for very large DICOM files in WSI)
-    ParsedDicomCache  dicomCache_;
 
     LuaScripting mainLua_;
     LuaScripting filterLua_;
@@ -238,7 +240,7 @@
     unsigned int limitFindInstances_;
     unsigned int limitFindResults_;
 
-    std::unique_ptr<MetricsRegistry>  metricsRegistry_;
+    boost::shared_ptr<MetricsRegistry>  metricsRegistry_;
     bool isHttpServerSecure_;
     bool isExecuteLuaEnabled_;
     bool isRestApiWriteToFileSystemEnabled_;
@@ -246,7 +248,7 @@
 
     std::unique_ptr<StorageCommitmentReports>  storageCommitmentReports_;
 
-    std::unique_ptr<ServerTranscoder> transcoder_;
+    boost::shared_ptr<ServerTranscoder> transcoder_;
     bool transcodeDicomProtocol_;
     bool isIngestTranscoding_;
     DicomTransferSyntax ingestTransferSyntax_;
@@ -277,6 +279,17 @@
                     FileContentType type,
                     const std::string& customData);
 
+    void RemoveFile(const FileInfo& attachment);
+
+    // This method corresponds to StorageAccessor::Write() in Orthanc <= 1.12.11
+    void CreateFile(FileInfo& info,
+                    const void* data,
+                    size_t size,
+                    FileContentType type,
+                    CompressionType compression,
+                    const std::string& precomputedMd5,
+                    const DicomInstanceToStore* instance);
+
     // This DicomModification object is intended to be used as a
     // "rules engine" when de-identifying logs for C-Find, C-Get, and
     // C-Move queries (new in Orthanc 1.8.2)
@@ -285,31 +298,16 @@
 
     boost::posix_time::ptime serverStartTimeUtc_;
 
-  public:
-    class DicomCacheLocker : public boost::noncopyable
-    {
-    private:
-      ServerContext&                               context_;
-      std::string                                  instancePublicId_;
-      std::unique_ptr<ParsedDicomCache::Accessor>  accessor_;
-      std::unique_ptr<ParsedDicomFile>             dicom_;
-      size_t                                       dicomSize_;
-      std::unique_ptr<Semaphore::Locker>           largeDicomLocker_;
-      std::string                                  buffer_;
+    // For streaming
+    boost::shared_ptr<DataSourceReader>                storageAreaReader_;
+    boost::shared_ptr<DataSourceReader>                dicomReader_;
+    boost::shared_ptr<DataSourceReader>                transcoderReader_;
+    boost::shared_ptr<DicomSequentialReader::Factory>  dicomSequentialReaderFactory_;
 
-    public:
-      DicomCacheLocker(ServerContext& context,
-                       const std::string& instancePublicId);
-
-      ~DicomCacheLocker();
-
-      ParsedDicomFile& GetDicom() const;
-
-      const std::string& GetBuffer();
-    };
-
+  public:
     ServerContext(IDatabaseWrapper& database,
                   IPluginStorageArea& area,
+                  ServerTranscoder* transcoder /* takes ownership */,
                   bool unitTesting,
                   size_t maxCompletedJobs,
                   bool readOnly);
@@ -324,11 +322,6 @@
       return index_;
     }
 
-    void SetMaximumStorageCacheSize(size_t size)
-    {
-      return storageCache_.SetMaximumSize(size);
-    }
-
     void SetPatientLevelEnabled(bool enabled);
 
     bool IsPatientLevelEnabled() const
@@ -397,42 +390,26 @@
     void ReadDicomAsJson(Json::Value& result,
                          const std::string& instancePublicId);  // TODO-FIND: Can this be removed?
 
-private:
-    void ReadDicomInternal(std::string& dicom,
-                           const std::string& instancePublicId,
-                           std::unique_ptr<Semaphore::Locker>& largeDicomLocker,
-                           std::size_t largeDicomThreshold);
+    FileInfo LookupDicomForInstance(const std::string& instancePublicId);
 
-    void ReadDicomInternal(std::string& dicom,
-                           std::string& attachmentId,
-                           const std::string& instancePublicId,
-                           std::unique_ptr<Semaphore::Locker>& largeDicomLocker,
-                           std::size_t largeDicomThreshold);
+    StorageAreaDataSource::Range* ReadAttachment(const FileInfo& attachment,
+                                                 bool uncompress);
 
-public:
-    void ReadDicom(std::string& dicom,
-                   const std::string& instancePublicId);
+    StorageAreaDataSource::Range* ReadAttachment(const FileInfo& attachment,
+                                                 const StorageRange& range,
+                                                 bool uncompress);
 
-    void ReadDicom(std::string& dicom,
-                   std::string& attachmentId,
-                   const std::string& instancePublicId);
+    StorageAreaDataSource::Range* ReadRawDicom(const std::string& instancePublicId);
 
-    void ReadDicomForHeader(std::string& dicom,
-                            const std::string& instancePublicId);
+    DicomDataSource::Dicom* ReadParsedDicom(const std::string& instancePublicId);
 
-    bool ReadDicomUntilPixelData(std::string& dicom,
-                                 const std::string& instancePublicId);
+    DicomDataSource::Dicom* ReadDicomUntilPixelData(const std::string& instancePublicId);
 
-    // This method is for low-level operations on "/instances/.../attachments/..."
-    void ReadAttachment(std::string& result,
-                        const FileInfo& attachment,
-                        bool uncompressIfNeeded,
-                        bool skipCache = false);
-
-    void ReadAttachmentRange(std::string& result,
-                             const FileInfo& attachment,
-                             const StorageRange& range,
-                             bool uncompressIfNeeded);
+    TranscoderDataSource::Transcoded* ReadTranscodedDicom(const std::string& instancePublicId,
+                                                          DicomTransferSyntax targetSyntax,
+                                                          TranscodingSopInstanceUidMode mode,
+                                                          bool hasLossyQuality,
+                                                          unsigned int lossyQuality);
 
     void SetStoreMD5ForAttachments(bool storeMD5);
 
@@ -585,27 +562,15 @@
     ImageAccessor* DecodeDicomFrame(const std::string& publicId,
                                     unsigned int frameIndex);
 
-    ImageAccessor* DecodeDicomFrame(const DicomInstanceToStore& dicom,
-                                    unsigned int frameIndex);
-
-    ImageAccessor* DecodeDicomFrame(const void* dicom,
-                                    size_t size,
-                                    unsigned int frameIndex);
-
     void PerformCStoreWithTranscoding(std::string& sopClassUid,
                                       std::string& sopInstanceUid,
                                       DicomStoreUserConnection& connection,
-                                      const std::string& dicom,
+                                      const void* dicomData,
+                                      size_t dicomSize,
                                       bool hasMoveOriginator,
                                       const std::string& moveOriginatorAet,
                                       uint16_t moveOriginatorId);
 
-    virtual bool TranscodeWithCache(std::string& target,
-                                    const std::string& source,
-                                    const std::string& sourceInstanceId,
-                                    const std::string& attachmentId, // for the storage cache
-                                    DicomTransferSyntax targetSyntax);
-
     bool IsTranscodeDicomProtocol() const
     {
       return transcodeDicomProtocol_;
@@ -635,12 +600,23 @@
 
     int64_t GetServerUpTime() const;
 
-    void PublishCacheMetrics();
+    /**
+     * The method "GetTranscoder()" should only be used when
+     * necessary. The use of "TranscoderDataSource::Execute()" is
+     * preferred to benefit from cache and from backpressure.
+     **/
+    const boost::shared_ptr<ServerTranscoder>& GetTranscoder() const;
 
-    void SetTranscoder(ServerTranscoder* transcoder /* takes ownership */);
+    DicomSequentialReader::Factory& GetDicomSequentialReaderFactory();
 
-    ServerTranscoder& GetTranscoder() const;
-
-    void ResetTranscoder();
+    void GetDataSourcesConfigurations(uint64_t& storageMemoryCapacityMb,
+                                      size_t& storageMemoryCacheMb,
+                                      unsigned int& storageReaderThreadsCount,
+                                      uint64_t& transcoderMemoryCapacityMb,
+                                      size_t& transcoderMemoryCacheMb,
+                                      unsigned int& transcoderReaderThreadsCount,
+                                      uint64_t& dicomParserMemoryCapacityMb,
+                                      size_t& dicomParserMemoryCacheMb,
+                                      unsigned int& dicomParserThreadsCount);
   };
 }
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -27,6 +27,7 @@
 #include "../../../OrthancFramework/Sources/Cache/SharedArchive.h"
 #include "../../../OrthancFramework/Sources/Compression/HierarchicalZipWriter.h"
 #include "../../../OrthancFramework/Sources/Constants.h"
+#include "../../../OrthancFramework/Sources/DataSource/DicomSequentialReader.h"
 #include "../../../OrthancFramework/Sources/DicomParsing/DicomDirWriter.h"
 #include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
 #include "../../../OrthancFramework/Sources/Logging.h"
@@ -37,7 +38,6 @@
 #include "../OrthancConfiguration.h"
 #include "../ServerContext.h"
 #include "../SimpleInstanceOrdering.h"
-#include "ThreadedInstancesLoader.h"
 
 #include <cassert>
 #include <stdio.h>
@@ -476,12 +476,12 @@
     {
     }
 
-    virtual void Preload(ThreadedInstancesLoader& instancesLoader) const = 0;
+    virtual void Preload(DicomSequentialReader& instancesLoader) const = 0;
 
     // In the "archive" flavor (without DICOMDIR), dicomDir is NULL
     // In the "media" flavor (with DICOMDIR), dicomDir is not NULL
     virtual void Execute(HierarchicalZipWriter& writer,
-                         ThreadedInstancesLoader& instancesLoader,
+                         DicomSequentialReader& instancesLoader,
                          DicomDirWriter* dicomDir) const = 0;
   };
 
@@ -495,17 +495,17 @@
       std::string   filename_;
 
     public:
-      OpenDirectoryCommand(const std::string& filename) :
+      explicit OpenDirectoryCommand(const std::string& filename) :
         filename_(filename)
       {
       }
 
-      virtual void Preload(ThreadedInstancesLoader& instancesLoader) const ORTHANC_OVERRIDE
+      virtual void Preload(DicomSequentialReader& instancesLoader) const ORTHANC_OVERRIDE
       {
       }
 
       virtual void Execute(HierarchicalZipWriter& writer,
-                           ThreadedInstancesLoader& instancesLoader,
+                           DicomSequentialReader& instancesLoader,
                            DicomDirWriter* dicomDir) const ORTHANC_OVERRIDE
       {
         writer.OpenDirectory(filename_);
@@ -516,12 +516,12 @@
     class CloseDirectoryCommand : public IZipCommand
     {
     public:
-      virtual void Preload(ThreadedInstancesLoader& instancesLoader) const ORTHANC_OVERRIDE
+      virtual void Preload(DicomSequentialReader& instancesLoader) const ORTHANC_OVERRIDE
       {
       }
 
       virtual void Execute(HierarchicalZipWriter& writer,
-                           ThreadedInstancesLoader& instancesLoader,
+                           DicomSequentialReader& instancesLoader,
                            DicomDirWriter* dicomDir) const ORTHANC_OVERRIDE
       {
         writer.CloseDirectory();
@@ -551,36 +551,43 @@
                dicomDirFolder == MEDIA_IMAGES_FOLDER);
       }
 
-      virtual void Preload(ThreadedInstancesLoader& instancesLoader) const ORTHANC_OVERRIDE
+      virtual void Preload(DicomSequentialReader& instancesLoader) const ORTHANC_OVERRIDE
       {
-        instancesLoader.PreloadDicomInstance(instanceId_, fileInfo_);
+        instancesLoader.Submit(fileInfo_);
       }
 
       virtual void Execute(HierarchicalZipWriter& writer,
-                           ThreadedInstancesLoader& instancesLoader,
+                           DicomSequentialReader& instancesLoader,
                            DicomDirWriter* dicomDir) const ORTHANC_OVERRIDE
       {
-        std::string content;
-
-        try
+        if (!instancesLoader.HasNext())
         {
-          LOG(INFO) << "Adding instance " << instanceId_ << " in zip";
-          instancesLoader.WaitDicomInstance(content, instanceId_);
+          throw OrthancException(ErrorCode_InternalError);
         }
-        catch (OrthancException& e)
+        else
         {
-          LOG(WARNING) << "An instance was removed after the job was issued: " << instanceId_;
-          return;
-        }
-
-        writer.OpenFile(filename_);
+          std::unique_ptr<DicomSequentialReader::Item> next;
 
-        writer.Write(content);
+          try
+          {
+            LOG(INFO) << "Adding instance " << instanceId_ << " in zip";
+            next.reset(instancesLoader.Next());
+          }
+          catch (OrthancException& e)
+          {
+            LOG(WARNING) << "An instance was removed after the job was issued: " << instanceId_;
+            return;
+          }
 
-        if (dicomDir != NULL)
-        {
-          std::unique_ptr<ParsedDicomFile> parsed(new ParsedDicomFile(content));
-          dicomDir->Add(dicomDirFolder_, filename_, *parsed);
+          const IMemoryBuffer& dicom = next->GetRawMemoryBuffer();
+
+          writer.OpenFile(filename_);
+          writer.Write(dicom.GetData(), dicom.GetSize());
+
+          if (dicomDir != NULL)
+          {
+            dicomDir->Add(dicomDirFolder_, filename_, next->GetParsedDicomFile());
+          }
         }
       }
     };
@@ -961,7 +968,7 @@
       return commands_.GetSize() + 1;
     }
 
-    void PreloadAllCommands(ThreadedInstancesLoader& instancesLoader)
+    void PreloadAllCommands(DicomSequentialReader& instancesLoader)
     {
       for (size_t i = 0; i < commands_.GetSize(); i++)
       {
@@ -969,7 +976,7 @@
       }
     }
 
-    void RunStep(ThreadedInstancesLoader& instancesLoader,
+    void RunStep(DicomSequentialReader& instancesLoader,
                  size_t index)
     {
       if (index > commands_.GetSize())
@@ -1223,8 +1230,19 @@
       instancesCount_ = writer_->GetInstancesCount();
       uncompressedSize_ = writer_->GetUncompressedSize();
 
-      instancesLoader_.reset(new ThreadedInstancesLoader(context_, loaderThreads_, transcode_, transferSyntax_, lossyQuality_, "ARCH"));
+      if (transcode_)
+      {
+        instancesLoader_.reset(context_.GetDicomSequentialReaderFactory().CreateForTranscodedRawMemoryBuffer(
+                                 transferSyntax_, TranscodingSopInstanceUidMode_AllowNew, hasLossyQuality_, lossyQuality_));
+      }
+      else
+      {
+        instancesLoader_.reset(context_.GetDicomSequentialReaderFactory().CreateForOriginalRawMemoryBuffer());
+      }
+
       writer_->PreloadAllCommands(*instancesLoader_);
+
+      instancesLoader_->Start();
     }
   }
 
@@ -1264,10 +1282,7 @@
       writer_.reset();
     }
 
-    if (instancesLoader_.get() != NULL)
-    {
-      instancesLoader_->Clear(false);
-    }
+    instancesLoader_.reset(NULL);
 
     if (asynchronousTarget_.get() != NULL)
     {
@@ -1336,10 +1351,7 @@
       asynchronousTarget_.reset();
 
       // clear the loader threads
-      if (instancesLoader_.get() != NULL)
-      {
-        instancesLoader_->Clear(true);
-      }
+      instancesLoader_.reset(NULL);
     }
   }
 
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.h	Wed Jul 15 11:42:31 2026 +0200
@@ -33,9 +33,9 @@
 
 namespace Orthanc
 {
+  class DicomSequentialReader;
   class ServerContext;
-  class ThreadedInstancesLoader;
-  
+
   class ArchiveJob : public IJob
   {
   private:
@@ -51,7 +51,7 @@
     std::unique_ptr<ZipWriter::IOutputStream>  synchronousTarget_;  // Only valid before "Start()"
     std::unique_ptr<TemporaryFile>        asynchronousTarget_;
     ServerContext&                        context_;
-    std::unique_ptr<ThreadedInstancesLoader>  instancesLoader_;
+    std::unique_ptr<DicomSequentialReader>  instancesLoader_;
     boost::shared_ptr<ArchiveIndex>       archive_;
     bool                                  isMedia_;
     bool                                  enableExtendedSopClass_;
--- a/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -48,19 +48,14 @@
     assert(IsStarted());
     OpenConnection();
 
-    if (instancesLoader_.get() == NULL)
-    {
-      StartLoaderThreads();
-    }
-
     LOG(INFO) << "Sending instance " << instance << " to modality \"" 
               << parameters_.GetRemoteModality().GetApplicationEntityTitle() << "\"";
 
-    std::string dicom;
+    std::unique_ptr<DicomSequentialReader::Item> item;
 
     try
     {
-      instancesLoader_->WaitDicomInstance(dicom, instance);
+      item.reset(WaitDicomInstance(instance));
     }
     catch (OrthancException& e)
     {
@@ -68,8 +63,10 @@
       return false;
     }
 
+    const IMemoryBuffer& dicom = item->GetRawMemoryBuffer();
+
     std::string sopClassUid, sopInstanceUid;
-    context_.PerformCStoreWithTranscoding(sopClassUid, sopInstanceUid, *connection_, dicom,
+    context_.PerformCStoreWithTranscoding(sopClassUid, sopInstanceUid, *connection_, dicom.GetData(), dicom.GetSize(),
                                           HasMoveOriginator(), moveOriginatorAet_, moveOriginatorId_);
 
     if (storageCommitment_)
--- a/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.h	Wed Jul 15 11:42:31 2026 +0200
@@ -56,11 +56,6 @@
     
     virtual bool HandleTrailingStep() ORTHANC_OVERRIDE;
 
-    virtual const char* GetLoaderPrefix() const ORTHANC_OVERRIDE
-    {
-      return "CSTO";
-    }
-
   public:
     explicit DicomModalityStoreJob(ServerContext& context);
 
--- a/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -97,8 +97,8 @@
 
     try
     {
-      ServerContext::DicomCacheLocker locker(GetContext(), instance);
-      modified.reset(locker.GetDicom().Clone(true));
+      std::unique_ptr<DicomDataSource::Dicom> dicom(GetContext().ReadParsedDicom(instance));
+      modified.reset(dicom->Clone());
     }
     catch (OrthancException&)
     {
@@ -201,8 +201,10 @@
     DicomMap dicom;
 
     {
-      ServerContext::DicomCacheLocker locker(GetContext(), instances.front());
-      OrthancConfiguration::DefaultExtractDicomSummary(dicom, locker.GetDicom());
+      std::unique_ptr<DicomDataSource::Dicom> loadedDicom(GetContext().ReadParsedDicom(instances.front()));
+      DicomDataSource::Dicom::Lock lock(*loadedDicom);
+
+      OrthancConfiguration::DefaultExtractDicomSummary(dicom, lock.GetContent());
     }
 
     const std::set<DicomTag> moduleTags = removals_;
--- a/OrthancServer/Sources/ServerJobs/Operations/DicomInstanceOperationValue.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/DicomInstanceOperationValue.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -30,6 +30,7 @@
 {
   void DicomInstanceOperationValue::ReadDicom(std::string& dicom) const
   {
-    context_.ReadDicom(dicom, id_);
+    std::unique_ptr<StorageAreaDataSource::Range> raw(context_.ReadRawDicom(id_));
+    raw->Copy(dicom);
   }
 }
--- a/OrthancServer/Sources/ServerJobs/Operations/ModifyInstanceOperation.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/ModifyInstanceOperation.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -87,8 +87,8 @@
     std::unique_ptr<ParsedDicomFile> modified;
     
     {
-      ServerContext::DicomCacheLocker lock(context_, instance.GetId());
-      modified.reset(lock.GetDicom().Clone(true));
+      std::unique_ptr<DicomDataSource::Dicom> dicom(context_.ReadParsedDicom(instance.GetId()));
+      modified.reset(dicom->Clone());
     }
 
     try
--- a/OrthancServer/Sources/ServerJobs/Operations/StoreScuOperation.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/StoreScuOperation.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -55,7 +55,8 @@
       instance.ReadDicom(dicom);
 
       std::string sopClassUid, sopInstanceUid;  // Unused
-      context_.PerformCStoreWithTranscoding(sopClassUid, sopInstanceUid, lock.GetConnection(), dicom,
+      context_.PerformCStoreWithTranscoding(sopClassUid, sopInstanceUid, lock.GetConnection(),
+                                            dicom.empty() ? NULL : dicom.c_str(), dicom.size(),
                                             false /* Not a C-MOVE */, "", 0);
     }
     catch (OrthancException& e)
--- a/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -29,6 +29,7 @@
 #include "../../../OrthancFramework/Sources/Logging.h"
 #include "../../../OrthancFramework/Sources/SerializationToolbox.h"
 #include "../ServerContext.h"
+#include "../ServerTranscoder.h"
 
 #include <dcmtk/dcmdata/dcfilefo.h>
 
@@ -37,10 +38,7 @@
 {
   bool OrthancPeerStoreJob::HandleInstance(const std::string& instance)
   {
-    if (instancesLoader_.get() == NULL)
-    {
-      StartLoaderThreads();
-    }
+    assert(IsStarted());
 
     if (client_.get() == NULL)
     {
@@ -62,8 +60,10 @@
 
     try
     {
-      std::string dicom;
-      instancesLoader_->WaitDicomInstance(dicom, instance);
+      std::unique_ptr<DicomSequentialReader::Item> item;
+      item.reset(WaitDicomInstance(instance));
+
+      const IMemoryBuffer& dicom = item->GetRawMemoryBuffer();
 
       if (transcode_)
       {
@@ -71,21 +71,21 @@
         syntaxes.insert(transferSyntax_);
         
         IDicomTranscoder::DicomImage source, transcoded;
-        source.SetExternalBuffer(dicom);
+        source.SetExternalBuffer(dicom.GetData(), dicom.GetSize());
 
-        if (context_.GetTranscoder().Transcode(transcoded, source, syntaxes, TranscodingSopInstanceUidMode_AllowNew))
+        if (context_.GetTranscoder()->Transcode(transcoded, source, syntaxes, TranscodingSopInstanceUidMode_AllowNew))
         {
           body.assign(reinterpret_cast<const char*>(transcoded.GetBufferData()),
                       transcoded.GetBufferSize());
         }
         else
         {
-          body.swap(dicom);
+          dicom.CopyToString(body);
         }
       }
       else
       {
-        body.swap(dicom);
+        dicom.CopyToString(body);
       }
     }
     catch (OrthancException& e)
--- a/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.h	Wed Jul 15 11:42:31 2026 +0200
@@ -49,11 +49,6 @@
     
     virtual bool HandleTrailingStep() ORTHANC_OVERRIDE;
 
-    virtual const char* GetLoaderPrefix() const ORTHANC_OVERRIDE
-    {
-      return "PSTO";
-    }
-
   public:
     explicit OrthancPeerStoreJob(ServerContext& context) :
       StoreJob(context),
--- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -24,10 +24,12 @@
 #include "../PrecompiledHeadersServer.h"
 #include "ResourceModificationJob.h"
 
+#include "../../../OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h"
 #include "../../../OrthancFramework/Sources/Logging.h"
 #include "../../../OrthancFramework/Sources/SerializationToolbox.h"
 #include "../DicomInstanceToStore.h"
 #include "../ServerContext.h"
+#include "../ServerTranscoder.h"
 
 #include <dcmtk/dcmdata/dcfilefo.h>
 #include <dcmtk/dcmdata/dcdeftag.h>
@@ -185,8 +187,10 @@
     {
       for (std::set<std::string>::const_iterator it = instancesToReconstruct_.begin(); it != instancesToReconstruct_.end(); ++it)
       {
-        ServerContext::DicomCacheLocker locker(GetContext(), *it);
-        const ParsedDicomFile& modifiedDicom = locker.GetDicom();
+        std::unique_ptr<DicomDataSource::Dicom> dicom(GetContext().ReadParsedDicom(*it));
+        DicomDataSource::Dicom::Lock dicomLock(*dicom);
+
+        const ParsedDicomFile& modifiedDicom = dicomLock.GetContent();
 
         GetContext().GetIndex().ReconstructInstance(modifiedDicom, false, ResourceType_Instance /* dummy */);
       }
@@ -219,8 +223,10 @@
  
     try
     {
-      ServerContext::DicomCacheLocker locker(GetContext(), instance);
-      const ParsedDicomFile& original = locker.GetDicom();
+      std::unique_ptr<DicomDataSource::Dicom> dicom(GetContext().ReadParsedDicom(instance));
+      DicomDataSource::Dicom::Lock lock(*dicom);
+
+      const ParsedDicomFile& original = lock.GetContent();
 
       originalHasher.reset(new DicomInstanceHasher(original.GetHasher()));
       modified.reset(original.Clone(true));
@@ -262,7 +268,7 @@
       source.AcquireParsed(*modified);  // "modified" is invalid below this point
       
       IDicomTranscoder::DicomImage transcoded;
-      if (GetContext().GetTranscoder().Transcode(transcoded, source, syntaxes, TranscodingSopInstanceUidMode_AllowNew))
+      if (GetContext().GetTranscoder()->Transcode(transcoded, source, syntaxes, TranscodingSopInstanceUidMode_AllowNew))
       {
         modified.reset(transcoded.ReleaseAsParsedDicomFile());
 
--- a/OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -72,8 +72,8 @@
 
     try
     {
-      ServerContext::DicomCacheLocker locker(GetContext(), instance);
-      modified.reset(locker.GetDicom().Clone(true));
+      std::unique_ptr<DicomDataSource::Dicom> dicom(GetContext().ReadParsedDicom(instance));
+      modified.reset(dicom->Clone());
     }
     catch (OrthancException&)
     {
--- a/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -290,9 +290,12 @@
           // Make sure that the DICOM file can be re-read by DCMTK
           // from the file storage, and that the actual SOP
           // class/instance UIDs do match
-          ServerContext::DicomCacheLocker locker(context_, orthancId[0]);
-          if (locker.GetDicom().GetTagValue(a, DICOM_TAG_SOP_CLASS_UID) &&
-              locker.GetDicom().GetTagValue(b, DICOM_TAG_SOP_INSTANCE_UID) &&
+
+          std::unique_ptr<DicomDataSource::Dicom> dicom(context_.ReadParsedDicom(orthancId[0]));
+          DicomDataSource::Dicom::Lock lock(*dicom);
+
+          if (lock.GetContent().GetTagValue(a, DICOM_TAG_SOP_CLASS_UID) &&
+              lock.GetContent().GetTagValue(b, DICOM_TAG_SOP_INSTANCE_UID) &&
               b == sopInstanceUids_[index])
           {
             if (a == sopClassUids_[index])
--- a/OrthancServer/Sources/ServerJobs/StoreJob.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/StoreJob.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -35,6 +35,24 @@
 
 namespace Orthanc
 {
+  class StoreJob::ReaderUserData : public IDynamicObject
+  {
+  private:
+    std::string instanceId_;
+
+  public:
+    explicit ReaderUserData(const std::string& instanceId) :
+      instanceId_(instanceId)
+    {
+    }
+
+    const std::string& GetInstanceId() const
+    {
+      return instanceId_;
+    }
+  };
+
+
   StoreJob::StoreJob(ServerContext& context) :
     context_(context)
   {
@@ -63,6 +81,51 @@
   }
 
 
+  DicomSequentialReader::Item* StoreJob::WaitDicomInstance(const std::string& instanceId)
+  {
+    assert(IsStarted());
+
+    if (instancesLoader_.get() == NULL)
+    {
+      instancesLoader_.reset(context_.GetDicomSequentialReaderFactory().CreateForOriginalRawMemoryBuffer());
+
+      const size_t position = GetPosition(); // in case we reload a job in progress, we shall not preload the first instances
+
+      if (instanceId != instancesIds_[position])
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      for (size_t i = position; i < instancesIds_.size(); ++i)
+      {
+        instancesLoader_->Submit(filesInfo_[i], new ReaderUserData(instancesIds_[i]));
+      }
+
+      instancesLoader_->Start();
+    }
+
+    if (!instancesLoader_->HasNext())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    std::unique_ptr<DicomSequentialReader::Item> next(instancesLoader_->Next());
+    assert(next.get() != NULL &&
+           next->HasUserData());
+
+    {
+      // This is just a sanity check, the class "MoveReaderUserData" could be removed
+      const ReaderUserData& userData = dynamic_cast<const ReaderUserData&>(next->GetUserData());
+      if (userData.GetInstanceId() != instanceId)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    return next.release();
+  }
+
+
   void StoreJob::AddInstances(const std::vector<std::string>& instancesIds,
                               const std::vector<FileInfo>& filesInfo)
   {
@@ -81,24 +144,6 @@
     }
   }
 
-
-  void StoreJob::StartLoaderThreads()
-  {
-    size_t loaderThreads = 1;
-    {
-      OrthancConfiguration::ReaderLock lock;
-      loaderThreads = lock.GetConfiguration().GetLoaderThreads();
-    }
-
-    instancesLoader_.reset(new ThreadedInstancesLoader(context_, loaderThreads, false, DicomTransferSyntax_LittleEndianImplicit /* dummy value not used*/, 0, GetLoaderPrefix()));
-
-    size_t position = GetPosition(); // in case we reload a job in progress, we shall not preload the first instances
-    for (size_t i = position; i < instancesIds_.size(); ++i)
-    {
-      instancesLoader_->PreloadDicomInstance(instancesIds_[i], filesInfo_[i]);
-    }
-  }
-
   
   void StoreJob::Stop(JobStopReason reason)
   {
--- a/OrthancServer/Sources/ServerJobs/StoreJob.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/StoreJob.h	Wed Jul 15 11:42:31 2026 +0200
@@ -24,27 +24,30 @@
 #pragma once
 
 #include "../../../OrthancFramework/Sources/Compatibility.h"
-#include "../../../OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h"
+#include "../../../OrthancFramework/Sources/DataSource/DicomSequentialReader.h"
 #include "../../../OrthancFramework/Sources/FileStorage/FileInfo.h"
-#include "ThreadedInstancesLoader.h"
+#include "../../../OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h"
 
 #include <vector>
 
 namespace Orthanc
 {
+  class DicomSequentialReader;
   class ServerContext;
 
   class StoreJob : public SetOfInstancesJob
   {
+  private:
+    class ReaderUserData;
+
+    std::unique_ptr<DicomSequentialReader>  instancesLoader_;
+    std::vector<std::string>                instancesIds_;
+    std::vector<FileInfo>                   filesInfo_;
+
   protected:
-    ServerContext&                             context_;
-    std::unique_ptr<ThreadedInstancesLoader>   instancesLoader_;
-    std::vector<std::string>                   instancesIds_;
-    std::vector<FileInfo>                      filesInfo_;
+    ServerContext&  context_;
 
-    virtual const char* GetLoaderPrefix() const = 0;
-
-    void StartLoaderThreads();
+    DicomSequentialReader::Item* WaitDicomInstance(const std::string& instanceId);
 
   public:
     explicit StoreJob(ServerContext& context);
--- a/OrthancServer/Sources/ServerJobs/ThreadedInstancesLoader.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,276 +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-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, 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 "ThreadedInstancesLoader.h"
-
-#include "../ServerContext.h"
-#include "../../../OrthancFramework/Sources/Logging.h"
-#include "../../../OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h"
-#include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
-
-static boost::mutex loaderThreadsCounterMutex;
-static uint32_t loaderThreadsCounter = 0;
-
-static std::string GetLoaderThreadName(const std::string& prefix)
-{
-  boost::mutex::scoped_lock lock(loaderThreadsCounterMutex);
-  std::string threadName = prefix + std::string("-LOAD-") +
-                                boost::lexical_cast<std::string>(loaderThreadsCounter++);
-  loaderThreadsCounter %= 1000000;
-
-  return threadName;
-}
-
-namespace Orthanc
-{
-  class InstanceToPreload : public Orthanc::IDynamicObject
-  {
-  private:
-    std::string id_;
-    FileInfo    fileInfo_;
-
-  public:
-    InstanceToPreload(const std::string& id,
-                      const FileInfo& fileInfo) :
-      id_(id),
-      fileInfo_(fileInfo)
-    {
-    }
-
-    const std::string& GetId() const
-    {
-      return id_;
-    }
-
-    const FileInfo& GetFileInfo() const
-    {
-      return fileInfo_;
-    }
-  };
-
-
-  ThreadedInstancesLoader::ThreadedInstancesLoader(ServerContext& context,
-                                                   size_t threadCount,
-                                                   bool transcode,
-                                                   DicomTransferSyntax transferSyntax,
-                                                   unsigned int lossyQuality,
-                                                   const std::string& nameForLogs4Char) :
-    context_(context),
-    transcode_(transcode),
-    transferSyntax_(transferSyntax),
-    lossyQuality_(lossyQuality),
-    nameForLogs4Char_(nameForLogs4Char),
-    availableInstancesSemaphore_(3 * threadCount),
-    instancesToPreload_(0), // no limit on the message queue, the flow control is performed by the availableInstancesSemaphore_
-    loadersShouldStop_(false)
-  {
-    assert(nameForLogs4Char_.size() <= 4);
-
-    if (threadCount < 1)
-    {
-      THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_InternalError);
-    }
-
-    for (size_t i = 0; i < threadCount; i++)
-    {
-      threads_.push_back(new boost::thread(PreloaderWorkerThread, this));
-    }
-  }
-
-
-  ThreadedInstancesLoader::~ThreadedInstancesLoader()
-  {
-    try
-    {
-      ThreadedInstancesLoader::Clear(false);
-    }
-    catch (const OrthancException& e)
-    {
-      // Don't throw exceptions in destructors
-      LOG(ERROR) << "Exception: " << e.What();
-    }
-  }
-
-
-  void ThreadedInstancesLoader::Clear(bool isAbort)
-  {
-    if (threads_.size() > 0)
-    {
-      loadersShouldStop_ = true; // no need to protect this by a mutex. This is the only "writer" and all loaders are "readers"
-
-      if (isAbort)
-      {
-        LOG(INFO) << "Cancelling the loader threads";
-        instancesToPreload_.Clear();
-      }
-      else
-      {
-        LOG(INFO) << "Waiting for loader threads to complete";
-      }
-
-      // unlock the loaders if they are waiting on this message queue (this happens when the job completes sucessfully)
-      for (size_t i = 0; i < threads_.size(); i++)
-      {
-        instancesToPreload_.Enqueue(NULL);
-      }
-
-      // unlock the loaders if they are waiting for room in the availableInstances (this happens when the job is interrupted)
-      availableInstancesSemaphore_.Release(threads_.size());
-
-      for (size_t i = 0; i < threads_.size(); i++)
-      {
-        if (threads_[i]->joinable())
-        {
-          threads_[i]->join();
-        }
-        delete threads_[i];
-      }
-
-      threads_.clear();
-      availableInstances_.clear();
-
-      LOG(INFO) << "Waiting for loader threads to complete - done";
-    }
-  }
-
-
-  void ThreadedInstancesLoader::PreloaderWorkerThread(ThreadedInstancesLoader* that)
-  {
-    Logging::ScopedCurrentThreadNameSetter setter(::GetLoaderThreadName(that->nameForLogs4Char_));
-
-    LOG(INFO) << "Loader thread has started";
-
-    while (true)
-    {
-      that->availableInstancesSemaphore_.Acquire(1); // reserve the slot early (since the instances are ordered, it is important that a single worker does not push dozens of small instances while we are waiting for a slot for a big instance)
-
-      std::unique_ptr<InstanceToPreload> instanceToPreload(dynamic_cast<InstanceToPreload*>(that->instancesToPreload_.Dequeue(0)));
-      if (instanceToPreload.get() == NULL || that->loadersShouldStop_)  // that's the signal to exit the thread
-      {
-        LOG(INFO) << "Loader thread has completed";
-        return;
-      }
-
-      const std::string& instanceId = instanceToPreload->GetId();
-      LOG(INFO) << "Loader thread is loading instance " << instanceId;
-
-      try
-      {
-        boost::shared_ptr<std::string> dicomContent(new std::string());
-        that->context_.ReadAttachment(*dicomContent, instanceToPreload->GetFileInfo(), true);
-
-        if (that->transcode_)
-        {
-          boost::shared_ptr<std::string> transcodedDicom(new std::string());
-          if (that->TranscodeDicom(*transcodedDicom, *dicomContent, instanceId))
-          {
-            dicomContent = transcodedDicom;
-          }
-        }
-
-        {
-          boost::mutex::scoped_lock lock(that->availableInstancesMutex_);
-          that->availableInstances_[instanceId] = dicomContent;
-          that->condInstanceAvailable_.notify_one();
-        }
-      }
-      catch (OrthancException& e)
-      {
-        LOG(ERROR) << "Failed to load instance " << instanceId << " error: " << e.GetDetails();
-        boost::mutex::scoped_lock lock(that->availableInstancesMutex_);
-        // store a NULL result to notify that we could not read the instance
-        that->availableInstances_[instanceId] = boost::shared_ptr<std::string>();
-        that->condInstanceAvailable_.notify_one();
-      }
-      catch (...)
-      {
-        LOG(ERROR) << "Failed to load instance " << instanceId << " unknown error";
-        boost::mutex::scoped_lock lock(that->availableInstancesMutex_);
-        // store a NULL result to notify that we could not read the instance
-        that->availableInstances_[instanceId] = boost::shared_ptr<std::string>();
-        that->condInstanceAvailable_.notify_one();
-      }
-    }
-  }
-
-
-  void ThreadedInstancesLoader::PreloadDicomInstance(const std::string& instanceId,
-                                                     const FileInfo& fileInfo)
-  {
-    instancesToPreload_.Enqueue(new InstanceToPreload(instanceId, fileInfo));
-  }
-
-
-  void ThreadedInstancesLoader::WaitDicomInstance(std::string& dicom,
-                                                  const std::string& instanceId)
-  {
-    boost::mutex::scoped_lock lock(availableInstancesMutex_);
-
-    // wait for this instance to be available but this might not be the one we are waiting for !
-    while (availableInstances_.find(instanceId) == availableInstances_.end())
-    {
-      condInstanceAvailable_.wait(lock);
-    }
-
-    boost::shared_ptr<std::string> dicomContent;
-
-    // this is the instance we were waiting for
-    dicomContent = availableInstances_[instanceId];
-    availableInstances_.erase(instanceId);
-    availableInstancesSemaphore_.Release(1);
-
-    if (dicomContent.get() == NULL)  // there has been an error while reading the file
-    {
-      throw OrthancException(ErrorCode_InexistentItem);
-    }
-
-    dicom.swap(*dicomContent);
-  }
-
-
-  bool ThreadedInstancesLoader::TranscodeDicom(std::string& transcodedBuffer,
-                                               const std::string& sourceBuffer,
-                                               const std::string& instanceId)
-  {
-    if (transcode_)
-    {
-      std::set<DicomTransferSyntax> syntaxes;
-      syntaxes.insert(transferSyntax_);
-
-      IDicomTranscoder::DicomImage source, transcoded;
-      source.SetExternalBuffer(sourceBuffer);
-
-      if (context_.GetTranscoder().Transcode(transcoded, source, syntaxes, TranscodingSopInstanceUidMode_AllowNew, lossyQuality_))
-      {
-        transcodedBuffer.assign(reinterpret_cast<const char*>(transcoded.GetBufferData()), transcoded.GetBufferSize());
-        return true;
-      }
-      else
-      {
-        LOG(INFO) << "Cannot transcode instance " << instanceId
-                  << " to transfer syntax: " << GetTransferSyntaxUid(transferSyntax_);
-      }
-    }
-
-    return false;
-  }
-}
--- a/OrthancServer/Sources/ServerJobs/ThreadedInstancesLoader.h	Wed Jul 08 11:45:33 2026 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,82 +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-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, 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 "../../../OrthancFramework/Sources/Compatibility.h"
-#include "../../../OrthancFramework/Sources/Enumerations.h"
-#include "../../../OrthancFramework/Sources/FileStorage/FileInfo.h"
-#include "../../../OrthancFramework/Sources/MultiThreading/BlockingSharedMessageQueue.h"
-#include "../../../OrthancFramework/Sources/MultiThreading/Semaphore.h"
-
-#include <string>
-#include <boost/noncopyable.hpp>
-
-
-namespace Orthanc
-{
-  class ServerContext;
-
-  class ThreadedInstancesLoader ORTHANC_FINAL : public boost::noncopyable
-  {
-  private:
-    // Parameters from the constructor
-    ServerContext&                      context_;
-    bool                                transcode_;
-    DicomTransferSyntax                 transferSyntax_;
-    unsigned int                        lossyQuality_;
-    std::string                         nameForLogs4Char_;
-
-    // Internal variables
-    boost::condition_variable           condInstanceAvailable_;
-    std::map<std::string, boost::shared_ptr<std::string> >  availableInstances_;
-    boost::mutex                        availableInstancesMutex_;
-    Semaphore                           availableInstancesSemaphore_;
-    BlockingSharedMessageQueue          instancesToPreload_;
-    std::vector<boost::thread*>         threads_;
-    bool                                loadersShouldStop_;
-
-    static void PreloaderWorkerThread(ThreadedInstancesLoader* that);
-
-    bool TranscodeDicom(std::string& transcodedBuffer,
-                        const std::string& sourceBuffer,
-                        const std::string& instanceId);
-
-  public:
-    ThreadedInstancesLoader(ServerContext& context,
-                            size_t threadCount,
-                            bool transcode,
-                            DicomTransferSyntax transferSyntax,
-                            unsigned int lossyQuality,
-                            const std::string& nameForLogs4Char);
-
-    ~ThreadedInstancesLoader();
-
-    void Clear(bool isAbort);
-
-    void PreloadDicomInstance(const std::string& instanceId,
-                              const FileInfo& fileInfo);
-
-    void WaitDicomInstance(std::string& dicom,
-                           const std::string& instanceId);
-  };
-}
--- a/OrthancServer/Sources/ServerToolbox.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/ServerToolbox.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -24,9 +24,8 @@
 #include "PrecompiledHeadersServer.h"
 #include "ServerToolbox.h"
 
+#include "../../OrthancFramework/Sources/Compression/ZlibCompressor.h"
 #include "../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h"
-#include "../../OrthancFramework/Sources/FileStorage/StorageAccessor.h"
-#include "../../OrthancFramework/Sources/FileStorage/StorageCache.h"
 #include "../../OrthancFramework/Sources/Logging.h"
 #include "../../OrthancFramework/Sources/OrthancException.h"
 #include "Database/IDatabaseWrapper.h"
@@ -96,6 +95,36 @@
     }
 
 
+    // This corresponds to StorageAccessor::Read() in Orthanc <= 1.12.11
+    static void ReadFile(std::string& content,
+                         IPluginStorageArea& storageArea,
+                         const FileInfo& info)
+    {
+      switch (info.GetCompressionType())
+      {
+        case CompressionType_None:
+        {
+          std::unique_ptr<IMemoryBuffer> buffer;
+          buffer.reset(storageArea.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), info.GetCustomData()));
+          buffer->MoveToString(content);
+          break;
+        }
+
+        case CompressionType_ZlibWithSize:
+        {
+          ZlibCompressor zlib;
+          std::unique_ptr<IMemoryBuffer> compressed;
+          compressed.reset(storageArea.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), info.GetCustomData()));
+          zlib.Uncompress(content, compressed->GetData(), compressed->GetSize());
+          break;
+        }
+
+        default:
+          throw OrthancException(ErrorCode_NotImplemented);
+      }
+    }
+
+
     void ReconstructMainDicomTags(IDatabaseWrapper::ITransaction& transaction,
                                   IPluginStorageArea& storageArea,
                                   ResourceType level)
@@ -146,10 +175,8 @@
         try
         {
           // Read and parse the content of the DICOM file
-          StorageAccessor accessor(storageArea);  // no cache
-
           std::string content;
-          accessor.Read(content, attachment);
+          ReadFile(content, storageArea, attachment);
 
           ParsedDicomFile dicom(content);
 
@@ -294,26 +321,31 @@
 
       if (limitToThisLevelDicomTags && instances.size() > 0) // in this case, we only need to rebuild one instance !
       {
-        ServerContext::DicomCacheLocker locker(context, instances.front());
-        context.GetIndex().ReconstructInstance(locker.GetDicom(), true, limitToLevel);
+        std::unique_ptr<DicomDataSource::Dicom> dicom(context.ReadParsedDicom(instances.front()));
+        DicomDataSource::Dicom::Lock lock(*dicom);
+
+        context.GetIndex().ReconstructInstance(lock.GetContent(), true, limitToLevel);
       }
       else
       {
         for (std::list<std::string>::const_iterator 
               it = instances.begin(); it != instances.end(); ++it)
         {
-          ServerContext::DicomCacheLocker locker(context, *it);
+          std::unique_ptr<DicomDataSource::Dicom> dicom(context.ReadParsedDicom(*it));
+          DicomDataSource::Dicom::Lock lock(*dicom);
 
           // Delay the reconstruction of DICOM-as-JSON to its next access through "ServerContext"
           context.GetIndex().DeleteAttachment(*it, FileContentType_DicomAsJson, false /* no revision */,
                                               -1 /* dummy revision */, "" /* dummy MD5 */);
           
-          context.GetIndex().ReconstructInstance(locker.GetDicom(), false, ResourceType_Instance /* dummy */);
+          context.GetIndex().ReconstructInstance(lock.GetContent(), false, ResourceType_Instance /* dummy */);
 
           if (reconstructFiles)
           {
+            std::unique_ptr<ParsedDicomFile> clone(lock.GetContent().Clone(true));
+
             std::string resultPublicId;  // ignored
-            std::unique_ptr<DicomInstanceToStore> dicomInstancetoStore(DicomInstanceToStore::CreateFromParsedDicomFile(locker.GetDicom()));
+            std::unique_ptr<DicomInstanceToStore> dicomInstancetoStore(DicomInstanceToStore::CreateFromParsedDicomFile(*clone));
 
             // TODO: TranscodeAndStore and specifically ServerIndex::Store have been "poluted" by the isReconstruct parameter
             // we should very likely refactor it
--- a/OrthancServer/Sources/ServerTranscoder.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/ServerTranscoder.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -24,22 +24,92 @@
 #include "PrecompiledHeadersServer.h"
 #include "ServerTranscoder.h"
 
+#include "../../OrthancFramework/Sources/Cache/LeastRecentlyUsedIndex.h"
+#include "../../OrthancFramework/Sources/DataSource/DicomDataSource.h"
+#include "../../OrthancFramework/Sources/DataSource/StorageAreaDataSource.h"
+#include "../../OrthancFramework/Sources/DataSource/TranscoderDataSource.h"
 #include "../../OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.h"
 #include "../../OrthancFramework/Sources/DicomFormat/DicomImageInformation.h"
 #include "../../OrthancFramework/Sources/Logging.h"
 #include "../../OrthancFramework/Sources/OrthancException.h"
 #include "../Plugins/Engine/OrthancPlugins.h"
+#include "DicomInstanceToStore.h"
 #include "OrthancConfiguration.h"
 
+#include <boost/thread.hpp>
 #include <dcmtk/dcmdata/dcfilefo.h>
 
 
+static const size_t MAX_CACHE_SIZE = 10000;
+
 namespace Orthanc
 {
+  namespace
+  {
+    enum WorkingSource
+    {
+      WorkingSource_Unknown,
+      WorkingSource_Builtin,
+      WorkingSource_PluginsDecoder,
+      WorkingSource_PluginsTranscoder
+    };
+  }
+
+  class ServerTranscoder::PImpl
+  {
+  private:
+    boost::mutex  mutex_;
+    LeastRecentlyUsedIndex<std::string, WorkingSource>  workingSources_;
+
+  public:
+    PImpl()
+    {
+    }
+
+    WorkingSource LookupWorkingSource(const std::string& attachmentId)
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      WorkingSource source;
+      if (workingSources_.Contains(attachmentId, source))
+      {
+        return source;
+      }
+      else
+      {
+        return WorkingSource_Unknown;
+      }
+    }
+
+    void SetWorkingSource(const std::string& attachmentId,
+                          WorkingSource source)
+    {
+      if (source == WorkingSource_Unknown)
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+      else
+      {
+        boost::mutex::scoped_lock lock(mutex_);
+
+        assert(MAX_CACHE_SIZE > 0);
+
+        if (workingSources_.GetSize() >= MAX_CACHE_SIZE &&
+            !workingSources_.Contains(attachmentId))
+        {
+          workingSources_.RemoveOldest();
+        }
+
+        workingSources_.AddOrMakeMostRecent(attachmentId, source);
+      }
+    }
+  };
+
   ServerTranscoder::ServerTranscoder(unsigned int maxConcurrentDcmtkTranscoder) :
 #if ORTHANC_ENABLE_PLUGINS == 1
     plugins_(NULL),
 #endif
+    pimpl_(new PImpl),
     dcmtkTranscoder_(new DcmtkTranscoder(maxConcurrentDcmtkTranscoder)),
     builtinDecoderTranscoderOrder_(BuiltinDecoderTranscoderOrder_After)
   {
@@ -68,14 +138,164 @@
 #endif
 
 
-  ImageAccessor* ServerTranscoder::DecodeFrame(const ParsedDicomFile& parsedDicom,
-                                               const void* buffer,
-                                               size_t size,
+  bool ServerTranscoder::HasPluginsDecoder() const
+  {
+#if ORTHANC_ENABLE_PLUGINS == 1
+    return (plugins_ != NULL &&
+            plugins_->HasCustomImageDecoder());
+#else
+    return false;
+#endif
+  }
+
+
+  bool ServerTranscoder::HasPluginsTranscoder() const
+  {
+#if ORTHANC_ENABLE_PLUGINS == 1
+    return (plugins_ != NULL &&
+            plugins_->HasCustomTranscoder());
+#else
+    return false;
+#endif
+  }
+
+
+  ImageAccessor* ServerTranscoder::DecodeFrameBuiltin(const ParsedDicomFile& dicom,
+                                                      unsigned int frameIndex)
+  {
+    // Use Orthanc's built-in decoder
+
+    try
+    {
+      return dicom.DecodeFrame(frameIndex);
+    }
+    catch (const OrthancException&) // NOLINT(bugprone-empty-catch)
+    {
+    }
+
+    return NULL;
+  }
+
+
+  ImageAccessor* ServerTranscoder::DecodeFrameUsingPluginsDecoder(const void* buffer,
+                                                                  size_t size,
+                                                                  unsigned int frameIndex)
+  {
+#if ORTHANC_ENABLE_PLUGINS == 1
+    if (HasPluginsDecoder())
+    {
+      try
+      {
+        return plugins_->Decode(buffer, size, frameIndex);
+      }
+      catch (const OrthancException&) // NOLINT(bugprone-empty-catch)
+      {
+      }
+    }
+#endif
+
+    return NULL;
+  }
+
+
+  ImageAccessor* ServerTranscoder::DecodeFrameUsingPluginsTranscoder(const void* buffer,
+                                                                     size_t size,
+                                                                     unsigned int frameIndex)
+  {
+    /**
+     * Contrarily to "ServerTranscoder::Transcode()", this method
+     * doesn't use "dcmtkTranscoder_", as "DecodeFrameBuiltin()" would
+     * have succeeded in this case.
+     **/
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+    if (HasPluginsTranscoder())
+    {
+      DicomImage transcoded;
+      DicomImage source;
+      std::set<DicomTransferSyntax> allowedSyntaxes;
+
+      source.SetExternalBuffer(buffer, size);
+      allowedSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
+
+      if (plugins_->Transcode(transcoded, source, allowedSyntaxes, TranscodingSopInstanceUidMode_AllowNew))
+      {
+        try
+        {
+          std::unique_ptr<ParsedDicomFile> dicom(transcoded.ReleaseAsParsedDicomFile());
+          return DecodeFrameBuiltin(*dicom, frameIndex);
+        }
+        catch (const OrthancException&) // NOLINT(bugprone-empty-catch)
+        {
+        }
+      }
+    }
+#endif
+
+    return NULL;
+  }
+
+
+  ImageAccessor* ServerTranscoder::DecodeFrameBuiltin(const boost::shared_ptr<DataSourceReader>& dicomReader,
+                                                      const FileInfo& attachment,
+                                                      unsigned int frameIndex)
+  {
+    std::unique_ptr<DicomDataSource::Dicom> dicom(
+      DicomDataSource::Execute(*dicomReader, DicomDataSource::CreateWholeRequest(attachment)));
+
+    DicomDataSource::Dicom::Lock lock(*dicom);
+
+    return DecodeFrameBuiltin(lock.GetContent(), frameIndex);
+  }
+
+
+  ImageAccessor* ServerTranscoder::DecodeFrameUsingPluginsDecoder(const boost::shared_ptr<DataSourceReader>& storageAreaReader,
+                                                                  const FileInfo& attachment,
+                                                                  unsigned int frameIndex)
+  {
+    if (!HasPluginsDecoder())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      std::unique_ptr<StorageAreaDataSource::Range> range(
+        StorageAreaDataSource::Execute(
+          *storageAreaReader, StorageAreaDataSource::CreateAttachmentRequest(attachment, true /* uncompress */)));
+
+      return DecodeFrameUsingPluginsDecoder(range->GetData(), range->GetSize(), frameIndex);
+    }
+  }
+
+
+  ImageAccessor* ServerTranscoder::DecodeFrameUsingPluginsTranscoder(const boost::shared_ptr<DataSourceReader>& transcoderReader,
+                                                                     const FileInfo& attachment,
+                                                                     unsigned int frameIndex)
+  {
+    if (!HasPluginsDecoder())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      std::unique_ptr<TranscoderDataSource::Transcoded> transcoded(
+        TranscoderDataSource::Execute(*transcoderReader, TranscoderDataSource::CreateRequest(
+                                        attachment, DicomTransferSyntax_LittleEndianExplicit,
+                                        TranscodingSopInstanceUidMode_AllowNew, false /* no lossy quality specified */, 0)));
+
+      TranscoderDataSource::Transcoded::LockAsParsed lock(*transcoded);
+
+      return DecodeFrameBuiltin(lock.GetContent(), frameIndex);
+    }
+  }
+
+
+  ImageAccessor* ServerTranscoder::DecodeFrame(const DicomInstanceToStore& image,
                                                unsigned int frameIndex)
   {
     { // check that the target image has a valid/reasonable size before decoding to avoid possible crash or OOB during transcoding
       DicomMap summary;
-      parsedDicom.ExtractDicomSummary(summary, 128);
+      image.GetSummary(summary);
 
       DicomImageInformation imageInfo(summary);
       imageInfo.ThrowIfInvalidFrameSize();
@@ -85,83 +305,123 @@
 
     if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before)
     {
-      // Use Orthanc's built-in decoder
-
-      try
+      decoded.reset(DecodeFrameBuiltin(image.GetParsedDicomFile(), frameIndex));
+      if (decoded.get() != NULL)
       {
-        decoded.reset(parsedDicom.DecodeFrame(frameIndex));
-        if (decoded.get() != NULL)
-        {
-          return decoded.release();
-        }
-      }
-      catch (const OrthancException&) // NOLINT(bugprone-empty-catch)
-      { // ignore, we'll try other alternatives
+        return decoded.release();
       }
     }
 
-#if ORTHANC_ENABLE_PLUGINS == 1
-    if (plugins_ != NULL &&
-        plugins_->HasCustomImageDecoder())
+    decoded.reset(DecodeFrameUsingPluginsDecoder(image.GetBufferData(), image.GetBufferSize(), frameIndex));
+    if (decoded.get() != NULL)
     {
-      try
-      {
-        decoded.reset(plugins_->Decode(buffer, size, frameIndex));
-      }
-      catch (const OrthancException&) // NOLINT(bugprone-empty-catch)
-      {
-      }
+      return decoded.release();
+    }
 
+    if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
+    {
+      decoded.reset(DecodeFrameBuiltin(image.GetParsedDicomFile(), frameIndex));
       if (decoded.get() != NULL)
       {
         return decoded.release();
       }
-      else if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
+    }
+
+    decoded.reset(DecodeFrameUsingPluginsTranscoder(image.GetBufferData(), image.GetBufferSize(), frameIndex));
+    if (decoded.get() != NULL)
+    {
+      return decoded.release();
+    }
+
+    return NULL;
+  }
+
+
+  ImageAccessor* ServerTranscoder::DecodeFrame(const boost::shared_ptr<DataSourceReader>& dicomReader,        // For built-in decoding
+                                               const boost::shared_ptr<DataSourceReader>& storageAreaReader,  // For plugin-based decoding
+                                               const boost::shared_ptr<DataSourceReader>& transcoderReader,   // For transcoding-based decoding
+                                               const FileInfo& attachment,
+                                               unsigned int frameIndex)
+  {
+    // Step 1: Try with the last decoder that successfully handled this DICOM attachment
+
+    const WorkingSource source = pimpl_->LookupWorkingSource(attachment.GetUuid());
+
+    std::unique_ptr<ImageAccessor> decoded;
+
+    switch (source)
+    {
+      case WorkingSource_Unknown:
+        break;
+
+      case WorkingSource_Builtin:
+        decoded.reset(DecodeFrameBuiltin(dicomReader, attachment, frameIndex));
+        break;
+
+      case WorkingSource_PluginsDecoder:
+        decoded.reset(DecodeFrameUsingPluginsDecoder(storageAreaReader, attachment, frameIndex));
+        break;
+
+      case WorkingSource_PluginsTranscoder:
+        decoded.reset(DecodeFrameUsingPluginsTranscoder(transcoderReader, attachment, frameIndex));
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    if (decoded.get() != NULL)
+    {
+      return decoded.release();
+    }
+
+    // Step 2: If this attachment is unknown, try the available decoders
+
+    if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before)
+    {
+      decoded.reset(DecodeFrameBuiltin(dicomReader, attachment, frameIndex));
+
+      if (decoded.get() != NULL)
       {
-        LOG(INFO) << "The installed image decoding plugins cannot handle an image, "
-                  << "fallback to the built-in DCMTK decoder";
+        pimpl_->SetWorkingSource(attachment.GetUuid(), WorkingSource_Builtin);
+        return decoded.release();
       }
     }
-#endif
+
+    if (HasPluginsDecoder())
+    {
+      decoded.reset(DecodeFrameUsingPluginsDecoder(storageAreaReader, attachment, frameIndex));
+
+      if (decoded.get() != NULL)
+      {
+        pimpl_->SetWorkingSource(attachment.GetUuid(), WorkingSource_PluginsDecoder);
+        return decoded.release();
+      }
+    }
 
     if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
     {
-      try
+      decoded.reset(DecodeFrameBuiltin(dicomReader, attachment, frameIndex));
+
+      if (decoded.get() != NULL)
       {
-        decoded.reset(parsedDicom.DecodeFrame(frameIndex));
-        if (decoded.get() != NULL)
-        {
-          return decoded.release();
-        }
-      }
-      catch (OrthancException& e)
-      {
-        LOG(INFO) << "Failed to decode a DICOM frame: " << e.GetDetails();
+        pimpl_->SetWorkingSource(attachment.GetUuid(), WorkingSource_Builtin);
+        return decoded.release();
       }
     }
 
-#if ORTHANC_ENABLE_PLUGINS == 1
-    if (plugins_ != NULL &&
-        plugins_->HasCustomTranscoder())
+    if (HasPluginsTranscoder())
     {
-      LOG(INFO) << "The plugins and built-in image decoders failed to decode a frame, "
-                << "trying to transcode the file to LittleEndianExplicit using the plugins.";
-      DicomImage explicitTemporaryImage;
-      DicomImage source;
-      std::set<DicomTransferSyntax> allowedSyntaxes;
+      decoded.reset(DecodeFrameUsingPluginsTranscoder(transcoderReader, attachment, frameIndex));
 
-      source.SetExternalBuffer(buffer, size);
-      allowedSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
-
-      if (Transcode(explicitTemporaryImage, source, allowedSyntaxes, TranscodingSopInstanceUidMode_AllowNew))
+      if (decoded.get() != NULL)
       {
-        std::unique_ptr<ParsedDicomFile> file(explicitTemporaryImage.ReleaseAsParsedDicomFile());
-        return file->DecodeFrame(frameIndex);
+        pimpl_->SetWorkingSource(attachment.GetUuid(), WorkingSource_PluginsTranscoder);
+        return decoded.release();
       }
     }
-#endif
 
-    return NULL;  // TODO-Streaming - Throw exception here?
+    return NULL;
   }
 
 
--- a/OrthancServer/Sources/ServerTranscoder.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/ServerTranscoder.h	Wed Jul 15 11:42:31 2026 +0200
@@ -24,12 +24,18 @@
 #pragma once
 
 #include "../../OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h"
+#include "../../OrthancFramework/Sources/FileStorage/FileInfo.h"
 #include "../../OrthancFramework/Sources/Images/ImageAccessor.h"
 #include "ServerEnumerations.h"
 
+#include <boost/shared_ptr.hpp>
+
 
 namespace Orthanc
 {
+  class DataSourceReader;
+  class DicomInstanceToStore;
+
 #if ORTHANC_ENABLE_PLUGINS == 1
   class OrthancPlugins;
 #endif
@@ -37,23 +43,58 @@
   class ServerTranscoder : public IDicomTranscoder
   {
   private:
+    class PImpl;
+
 #if ORTHANC_ENABLE_PLUGINS == 1
     OrthancPlugins* plugins_;
 #endif
 
+    boost::shared_ptr<PImpl>           pimpl_;
     std::unique_ptr<IDicomTranscoder>  dcmtkTranscoder_;
     BuiltinDecoderTranscoderOrder      builtinDecoderTranscoderOrder_;
 
+    ImageAccessor* DecodeFrameBuiltin(const ParsedDicomFile& dicom,
+                                      unsigned int frameIndex);
+
+    ImageAccessor* DecodeFrameUsingPluginsDecoder(const void* buffer,
+                                                  size_t size,
+                                                  unsigned int frameIndex);
+
+    ImageAccessor* DecodeFrameUsingPluginsTranscoder(const void* buffer,
+                                                     size_t size,
+                                                     unsigned int frameIndex);
+
+    ImageAccessor* DecodeFrameBuiltin(const boost::shared_ptr<DataSourceReader>& dicomReader,
+                                      const FileInfo& attachment,
+                                      unsigned int frameIndex);
+
+    ImageAccessor* DecodeFrameUsingPluginsDecoder(const boost::shared_ptr<DataSourceReader>& storageAreaReader,
+                                                  const FileInfo& attachment,
+                                                  unsigned int frameIndex);
+
+    ImageAccessor* DecodeFrameUsingPluginsTranscoder(const boost::shared_ptr<DataSourceReader>& transcoderReader,
+                                                     const FileInfo& attachment,
+                                                     unsigned int frameIndex);
+
   public:
-    ServerTranscoder(unsigned int maxConcurrentDcmtkTranscoder);
+    explicit ServerTranscoder(unsigned int maxConcurrentDcmtkTranscoder);
 
 #if ORTHANC_ENABLE_PLUGINS == 1
     void SetPlugins(OrthancPlugins& plugins);
 #endif
 
-    ImageAccessor* DecodeFrame(const ParsedDicomFile& parsedDicom,
-                               const void* buffer,  // buffer that is the source of the ParsedDicomFile
-                               size_t size,
+    bool HasPluginsDecoder() const;
+
+    bool HasPluginsTranscoder() const;
+
+    // This version should only be used by plugins
+    ImageAccessor* DecodeFrame(const DicomInstanceToStore& image,
+                               unsigned int frameIndex);
+
+    ImageAccessor* DecodeFrame(const boost::shared_ptr<DataSourceReader>& dicomReader,        // For built-in decoding
+                               const boost::shared_ptr<DataSourceReader>& storageAreaReader,  // For plugin-based decoding
+                               const boost::shared_ptr<DataSourceReader>& transcoderReader,   // For transcoding-based decoding
+                               const FileInfo& attachment,
                                unsigned int frameIndex);
 
     // This method can be used even if the global option
--- a/OrthancServer/Sources/main.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/Sources/main.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -53,9 +53,11 @@
 #include "ServerJobs/DicomRetrieveScuBaseJob.h"
 #include "ServerJobs/StorageCommitmentScpJob.h"
 #include "ServerToolbox.h"
+#include "ServerTranscoder.h"
 #include "StorageCommitmentReports.h"
 
 #include <boost/algorithm/string/predicate.hpp>
+#include <dcmtk/dcmnet/dimse.h>  // For STATUS_STORE_Error_CannotUnderstand
 
 #if defined(_WIN32) || defined(__CYGWIN__)
 #include <windows.h>
@@ -1116,7 +1118,7 @@
       httpDescribeErrors = lock.GetConfiguration().GetBooleanParameter("HttpDescribeErrors");
   
       // HTTP server
-      httpServer.SetThreadsCount(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpThreadsCount"));
+      httpServer.SetThreadsCount(lock.GetConfiguration().GetHttpThreadsCount());
       httpServer.SetPortNumber(lock.GetConfiguration().GetHttpPort());
       std::set<std::string> httpBindAddresses;
       lock.GetConfiguration().GetSetOfStringsParameter(httpBindAddresses, "HttpBindAddresses");
@@ -1389,7 +1391,7 @@
       dicomServer.SetCalledApplicationEntityTitleCheck(lock.GetConfiguration().GetBooleanParameter("DicomCheckCalledAet"));
       dicomServer.SetAssociationTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomScpTimeout"));
       dicomServer.SetPortNumber(lock.GetConfiguration().GetDicomPort());
-      dicomServer.SetThreadsCount(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomThreadsCount"));
+      dicomServer.SetThreadsCount(lock.GetConfiguration().GetDicomThreadsCount());
       dicomServer.SetApplicationEntityTitle(lock.GetConfiguration().GetOrthancAET());
 
       // Configuration of DICOM TLS for Orthanc SCP (since Orthanc 1.9.0)
@@ -1611,19 +1613,14 @@
         lock.GetConfiguration().SetServerIndex(context.GetIndex());
       }
 
-      std::unique_ptr<ServerTranscoder> transcoder(new ServerTranscoder(maxDcmtkConcurrentTranscoders));
-
 #if ORTHANC_ENABLE_PLUGINS == 1
       if (plugins_ != NULL)
       {
         plugins_->SetServerContext(context_);
         context_.SetPlugins(*plugins_);
         context_.GetIndex().SetMaxDatabaseRetries(plugins_->GetMaxDatabaseRetries());
-        transcoder->SetPlugins(*plugins_);
       }
 #endif
-
-      context.SetTranscoder(transcoder.release());
     }
 
     ~ServerContextConfigurator()
@@ -1640,8 +1637,6 @@
         context_.ResetPlugins();
       }
 #endif
-
-      context_.ResetTranscoder();
     }
   };
 }
@@ -1717,8 +1712,14 @@
     DicomAssociationParameters::SetDefaultRemoteCertificateRequired(
       lock.GetConfiguration().GetBooleanParameter(KEY_DICOM_TLS_REMOTE_CERTIFICATE_REQUIRED));
   }
+
+  std::unique_ptr<ServerTranscoder> transcoder(new ServerTranscoder(maxDcmtkConcurrentTranscoders));
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+  transcoder->SetPlugins(*plugins);
+#endif
   
-  ServerContext context(database, storageArea, false /* not running unit tests */, maxCompletedJobs, readOnly);
+  ServerContext context(database, storageArea, transcoder.release(), false /* not running unit tests */, maxCompletedJobs, readOnly);
 
   {
     OrthancConfiguration::ReaderLock lock;
@@ -1796,26 +1797,6 @@
         context.GetIndex().SetMaximumStorageMode(MaxStorageMode_Recycle);
       }
     }
-
-    // note: this config is valid in ReadOnlyMode
-    try
-    {
-      uint64_t size = lock.GetConfiguration().GetMaximumStorageCacheSize();
-      if (size == 0)
-      {
-        LOG(WARNING) << "Storage cache is disabled";
-      }
-      else
-      {
-        LOG(WARNING) << "Storage cache size is " << size << " MB";
-      }
-      
-      context.SetMaximumStorageCacheSize(size * 1024 * 1024);
-    }
-    catch (...)
-    {
-      context.SetMaximumStorageCacheSize(128);
-    }
   }
 
   {
@@ -2090,6 +2071,10 @@
     {
       Logging::SetThreadNamesEnabled(false);
     }
+    else if (argument == "--logs-no-context")
+    {
+      Logging::SetThreadContextsEnabled(false);
+    }
     else if (argument == "--trace")
     {
       SetGlobalVerbosity(Verbosity_Trace);
@@ -2188,7 +2173,8 @@
           SQLiteDatabaseWrapper inMemoryDatabase;
           inMemoryDatabase.Open();
           PluginStorageAreaAdapter inMemoryStorage(new MemoryStorageArea);
-          ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */, false /* readonly */);
+          ServerContext context(inMemoryDatabase, inMemoryStorage, NULL /* no transcoder */,
+                                true /* unit testing */, 0 /* max completed jobs */, false /* readonly */);
           OrthancRestApi restApi(context, false /* no Orthanc Explorer */);
           restApi.GenerateOpenApiDocumentation(openapi);
           context.Stop();
@@ -2239,7 +2225,8 @@
           SQLiteDatabaseWrapper inMemoryDatabase;
           inMemoryDatabase.Open();
           PluginStorageAreaAdapter inMemoryStorage(new MemoryStorageArea);
-          ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */, false /* readonly */);
+          ServerContext context(inMemoryDatabase, inMemoryStorage, NULL /* no transcoder */,
+                                true /* unit testing */, 0 /* max completed jobs */, false /* readonly */);
           OrthancRestApi restApi(context, false /* no Orthanc Explorer */);
           restApi.GenerateReStructuredTextCheatSheet(cheatsheet, "https://orthanc.uclouvain.be/api/index.html");
           context.Stop();
--- a/OrthancServer/UnitTestsSources/ServerConfigTests.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/UnitTestsSources/ServerConfigTests.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -39,7 +39,7 @@
   PluginStorageAreaAdapter storage(new MemoryStorageArea);
   SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
   db.Open();
-  ServerContext context(db, storage, true /* running unit tests */, 10, false);
+  ServerContext context(db, storage, NULL /* no transcoder */, true /* running unit tests */, 10, false);
 
   { // default config -> all SOP Classes should be accepted
     std::set<std::string> s;
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -730,7 +730,7 @@
   PluginStorageAreaAdapter storage(new FilesystemStorage(path));
   SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
   db.Open();
-  ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */);
+  ServerContext context(db, storage, NULL /* no transcoder */, true /* running unit tests */, 10, false /* readonly */);
   context.SetupJobsEngine(true, false);
 
   ServerIndex& index = context.GetIndex();
@@ -812,7 +812,7 @@
   PluginStorageAreaAdapter storage(new FilesystemStorage(path));
   SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
   db.Open();
-  ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */);
+  ServerContext context(db, storage, NULL /* no transcoder */, true /* running unit tests */, 10, false /* readonly */);
   context.SetupJobsEngine(true, false);
   ServerIndex& index = context.GetIndex();
 
@@ -935,7 +935,7 @@
     PluginStorageAreaAdapter storage(new MemoryStorageArea);
     SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
     db.Open();
-    ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */);
+    ServerContext context(db, storage, NULL /* no transcoder */, true /* running unit tests */, 10, false /* readonly */);
     context.SetupJobsEngine(true, false);
     context.SetCompressionEnabled(true);
 
@@ -1002,9 +1002,11 @@
     ASSERT_EQ("name", tmp["0010,0010"]["Value"].asString());
     
     {
-      ServerContext::DicomCacheLocker locker(context, id);
+      std::unique_ptr<DicomDataSource::Dicom> dicom(context.ReadParsedDicom(id));
+      DicomDataSource::Dicom::Lock lock(*dicom);
+
       std::string tmp;
-      locker.GetDicom().GetTagValue(tmp, DICOM_TAG_PATIENT_NAME);
+      lock.GetContent().GetTagValue(tmp, DICOM_TAG_PATIENT_NAME);
       ASSERT_EQ("name", tmp);
     }
 
@@ -1057,9 +1059,11 @@
       ASSERT_EQ("overwritten", tmp["0010,0010"]["Value"].asString());
     
       {
-        ServerContext::DicomCacheLocker locker(context, id);
+        std::unique_ptr<DicomDataSource::Dicom> dicom(context.ReadParsedDicom(id));
+        DicomDataSource::Dicom::Lock lock(*dicom);
+
         std::string tmp;
-        locker.GetDicom().GetTagValue(tmp, DICOM_TAG_PATIENT_NAME);
+        lock.GetContent().GetTagValue(tmp, DICOM_TAG_PATIENT_NAME);
         ASSERT_EQ("overwritten", tmp);
       }
     }
@@ -1074,9 +1078,11 @@
       ASSERT_EQ("name", tmp["0010,0010"]["Value"].asString());
     
       {
-        ServerContext::DicomCacheLocker locker(context, id);
+        std::unique_ptr<DicomDataSource::Dicom> dicom(context.ReadParsedDicom(id));
+        DicomDataSource::Dicom::Lock lock(*dicom);
+
         std::string tmp;
-        locker.GetDicom().GetTagValue(tmp, DICOM_TAG_PATIENT_NAME);
+        lock.GetContent().GetTagValue(tmp, DICOM_TAG_PATIENT_NAME);
         ASSERT_EQ("name", tmp);
       }
     }
@@ -1100,7 +1106,7 @@
     PluginStorageAreaAdapter storage(new MemoryStorageArea);
     SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
     db.Open();
-    ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */);
+    ServerContext context(db, storage, NULL /* no transcoder */, true /* running unit tests */, 10, false /* readonly */);
     context.SetupJobsEngine(true, false);
     context.SetCompressionEnabled(compression);
 
--- a/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -566,7 +566,7 @@
       storage_(new MemoryStorageArea)
     {
       db_.Open();
-      context_.reset(new ServerContext(db_, storage_, true /* running unit tests */, 10, false /* readonly */));
+      context_.reset(new ServerContext(db_, storage_, NULL /* no transcoder */, true /* running unit tests */, 10, false /* readonly */));
       context_->SetupJobsEngine(true, false);
     }
 
@@ -1021,9 +1021,11 @@
   std::string study, series;
 
   {
-    ServerContext::DicomCacheLocker lock(GetContext(), instance);
-    study = lock.GetDicom().GetHasher().HashStudy();
-    series = lock.GetDicom().GetHasher().HashSeries();
+    std::unique_ptr<DicomDataSource::Dicom> dicom(GetContext().ReadParsedDicom(instance));
+    DicomDataSource::Dicom::Lock lock(*dicom);
+
+    study = lock.GetContent().GetHasher().HashStudy();
+    series = lock.GetContent().GetHasher().HashSeries();
   }
 
   {
--- a/OrthancServer/UnitTestsSources/SizeOfTests.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/UnitTestsSources/SizeOfTests.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -24,6 +24,11 @@
 #include "PrecompiledHeadersUnitTests.h"
 #include <gtest/gtest.h>
 
+#if defined(__GNUC__) || defined(__clang__)
+#  pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+#endif
+
+
 #include "../../OrthancFramework/Sources/Cache/ICachePageProvider.h"
 #include "../../OrthancFramework/Sources/Cache/ICacheable.h"
 #include "../../OrthancFramework/Sources/Cache/LeastRecentlyUsedIndex.h"
@@ -101,7 +106,6 @@
 #include "../../OrthancFramework/Sources/FileStorage/IStorageArea.h"
 #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h"
 #include "../../OrthancFramework/Sources/FileStorage/StorageAccessor.h"
-#include "../../OrthancFramework/Sources/FileStorage/StorageCache.h"
 #include "../../OrthancFramework/Sources/HttpClient.h"
 #include "../../OrthancFramework/Sources/HttpServer/BufferHttpSender.h"
 #include "../../OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.h"
--- a/OrthancServer/UnitTestsSources/SizeOfTests.impl.h	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/UnitTestsSources/SizeOfTests.impl.h	Wed Jul 15 11:42:31 2026 +0200
@@ -105,7 +105,6 @@
   printf("sizeof(::Orthanc::SharedLibrary) == %d\n", static_cast<int>(sizeof(::Orthanc::SharedLibrary)));
   printf("sizeof(::Orthanc::SharedMessageQueue) == %d\n", static_cast<int>(sizeof(::Orthanc::SharedMessageQueue)));
   printf("sizeof(::Orthanc::StorageAccessor) == %d\n", static_cast<int>(sizeof(::Orthanc::StorageAccessor)));
-  printf("sizeof(::Orthanc::StorageCache) == %d\n", static_cast<int>(sizeof(::Orthanc::StorageCache)));
   printf("sizeof(::Orthanc::StreamBlockReader) == %d\n", static_cast<int>(sizeof(::Orthanc::StreamBlockReader)));
   printf("sizeof(::Orthanc::StringMatcher) == %d\n", static_cast<int>(sizeof(::Orthanc::StringMatcher)));
   printf("sizeof(::Orthanc::StringOperationValue) == %d\n", static_cast<int>(sizeof(::Orthanc::StringOperationValue)));
--- a/OrthancServer/UnitTestsSources/UnitTestsMain.cpp	Wed Jul 08 11:45:33 2026 +0200
+++ b/OrthancServer/UnitTestsSources/UnitTestsMain.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -544,6 +544,7 @@
 #endif
 
   Logging::Initialize();
+  Logging::SetCurrentThreadName("MAIN");
   SetGlobalVerbosity(Verbosity_Verbose);
   Toolbox::DetectEndianness();
   SystemToolbox::MakeDirectory(SystemToolbox::PathFromUtf8("UnitTestsResults"));
--- a/TODO	Wed Jul 08 11:45:33 2026 +0200
+++ b/TODO	Wed Jul 15 11:42:31 2026 +0200
@@ -71,8 +71,6 @@
     "admin": {
       "Password": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", 
       "Hashing": "sha1"} }
-* Have a single instance of "ThreadedInstancesLoader" shared by all the jobs
-  in the Orthanc server to prevent resource exhaustion.
 
 
 ============================