changeset 7102:9119f3ee5a6c

[BREAKING] integration streaming->mainline
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 13 Aug 2026 14:38:28 +0000
parents db0d6743ee4e (current diff) 35d418dd88e8 (diff)
children ec5d834c67f2
files
diffstat 149 files changed, 5519 insertions(+), 3404 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Wed Aug 12 13:21:02 2026 +0200
+++ b/.hgignore	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/NEWS	Thu Aug 13 14:38:28 2026 +0000
@@ -4,13 +4,26 @@
 General
 -------
 
-* Multiple paths to configuration files can now be provided, instead of a single file or a single folder
-* Split the configuration into a main file and a second file for advanced settings
-* Orthanc will now refuse to start if you have "AuthenticationEnabled" and "RemoteAccessAllowed"
-  set to true and have not defined any users in "RegisteredUsers".
+* Complete rewrite of access to storage area and to DICOM parsing/transcoding to improve caching
+  and reduce memory usage. This change introduces the following new configuration options:
+  - "DicomParserCacheSize"
+  - "DicomParserMemoryCapacity"
+  - "DicomParserThreadsCount"
+  - "SequentialDicomReaderThreadsCount"
+  - "SequentialDicomReaderWindowCapacity"
+  - "SequentialDicomReaderWindowSize"
+  - "StorageLoaderThreadsCount"
+  - "StorageMemoryCapacity"
+  - "TranscoderCacheSize"
+  - "TranscoderMemoryCapacity"
+  - "TranscoderThreadsCount"
+* Split the configuration into a main file and a separate file for advanced settings.
+* Multiple paths to configuration files can now be provided, instead of a single file or a single folder.
+* Orthanc will now refuse to start if both options "AuthenticationEnabled" and "RemoteAccessAllowed"
+  are set to true, but no user is defined in "RegisteredUsers".
 * New values for "OverwriteInstances" configuration. In previous versions, this configuration
-  was a Boolean and it is now a string whose allowed values are "Never", "Always",
-  and "IfChanged". For backward compatibility, you may still provide a Boolean value
+  was a Boolean. It is now a string whose allowed values are "Never", "Always",
+  and "IfChanged". For backward compatibility, a Boolean value can still be provided
   (true = "Always" and false = "Never").
   Compared to previous versions, in the "Never" and "IfChanged" mode, the number of disks and 
   DB operations are reduced when uploading the same instance again.
@@ -18,57 +31,63 @@
 * When the "IngestTranscoding" configuration is set to a compressed transfer syntax, Orthanc
   no longer transcodes DICOM files that do not have any pixel data (e.g. DICOM-SR, RTSTRUCT,
   Encapsulated PDF,...). Such files are sometimes poorly handled by other software.
-* In Lua, added new functions to manage key-value stores in the Orthanc DB (provided that
-  the DB backend supports it: currently, PostgreSQL and SQLite):
-  - 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 
-    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 
-    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).
-  - "Name" was documented as "MyOrthanc" while its default value is "ORTHANC".
-* Orthanc no longer accepts DICOM association from modalities whose AET contains invalid characters.
-* Orthanc now ignores leading/trailing spaces in AET.
-* Experimental: New "Manufacturer" "SiemensSyngoCT" to propose both the SCU and SCP C-Store roles when
-  performing a C-Get SCU.
+* Logs:
+  - Lines of log can now contain additional contextual information like the job ID they relate to.
+  - A new "--logs-no-context" command-line option restores the previous behavior for backward
+    compatibility and to reduce log line length.
+  - A new "--logs-thread-names-in-context" command-line option adds information about the thread that
+    requested a service to the log context. This option is mainly intended for developers.
+* Experimental: New manufacturer "SiemensSyngoCT" available in "DicomModalities" to propose
+  both the SCU and SCP C-STORE roles when performing a C-GET SCU.
 
 REST API
 --------
 
-* API version upgraded to 33
+* API version upgraded to 34 (34 in streaming branch, 33 in default and 1.12.11 was 30 ! 
+  TODO: for the release, we may go backward to 31 but we need to change the conditional 
+  tests - see this commit: https://orthanc.uclouvain.be/hg/orthanc-tests/rev/3a440ee47213)
 * New fields reported in the "/system" route:
-  - "OverwriteInstancesMode" (Boolean field "OverwriteInstances" is kept for backward
-    compatibility)
-  - "MaximumStorageCacheSize"
+  - "OverwriteInstancesMode" (Boolean field "OverwriteInstances" is kept for backward compatibility)
+  - "Performance" reports configuration options that may affect performance of Orthanc
   - "StoreMD5ForAttachments"
+  - "ThirdPartyPatches" lists the patches applied to the third-party libraries
+  - "ThirdPartyVersions" lists the versions of the main third-party libraries
 * The "LocalAet" field of the payload to "/modalities/../move", "/modalities/../store", 
   "/modalities/../get", "queries/../answers/../retrieve" now always overwrites the "DicomAet"
   and the "DicomModalities.LocalAet" configurations.
-* Optimized the "/patients|studies|series|instances/../statistics" routes.
-* Updated the naming structure of the zip downloaded from "/../../archive" routes.  Notably added
-  SeriesNumber at the series level and now using " - " instead of " " to separate DICOM tags used
-  in folder names.
+* Optimized the "/{patients|studies|series|instances}/../statistics" routes.
+* Updated the naming structure of ZIP archives downloaded from "/../../archive" routes.
+  Notably added "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.
+  - New metrics related to the refactored architecture to access the storage area and DICOM
+    parsing/transcoding: "{orthanc_storage|orthanc_dicom_parser|orthanc_transcoder}_
+    {cache_count|cache_hit_count|cache_miss_count|cache_size_mb|memory_capacity_mb|
+    memory_count|memory_max_usage_mb|memory_usage_mb|available_threads}",
+    as well as "orthanc_sequential_reader_available_threads"
 * The "/tools/bulk-delete" route now also accepts a payload of this type:
-  {"Resources": [{"Level": "Study", "ID": "..."}]}.  This payload reduces the number
+  {"Resources": [{"Level": "Study", "ID": "..."}]}. This payload reduces the number
   of calls to the SQL DB and is more suitable for the authorization plugin.
 
+Lua
+---
+
+* Added new functions to manage key-value stores in the Orthanc database (provided that the
+  database backend supports them; currently, only PostgreSQL and SQLite):
+  - StoreKeyValue(storeId, key, value)
+  - value = GetKeyValue(storeId, key)
+  - DeleteKeyValue(storeId, key)
+
 Plugin SDK
 ----------
 
 * Added OrthancPluginClearCurrentThreadName() to avoid storing many thread
-  names when dynamically creating/killing threads in a plugin.
+  names when dynamically creating/killing threads in a plugin
 * Added OD, OL, UC, UR, OV, SV, and UV values to the OrthancPluginValueRepresentation enumeration
 * Added OrthancPluginDicomWebBinaryMode_ArrayOfValues to generate DICOMweb
   representations with OW, OL, OV, OF, and OD value representations as array of values
@@ -79,11 +98,25 @@
 * Added support for OV, SV, and UV value representations
 * Added support for multiple values in AT value representation
 * Improved support of OW, OL, OV, OF, and OD value representations in DICOMweb JSON
+* C-FIND SCP:
+  - When Orthanc was requested to return a sequence in a C-FIND query, it did not return
+    any sequence in the C-FIND answer if the matched resource *did* contain the requested sequence, and
+    it returned 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, it did not return resources that
+    did not contain the private tag, even though the tag was not used as a filter in the query. Resources
+    without the private tag are now correctly returned in the response.
+* Fix mismatches between the configuration default values and the documentation:
+  - "HttpTimeout" was documented as 60 while its default value is 0 (no timeout)
+  - "Name" was documented as "MyOrthanc" while its default value is "ORTHANC"
+* Orthanc no longer accepts DICOM association from modalities whose AET contains invalid characters
+* Orthanc now ignores leading/trailing spaces in AET
+* The configuration options "LoaderThreads" and "ZipLoaderThreads" have been removed from
+  the default configuration file and are only used for backward compatibility with new options
 * Fix resubmit/pause/cancel and reloading of OrthancPeerStore and DicomModalityStore jobs 
   (bug introduced in 1.12.10)
-* Fix OrthancPeerStore jobs that was using the loader threads only if transcoding
-  was required.
-* Fix usage of "LocalAet" in C-Find and "queries/../answers/../retrieve".
+* Fix OrthancPeerStore jobs that was using the loader threads only if transcoding was required
+* Fix usage of "LocalAet" in C-FIND and "queries/../answers/../retrieve"
 * Fix Orthanc::ImageAccessor that was broken in Orthanc Framework 1.12.11
 * Fix a memory leak in C-GET:
   https://github.com/orthanc-server/orthanc-builder/issues/36
@@ -91,12 +124,11 @@
   https://discourse.orthanc-server.org/t/orthanc-1-12-11-performance-issues-with-deformable-registrations/6448)
 * Fix a Denial of Service via Deeply Nested DICOM Sequences (CVE-2026-10528):
   https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=258
-  Security issue reported by Jose Lopez Martinez (aka elpe_pinillo) from Deloitte.
+  Security issue reported by Jose Lopez Martinez (aka elpe_pinillo) from Deloitte
 * Fixed various OOB read/write (TODO: still waiting for CVE ID)
 * Improved preview and rendering of multiframe instances a with a Color Lookup Table; 
-  notably, some cardiac US instances.
-* Lua: Fix the "ReceivedCStoreInstanceFilter" Lua callback whose return value was
-  not taken into account.
+  notably, some cardiac US instances
+* Lua: Fix the "ReceivedCStoreInstanceFilter()" callback whose return value was not taken into account
 * Upgraded dependencies for static builds:
   - dcmtk 3.7.0 hot-fix for CVE-2026-10528:
     https://github.com/DCMTK/dcmtk/commit/885ff0f10372bd589b5f44cea974f28a3964cb0f
--- a/OrthancFramework/Resources/CMake/CivetwebConfiguration.cmake	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Resources/CMake/CivetwebConfiguration.cmake	Thu Aug 13 14:38:28 2026 +0000
@@ -46,6 +46,7 @@
 
   DownloadPackage(${CIVETWEB_MD5} ${CIVETWEB_URL} "${CIVETWEB_SOURCES_DIR}")
 
+  list(APPEND ORTHANC_PATCHES_LIST "civetweb-1.16.patch")  # Must be out of "FirstRun"
   if (FirstRun)
     execute_process(
       COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
--- a/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.7.0.cmake	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.7.0.cmake	Thu Aug 13 14:38:28 2026 +0000
@@ -52,6 +52,8 @@
 DownloadPackage(${DCMTK_MD5} ${DCMTK_URL} "${DCMTK_SOURCES_DIR}")
 
 
+list(APPEND ORTHANC_PATCHES_LIST "dcmtk-3.7.0-max-nested-sequence.patch")  # Must be out of "FirstRun"
+
 if (FirstRun)
   # Apply the patches
   execute_process(
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake	Thu Aug 13 14:38:28 2026 +0000
@@ -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()
 
@@ -736,6 +739,40 @@
 
 
 #####################################################################
+## Publishing the list of the applied patches
+#####################################################################
+
+list(LENGTH ORTHANC_PATCHES_LIST ORTHANC_PATCHES_COUNT)
+
+if (ORTHANC_PATCHES_COUNT GREATER 0)
+  set(ORTHANC_PATCHES_C_BODY "")
+
+  foreach(PATCH IN LISTS ORTHANC_PATCHES_LIST)
+    if (ORTHANC_PATCHES_C_BODY)
+      string(APPEND ORTHANC_PATCHES_C_BODY "\",\n  \"")
+    endif()
+    string(APPEND ORTHANC_PATCHES_C_BODY "${PATCH}")
+  endforeach()
+
+  set(ORTHANC_PATCHES_C_BODY "\"${ORTHANC_PATCHES_C_BODY}\"")
+else()
+  set(ORTHANC_PATCHES_C_BODY "")
+endif()
+
+configure_file(
+  ${CMAKE_CURRENT_LIST_DIR}/PatchList.h.in
+  ${AUTOGENERATED_DIR}/PatchList.h
+  @ONLY
+  )
+
+configure_file(
+  ${CMAKE_CURRENT_LIST_DIR}/PatchList.cpp.in
+  ${AUTOGENERATED_DIR}/PatchList.cpp
+  @ONLY
+  )
+
+
+#####################################################################
 ## Gathering of all the source code
 #####################################################################
 
@@ -768,6 +805,8 @@
 
   ${CMAKE_CURRENT_LIST_DIR}/../../Resources/ThirdParty/md5/md5.c
   ${CMAKE_CURRENT_LIST_DIR}/../../Resources/ThirdParty/base64/base64.cpp
+
+  ${AUTOGENERATED_DIR}/PatchList.cpp
   )
 
 if (ENABLE_ZLIB)
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Thu Aug 13 14:38:28 2026 +0000
@@ -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 "33")
+set(ORTHANC_API_VERSION "34")
 
 
 #####################################################################
@@ -111,6 +111,10 @@
 mark_as_advanced(USE_PUGIXML)
 
 
+# Must be filled with the list of patches applied to system-wide libraries (new in Orthanc 1.13.0)
+set(ORTHANC_PATCHES_LIST)
+
+
 #####################################################################
 ## Internal CMake parameters to enable the optional subcomponents of
 ## the Orthanc framework
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/PatchList.cpp.in	Thu Aug 13 14:38:28 2026 +0000
@@ -0,0 +1,32 @@
+/**
+ * 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 "PatchList.h"
+
+const char *const ORTHANC_PATCHES[] =
+{
+  @ORTHANC_PATCHES_C_BODY@
+};
+
+const unsigned int ORTHANC_PATCHES_COUNT = @ORTHANC_PATCHES_COUNT@;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/PatchList.h.in	Thu Aug 13 14:38:28 2026 +0000
@@ -0,0 +1,29 @@
+/**
+ * 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
+
+extern const char *const  ORTHANC_PATCHES[];
+
+extern const unsigned int ORTHANC_PATCHES_COUNT;
--- a/OrthancFramework/Resources/CheckOrthancFrameworkSymbols.py	Wed Aug 12 13:21:02 2026 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,351 +0,0 @@
-#!/usr/bin/env python
-
-# 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/>.
-
-
-##
-## This maintenance script detects all the public methods in the
-## Orthanc framework that come with an inlined implementation in the
-## header file. Such methods can break the ABI of the shared library,
-## as the actual implementation might change over versions.
-##
-
-
-# Ubuntu 20.04:
-# sudo apt-get install python-clang-6.0
-# ./ParseWebAssemblyExports.py --libclang=libclang-6.0.so.1 ./Test.cpp
-
-# Ubuntu 18.04:
-# sudo apt-get install python-clang-4.0
-# ./ParseWebAssemblyExports.py --libclang=libclang-4.0.so.1 ./Test.cpp
-
-# Ubuntu 14.04:
-# ./ParseWebAssemblyExports.py --libclang=libclang-3.6.so.1 ./Test.cpp
-
-
-import os
-import sys
-import clang.cindex
-import argparse
-
-##
-## Parse the command-line arguments
-##
-
-parser = argparse.ArgumentParser(description = 'Parse WebAssembly C++ source file, and create a basic JavaScript wrapper.')
-parser.add_argument('--libclang',
-                    default = '',
-                    help = 'manually provides the path to the libclang shared library')
-parser.add_argument('--target-cpp-size',
-                    default = '',
-                    help = 'where to store C++ source to display the size of each public class')
-
-args = parser.parse_args()
-
-
-if len(args.libclang) != 0:
-    clang.cindex.Config.set_library_file(args.libclang)
-
-index = clang.cindex.Index.create()
-
-
-ROOT = os.path.abspath(os.path.dirname(sys.argv[0]))
-SOURCES = []
-
-for root, dirs, files in os.walk(os.path.join(ROOT, '..', 'Sources')):
-    for name in files:
-        if (os.path.splitext(name)[1] == '.h' and
-            not name.endswith('.impl.h')):
-            SOURCES.append(os.path.join(root, name))
-
-AMALGAMATION = '/tmp/CheckOrthancFrameworkSymbols.cpp'
-            
-with open(AMALGAMATION, 'w') as f:
-    f.write('#include "%s"\n' % os.path.join(ROOT, '..', 'Sources', 'OrthancFramework.h'))
-    for source in SOURCES:
-        f.write('#include "%s"\n' % source)
-            
-
-tu = index.parse(AMALGAMATION, [
-    '--std=c++11',
-    '-DORTHANC_BUILDING_FRAMEWORK_LIBRARY=1',
-    '-DORTHANC_BUILD_UNIT_TESTS=0',
-    '-DORTHANC_ENABLE_BASE64=1',
-    '-DORTHANC_ENABLE_CIVETWEB=1',
-    '-DORTHANC_ENABLE_CURL=1',
-    '-DORTHANC_ENABLE_DCMTK=1',
-    '-DORTHANC_ENABLE_DCMTK_JPEG=1',
-    '-DORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS=1',
-    '-DORTHANC_ENABLE_DCMTK_NETWORKING=1',
-    '-DORTHANC_ENABLE_DCMTK_TRANSCODING=1',
-    '-DORTHANC_ENABLE_JPEG=1',
-    '-DORTHANC_ENABLE_LOCALE=1',
-    '-DORTHANC_ENABLE_LOGGING=1',
-    '-DORTHANC_ENABLE_LOGGING_STDIO=0',
-    '-DORTHANC_ENABLE_LUA=1',
-    '-DORTHANC_ENABLE_MD5=1',
-    '-DORTHANC_ENABLE_MONGOOSE=1',
-    '-DORTHANC_ENABLE_PKCS11=1',
-    '-DORTHANC_ENABLE_PNG=1',
-    '-DORTHANC_ENABLE_PUGIXML=1',
-    '-DORTHANC_ENABLE_SQLITE=1',
-    '-DORTHANC_ENABLE_SSL=1',
-    '-DORTHANC_ENABLE_ZLIB=1',
-    '-DORTHANC_SANDBOXED=0',
-    '-DORTHANC_SQLITE_STANDALONE=0',
-    '-DORTHANC_SQLITE_VERSION=3027001',
-    '-I/usr/include/jsoncpp',  # On Ubuntu 18.04
-    '-I/usr/include/lua5.3',   # On Ubuntu 18.04
-])
-
-
-if len(tu.diagnostics) != 0:
-    for d in tu.diagnostics:
-        print('  ** %s' % d)
-    print('')
-    raise Exception('Error')
-
-
-
-FILES = []
-COUNT = 0
-ALL_TYPES = []
-
-def ReportProblem(message, fqn, cursor):
-    global FILES, COUNT
-    FILES.append(os.path.normpath(str(cursor.location.file)))
-    COUNT += 1
-
-    print('%s: %s::%s()' % (message, '::'.join(fqn), cursor.spelling))
-
-
-def ExploreClass(child, fqn):
-    # Safety check
-    if (child.kind != clang.cindex.CursorKind.CLASS_DECL and
-        child.kind != clang.cindex.CursorKind.STRUCT_DECL):
-        raise Exception()
-
-    # Ignore forward declaration of classes
-    if not child.is_definition():
-        return
-    
-
-    ##
-    ## Verify that the class is publicly exported (its visibility must
-    ## be "default")
-    ##
-    visible = False
-
-    for i in child.get_children():
-        if (i.kind == clang.cindex.CursorKind.VISIBILITY_ATTR and
-            i.spelling == 'default'):
-            visible = True
-
-    if not visible:
-        return
-
-    global ALL_TYPES
-    ALL_TYPES.append('::'.join(fqn))
-    
-    
-    ##
-    ## Ignore pure abstract interfaces, by checking the following
-    ## criteria:
-    ##   - It must be a C++ class (not a struct)
-    ##   - It must start with "I"
-    ##   - All its methods must be pure virtual (abstract) and public
-    ##   - Its destructor must be public, virtual, and must do nothing
-    ##
-    
-    if (child.kind == clang.cindex.CursorKind.CLASS_DECL and
-        child.spelling[0] == 'I' and
-        child.spelling[1].isupper()):
-        abstract = True
-        isPublic = False
-
-        for i in child.get_children():
-            if i.kind == clang.cindex.CursorKind.VISIBILITY_ATTR:      # "default"
-                pass
-            elif i.kind == clang.cindex.CursorKind.CXX_ACCESS_SPEC_DECL:
-                isPublic = (i.access_specifier == clang.cindex.AccessSpecifier.PUBLIC)
-            elif i.kind == clang.cindex.CursorKind.CXX_BASE_SPECIFIER:
-                if i.spelling != 'boost::noncopyable':
-                    abstract = False
-            elif isPublic:
-                if i.kind == clang.cindex.CursorKind.CXX_METHOD:
-                    if i.is_pure_virtual_method():
-                        pass  # pure virtual is ok
-                    elif i.is_static_method():
-                        # static method without an inline implementation is ok
-                        for j in i.get_children():
-                            if j.kind == clang.cindex.CursorKind.COMPOUND_STMT:
-                                abstract = False
-                    else:
-                        abstract = False
-                elif (i.kind == clang.cindex.CursorKind.DESTRUCTOR and
-                      i.is_virtual_method()):
-                    # The destructor must be virtual, and must do nothing
-                    c = list(i.get_children())
-                    if (len(c) != 1 or
-                        c[0].kind != clang.cindex.CursorKind.COMPOUND_STMT or
-                        len(list(c[0].get_children())) != 0):
-                        abstract = False
-                elif i.kind == clang.cindex.CursorKind.CLASS_DECL:
-                    ExploreClass(i, fqn + [ i.spelling ])
-                elif (i.kind == clang.cindex.CursorKind.TYPEDEF_DECL or  # Allow "typedef"
-                      i.kind == clang.cindex.CursorKind.ENUM_DECL):      # Allow enums
-                    pass
-                else:
-                    abstract = False
-
-        if abstract:
-            print('Detected a pure interface (this is fine): %s' % ('::'.join(fqn)))
-        else:
-            ReportProblem('Not a pure interface', fqn, child)
-
-        return
-                  
-
-    ##
-    ## We are facing a standard C++ class or struct
-    ##
-    
-    isPublic = (child.kind == clang.cindex.CursorKind.STRUCT_DECL)
-
-    membersCount = 0
-    membersSize = 0
-
-    for i in child.get_children():
-        if (i.kind == clang.cindex.CursorKind.VISIBILITY_ATTR or    # "default"
-            i.kind == clang.cindex.CursorKind.CXX_BASE_SPECIFIER):  # base class
-            pass
-        
-        elif i.kind == clang.cindex.CursorKind.CXX_ACCESS_SPEC_DECL:
-            isPublic = (i.access_specifier == clang.cindex.AccessSpecifier.PUBLIC)
-
-        elif i.kind == clang.cindex.CursorKind.CLASS_DECL:
-            # This is a subclass
-            if isPublic:
-                ExploreClass(i, fqn + [ i.spelling ])
-
-        elif (i.kind == clang.cindex.CursorKind.CXX_METHOD or
-              i.kind == clang.cindex.CursorKind.CONSTRUCTOR or
-              i.kind == clang.cindex.CursorKind.DESTRUCTOR):
-            if isPublic:
-                hasImplementation = False
-                for j in i.get_children():
-                    if j.kind == clang.cindex.CursorKind.COMPOUND_STMT:
-                        hasImplementation = True
-
-                if hasImplementation:
-                    ReportProblem('Exported public method with an implementation', fqn, i)
-
-        elif i.kind == clang.cindex.CursorKind.VAR_DECL:
-            raise Exception('Unsupported: %s, %s' % (i.kind, i.location))
-
-        elif i.kind == clang.cindex.CursorKind.FUNCTION_TEMPLATE:
-            # An inline function template is OK, as it is not added to
-            # a shared library, but compiled by the client of the library
-            if isPublic:
-                print('Detected a template function (this is fine, but avoid it as much as possible): %s' % ('::'.join(fqn + [ i.spelling ])))
-                hasImplementation = False
-                for j in i.get_children():
-                    if j.kind == clang.cindex.CursorKind.COMPOUND_STMT:
-                        hasImplementation = True
-
-                if not hasImplementation:
-                    ReportProblem('Exported template function without an inline implementation', fqn, i)
-
-        elif (i.kind == clang.cindex.CursorKind.TYPEDEF_DECL or  # Allow "typedef"
-              i.kind == clang.cindex.CursorKind.ENUM_DECL):      # Allow enums
-            pass
-
-        elif i.kind == clang.cindex.CursorKind.FRIEND_DECL:
-            children = list(i.get_children())
-            if (isPublic and
-                (len(children) != 1 or
-                 not children[0].displayname in [
-                     # This is supported for ABI compatibility with Orthanc <= 1.8.0
-                     'operator<<(std::ostream &, const Orthanc::DicomTag &)',
-                 ])):
-                raise Exception('Unsupported: %s, %s' % (i.kind, i.location))
-
-        elif i.kind == clang.cindex.CursorKind.FIELD_DECL:
-            # TODO
-            if i.type.get_size() > 0:
-                membersSize += i.type.get_size()
-            membersCount += 1
-            
-        else:
-            if isPublic:
-                raise Exception('Unsupported: %s, %s' % (i.kind, i.location))
-
-    #print('Size of %s => (%d,%d)' % ('::'.join(fqn), membersCount, membersSize))
-
-
-def ExploreNamespace(node, namespace):
-    for child in node.get_children():
-        fqn = namespace + [ child.spelling ]
-        
-        if child.kind == clang.cindex.CursorKind.NAMESPACE:
-            ExploreNamespace(child, fqn)
-
-        elif (child.kind == clang.cindex.CursorKind.CLASS_DECL or
-              child.kind == clang.cindex.CursorKind.STRUCT_DECL):
-            ExploreClass(child, fqn)
-
-        elif child.kind == clang.cindex.CursorKind.FUNCTION_DECL:
-            visible = False
-            hasImplementation = False
-            for i in child.get_children():
-                if (i.kind == clang.cindex.CursorKind.VISIBILITY_ATTR and
-                    i.spelling == 'default'):
-                    visible = True
-                elif i.kind == clang.cindex.CursorKind.COMPOUND_STMT:
-                    hasImplementation = True
-
-            if visible and hasImplementation:
-                ReportProblem('Exported public function with an implementation', fqn, i)
-
-
-
-print('')
-
-for node in tu.cursor.get_children():
-    if (node.kind == clang.cindex.CursorKind.NAMESPACE and
-        node.spelling == 'Orthanc'):
-        ExploreNamespace(node, [ 'Orthanc' ])
-
-
-if args.target_cpp_size != '':
-    with open(args.target_cpp_size, 'w') as f:
-        for t in sorted(ALL_TYPES):
-            f.write('  printf("sizeof(::%s) == %%d\\n", static_cast<int>(sizeof(::%s)));\n' % (t, t))
-
-
-print('\nTotal of possibly problematic methods: %d' % COUNT)
-
-print('\nProblematic files:\n')
-for i in sorted(list(set(FILES))):
-    print(i)
-
-print('')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Graveyard/CheckOrthancFrameworkSymbols.py	Thu Aug 13 14:38:28 2026 +0000
@@ -0,0 +1,351 @@
+#!/usr/bin/env python
+
+# 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/>.
+
+
+##
+## This maintenance script detects all the public methods in the
+## Orthanc framework that come with an inlined implementation in the
+## header file. Such methods can break the ABI of the shared library,
+## as the actual implementation might change over versions.
+##
+
+
+# Ubuntu 20.04:
+# sudo apt-get install python-clang-6.0
+# ./ParseWebAssemblyExports.py --libclang=libclang-6.0.so.1 ./Test.cpp
+
+# Ubuntu 18.04:
+# sudo apt-get install python-clang-4.0
+# ./ParseWebAssemblyExports.py --libclang=libclang-4.0.so.1 ./Test.cpp
+
+# Ubuntu 14.04:
+# ./ParseWebAssemblyExports.py --libclang=libclang-3.6.so.1 ./Test.cpp
+
+
+import os
+import sys
+import clang.cindex
+import argparse
+
+##
+## Parse the command-line arguments
+##
+
+parser = argparse.ArgumentParser(description = 'Parse WebAssembly C++ source file, and create a basic JavaScript wrapper.')
+parser.add_argument('--libclang',
+                    default = '',
+                    help = 'manually provides the path to the libclang shared library')
+parser.add_argument('--target-cpp-size',
+                    default = '',
+                    help = 'where to store C++ source to display the size of each public class')
+
+args = parser.parse_args()
+
+
+if len(args.libclang) != 0:
+    clang.cindex.Config.set_library_file(args.libclang)
+
+index = clang.cindex.Index.create()
+
+
+ROOT = os.path.abspath(os.path.dirname(sys.argv[0]))
+SOURCES = []
+
+for root, dirs, files in os.walk(os.path.join(ROOT, '..', 'Sources')):
+    for name in files:
+        if (os.path.splitext(name)[1] == '.h' and
+            not name.endswith('.impl.h')):
+            SOURCES.append(os.path.join(root, name))
+
+AMALGAMATION = '/tmp/CheckOrthancFrameworkSymbols.cpp'
+            
+with open(AMALGAMATION, 'w') as f:
+    f.write('#include "%s"\n' % os.path.join(ROOT, '..', 'Sources', 'OrthancFramework.h'))
+    for source in SOURCES:
+        f.write('#include "%s"\n' % source)
+            
+
+tu = index.parse(AMALGAMATION, [
+    '--std=c++11',
+    '-DORTHANC_BUILDING_FRAMEWORK_LIBRARY=1',
+    '-DORTHANC_BUILD_UNIT_TESTS=0',
+    '-DORTHANC_ENABLE_BASE64=1',
+    '-DORTHANC_ENABLE_CIVETWEB=1',
+    '-DORTHANC_ENABLE_CURL=1',
+    '-DORTHANC_ENABLE_DCMTK=1',
+    '-DORTHANC_ENABLE_DCMTK_JPEG=1',
+    '-DORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS=1',
+    '-DORTHANC_ENABLE_DCMTK_NETWORKING=1',
+    '-DORTHANC_ENABLE_DCMTK_TRANSCODING=1',
+    '-DORTHANC_ENABLE_JPEG=1',
+    '-DORTHANC_ENABLE_LOCALE=1',
+    '-DORTHANC_ENABLE_LOGGING=1',
+    '-DORTHANC_ENABLE_LOGGING_STDIO=0',
+    '-DORTHANC_ENABLE_LUA=1',
+    '-DORTHANC_ENABLE_MD5=1',
+    '-DORTHANC_ENABLE_MONGOOSE=1',
+    '-DORTHANC_ENABLE_PKCS11=1',
+    '-DORTHANC_ENABLE_PNG=1',
+    '-DORTHANC_ENABLE_PUGIXML=1',
+    '-DORTHANC_ENABLE_SQLITE=1',
+    '-DORTHANC_ENABLE_SSL=1',
+    '-DORTHANC_ENABLE_ZLIB=1',
+    '-DORTHANC_SANDBOXED=0',
+    '-DORTHANC_SQLITE_STANDALONE=0',
+    '-DORTHANC_SQLITE_VERSION=3027001',
+    '-I/usr/include/jsoncpp',  # On Ubuntu 18.04
+    '-I/usr/include/lua5.3',   # On Ubuntu 18.04
+])
+
+
+if len(tu.diagnostics) != 0:
+    for d in tu.diagnostics:
+        print('  ** %s' % d)
+    print('')
+    raise Exception('Error')
+
+
+
+FILES = []
+COUNT = 0
+ALL_TYPES = []
+
+def ReportProblem(message, fqn, cursor):
+    global FILES, COUNT
+    FILES.append(os.path.normpath(str(cursor.location.file)))
+    COUNT += 1
+
+    print('%s: %s::%s()' % (message, '::'.join(fqn), cursor.spelling))
+
+
+def ExploreClass(child, fqn):
+    # Safety check
+    if (child.kind != clang.cindex.CursorKind.CLASS_DECL and
+        child.kind != clang.cindex.CursorKind.STRUCT_DECL):
+        raise Exception()
+
+    # Ignore forward declaration of classes
+    if not child.is_definition():
+        return
+    
+
+    ##
+    ## Verify that the class is publicly exported (its visibility must
+    ## be "default")
+    ##
+    visible = False
+
+    for i in child.get_children():
+        if (i.kind == clang.cindex.CursorKind.VISIBILITY_ATTR and
+            i.spelling == 'default'):
+            visible = True
+
+    if not visible:
+        return
+
+    global ALL_TYPES
+    ALL_TYPES.append('::'.join(fqn))
+    
+    
+    ##
+    ## Ignore pure abstract interfaces, by checking the following
+    ## criteria:
+    ##   - It must be a C++ class (not a struct)
+    ##   - It must start with "I"
+    ##   - All its methods must be pure virtual (abstract) and public
+    ##   - Its destructor must be public, virtual, and must do nothing
+    ##
+    
+    if (child.kind == clang.cindex.CursorKind.CLASS_DECL and
+        child.spelling[0] == 'I' and
+        child.spelling[1].isupper()):
+        abstract = True
+        isPublic = False
+
+        for i in child.get_children():
+            if i.kind == clang.cindex.CursorKind.VISIBILITY_ATTR:      # "default"
+                pass
+            elif i.kind == clang.cindex.CursorKind.CXX_ACCESS_SPEC_DECL:
+                isPublic = (i.access_specifier == clang.cindex.AccessSpecifier.PUBLIC)
+            elif i.kind == clang.cindex.CursorKind.CXX_BASE_SPECIFIER:
+                if i.spelling != 'boost::noncopyable':
+                    abstract = False
+            elif isPublic:
+                if i.kind == clang.cindex.CursorKind.CXX_METHOD:
+                    if i.is_pure_virtual_method():
+                        pass  # pure virtual is ok
+                    elif i.is_static_method():
+                        # static method without an inline implementation is ok
+                        for j in i.get_children():
+                            if j.kind == clang.cindex.CursorKind.COMPOUND_STMT:
+                                abstract = False
+                    else:
+                        abstract = False
+                elif (i.kind == clang.cindex.CursorKind.DESTRUCTOR and
+                      i.is_virtual_method()):
+                    # The destructor must be virtual, and must do nothing
+                    c = list(i.get_children())
+                    if (len(c) != 1 or
+                        c[0].kind != clang.cindex.CursorKind.COMPOUND_STMT or
+                        len(list(c[0].get_children())) != 0):
+                        abstract = False
+                elif i.kind == clang.cindex.CursorKind.CLASS_DECL:
+                    ExploreClass(i, fqn + [ i.spelling ])
+                elif (i.kind == clang.cindex.CursorKind.TYPEDEF_DECL or  # Allow "typedef"
+                      i.kind == clang.cindex.CursorKind.ENUM_DECL):      # Allow enums
+                    pass
+                else:
+                    abstract = False
+
+        if abstract:
+            print('Detected a pure interface (this is fine): %s' % ('::'.join(fqn)))
+        else:
+            ReportProblem('Not a pure interface', fqn, child)
+
+        return
+                  
+
+    ##
+    ## We are facing a standard C++ class or struct
+    ##
+    
+    isPublic = (child.kind == clang.cindex.CursorKind.STRUCT_DECL)
+
+    membersCount = 0
+    membersSize = 0
+
+    for i in child.get_children():
+        if (i.kind == clang.cindex.CursorKind.VISIBILITY_ATTR or    # "default"
+            i.kind == clang.cindex.CursorKind.CXX_BASE_SPECIFIER):  # base class
+            pass
+        
+        elif i.kind == clang.cindex.CursorKind.CXX_ACCESS_SPEC_DECL:
+            isPublic = (i.access_specifier == clang.cindex.AccessSpecifier.PUBLIC)
+
+        elif i.kind == clang.cindex.CursorKind.CLASS_DECL:
+            # This is a subclass
+            if isPublic:
+                ExploreClass(i, fqn + [ i.spelling ])
+
+        elif (i.kind == clang.cindex.CursorKind.CXX_METHOD or
+              i.kind == clang.cindex.CursorKind.CONSTRUCTOR or
+              i.kind == clang.cindex.CursorKind.DESTRUCTOR):
+            if isPublic:
+                hasImplementation = False
+                for j in i.get_children():
+                    if j.kind == clang.cindex.CursorKind.COMPOUND_STMT:
+                        hasImplementation = True
+
+                if hasImplementation:
+                    ReportProblem('Exported public method with an implementation', fqn, i)
+
+        elif i.kind == clang.cindex.CursorKind.VAR_DECL:
+            raise Exception('Unsupported: %s, %s' % (i.kind, i.location))
+
+        elif i.kind == clang.cindex.CursorKind.FUNCTION_TEMPLATE:
+            # An inline function template is OK, as it is not added to
+            # a shared library, but compiled by the client of the library
+            if isPublic:
+                print('Detected a template function (this is fine, but avoid it as much as possible): %s' % ('::'.join(fqn + [ i.spelling ])))
+                hasImplementation = False
+                for j in i.get_children():
+                    if j.kind == clang.cindex.CursorKind.COMPOUND_STMT:
+                        hasImplementation = True
+
+                if not hasImplementation:
+                    ReportProblem('Exported template function without an inline implementation', fqn, i)
+
+        elif (i.kind == clang.cindex.CursorKind.TYPEDEF_DECL or  # Allow "typedef"
+              i.kind == clang.cindex.CursorKind.ENUM_DECL):      # Allow enums
+            pass
+
+        elif i.kind == clang.cindex.CursorKind.FRIEND_DECL:
+            children = list(i.get_children())
+            if (isPublic and
+                (len(children) != 1 or
+                 not children[0].displayname in [
+                     # This is supported for ABI compatibility with Orthanc <= 1.8.0
+                     'operator<<(std::ostream &, const Orthanc::DicomTag &)',
+                 ])):
+                raise Exception('Unsupported: %s, %s' % (i.kind, i.location))
+
+        elif i.kind == clang.cindex.CursorKind.FIELD_DECL:
+            # TODO
+            if i.type.get_size() > 0:
+                membersSize += i.type.get_size()
+            membersCount += 1
+            
+        else:
+            if isPublic:
+                raise Exception('Unsupported: %s, %s' % (i.kind, i.location))
+
+    #print('Size of %s => (%d,%d)' % ('::'.join(fqn), membersCount, membersSize))
+
+
+def ExploreNamespace(node, namespace):
+    for child in node.get_children():
+        fqn = namespace + [ child.spelling ]
+        
+        if child.kind == clang.cindex.CursorKind.NAMESPACE:
+            ExploreNamespace(child, fqn)
+
+        elif (child.kind == clang.cindex.CursorKind.CLASS_DECL or
+              child.kind == clang.cindex.CursorKind.STRUCT_DECL):
+            ExploreClass(child, fqn)
+
+        elif child.kind == clang.cindex.CursorKind.FUNCTION_DECL:
+            visible = False
+            hasImplementation = False
+            for i in child.get_children():
+                if (i.kind == clang.cindex.CursorKind.VISIBILITY_ATTR and
+                    i.spelling == 'default'):
+                    visible = True
+                elif i.kind == clang.cindex.CursorKind.COMPOUND_STMT:
+                    hasImplementation = True
+
+            if visible and hasImplementation:
+                ReportProblem('Exported public function with an implementation', fqn, i)
+
+
+
+print('')
+
+for node in tu.cursor.get_children():
+    if (node.kind == clang.cindex.CursorKind.NAMESPACE and
+        node.spelling == 'Orthanc'):
+        ExploreNamespace(node, [ 'Orthanc' ])
+
+
+if args.target_cpp_size != '':
+    with open(args.target_cpp_size, 'w') as f:
+        for t in sorted(ALL_TYPES):
+            f.write('  printf("sizeof(::%s) == %%d\\n", static_cast<int>(sizeof(::%s)));\n' % (t, t))
+
+
+print('\nTotal of possibly problematic methods: %d' % COUNT)
+
+print('\nProblematic files:\n')
+for i in sorted(list(set(FILES))):
+    print(i)
+
+print('')
--- a/OrthancFramework/Sources/Cache/ICachePageProvider.h	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/Cache/ICachePageProvider.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/Cache/ICacheable.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryCache.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryCache.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryObjectCache.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryObjectCache.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryStringCache.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/Cache/SharedObjectCache.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -81,10 +81,14 @@
   }
 
 
-  SharedObjectCache::SharedObjectCache(size_t capacity) :
+  SharedObjectCache::SharedObjectCache(uint64_t capacity) :
     capacity_(capacity),
     currentSize_(0)
   {
+    if (static_cast<uint64_t>(static_cast<size_t>(capacity)) != capacity)
+    {
+      throw OrthancException(ErrorCode_NotEnoughMemory);
+    }
   }
 
 
@@ -189,4 +193,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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/Cache/SharedObjectCache.h	Thu Aug 13 14:38:28 2026 +0000
@@ -43,13 +43,13 @@
     Mutex                                mutex_;
     LeastRecentlyUsedIndex<std::string>  lru_;
     Content                              content_;
-    size_t                               capacity_;
+    const size_t                         capacity_;
     size_t                               currentSize_;
 
     void MakeRoom(size_t newObjectSize);
 
   public:
-    explicit SharedObjectCache(size_t capacity);
+    explicit SharedObjectCache(uint64_t capacity);
 
     ~SharedObjectCache();
 
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/Compatibility.h	Thu Aug 13 14:38:28 2026 +0000
@@ -36,12 +36,24 @@
 
 
 // Macro "ORTHANC_DEPRECATED" tags a function as having been deprecated
-#if (__cplusplus >= 201402L)  // C++14
-#  define ORTHANC_DEPRECATED(f) [[deprecated]] f
-#elif defined(__GNUC__) || defined(__clang__)
+#if defined(__GNUC__) || defined(__clang__)
 #  define ORTHANC_DEPRECATED(f) f __attribute__((deprecated))
 #elif defined(_MSC_VER)
 #  define ORTHANC_DEPRECATED(f) __declspec(deprecated) f
+#elif (__cplusplus >= 201402L)  // C++14
+#  define ORTHANC_DEPRECATED(f) [[deprecated]] f
+#else
+#  define ORTHANC_DEPRECATED
+#endif
+
+
+// Macro "ORTHANC_DEPRECATED_CLASS" tags a class as having been deprecated
+#if defined(__GNUC__) || defined(__clang__)
+#  define ORTHANC_DEPRECATED_CLASS(f) __attribute__((deprecated)) f
+#elif defined(_MSC_VER)
+#  define ORTHANC_DEPRECATED_CLASS(f) __declspec(deprecated) f
+#elif (__cplusplus >= 201402L)  // C++14
+#  define ORTHANC_DEPRECATED_CLASS(f) [[deprecated]] f
 #else
 #  define ORTHANC_DEPRECATED
 #endif
--- a/OrthancFramework/Sources/Constants.h	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/Constants.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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	Thu Aug 13 14:38:28 2026 +0000
@@ -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	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DataSource/DataSourceAnswer.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DataSource/DataSourceAnswer.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DataSource/DataSourceMemoryBudget.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DataSource/DataSourceMemoryBudget.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DataSource/DataSourceReader.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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);
@@ -179,15 +284,45 @@
   }
 
 
-  void DataSourceReader::CreateCache(size_t capacity)
+  void DataSourceReader::CreateCache(uint64_t capacity)
   {
     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 +335,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 +352,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::GetCacheSize() 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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DataSource/DataSourceReader.h	Thu Aug 13 14:38:28 2026 +0000
@@ -34,6 +34,7 @@
 
 namespace Orthanc
 {
+  class MetricsRegistry;
   class SharedObjectCache;
 
   namespace Internals
@@ -43,36 +44,100 @@
 
   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_;
     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 CreateCache(size_t capacity);
+    void SetMetricsConfiguration(const MetricsConfiguration& configuration);
+
+    void CreateCache(uint64_t capacity);
+
+    /**
+     * Apply backpressure by limiting memory for pending read
+     * tasks. Further reads block until memory is released.
+     **/
+    void SetCapacity(uint64_t maximumMemory);
 
-    void SetMaximumMemory(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 GetCacheSize() 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	Thu Aug 13 14:38:28 2026 +0000
@@ -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	Thu Aug 13 14:38:28 2026 +0000
@@ -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 /* maximum 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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DataSource/DicomDataSource.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DataSource/DicomDataSource.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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	Thu Aug 13 14:38:28 2026 +0000
@@ -0,0 +1,347 @@
+/**
+ * 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"
+
+#include <dcmtk/dcmdata/dcfilefo.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	Thu Aug 13 14:38:28 2026 +0000
@@ -0,0 +1,164 @@
+/**
+ * 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 /* maximum number of elements in the sliding window */,
+              uint64_t windowCapacity /* if 0, memory usage is only controlled by the size of the sliding window */);
+
+      const boost::shared_ptr<IExecutorService>& GetExecutorService() const
+      {
+        return executor_;
+      }
+
+      /**
+       * 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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DataSource/IDataIdentifier.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DataSource/IDataSource.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DataSource/StorageAreaDataSource.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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(const 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.GetCacheSize();
+
+    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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DataSource/StorageAreaDataSource.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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	Thu Aug 13 14:38:28 2026 +0000
@@ -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	Thu Aug 13 14:38:28 2026 +0000
@@ -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/DicomFormat/DicomTag.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomTag.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -314,13 +314,4 @@
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
   }
-
-
-#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
-  std::ostream& operator<< (std::ostream& o, const DicomTag& tag)
-  {
-    tag.FormatStream(o);
-    return o;
-  }
-#endif
 }
--- a/OrthancFramework/Sources/DicomFormat/DicomTag.h	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomTag.h	Thu Aug 13 14:38:28 2026 +0000
@@ -74,10 +74,6 @@
 
     static void AddTagsForModule(std::set<DicomTag>& target,
                                  DicomModule module);
-
-#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
-    ORTHANC_PUBLIC ORTHANC_DEPRECATED(friend std::ostream& operator<<(std::ostream& o, const DicomTag& tag));
-#endif
   };
 
   // Aliases for the most useful tags
--- a/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -229,28 +229,4 @@
   {
     complete_ = isComplete;
   }
-
-
-#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
-  void DicomFindAnswers::Add(ParsedDicomFile& dicom)
-  {
-    return Add(const_cast<const ParsedDicomFile&>(dicom));
-  }
-
-  void DicomFindAnswers::ToJson(Json::Value& target,
-                                size_t index,
-                                bool simplify) const
-  {
-    DicomToJsonFormat format = (simplify ? DicomToJsonFormat_Human : DicomToJsonFormat_Full);
-    ToJson(target, index, format);
-  }
-
-
-  void DicomFindAnswers::ToJson(Json::Value& target,
-                                bool simplify) const
-  {
-    DicomToJsonFormat format = (simplify ? DicomToJsonFormat_Human : DicomToJsonFormat_Full);
-    ToJson(target, format);
-  }
-#endif
 }
--- a/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.h	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.h	Thu Aug 13 14:38:28 2026 +0000
@@ -38,18 +38,6 @@
 
     void AddAnswerInternal(ParsedDicomFile* answer);
 
-#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
-    // Alias for binary compatibility with Orthanc Framework 1.7.2 => don't use it anymore
-    void Add(ParsedDicomFile& dicom);
-
-    void ToJson(Json::Value& target,
-                bool simplify) const;
-
-    void ToJson(Json::Value& target,
-                size_t index,
-                bool simplify) const;
-#endif
-
   public:
     explicit DicomFindAnswers(bool isWorklist);
 
--- a/OrthancFramework/Sources/DicomNetworking/TimeoutDicomConnectionManager.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/TimeoutDicomConnectionManager.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -28,6 +28,9 @@
 #include "../Logging.h"
 #include "../OrthancException.h"
 
+#include <dcmtk/dcmdata/dcfilefo.h>
+
+
 namespace Orthanc
 {
   static boost::posix_time::ptime GetNow()
--- a/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -128,9 +128,6 @@
                              "Cannot determine the transfer syntax");
     }
 
-    uint16_t bitsStored;
-    bool hasBitsStored = GetBitsStored(bitsStored, *dicom.getDataset());
-    
     if (allowedSyntaxes.find(syntax) != allowedSyntaxes.end())
     {
       // No transcoding is needed
@@ -165,6 +162,11 @@
                                          mode == TranscodingSopInstanceUidMode_Preserve);
 
 #if ORTHANC_ENABLE_DCMTK_JPEG == 1
+    uint16_t bitsStored;
+    bool hasBitsStored = GetBitsStored(bitsStored, *dicom.getDataset());
+#endif
+
+#if ORTHANC_ENABLE_DCMTK_JPEG == 1
     if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess1) != allowedSyntaxes.end())
     {
       if (!allowNewSopInstanceUid)
--- a/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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/Internals/DicomImageDecoder.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -1275,13 +1275,4 @@
     IImageWriter::WriteToMemory(writer, result, *image);
   }
 #endif
-
-
-#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
-  ImageAccessor *DicomImageDecoder::Decode(ParsedDicomFile& dataset,
-                                           unsigned int frame)
-  {
-    return Decode(*dataset.GetDcmtkObject().getDataset(), frame);
-  }
-#endif
 }
--- a/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h	Thu Aug 13 14:38:28 2026 +0000
@@ -86,12 +86,6 @@
                                     ImageExtractionMode mode,
                                     bool invert);
 
-#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
-    // Alias for binary compatibility with Orthanc Framework 1.7.2 => don't use it anymore
-    static ImageAccessor *Decode(ParsedDicomFile& dataset,
-                                 unsigned int frame);
-#endif
-
   public:
     static bool IsPsmctRle1(DcmDataset& dataset);
 
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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/DicomParsing/ParsedDicomFile.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -2379,56 +2379,4 @@
       throw OrthancException(ErrorCode_NotImplemented, "Cannot encapsulate pixel data from MIME type: " + mime);
     }
   }
-
-
-#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
-  // Alias for binary compatibility with Orthanc Framework 1.7.2 => don't use it anymore
-  void ParsedDicomFile::DatasetToJson(Json::Value& target,
-                                      DicomToJsonFormat format,
-                                      DicomToJsonFlags flags,
-                                      unsigned int maxStringLength)
-  {
-    return const_cast<const ParsedDicomFile&>(*this).DatasetToJson(target, format, flags, maxStringLength);
-  }
-
-  DcmFileFormat& ParsedDicomFile::GetDcmtkObject() const
-  {
-    return const_cast<ParsedDicomFile&>(*this).GetDcmtkObject();
-  }
-
-  void ParsedDicomFile::Apply(ITagVisitor& visitor)
-  {
-    const_cast<const ParsedDicomFile&>(*this).Apply(visitor);
-  }
-
-  ParsedDicomFile* ParsedDicomFile::Clone(bool keepSopInstanceUid)
-  {
-    return const_cast<const ParsedDicomFile&>(*this).Clone(keepSopInstanceUid);
-  }
-  
-  bool ParsedDicomFile::LookupTransferSyntax(std::string& result)
-  {
-    return const_cast<const ParsedDicomFile&>(*this).LookupTransferSyntax(result);
-  }
-  
-  bool ParsedDicomFile::LookupTransferSyntax(std::string& result) const
-  {
-    DicomTransferSyntax s;
-    if (LookupTransferSyntax(s))
-    {
-      result = GetTransferSyntaxUid(s);
-      return true;
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-  bool ParsedDicomFile::GetTagValue(std::string& value,
-                                    const DicomTag& tag)
-  {
-    return const_cast<const ParsedDicomFile&>(*this).GetTagValue(value, tag);
-  }
-#endif
 }
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h	Thu Aug 13 14:38:28 2026 +0000
@@ -110,21 +110,6 @@
 
     explicit ParsedDicomFile(DcmFileFormat* dicom);  // This takes ownership (no clone)
 
-#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
-    // Alias for binary compatibility with Orthanc Framework 1.7.2 => don't use it anymore
-    void DatasetToJson(Json::Value& target, 
-                       DicomToJsonFormat format,
-                       DicomToJsonFlags flags,
-                       unsigned int maxStringLength);    
-    DcmFileFormat& GetDcmtkObject() const;
-    void Apply(ITagVisitor& visitor);
-    ParsedDicomFile* Clone(bool keepSopInstanceUid);
-    bool LookupTransferSyntax(std::string& result);
-    bool LookupTransferSyntax(std::string& result) const;
-    bool GetTagValue(std::string& value,
-                     const DicomTag& tag);
-#endif
-
   public:
     explicit ParsedDicomFile(bool createIdentifiers);  // Create a minimal DICOM instance
 
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -333,24 +333,4 @@
   {
     return boost::filesystem::space(root_).available;
   }
-
-
-#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
-  FilesystemStorage::FilesystemStorage(std::string root) :
-    fsyncOnWrite_(false)
-  {
-    Setup(root);
-  }
-#endif
-
-
-#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
-  void FilesystemStorage::Read(std::string& content,
-                               const std::string& uuid,
-                               FileContentType type)
-  {
-    std::unique_ptr<IMemoryBuffer> buffer(ReadWhole(uuid, type));
-    buffer->MoveToString(content);
-  }
-#endif
 }
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.h	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.h	Thu Aug 13 14:38:28 2026 +0000
@@ -57,18 +57,6 @@
 
     void Setup(const boost::filesystem::path& root);
     
-#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
-    // Alias for binary compatibility with Orthanc Framework 1.7.2 => don't use it anymore
-    explicit FilesystemStorage(std::string root);
-#endif
-
-#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
-    // Binary compatibility with Orthanc Framework <= 1.8.2
-    void Read(std::string& content,
-              const std::string& uuid,
-              FileContentType type);
-#endif
-
   public:
     explicit FilesystemStorage(const boost::filesystem::path& root);
 
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 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 Aug 12 13:21:02 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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageRange.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpOutput.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpOutput.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/IMemoryBuffer.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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/ImageAccessor.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/Images/ImageAccessor.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -375,17 +375,4 @@
 
     format_ = format;
   }
-
-
-#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
-  void* ImageAccessor::GetBuffer() const
-  {
-    return const_cast<ImageAccessor&>(*this).GetBuffer();
-  }
-
-  void* ImageAccessor::GetRow(unsigned int y) const
-  {
-    return const_cast<ImageAccessor&>(*this).GetRow(y);
-  }
-#endif
 }
--- a/OrthancFramework/Sources/Images/ImageAccessor.h	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/Images/ImageAccessor.h	Thu Aug 13 14:38:28 2026 +0000
@@ -63,12 +63,6 @@
       return reinterpret_cast<T*>(row) [x];
     }
 
-#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
-    // Alias for binary compatibility with Orthanc Framework 1.7.2 => don't use it anymore
-    void* GetBuffer() const;
-    void* GetRow(unsigned int y) const;
-#endif
-
   public:
     ImageAccessor();
 
--- a/OrthancFramework/Sources/Images/ImageBuffer.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/Images/ImageBuffer.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobsEngine.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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))
@@ -217,15 +219,22 @@
 
   void JobsEngine::SetWorkersCount(size_t count)
   {
-    boost::mutex::scoped_lock lock(stateMutex_);
-      
-    if (state_ != State_Setup)
+    if (count == 0)
     {
-      // Can only be invoked before calling "Start()"
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
 
-    workers_.resize(count);
+    {
+      boost::mutex::scoped_lock lock(stateMutex_);
+
+      if (state_ != State_Setup)
+      {
+        // Can only be invoked before calling "Start()"
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+
+      workers_.resize(count);
+    }
   }
 
 
@@ -254,18 +263,7 @@
 
     retryHandler_ = boost::thread(RetryHandler, this);
 
-    if (workers_.size() == 0)
-    {
-      // Use all the available CPUs
-      size_t n = boost::thread::hardware_concurrency();
-      
-      if (n == 0)
-      {
-        n = 1;
-      }
-
-      workers_.resize(n);
-    }      
+    assert(!workers_.empty());
 
     for (size_t i = 0; i < workers_.size(); i++)
     {
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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/JobsEngine/Operations/JobOperationValues.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -33,14 +33,6 @@
 
 namespace Orthanc
 {
-#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
-  void JobOperationValues::Append(JobOperationValue* value)
-  {
-    throw OrthancException(ErrorCode_DiscontinuedAbi, "Removed in 1.8.1");
-  }
-#endif
-    
-
   void JobOperationValues::Append(JobOperationValues& target,
                                   bool clear)
   {
--- a/OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.h	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.h	Thu Aug 13 14:38:28 2026 +0000
@@ -33,27 +33,10 @@
 {
   class IJobUnserializer;
 
-#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
-  class JobOperationValue
-  {
-    /**
-     * This is for ABI compatibility with Orthanc framework <= 1.8.0,
-     * only to be able to run unit tests from Orthanc 1.7.2 to
-     * 1.8.0. The class was moved to "IJobOperationValue" in 1.8.1,
-     * and its memory layout has changed. Don't use this anymore.
-     **/
-  };
-#endif
-
   class ORTHANC_PUBLIC JobOperationValues : public boost::noncopyable
   {
   private:
     std::vector<IJobOperationValue*>   values_;
-
-#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
-    // For binary compatibility with Orthanc <= 1.8.0
-    ORTHANC_DEPRECATED(void Append(JobOperationValue* value));
-#endif
     
     void Append(JobOperationValues& target,
                 bool clear);
--- a/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -238,15 +238,6 @@
   }
 
 
-#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
-  void SequenceOfOperationsJob::Lock::AddInput(size_t index,
-                                               const JobOperationValue& value)
-  {
-    throw OrthancException(ErrorCode_DiscontinuedAbi, "Removed in 1.8.1");
-  }
-#endif
-
-
   SequenceOfOperationsJob::Lock::Lock(SequenceOfOperationsJob& that) :
     that_(that),
     lock_(that.mutex_)
--- a/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h	Thu Aug 13 14:38:28 2026 +0000
@@ -87,11 +87,6 @@
       SequenceOfOperationsJob&   that_;
       boost::mutex::scoped_lock  lock_;
 
-#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
-      ORTHANC_DEPRECATED(void AddInput(size_t index,
-                                       const JobOperationValue& value));
-#endif
-      
     public:
       explicit Lock(SequenceOfOperationsJob& that);
 
--- a/OrthancFramework/Sources/Logging.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/Logging.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -46,8 +46,6 @@
 {
   namespace Logging
   {
-    static bool logCallerThreadNameInContext = false; // add a "from THREADNAME" in the context everytime the context is copied from a thread to the other to help track the runnable/callable journey
-
     static const uint32_t ALL_CATEGORIES_MASK = 0xffffffff;
     
     static uint32_t infoCategoriesMask_ = 0;
@@ -953,6 +951,7 @@
 static ThreadsInformations                      threadsInformations_;
 static bool                                     enableThreadNames_ = true;
 static bool                                     enableThreadContexts_ = true;
+static bool                                     logCallerThreadNameInContext_ = false; // add a "from THREADNAME" in the context everytime the context is copied from a thread to the other to help track the runnable/callable journey
 static std::list<Orthanc::Logging::ILoggingListener*> loggingListeners_;
 static boost::shared_mutex                            loggingListenersMutex_;
 
@@ -971,6 +970,11 @@
       enableThreadContexts_ = enabled;
     }
 
+    void SetThreadNamesInContextsEnabled(bool enabled)
+    {
+      logCallerThreadNameInContext_ = enabled;
+    }
+
     static void GetLogPath(boost::filesystem::path& log,
                            boost::filesystem::path& link,
                            const std::string& suffix,
@@ -1129,7 +1133,7 @@
     ThreadContextMemento::ScopedSetter::ScopedSetter(const ThreadContextMemento& memento) :
       count_(0)
     {
-      if (logCallerThreadNameInContext && enableThreadNames_ && !memento.pimpl_->threadName_.empty())
+      if (logCallerThreadNameInContext_ && enableThreadNames_ && !memento.pimpl_->threadName_.empty())
       {
         PushCurrentThreadContext("from " + memento.pimpl_->threadName_);
         count_++;
--- a/OrthancFramework/Sources/Logging.h	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/Logging.h	Thu Aug 13 14:38:28 2026 +0000
@@ -139,6 +139,8 @@
 
     ORTHANC_PUBLIC void SetThreadContextsEnabled(bool enabled);
 
+    ORTHANC_PUBLIC void SetThreadNamesInContextsEnabled(bool enabled);
+
     ORTHANC_PUBLIC void SetCurrentThreadName(const std::string& name);
 
     ORTHANC_PUBLIC bool HasCurrentThreadName();
@@ -205,7 +207,6 @@
       ~ScopedCurrentThreadContextSetter();
     };
 
-
     struct ORTHANC_LOCAL NullStream : public std::ostream
     {
       NullStream() : 
--- a/OrthancFramework/Sources/MallocMemoryBuffer.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/MallocMemoryBuffer.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/MallocMemoryBuffer.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/MultiThreading/Mutex.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/MultiThreading/ThreadPool.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/MultiThreading/ThreadPool.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/StringMemoryBuffer.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/StringMemoryBuffer.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/Sources/Toolbox.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -3172,7 +3172,7 @@
     if (sizeInBytes < 1024)
     {
       std::ostringstream oss;
-      oss << sizeInBytes << "bytes";
+      oss << sizeInBytes << " bytes";
       return oss.str();
     }
     else
--- a/OrthancFramework/UnitTestsSources/FileStorageTests.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/UnitTestsSources/JobsTests.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/UnitTestsSources/MemoryCacheTests.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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/SharedLibraryUnitTests.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/UnitTestsSources/SharedLibraryUnitTests.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -41,6 +41,7 @@
 {
   Orthanc::InitializeFramework("", true);
   
+  Orthanc::Logging::SetCurrentThreadName("MAIN");
   Orthanc::Logging::EnableInfoLevel(true);
   Orthanc::Toolbox::DetectEndianness();
   Orthanc::SystemToolbox::MakeDirectory(Orthanc::SystemToolbox::PathFromUtf8("UnitTestsResults"));
--- a/OrthancFramework/UnitTestsSources/ToolboxTests.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancFramework/UnitTestsSources/ToolboxTests.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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));
@@ -513,13 +513,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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/CMakeLists.txt	Thu Aug 13 14:38:28 2026 +0000
@@ -129,6 +129,7 @@
   ${CMAKE_SOURCE_DIR}/Sources/OrthancRestApi/OrthancRestModalities.cpp
   ${CMAKE_SOURCE_DIR}/Sources/OrthancRestApi/OrthancRestResources.cpp
   ${CMAKE_SOURCE_DIR}/Sources/OrthancRestApi/OrthancRestSystem.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/OrthancRestApi/ThirdPartyVersions.cpp
   ${CMAKE_SOURCE_DIR}/Sources/OrthancWebDav.cpp
   ${CMAKE_SOURCE_DIR}/Sources/OutgoingDicomInstance.cpp
   ${CMAKE_SOURCE_DIR}/Sources/QueryRetrieveHandler.cpp
@@ -164,7 +165,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
@@ -202,7 +202,6 @@
     ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerConfigTests.cpp
     ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerIndexTests.cpp
     ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerJobsTests.cpp
-    ${CMAKE_SOURCE_DIR}/UnitTestsSources/SizeOfTests.cpp
     ${CMAKE_SOURCE_DIR}/UnitTestsSources/UnitTestsMain.cpp
     ${CMAKE_SOURCE_DIR}/UnitTestsSources/VersionsTests.cpp
     )
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer32.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer32.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer64.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer64.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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.13.0")
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginClearCurrentThreadName(
--- a/OrthancServer/Resources/AdvancedConfiguration.json	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Resources/AdvancedConfiguration.json	Thu Aug 13 14:38:28 2026 +0000
@@ -321,23 +321,91 @@
   // Orthanc <= 1.8.1.
   "MallocArenaMax" : 5,
 
-  // Default number of loader threads to use in jobs that read multiple files
-  // from the storage area when executing some tasks, including:
-  // - generating archive/media,
-  // - executing a C-Store,
-  // - transmitting resources through Orthanc peering.
-  // A value of 0 and 1 are equivalent: a single thread is used.
-  // A value > 1 is meaningful only if the storage is a distributed network storage
-  // (e.g., object storage plugin).
-  // (new in Orthanc 1.12.11)
-  // Note, from 1.10.0 to 1.12.10, a "ZipLoaderThreads" configuration option
-  // was available only for the ZIP archive/media. It is still available
-  // for backward compatibility.
-  "LoaderThreads" : 1,
-
   // Maximum number of DCMTK transcoders that are simultaneously running
   // at any given time. A value of "0" indicates to use all the
   // available CPU logical cores. Prior to Orthanc 1.12.6, there was no limit.
   // (new in Orthanc 1.12.6)
-  "MaximumConcurrentDcmtkTranscoders" : 0
+  "MaximumConcurrentDcmtkTranscoders" : 0,
+
+  // Number of threads that are used to parse DICOM files (e.g., to
+  // access the value of DICOM tags that are not indexed in the
+  // Orthanc database). You should monitor the
+  // "orthanc_dicom_parser_available_threads" metrics to determine the
+  // optimal value for your setup. (new in Orthanc 1.13.0)
+  "DicomParserThreadsCount" : 2,
+
+  // Peak amount of RAM (in MB) that can be allocated by the threads
+  // that parse the DICOM files. Note that this limit can be exceeded
+  // when a single DICOM instance is larger than this option. 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. (new in Orthanc 1.13.0)
+  "DicomParserMemoryCapacity" : 256,
+
+  // Maximum size of the cache of parsed DICOM files (in MB). This
+  // cache is stored in RAM and contains a parsed version of recently
+  // read DICOM files. A value of "0" disables this cache. (new in
+  // Orthanc 1.13.0 - in prior versions, this value was hardcoded to 128MB)
+  "DicomParserCacheSize" : 256,
+
+  // Number of threads that are used to transcode DICOM files. You
+  // should monitor the "orthanc_transcoder_available_threads" metrics
+  // to determine the optimal value for your setup. (new in Orthanc 1.13.0)
+  "TranscoderThreadsCount" : 4,
+
+  // Peak amount of RAM (in MB) that can be allocated by the threads
+  // that transcode DICOM instances. Note that this limit can be
+  // exceeded when a single transcoded instance is larger than this
+  // option. 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. (new in Orthanc 1.13.0)
+  "TranscoderMemoryCapacity" : 256,
+
+  // Maximum size of the cache of transcoded DICOM files (in MB). This
+  // cache is stored in RAM and contains transcoded versions of
+  // recently read DICOM files. A value of "0" disables this
+  // cache. (new in Orthanc 1.13.0)
+  "TranscoderCacheSize" : 256,
+
+  // Number of threads for the sequential access to DICOM
+  // instances. When a thread in Orthanc needs a set of DICOM
+  // instances in a predefined order, it cannot rely directly on
+  // loader threads, which may provide the instances out of
+  // order. This is notably the case for archive jobs, C-STORE SCU
+  // connections, and C-GET SCP and C-MOVE SCP handlers. In such
+  // situations, Orthanc uses a sliding-window buffer that is
+  // populated by a set of threads calling the out-of-order loader
+  // threads. These preloading threads are part of a pool that is
+  // global to Orthanc. The threads are paused if the buffer grows too
+  // large before the calling thread consumes the instances. By
+  // default, this option has the same value as
+  // "StorageLoaderThreadsCount". You should monitor the
+  // "orthanc_sequential_reader_available_threads" metrics to determine the
+  // optimal value for your setup and usage. (new in Orthanc 1.13.0)
+  // "SequentialDicomReaderThreadsCount" : 4,
+
+  // Each thread that accesses DICOM instances in sequential order
+  // creates a local sliding-window buffer that is preloaded by the
+  // global thread pool (cf. "SequentialDicomReaderThreadsCount").
+  // This configuration option specifies the maximum number of DICOM
+  // instances that can be preloaded before they are consumed by the
+  // calling thread. Together with
+  // "SequentialDicomReaderWindowCapacity", this option determines the
+  // amount of memory used by each calling thread that requires
+  // sequential access to DICOM instances. This is roughly equivalent
+  // to the older "LoaderThreads" configuration: By default, this
+  // option is therefore set to the same value as "LoaderThreads", if
+  // the latter is available. (new in Orthanc 1.13.0)
+  "SequentialDicomReaderWindowSize" : 4,
+
+  // Maximum amount of RAM (in MB) allocated to each local
+  // sliding-window buffer for each thread accessing a set of DICOM
+  // instances in sequential order (see
+  // "SequentialDicomReaderThreadsCount"). The global thread pool
+  // pauses preloading as soon as either the
+  // "SequentialDicomReaderWindowSize" or
+  // "SequentialDicomReaderWindowCapacity" limit is reached. Note that
+  // this limit can be exceeded when a single file is larger than the
+  // value specified by this option. (new in Orthanc 1.13.0)
+  "SequentialDicomReaderWindowCapacity" : 128
 }
--- a/OrthancServer/Resources/Configuration.json	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Resources/Configuration.json	Thu Aug 13 14:38:28 2026 +0000
@@ -18,9 +18,9 @@
   "StorageDirectory" : "OrthancStorage",
 
   // Path to the directory that holds the SQLite index (if unset, the
-  // value of StorageDirectory is used). This index could be stored on
+  // value of "StorageDirectory" is used). This index could be stored on
   // a RAM-drive or a SSD device for performance reasons.
-  "IndexDirectory" : "OrthancStorage",
+  // "IndexDirectory" : "OrthancStorage",
 
   // Enable the transparent compression of the DICOM instances
   "StorageCompression" : false,
@@ -37,21 +37,37 @@
   // Action to take when the maximum storage is reached.
   // By default, the patients are recycled ("Recycle" mode).
   // In "Reject" mode, the sender will receive a 0xA700 DIMSE status code
-  // if the instance was sent through C-Store, a 507 HTTP status code
+  // if the instance was sent through C-STORE, a 507 HTTP status code
   // if using the REST API and a 0xA700 Failure reason when using
-  // DICOMweb Stow-RS.
+  // DICOMweb STOW-RS.
   // Note: this value is taken into account only if you have set 
   // a MaximumStorageSize != 0 or a MaximumPatientCount != 0
   // Allowed values: "Recycle", "Reject"
   // (new in Orthanc 1.11.2)
   "MaximumStorageMode" : "Recycle",
 
-  // Maximum size of the storage cache in MB.  The storage cache
-  // is stored in RAM and contains a copy of recently accessed
-  // files (written or read).  A value of "0" indicates the cache
-  // is disabled.  (new in Orthanc 1.10.0)
+  // Maximum size of the storage cache in MB. The storage cache is
+  // stored in RAM and contains a copy of recently accessed files
+  // (written or read). A value of "0" disables this cache. (new in
+  // Orthanc 1.10.0)
   "MaximumStorageCacheSize" : 128,
 
+  // Number of threads that are used to read files from the storage.
+  // 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. (new in Orthanc 1.13.0)
+  "StorageLoaderThreadsCount" : 4,
+
+  // Peak amount of RAM (in MB) that can be allocated by the threads
+  // loading from the storage area. Note that this limit can be
+  // exceeded when a single file is larger than this option. 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. (new in Orthanc 1.13.0)
+  "StorageMemoryCapacity" : 512,
+
   // List of paths to the custom Lua scripts that are to be loaded
   // into this instance of Orthanc
   "LuaScripts" : [
@@ -67,9 +83,9 @@
   ],
 
   // Maximum number of processing jobs that are simultaneously running
-  // at any given time. A value of "0" indicates to use all the
-  // available CPU logical cores. To emulate Orthanc <= 1.3.2, set
-  // this value to "1".
+  // at any given time in the Orthanc jobs engine. A value of "0"
+  // indicates to use all the available CPU logical cores. To emulate
+  // Orthanc <= 1.3.2, set this value to "1".
   "ConcurrentJobs" : 2,
 
 
@@ -101,7 +117,7 @@
   // all to Orthanc while, in the second case, external HTTP clients
   // will be able to connect but will receive a 401 Unauthorized
   // HTTP status code.
-  //"HttpBindAddresses" : ["1.2.3.4", "127.0.0.1"]
+  // "HttpBindAddresses" : ["1.2.3.4", "127.0.0.1"]
 
   // Number of threads that are used by the embedded HTTP server.
   "HttpThreadsCount" : 50,
@@ -467,9 +483,9 @@
   // be run when Orthanc is acting as SCP. 
   // Note: This is not limiting the number of concurrent connections
   // but the number of concurrent DICOM operations.
-  // For instance, with a single thread, if a C-Find request is received during
-  // e.g. the transcoding of an incoming C-Store, the C-Find will
-  // be processed only at the end of the C-Store operation but both
+  // For instance, with a single thread, if a C-FIND request is received during
+  // e.g. the transcoding of an incoming C-STORE, the C-FIND will
+  // be processed only at the end of the C-STORE operation but both
   // DICOM associations will remain active.
   // (new in Orthanc 1.10.0, before this version, the value was fixed to 4)
   "DicomThreadsCount" : 4,
@@ -713,7 +729,9 @@
 
   // Whether Orthanc transcodes DICOM files to an uncompressed
   // transfer syntax over the DICOM protocol, if the remote modality
-  // does not support compressed transfer syntaxes (new in Orthanc 1.7.0).
+  // does not support compressed transfer syntaxes. This option is
+  // used in outgoing C-STORE SCU commands (including if synchronous
+  // C-MOVE is used), and by the C-GET SCP handler. (new in Orthanc 1.7.0).
   "TranscodeDicomProtocol" : true,
 
   // If some plugin to decode/transcode DICOM instances is installed,
--- a/OrthancServer/Resources/ImplementationNotes/JobsEngineClasses.txt	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Resources/ImplementationNotes/JobsEngineClasses.txt	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Resources/RunCppCheck-2.1.sh	Thu Aug 13 14:38:28 2026 +0000
@@ -10,34 +10,34 @@
 fi
 
 cat <<EOF > /tmp/cppcheck-suppressions.txt
-assertWithSideEffect:../../OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp:451
+assertWithSideEffect:../../OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp:460
 assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:1028
 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:3192
+assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:293
 constParameter:../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp
 knownArgument:../../OrthancFramework/UnitTestsSources/ImageTests.cpp
 knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp
 nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:321
-stlFindInsert:../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1536
+stlFindInsert:../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1545
 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:166
 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
+syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:345
 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:95
 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.0.sh	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Resources/RunCppCheck-2.17.0.sh	Thu Aug 13 14:38:28 2026 +0000
@@ -10,56 +10,69 @@
 fi
 
 cat <<EOF > /tmp/cppcheck-suppressions.txt
-assertWithSideEffect:../../OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp:454
-assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:1026
-assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:277
 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:292
 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:391
-assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3058
-assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:286
-nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:322
-stlFindInsert:../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1525
+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:66
+constParameterPointer:../../OrthancFramework/Sources/Logging.cpp:450
+constParameterPointer:../../OrthancFramework/Sources/Logging.cpp:454
+constParameterPointer:../../OrthancFramework/Sources/Toolbox.cpp:3467
+knownConditionTrueFalse:../../OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp:112
+knownConditionTrueFalse:../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp:444
+knownConditionTrueFalse:../../OrthancFramework/Sources/Images/PamReader.cpp:204
+knownConditionTrueFalse:../../OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp:345
+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
+knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2457
+knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2458
+knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2459
+knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2460
+nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:321
 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:166
 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:74
-stlFindInsert:../../OrthancServer/Sources/Database/MainDicomTagsRegistry.cpp:65
-stlFindInsert:../../OrthancServer/Sources/OrthancWebDav.cpp:328
-stlFindInsert:../../OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp:41
-stlFindInsert:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:361
-stlFindInsert:../../OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp:191
+stlFindInsert:../../OrthancServer/Sources/Database/ResourcesContent.h:141
 syntaxError:../../OrthancFramework/Sources/SQLite/FunctionContext.h:53
 syntaxError:../../OrthancFramework/UnitTestsSources/DicomMapTests.cpp:74
 syntaxError:../../OrthancFramework/UnitTestsSources/ZipTests.cpp:133
-syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:322
-uninitMemberVar:../../OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp:417
-unreadVariable:../../OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp:1173
-useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:91
+syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:345
+useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:95
 useInitializationList:../../OrthancFramework/Sources/Images/PngWriter.cpp:99
-useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:275
+useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:277
+variableScope:../../OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp:230
+variableScope:../../OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp:98
 EOF
 
 CPPCHECK_BUILD_DIR=/tmp/cppcheck-build-dir-2.17.0/
 mkdir -p ${CPPCHECK_BUILD_DIR}
 
-${CPPCHECK} -j8 --enable=all --std=gnu++11 --library=boost \
+${CPPCHECK} -j8 --enable=all --quiet --std=c++11 \
             --cppcheck-build-dir=${CPPCHECK_BUILD_DIR} \
             --suppressions-list=/tmp/cppcheck-suppressions.txt \
-            -I/usr/include/ \
-            -I/usr/include/jsoncpp/ \
-            -I/usr/include/linux/ \
-            -I/usr/include/c++/11/ \
-            -I/usr/include/c++/11/tr1/ \
-            -I/usr/include/x86_64-linux-gnu/c++/11/ \
+            --suppress=unusedFunction \
+            --suppress=missingIncludeSystem \
+            --suppress=missingInclude \
+            --suppress=useStlAlgorithm \
+            --check-level=exhaustive \
             -DBOOST_HAS_DATE_TIME=1 \
             -DBOOST_HAS_FILESYSTEM_V3=1 \
             -DBOOST_HAS_REGEX=1 \
             -DCIVETWEB_HAS_DISABLE_KEEP_ALIVE=1 \
             -DCIVETWEB_HAS_WEBDAV_WRITING=1 \
             -DDCMTK_VERSION_NUMBER=369 \
+            -DHAVE_MALLOPT=1 \
             -DJCONFIG_INCLUDED \
-            -DHAVE_MALLOPT=1 \
-            -DMONGOOSE_USE_CALLBACKS=1 \
             -DJSONCPP_VERSION_MAJOR=1 \
             -DJSONCPP_VERSION_MINOR=0 \
+            -DMONGOOSE_USE_CALLBACKS=1 \
             -DORTHANC_BUILDING_FRAMEWORK_LIBRARY=0 \
             -DORTHANC_BUILD_UNIT_TESTS=1 \
             -DORTHANC_ENABLE_BASE64=1 \
@@ -86,8 +99,9 @@
             -DORTHANC_ENABLE_ZLIB=1 \
             -DORTHANC_SANDBOXED=0 \
             -DORTHANC_SQLITE_VERSION=3027001 \
-            -DORTHANC_UNIT_TESTS_LINK_FRAMEWORK=0 \
+            -DORTHANC_UNIT_TESTS_LINK_FRAMEWORK=1 \
             -DORTHANC_USE_SYSTEM_MINIZIP=0 \
+            -DORTHANC_VERSION="\"mainline\"" \
             -DPUGIXML_VERSION=150 \
             -DUNIT_TESTS_WITH_HTTP_CONNEXIONS=1 \
             -D__BYTE_ORDER=__LITTLE_ENDIAN \
--- a/OrthancServer/Resources/RunCppCheck-2.17.1.sh	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Resources/RunCppCheck-2.17.1.sh	Thu Aug 13 14:38:28 2026 +0000
@@ -12,31 +12,30 @@
 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
+constParameterPointer:../../OrthancFramework/Sources/Toolbox.cpp:3467
 knownConditionTrueFalse:../../OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp:112
-knownConditionTrueFalse:../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp:440
+knownConditionTrueFalse:../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp:444
 knownConditionTrueFalse:../../OrthancFramework/Sources/Images/PamReader.cpp:204
-knownConditionTrueFalse:../../OrthancFramework/Sources/Images/PamReader.cpp:207
 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
-knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2450
 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
+knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2457
+knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2458
+knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2459
+knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2460
 nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:321
 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:166
 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:74
@@ -44,12 +43,12 @@
 syntaxError:../../OrthancFramework/Sources/SQLite/FunctionContext.h:53
 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
+syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:345
+useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:95
 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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Resources/RunCppCheck-2.20.0.sh	Thu Aug 13 14:38:28 2026 +0000
@@ -10,32 +10,32 @@
 fi
 
 cat <<EOF > /tmp/cppcheck-suppressions.txt
-assertWithSideEffect:../../OrthancFramework/Sources/DataSource/DataSourceReader.cpp:220
+assertWithSideEffect:../../OrthancFramework/Sources/DataSource/DataSourceReader.cpp:356
 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:292
 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:391
-assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:287
-constParameterPointer:../../OrthancFramework/Sources/Logging.cpp:452
-constParameterPointer:../../OrthancFramework/Sources/Logging.cpp:456
+assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:293
+constParameterPointer:../../OrthancFramework/Sources/Logging.cpp:450
+constParameterPointer:../../OrthancFramework/Sources/Logging.cpp:454
 constParameterPointer:../../OrthancFramework/Sources/Toolbox.cpp:3467
 missingInclude:../../OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp:41
 nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:321
-stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:166
-stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:74
-stlFindInsert:../../OrthancServer/Sources/Database/ResourcesContent.h:141
-syntaxError:../../OrthancFramework/Sources/SQLite/FunctionContext.h
-syntaxError:../../OrthancFramework/UnitTestsSources/DicomMapTests.cpp:74
-syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:325
-useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:95
-useInitializationList:../../OrthancFramework/Sources/Images/PngWriter.cpp:99
-useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:280
-variableScope:../../OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp:230
-variableScope:../../OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp:98
 sizeofCalculation:../../OrthancFramework/UnitTestsSources/ToolboxTests.cpp:517
 sizeofCalculation:../../OrthancFramework/UnitTestsSources/ToolboxTests.cpp:518
 sizeofCalculation:../../OrthancFramework/UnitTestsSources/ToolboxTests.cpp:519
 sizeofCalculation:../../OrthancFramework/UnitTestsSources/ToolboxTests.cpp:520
 sizeofCalculation:../../OrthancFramework/UnitTestsSources/ToolboxTests.cpp:521
 sizeofCalculation:../../OrthancFramework/UnitTestsSources/ToolboxTests.cpp:522
+stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:166
+stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:74
+stlFindInsert:../../OrthancServer/Sources/Database/ResourcesContent.h:141
+syntaxError:../../OrthancFramework/Sources/SQLite/FunctionContext.h
+syntaxError:../../OrthancFramework/UnitTestsSources/DicomMapTests.cpp:74
+syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:345
+useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:95
+useInitializationList:../../OrthancFramework/Sources/Images/PngWriter.cpp:99
+useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:277
+variableScope:../../OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp:230
+variableScope:../../OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp:98
 EOF
 
 CPPCHECK_BUILD_DIR=/tmp/cppcheck-build-dir-2.20.0/
--- a/OrthancServer/Sources/IDicomImageDecoder.h	Wed Aug 12 13:21:02 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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/OrthancConfiguration.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -47,8 +47,10 @@
 static const char* const WARNINGS = "Warnings";
 static const char* const JOBS_ENGINE_THREADS_COUNT = "JobsEngineThreadsCount";
 static const char* const DICOM_LOSSY_TRANSCODING_QUALITY = "DicomLossyTranscodingQuality";
-static const char* const CONFIG_LOADER_THREADS = "LoaderThreads";
-static const char* const CONFIG_ZIP_LOADER_THREADS = "ZipLoaderThreads"; // for backward compatibility only
+
+static const char* const ORTHANC_CONFIG_LOADER_THREADS = "LoaderThreads";         // for backward compatibility only
+static const char* const ORTHANC_CONFIG_ZIP_LOADER_THREADS = "ZipLoaderThreads";  // for backward compatibility only
+
 
 namespace Orthanc
 {
@@ -1133,6 +1135,21 @@
   }
 
 
+  unsigned int OrthancConfiguration::GetConcurrentJobs() const
+  {
+    unsigned int jobs = GetUnsignedIntegerParameter(ORTHANC_CONFIG_CONCURRENT_JOBS);
+
+    if (jobs == 0)
+    {
+      return SystemToolbox::GetHardwareConcurrency();
+    }
+    else
+    {
+      return jobs;
+    }
+  }
+
+
   void OrthancConfiguration::Format(std::string& result) const
   {
     Toolbox::WriteStyledJson(result, userConfiguration_);
@@ -1484,23 +1501,44 @@
     }
   }
 
-  unsigned int OrthancConfiguration::GetLoaderThreads() const
+
+  bool OrthancConfiguration::LookupCompatibilityLoaderThreads(unsigned int& threadsCount,
+                                                              std::string& fromOption) const
   {
-    // from 1.10.0 to 1.12.10, only CONFIG_ZIP_LOADER_THREADS was available -> read from it if CONFIG_LOADER_THREADS is not specified.
-    unsigned int loaderThreads = 1; // old ZipLoaderThreads default value
+    // This method corresponds to OrthancConfiguration::GetLoaderThreads() in Orthanc <= 1.12.11
+    bool found;
 
-    if (!LookupUnsignedIntegerParameter(loaderThreads, CONFIG_LOADER_THREADS))
+    if (LookupUnsignedIntegerParameter(threadsCount, ORTHANC_CONFIG_LOADER_THREADS))
     {
-      LookupUnsignedIntegerParameter(loaderThreads, CONFIG_ZIP_LOADER_THREADS); // we cannot use GetUnsignedIntegerParameter() because there is no default value for this old configuration
+      found = true;
+      fromOption = ORTHANC_CONFIG_LOADER_THREADS;
     }
-
-    if (loaderThreads <= 1)
+    else if (LookupUnsignedIntegerParameter(threadsCount, ORTHANC_CONFIG_ZIP_LOADER_THREADS))
     {
-      return 1; // 0 is not a valid internal value anymore
+      // from 1.10.0 to 1.12.10, only CONFIG_ZIP_LOADER_THREADS was available
+      // => read from it if CONFIG_LOADER_THREADS is not specified.
+      found = true;
+      fromOption = ORTHANC_CONFIG_ZIP_LOADER_THREADS;
     }
     else
     {
-      return loaderThreads;
+      // In Orthanc <= 1.12.11, the returned number of threads was 1 in this case,
+      // corresponding to the old ZipLoaderThreads default value
+      found = false;
+    }
+
+    if (found)
+    {
+      if (threadsCount < 1)
+      {
+        threadsCount = 1;  // 0 is not a valid internal value anymore
+      }
+
+      return true;
+    }
+    else
+    {
+      return false;
     }
   }
 }
--- a/OrthancServer/Sources/OrthancConfiguration.h	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/OrthancConfiguration.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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,9 +53,14 @@
 #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_INDEX_DIRECTORY "IndexDirectory"
 #define ORTHANC_CONFIG_STORAGE_DIRECTORY "StorageDirectory"
 
 
+
 namespace Orthanc
 {
   class DicomMap;
@@ -194,21 +198,25 @@
     bool LookupStringParameter(std::string& target,
                                const std::string& parameter) const;
 
+    // If the parameter is not part of the user configuration, it it retrieved from the default configuration
     std::string GetStringParameter(const std::string& parameter) const;
     
     bool LookupIntegerParameter(int& target,
                                 const std::string& parameter) const;
 
+    // If the parameter is not part of the user configuration, it it retrieved from the default configuration
     int GetIntegerParameter(const std::string& parameter) const;
 
     bool LookupUnsignedIntegerParameter(unsigned int& target,
                                         const std::string& parameter) const;
 
+    // If the parameter is not part of the user configuration, it it retrieved from the default configuration
     unsigned int GetUnsignedIntegerParameter(const std::string& parameter) const;
 
     bool LookupBooleanParameter(bool& target,
                                 const std::string& parameter) const;
 
+    // If the parameter is not part of the user configuration, it it retrieved from the default configuration
     bool GetBooleanParameter(const std::string& parameter) const;
 
     void GetDicomModalityUsingSymbolicName(RemoteModalityParameters& modality,
@@ -259,7 +267,17 @@
 
     void RemovePeer(const std::string& symbolicName);
 
-    unsigned int GetLoaderThreads() const;
+    unsigned int GetConcurrentJobs() const;
+
+    unsigned int GetHttpThreadsCount() const
+    {
+      return GetUnsignedIntegerParameter(ORTHANC_CONFIG_HTTP_THREADS_COUNT);
+    }
+
+    unsigned int GetDicomThreadsCount() const
+    {
+      return GetUnsignedIntegerParameter(ORTHANC_CONFIG_DICOM_THREADS_COUNT);
+    }
 
     void Format(std::string& result) const;
     
@@ -288,11 +306,6 @@
 
     std::string GetOrthancAET() const;
 
-    std::string GetOrthancName() const
-    {
-      return GetStringParameter(ORTHANC_CONFIG_NAME);
-    }
-
     std::string GetMaximumStorageMode() const
     {
       return GetStringParameter(ORTHANC_CONFIG_MAXIMUM_STORAGE_MODE);
@@ -302,10 +315,6 @@
     {
       return GetStringParameter(ORTHANC_CONFIG_DICOM_DEFAULT_RETRIEVE_METHOD);
     }
-    unsigned int GetMaximumStorageCacheSize() const
-    {
-      return GetUnsignedIntegerParameter(ORTHANC_CONFIG_MAXIMUM_STORAGE_CACHE_SIZE);
-    }
 
     unsigned int GetMaximumStorageSize() const
     {
@@ -369,5 +378,9 @@
 
     static void ParseAcceptedTransferSyntaxes(std::set<DicomTransferSyntax>& target,
                                               const Json::Value& source);
+
+    // This method provides backward compatibility against old configuration options
+    bool LookupCompatibilityLoaderThreads(unsigned int& threadsCount,
+                                          std::string& fromOption) const;
   };
 }
--- a/OrthancServer/Sources/OrthancGetRequestHandler.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/OrthancGetRequestHandler.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/OrthancGetRequestHandler.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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/OrthancInitialization.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/OrthancInitialization.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -432,14 +432,13 @@
       lock.GetConfiguration().GetStringParameter(ORTHANC_CONFIG_STORAGE_DIRECTORY);
 
     std::string indexDirectoryStr;
-    if (!lock.GetConfiguration().LookupStringParameter(indexDirectoryStr, "IndexDirectory"))
+    if (!lock.GetConfiguration().LookupStringParameter(indexDirectoryStr, ORTHANC_CONFIG_INDEX_DIRECTORY))
     {
       indexDirectoryStr = storageDirectoryStr;
     }
 
     // Open the database
-    boost::filesystem::path indexDirectory = lock.GetConfiguration().InterpretStringParameterAsPath(
-      indexDirectoryStr);
+    boost::filesystem::path indexDirectory = lock.GetConfiguration().InterpretStringParameterAsPath(indexDirectoryStr);
 
     LOG(WARNING) << "SQLite index directory: " << SystemToolbox::PathToUtf8(indexDirectory);
 
--- a/OrthancServer/Sources/OrthancMoveRequestHandler.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/OrthancMoveRequestHandler.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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/OrthancRestArchive.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -124,7 +124,6 @@
                                DicomTransferSyntax& syntax,  /* out */
                                unsigned int& lossyQuality,   /* out */
                                int& priority,                /* out */
-                               unsigned int& loaderThreads,  /* out */
                                std::string& filename,        /* out */
                                Json::Value& userData,        /* out */
                                bool& allowUtf8,              /* out (new in Orthanc 1.12.11) */
@@ -185,11 +184,6 @@
       userData = body[KEY_USER_DATA].asString();
     }
 
-    {
-      OrthancConfiguration::ReaderLock lock;
-      loaderThreads = lock.GetConfiguration().GetLoaderThreads();
-    }
-
     // New in Orthanc 1.12.11
     if (body.type() == Json::objectValue &&
         body.isMember(KEY_ALLOW_UTF8))
@@ -630,14 +624,13 @@
       bool synchronous, extended, transcode;
       DicomTransferSyntax transferSyntax;
       int priority;
-      unsigned int loaderThreads;
       std::string filename;
       unsigned int lossyQuality;
       Json::Value userData;
       bool allowUtf8;
 
       GetJobParameters(synchronous, extended, transcode, transferSyntax, lossyQuality,
-                       priority, loaderThreads, filename, userData, allowUtf8,
+                       priority, filename, userData, allowUtf8,
                        body, DEFAULT_IS_EXTENDED, "Archive.zip", defaultUtf8);
       
       std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended, ResourceType_Patient));
@@ -649,7 +642,6 @@
         job->SetLossyQuality(lossyQuality);
       }
       
-      job->SetLoaderThreads(loaderThreads);
       job->SetAllowUtf8(allowUtf8);
       job->SetUserData(userData);
 
@@ -785,12 +777,6 @@
       job->SetLossyQuality(GetLossyQuality(call));
     }
 
-    {
-      OrthancConfiguration::ReaderLock lock;
-      unsigned int loaderThreads = lock.GetConfiguration().GetLoaderThreads();
-      job->SetLoaderThreads(loaderThreads);
-    }
-
     SubmitJob(call.GetOutput(), context, job, 0 /* priority */,
               true /* synchronous */, filename);
   }
@@ -832,14 +818,13 @@
       bool synchronous, extended, transcode;
       DicomTransferSyntax transferSyntax;
       int priority;
-      unsigned int loaderThreads;
       std::string filename;
       unsigned int lossyQuality;
       Json::Value userData;
       bool allowUtf8;
 
       GetJobParameters(synchronous, extended, transcode, transferSyntax, lossyQuality,
-                       priority, loaderThreads, filename, userData, allowUtf8,
+                       priority, filename, userData, allowUtf8,
                        body, false /* by default, not extented */, id + ".zip", defaultUtf8);
       
       std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended, LEVEL));
@@ -851,7 +836,6 @@
         job->SetLossyQuality(lossyQuality);
       }
 
-      job->SetLoaderThreads(loaderThreads);
       job->SetAllowUtf8(allowUtf8);
       job->SetUserData(userData);
 
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -32,6 +32,9 @@
 #include "../OrthancConfiguration.h"
 #include "../OrthancInitialization.h"
 #include "../ServerContext.h"
+#include "ThirdPartyVersions.h"
+
+#include <PatchList.h>
 
 #include <boost/algorithm/string/predicate.hpp>
 
@@ -88,6 +91,9 @@
     static const char* const HAS_QUEUES = "HasQueues";
     static const char* const HAS_EXTENDED_FIND = "HasExtendedFind";
     static const char* const HAS_RESERVE_QUEUE_VALUE = "HasReserveQueueValue";
+    static const char* const PERFORMANCE = "Performance";
+    static const char* const THIRD_PARTY_PATCHES = "ThirdPartyPatches";
+    static const char* const THIRD_PARTY_VERSIONS = "ThirdPartyVersions";
 
     if (call.IsDocumentation())
     {
@@ -122,13 +128,11 @@
         .SetAnswerField(ORTHANC_CONFIG_STORAGE_COMPRESSION, RestApiCallDocumentation::Type_Boolean,
                         "Whether storage compression is enabled (new in Orthanc 1.11.0)")
         .SetAnswerField(ORTHANC_CONFIG_OVERWRITE_INSTANCES, RestApiCallDocumentation::Type_Boolean,
-                        "Whether instances are overwritten when re-ingested (new in Orthanc 1.11.0 and kept as a bool for backward compatibility)")
+                        "Whether instances are overwritten when re-ingested (new in Orthanc 1.11.0 and kept as a Boolean for backward compatibility)")
         .SetAnswerField(OVERWRITE_INSTANCES_MODE, RestApiCallDocumentation::Type_String,
-                        "Whether instances are overwritten when re-ingested (new in Orthanc 1.13.0)")
+                        "Overwriting mode governing how instances are re-ingested (new in Orthanc 1.13.0)")
         .SetAnswerField(ORTHANC_CONFIG_INGEST_TRANSCODING, RestApiCallDocumentation::Type_String,
                         "Whether instances are transcoded when ingested into Orthanc (`""` if no transcoding is performed) (new in Orthanc 1.11.0)")
-        .SetAnswerField(ORTHANC_CONFIG_MAXIMUM_STORAGE_CACHE_SIZE, RestApiCallDocumentation::Type_Number,
-                        std::string("The configured ") + ORTHANC_CONFIG_MAXIMUM_STORAGE_CACHE_SIZE + " in MB (new in Orthanc 1.13.0)")
         .SetAnswerField(ORTHANC_CONFIG_STORE_MD5_FOR_ATTACHMENTS, RestApiCallDocumentation::Type_Boolean,
                         std::string("The configured ") + ORTHANC_CONFIG_STORE_MD5_FOR_ATTACHMENTS + " (new in Orthanc 1.13.0)")
         .SetAnswerField(ORTHANC_CONFIG_MAXIMUM_STORAGE_SIZE, RestApiCallDocumentation::Type_Number,
@@ -149,6 +153,12 @@
                         "Whether Orthanc is running in read only mode (new in Orthanc 1.12.5)")
         .SetAnswerField(ORTHANC_CONFIG_PATIENT_LEVEL_ENABLED, RestApiCallDocumentation::Type_Boolean,
                         "Whether Patient level routes and sanity checks are enabled (new in Orthanc 1.12.11)")
+        .SetAnswerField(PERFORMANCE, RestApiCallDocumentation::Type_JsonObject,
+                        "The performance options from the configuration file, with sizes expressed in MB (new in Orthanc 1.13.0)")
+        .SetAnswerField(THIRD_PARTY_PATCHES, RestApiCallDocumentation::Type_JsonListOfStrings,
+                        "Patches applied to the third-party libraries included in this binary version of Orthanc (new in Orthanc 1.13.0)")
+        .SetAnswerField(THIRD_PARTY_VERSIONS, RestApiCallDocumentation::Type_JsonObject,
+                        "Versions of the third-party libraries (new in Orthanc 1.13.0)")
         .SetHttpGetSample("https://orthanc.uclouvain.be/demo/system", true);
       return;
     }
@@ -164,12 +174,12 @@
 
     {
       OrthancConfiguration::ReaderLock lock;
-      result[ORTHANC_CONFIG_NAME] = lock.GetConfiguration().GetOrthancName();
+
+      result[ORTHANC_CONFIG_NAME] = lock.GetConfiguration().GetStringParameter(ORTHANC_CONFIG_NAME);
       result[ORTHANC_CONFIG_DICOM_AET] = lock.GetConfiguration().GetOrthancAET();
       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.13.0
       result[ORTHANC_CONFIG_STORE_MD5_FOR_ATTACHMENTS] = lock.GetConfiguration().HasStoreMD5ForAttachments(); // New in Orthanc 1.13.0
       result[ORTHANC_CONFIG_STORAGE_COMPRESSION] = lock.GetConfiguration().HasStorageCompression(); // New in Orthanc 1.11.0
       result[ORTHANC_CONFIG_DATABASE_SERVER_IDENTIFIER] = lock.GetConfiguration().GetDatabaseServerIdentifier();
@@ -179,6 +189,8 @@
       result[ORTHANC_CONFIG_DICOM_DEFAULT_RETRIEVE_METHOD] = lock.GetConfiguration().GetDicomDefaultRetrieveMethod();
     }
 
+    context.ExportPerformanceParameters(result[PERFORMANCE]);
+
     DicomTransferSyntax ingestTransferSyntax;
     if (context.LookupIngestTranscoding(ingestTransferSyntax))
     {
@@ -227,7 +239,26 @@
     result[CAPABILITIES][HAS_KEY_VALUE_STORES] = OrthancRestApi::GetIndex(call).HasKeyValueStoresSupport();
     result[CAPABILITIES][HAS_QUEUES] = OrthancRestApi::GetIndex(call).HasQueuesSupport();
     result[CAPABILITIES][HAS_RESERVE_QUEUE_VALUE] = OrthancRestApi::GetIndex(call).HasReserveQueueValueSupport();
-    
+
+    {
+      // New in Orthanc 1.13.0
+      Json::Value patches = Json::arrayValue;
+
+      for (unsigned int i = 0; i < ORTHANC_PATCHES_COUNT; i++)
+      {
+        patches.append(ORTHANC_PATCHES[i]);
+      }
+
+      result[THIRD_PARTY_PATCHES] = patches;
+    }
+
+    {
+      // New in Orthanc 1.13.0
+      Json::Value versions;
+      GetThirdPartyVersions(versions);
+      result[THIRD_PARTY_VERSIONS] = versions;
+    }
+
     call.GetOutput().AnswerJson(result);
   }
 
@@ -1011,8 +1042,6 @@
     registry.SetIntegerValue("orthanc_up_time_s", serverUpTime);
     registry.SetIntegerValue("orthanc_last_change", lastChange["Last"].asInt64());
 
-    context.PublishCacheMetrics();
-
     std::string s;
     registry.ExportPrometheusText(s);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancRestApi/ThirdPartyVersions.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -0,0 +1,128 @@
+/**
+ * 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 "../PrecompiledHeadersServer.h"
+#include "ThirdPartyVersions.h"
+
+#include "../../../OrthancFramework/Sources/Toolbox.h"
+
+#include <boost/lexical_cast.hpp>
+#include <boost/version.hpp>
+#include <curl/curl.h>
+#include <google/protobuf/stubs/common.h>
+#include <jpeglib.h>
+#include <json/version.h>
+#include <lua.h>
+#include <png.h>
+#include <sqlite3.h>
+#include <zlib.h>
+
+#if ORTHANC_ENABLE_CIVETWEB == 1
+#  include <civetweb.h>
+#endif
+
+#if ORTHANC_ENABLE_SSL == 1
+#  include <openssl/opensslv.h>
+#endif
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+#  include <pugixml.hpp>
+#endif
+
+namespace Orthanc
+{
+  void GetThirdPartyVersions(Json::Value& target)
+  {
+    target = Json::objectValue;
+
+    target["boost"] = (boost::lexical_cast<std::string>(BOOST_VERSION / 100000) + "." +
+                       boost::lexical_cast<std::string>(BOOST_VERSION / 100 % 1000) + "." +
+                       boost::lexical_cast<std::string>(BOOST_VERSION % 100));
+
+#if ORTHANC_ENABLE_CIVETWEB == 1
+    target["civetweb"] = (boost::lexical_cast<std::string>(CIVETWEB_VERSION_MAJOR) + "." +
+                          boost::lexical_cast<std::string>(CIVETWEB_VERSION_MINOR) + "." +
+                          boost::lexical_cast<std::string>(CIVETWEB_VERSION_PATCH));
+#endif
+
+    target["dcmtk"] = (boost::lexical_cast<std::string>(DCMTK_VERSION_NUMBER / 100) + "." +
+                       boost::lexical_cast<std::string>(DCMTK_VERSION_NUMBER / 10 % 10) + "." +
+                       boost::lexical_cast<std::string>(DCMTK_VERSION_NUMBER % 10));
+
+    target["jsoncpp"] = JSONCPP_VERSION_STRING;
+
+    target["libcurl"] = LIBCURL_VERSION;
+
+    target["libjpeg"] = (boost::lexical_cast<std::string>(JPEG_LIB_VERSION / 10) + "." +
+                         boost::lexical_cast<std::string>(JPEG_LIB_VERSION % 10));
+
+    target["libpng"] = (boost::lexical_cast<std::string>(PNG_LIBPNG_VER_MAJOR) + "." +
+                        boost::lexical_cast<std::string>(PNG_LIBPNG_VER_MINOR) + "." +
+                        boost::lexical_cast<std::string>(PNG_LIBPNG_VER_RELEASE));
+
+#if defined(LUA_VERSION_MAJOR)
+    target["lua"] = (boost::lexical_cast<std::string>(LUA_VERSION_MAJOR) + "." +
+                     boost::lexical_cast<std::string>(LUA_VERSION_MINOR) + "." +
+                     boost::lexical_cast<std::string>(LUA_VERSION_RELEASE));
+#else
+    target["lua"] = (boost::lexical_cast<std::string>(LUA_VERSION_NUM / 100) + "." +
+                     boost::lexical_cast<std::string>(LUA_VERSION_NUM % 100));
+#endif
+
+#if ORTHANC_ENABLE_SSL == 1
+#  if defined(OPENSSL_VERSION_MAJOR)
+    target["openssl"] = (boost::lexical_cast<std::string>(OPENSSL_VERSION_MAJOR) + "." +
+                         boost::lexical_cast<std::string>(OPENSSL_VERSION_MINOR) + "." +
+                         boost::lexical_cast<std::string>(OPENSSL_VERSION_PATCH));
+#  else
+    {
+      // Backward compatibility for OpenSSL 1.x
+      std::vector<std::string> v;
+      Toolbox::TokenizeString(v, OPENSSL_VERSION_TEXT, ' ' );
+      if (v.size() >= 2)
+      {
+        target["openssl"] = v[1];
+      }
+      else
+      {
+        target["openssl"] = "unknown";
+      }
+    }
+#  endif
+#endif
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+    target["pugixml"] = (boost::lexical_cast<std::string>(PUGIXML_VERSION / 1000) + "." +
+                         boost::lexical_cast<std::string>(PUGIXML_VERSION / 10 % 100) + "." +
+                         boost::lexical_cast<std::string>(PUGIXML_VERSION % 10));
+#endif
+
+    target["protobuf"] = (boost::lexical_cast<std::string>(GOOGLE_PROTOBUF_VERSION / 1000000) + "." +
+                          boost::lexical_cast<std::string>(GOOGLE_PROTOBUF_VERSION / 1000 % 1000) + "." +
+                          boost::lexical_cast<std::string>(GOOGLE_PROTOBUF_VERSION % 1000));
+
+    target["sqlite"] = SQLITE_VERSION;
+
+    target["zlib"] = ZLIB_VERSION;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancRestApi/ThirdPartyVersions.h	Thu Aug 13 14:38:28 2026 +0000
@@ -0,0 +1,31 @@
+/**
+ * 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 <json/value.h>
+
+namespace Orthanc
+{
+  void GetThirdPartyVersions(Json::Value& target);
+}
--- a/OrthancServer/Sources/OrthancWebDav.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/OrthancWebDav.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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,70 @@
 #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
+
+// Symbolic name for performance-related configuration options
+static const char* const ORTHANC_CONFIG_STORAGE_LOADER_THREADS_COUNT = "StorageLoaderThreadsCount";
+static const char* const ORTHANC_CONFIG_STORAGE_MEMORY_CAPACITY = "StorageMemoryCapacity";
+static const char* const ORTHANC_CONFIG_MAXIMUM_STORAGE_CACHE_SIZE = "MaximumStorageCacheSize";
+static const char* const ORTHANC_CONFIG_DICOM_PARSER_SOURCE_THREADS_COUNT = "DicomParserThreadsCount";
+static const char* const ORTHANC_CONFIG_DICOM_PARSER_MEMORY_CAPACITY = "DicomParserMemoryCapacity";
+static const char* const ORTHANC_CONFIG_DICOM_PARSER_CACHE_SIZE = "DicomParserCacheSize";
+static const char* const ORTHANC_CONFIG_TRANSCODER_THREADS_COUNT = "TranscoderThreadsCount";
+static const char* const ORTHANC_CONFIG_TRANSCODER_MEMORY_CAPACITY = "TranscoderMemoryCapacity";
+static const char* const ORTHANC_CONFIG_TRANSCODER_CACHE_SIZE = "TranscoderCacheSize";
+static const char* const ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_THREADS_COUNT = "SequentialDicomReaderThreadsCount";
+static const char* const ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_WINDOW_SIZE = "SequentialDicomReaderWindowSize";
+static const char* const ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_WINDOW_CAPACITY = "SequentialDicomReaderWindowCapacity";
+
+
+// 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";
+
+
+// New metrics in Orthanc 1.13.0
+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_COUNT                  = "orthanc_storage_memory_count";
+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_USAGE_MB               = "orthanc_storage_memory_usage_mb";
+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_COUNT                  = "orthanc_dicom_parser_memory_count";
+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_USAGE_MB               = "orthanc_dicom_parser_memory_usage_mb";
+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_COUNT                    = "orthanc_transcoder_memory_count";
+static const char* const METRICS_TRANSCODER_MEMORY_MAX_USAGE_MB             = "orthanc_transcoder_memory_max_usage_mb";
+static const char* const METRICS_TRANSCODER_MEMORY_USAGE_MB                 = "orthanc_transcoder_memory_usage_mb";
+static const char* const METRICS_TRANSCODER_THREAD_POOL_AVAILABLE_THREADS   = "orthanc_transcoder_available_threads";
+
+static const char* const METRICS_SEQUENTIAL_READER_THREAD_POOL_AVAILABLE_THREADS = "orthanc_sequential_reader_available_threads";
+
+
 
 
 /**
@@ -105,6 +168,97 @@
   }
 
 
+  void ServerContext::StoreFile(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 +515,29 @@
   }
 
 
-  void ServerContext::PublishCacheMetrics()
+  static void GetMemorySizeConfiguration(uint64_t& result,
+                                         const OrthancConfiguration::ReaderLock& lock,
+                                         const char* parameter)
   {
-    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()));
+    unsigned int mb;
+    if (!lock.GetConfiguration().LookupUnsignedIntegerParameter(mb, parameter))
+    {
+      mb = lock.GetConfiguration().GetUnsignedIntegerParameter(parameter);
+      LOG(WARNING) << "Performance option \"" << parameter << "\" is not defined in your configuration, setting it to "
+                   << mb << "MB. Depending on the available memory on the system, you may want to adapt this value.";
+    }
+    else
+    {
+      LOG(INFO) << "Performance option \"" << parameter << "\" is set to " << mb << "MB";
+    }
+
+    result = static_cast<uint64_t>(mb) * MEGABYTE;
   }
 
 
   ServerContext::ServerContext(IDatabaseWrapper& database,
                                IPluginStorageArea& area,
+                               ServerTranscoder* transcoder /* takes ownership */,
                                bool unitTesting,
                                size_t maxCompletedJobs,
                                bool readOnly) :
@@ -384,7 +546,6 @@
     compressionEnabled_(false),
     storeMD5_(true),
     largeDicomThrottler_(1),
-    dicomCache_(DICOM_CACHE_SIZE),
     mainLua_(*this),
     filterLua_(*this),
     luaListener_(*this),
@@ -400,6 +561,8 @@
     isExecuteLuaEnabled_(false),
     isRestApiWriteToFileSystemEnabled_(false),
     overwriteInstances_(OverwriteInstancesMode_Never),
+    transcoder_(transcoder),
+    transcodeDicomProtocol_(true),
     isIngestTranscoding_(false),
     ingestTranscodingOfUncompressed_(true),
     ingestTranscodingOfCompressed_(true),
@@ -419,7 +582,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 +713,199 @@
 #else
       LOG(INFO) << "Your platform does not support malloc_trim(), not starting the memory trimming thread";
 #endif
+
+      unsigned int storageLoaderThreads, transcoderThreads, dicomParserThreads;
+      unsigned int sequentialReaderThreads, sequentialReaderWindowSize;
+
+      uint64_t storageCacheSize, storageMemoryCapacity, dicomParserMemoryCapacity;
+      uint64_t dicomParserCacheSize, transcoderMemoryCapacity, transcoderCacheSize, sequentialReaderWindowCapacity;
+      
+      {
+        OrthancConfiguration::ReaderLock lock;
+
+        GetMemorySizeConfiguration(storageCacheSize, lock, ORTHANC_CONFIG_MAXIMUM_STORAGE_CACHE_SIZE);
+
+        if (storageCacheSize == 0)
+        {
+          LOG(WARNING) << "Storage cache is disabled";
+        }
+
+        unsigned int compatibilityLoaderThreadsCount;
+        std::string compatibilityLoaderThreadsOption;
+        bool hasCompatibilityLoaderThreads = lock.GetConfiguration().LookupCompatibilityLoaderThreads(
+          compatibilityLoaderThreadsCount, compatibilityLoaderThreadsOption);
+
+        if (!lock.GetConfiguration().LookupUnsignedIntegerParameter(storageLoaderThreads, ORTHANC_CONFIG_STORAGE_LOADER_THREADS_COUNT))
+        {
+          const unsigned int concurrentJobs = lock.GetConfiguration().GetConcurrentJobs();
+
+          if (hasCompatibilityLoaderThreads)
+          {
+            storageLoaderThreads = concurrentJobs * compatibilityLoaderThreadsCount;
+          }
+          else
+          {
+            storageLoaderThreads = concurrentJobs;
+          }
+
+          static const unsigned int CAP_LOW = 4;
+          static const unsigned int CAP_HIGH = 50;
+
+          storageLoaderThreads = std::max(storageLoaderThreads, CAP_LOW);
+          storageLoaderThreads = std::min(storageLoaderThreads, CAP_HIGH);
+
+          LOG(WARNING) << "Performance option \"" << ORTHANC_CONFIG_STORAGE_LOADER_THREADS_COUNT
+                       << "\" is not defined in your configuration, setting it to " << storageLoaderThreads
+                       << ", based on the \"" << ORTHANC_CONFIG_CONCURRENT_JOBS << "\" "
+                       << (hasCompatibilityLoaderThreads ? "and the \"" + compatibilityLoaderThreadsOption + "\" options" : "option")
+                       << " clamped to the range [" << CAP_LOW << "," << CAP_HIGH << "]";
+        }
+        else
+        {
+          LOG(INFO) << "Performance option \"" << ORTHANC_CONFIG_STORAGE_LOADER_THREADS_COUNT << "\" is set to " << storageLoaderThreads;
+        }
+
+        transcoderThreads = lock.GetConfiguration().GetUnsignedIntegerParameter(ORTHANC_CONFIG_TRANSCODER_THREADS_COUNT);
+        LOG(INFO) << "Performance option \"" << ORTHANC_CONFIG_TRANSCODER_THREADS_COUNT << "\" is set to " << transcoderThreads;
+
+        dicomParserThreads = lock.GetConfiguration().GetUnsignedIntegerParameter(ORTHANC_CONFIG_DICOM_PARSER_SOURCE_THREADS_COUNT);
+        LOG(INFO) << "Performance option \"" << ORTHANC_CONFIG_DICOM_PARSER_SOURCE_THREADS_COUNT << "\" is set to " << dicomParserThreads;
+
+        GetMemorySizeConfiguration(storageMemoryCapacity, lock, ORTHANC_CONFIG_STORAGE_MEMORY_CAPACITY);
+        GetMemorySizeConfiguration(dicomParserMemoryCapacity, lock, ORTHANC_CONFIG_DICOM_PARSER_MEMORY_CAPACITY);
+        GetMemorySizeConfiguration(dicomParserCacheSize, lock, ORTHANC_CONFIG_DICOM_PARSER_CACHE_SIZE);
+        GetMemorySizeConfiguration(transcoderMemoryCapacity, lock, ORTHANC_CONFIG_TRANSCODER_MEMORY_CAPACITY);
+        GetMemorySizeConfiguration(transcoderCacheSize, lock, ORTHANC_CONFIG_TRANSCODER_CACHE_SIZE);
+        GetMemorySizeConfiguration(sequentialReaderWindowCapacity, lock, ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_WINDOW_CAPACITY);
+
+        if (!lock.GetConfiguration().LookupUnsignedIntegerParameter(sequentialReaderThreads, ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_THREADS_COUNT))
+        {
+          LOG(WARNING) << "Performance option \"" << ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_THREADS_COUNT
+                       << "\" is not defined in your configuration, setting it to the same value as \""
+                       << ORTHANC_CONFIG_STORAGE_LOADER_THREADS_COUNT << "\": " << storageLoaderThreads;
+          sequentialReaderThreads = storageLoaderThreads;
+        }
+        else
+        {
+          LOG(INFO) << "Performance option \"" << ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_THREADS_COUNT
+                    << "\" is set to " << sequentialReaderThreads;
+        }
+
+        if (!lock.GetConfiguration().LookupUnsignedIntegerParameter(sequentialReaderWindowSize, ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_WINDOW_SIZE))
+        {
+          if (hasCompatibilityLoaderThreads)
+          {
+            LOG(WARNING) << "Performance option \"" << ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_WINDOW_SIZE
+                         << "\" is not defined in your configuration, setting it to the same value as \""
+                         << compatibilityLoaderThreadsOption << "\": " << compatibilityLoaderThreadsCount;
+            sequentialReaderWindowSize = compatibilityLoaderThreadsCount;
+          }
+          else
+          {
+            sequentialReaderWindowSize = lock.GetConfiguration().GetUnsignedIntegerParameter(ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_WINDOW_SIZE);
+            LOG(WARNING) << "Performance option \"" << ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_WINDOW_SIZE
+                         << "\" is not defined in your configuration, setting it to " << sequentialReaderWindowSize;
+          }
+        }
+        else
+        {
+          LOG(INFO) << "Performance option \"" << 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(storageMemoryCapacity);
+        storageAreaReader_->CreateCache(storageCacheSize);
+      }
+
+      {
+        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(dicomParserMemoryCapacity);
+        dicomReader_->CreateCache(dicomParserCacheSize);
+      }
+
+      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(transcoderMemoryCapacity);
+        transcoderReader_->CreateCache(transcoderCacheSize);
+      }
+
+      {
+        std::unique_ptr<ThreadPool> pool(new ThreadPool);
+        pool->SetThreadsCount(sequentialReaderThreads);
+        pool->SetLoggingThreadName("SEQ-READER");
+        pool->SetDequeueTimeout(100);
+        pool->SetMetricsConfiguration(*metricsRegistry_, METRICS_SEQUENTIAL_READER_THREAD_POOL_AVAILABLE_THREADS);
+        pool->Start();
+
+        boost::shared_ptr<IExecutorService> executor(pool.release());
+        dicomSequentialReaderFactory_.reset(new DicomSequentialReader::Factory(
+                                              executor, storageAreaReader_, dicomReader_, transcoderReader_,
+                                              sequentialReaderWindowSize,
+                                              sequentialReaderWindowCapacity));
+      }
     }
     catch (OrthancException&)
     {
@@ -656,8 +1012,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::StoreFile(), 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 +1088,6 @@
     try
     {
       MetricsRegistry::Timer timer(GetMetricsRegistry(), "orthanc_store_dicom_duration_ms");
-      StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
 
       DicomInstanceHasher hasher(summary);
       resultPublicId = hasher.HashInstance();
@@ -809,12 +1178,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 +1186,9 @@
 
       if (!isAdoption)
       {
-        accessor.Write(dicomInfo, dicom.GetBufferData(), dicom.GetBufferSize(), FileContentType_Dicom, compression, dicomMd5, storeMD5_, &dicom);
+        StoreFile(dicomInfo, dicom.GetBufferData(), dicom.GetBufferSize(),
+                  FileContentType_Dicom, compression, dicomMd5, &dicom);
+
         attachments.push_back(dicomInfo);
       }
       else
@@ -836,7 +1201,9 @@
           (!area_.HasEfficientReadRange() ||
            compressionEnabled_))
       {
-        accessor.Write(dicomUntilPixelData, dicom.GetBufferData(), pixelDataOffset, FileContentType_DicomUntilPixelData, compression, storeMD5_, NULL);
+        StoreFile(dicomUntilPixelData, dicom.GetBufferData(), pixelDataOffset, FileContentType_DicomUntilPixelData,
+                  compression, "" /* MD5 will be computed if needed */, NULL);
+
         attachments.push_back(dicomUntilPixelData);
       }
 
@@ -860,11 +1227,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 +1250,12 @@
       {
         if (!isAdoption)
         {
-          accessor.Remove(dicomInfo);
+          RemoveFile(dicomInfo);
         }
 
         if (dicomUntilPixelData.IsValid())
         {
-          accessor.Remove(dicomUntilPixelData);
+          RemoveFile(dicomUntilPixelData);
         }
       }
 
@@ -1092,7 +1459,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 +1495,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 +1530,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 */));
+
+      StoreFile(modified, range->GetData(), range->GetSize(), attachmentType, compression,
+                "" /* MD5 will be computed if needed */, NULL);
+    }
 
     try
     {
@@ -1171,13 +1547,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 +1666,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 +1714,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 +1735,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 +1752,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 +1787,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 +1805,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 +1924,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);
+    StoreFile(attachment, data, size, attachmentType, compression, "" /* MD5 will be computed if needed */, NULL);
 
     try
     {
@@ -1686,7 +1935,7 @@
         newRevision, attachment, resourceId, hasOldRevision, oldRevision, oldMD5);
       if (status != StoreStatus_Success)
       {
-        accessor.Remove(attachment);
+        RemoveFile(attachment);
         return false;
       }
       else
@@ -1697,7 +1946,7 @@
     catch (OrthancException&)
     {
       // Fixed in Orthanc 1.9.6
-      accessor.Remove(attachment);
+      RemoveFile(attachment);
       throw;
     }
   }
@@ -1707,24 +1956,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 +2192,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 +2270,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 +2469,62 @@
   }
 
 
-  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::ExportPerformanceParameters(Json::Value& target)
+  {
+    target = Json::objectValue;
 
-  void ServerContext::ResetTranscoder()
-  {
-    if (transcoder_.get() == NULL)
+    target[ORTHANC_CONFIG_MAXIMUM_STORAGE_CACHE_SIZE] = BytesToMegabytes(storageAreaReader_->GetCacheSize());
+    target[ORTHANC_CONFIG_STORAGE_MEMORY_CAPACITY] = BytesToMegabytes(storageAreaReader_->GetCapacity());
+
+    target[ORTHANC_CONFIG_DICOM_PARSER_CACHE_SIZE] = BytesToMegabytes(dicomReader_->GetCacheSize());
+    target[ORTHANC_CONFIG_DICOM_PARSER_MEMORY_CAPACITY] = BytesToMegabytes(dicomReader_->GetCapacity());
+
+    target[ORTHANC_CONFIG_TRANSCODER_CACHE_SIZE] = BytesToMegabytes(transcoderReader_->GetCacheSize());
+    target[ORTHANC_CONFIG_TRANSCODER_MEMORY_CAPACITY] = BytesToMegabytes(transcoderReader_->GetCapacity());
+
+    {
+      boost::shared_ptr<IExecutorService> service = storageAreaReader_->GetExecutorService();
+      target[ORTHANC_CONFIG_STORAGE_LOADER_THREADS_COUNT] = dynamic_cast<ThreadPool&>(*service).GetThreadsCount();
+    }
+
     {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      boost::shared_ptr<IExecutorService> service = dicomReader_->GetExecutorService();
+      target[ORTHANC_CONFIG_DICOM_PARSER_SOURCE_THREADS_COUNT] = dynamic_cast<ThreadPool&>(*service).GetThreadsCount();
+    }
+
+    {
+      boost::shared_ptr<IExecutorService> service = transcoderReader_->GetExecutorService();
+      target[ORTHANC_CONFIG_TRANSCODER_THREADS_COUNT] = dynamic_cast<ThreadPool&>(*service).GetThreadsCount();
     }
-    else
+
     {
-      transcoder_.reset(NULL);
+      boost::shared_ptr<IExecutorService> service = dicomSequentialReaderFactory_->GetExecutorService();
+      target[ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_THREADS_COUNT] = dynamic_cast<ThreadPool&>(*service).GetThreadsCount();
+    }
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      target[ORTHANC_CONFIG_HTTP_THREADS_COUNT] = lock.GetConfiguration().GetHttpThreadsCount();
+      target[ORTHANC_CONFIG_DICOM_THREADS_COUNT] = lock.GetConfiguration().GetDicomThreadsCount();
+      target[ORTHANC_CONFIG_CONCURRENT_JOBS] = lock.GetConfiguration().GetConcurrentJobs();
     }
   }
 }
--- a/OrthancServer/Sources/ServerContext.h	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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,19 @@
                     FileContentType type,
                     const std::string& customData);
 
+    void RemoveFile(const FileInfo& attachment);
+
+    // This method corresponds to StorageAccessor::Write() in Orthanc
+    // <= 1.12.11. Don't name this method "CreateFile()", otherwise it
+    // could be renamed "CreateFileA()" by Microsoft Windows SDK macros.
+    void StoreFile(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 +300,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 +324,6 @@
       return index_;
     }
 
-    void SetMaximumStorageCacheSize(size_t size)
-    {
-      return storageCache_.SetMaximumSize(size);
-    }
-
     void SetPatientLevelEnabled(bool enabled);
 
     bool IsPatientLevelEnabled() const
@@ -397,42 +392,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 +564,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 +602,15 @@
 
     int64_t GetServerUpTime() const;
 
-    void PublishCacheMetrics();
-
-    void SetTranscoder(ServerTranscoder* transcoder /* takes ownership */);
+    /**
+     * 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;
 
-    ServerTranscoder& GetTranscoder() const;
+    DicomSequentialReader::Factory& GetDicomSequentialReaderFactory();
 
-    void ResetTranscoder();
+    void ExportPerformanceParameters(Json::Value& target);
   };
 }
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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;
   };
 
@@ -500,12 +500,12 @@
       {
       }
 
-      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())
@@ -1037,7 +1044,6 @@
     transferSyntax_(DicomTransferSyntax_LittleEndianImplicit),
     hasLossyQuality_(false),
     lossyQuality_(100),
-    loaderThreads_(1),
     allowUtf8_(false)
   {
   }
@@ -1152,19 +1158,6 @@
   }
 
 
-  void ArchiveJob::SetLoaderThreads(unsigned int loaderThreads)
-  {
-    if (writer_.get() != NULL)   // Already started
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      loaderThreads_ = std::max(1u, loaderThreads);
-    }
-  }
-
-
   void ArchiveJob::SetAllowUtf8(bool allowUtf8)
   {
     if (writer_.get() != NULL)   // Already started
@@ -1223,8 +1216,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 +1268,7 @@
       writer_.reset();
     }
 
-    if (instancesLoader_.get() != NULL)
-    {
-      instancesLoader_->Clear(false);
-    }
+    instancesLoader_.reset(NULL);
 
     if (asynchronousTarget_.get() != NULL)
     {
@@ -1336,10 +1337,7 @@
       asynchronousTarget_.reset();
 
       // clear the loader threads
-      if (instancesLoader_.get() != NULL)
-      {
-        instancesLoader_->Clear(true);
-      }
+      instancesLoader_.reset(NULL);
     }
   }
 
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.h	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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_;
@@ -72,9 +72,6 @@
     bool                 hasLossyQuality_;
     unsigned int         lossyQuality_;
 
-    // New in Orthanc 1.10.0
-    unsigned int         loaderThreads_;
-
     // New in Orthanc 1.12.11
     bool                 allowUtf8_;
 
@@ -112,8 +109,6 @@
 
     void SetLossyQuality(unsigned int lossyQuality);
 
-    void SetLoaderThreads(unsigned int loaderThreads);
-
     void SetAllowUtf8(bool allowUtf8);
 
     virtual void Reset() ORTHANC_OVERRIDE;
--- a/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/DicomInstanceOperationValue.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/ModifyInstanceOperation.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/StoreScuOperation.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/StoreJob.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/StoreJob.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 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 Aug 12 13:21:02 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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/ServerToolbox.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/ServerTranscoder.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -24,22 +24,93 @@
 #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/DicomFormat/DicomImageInformation.h"
 #include "../../OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.h"
-#include "../../OrthancFramework/Sources/DicomFormat/DicomImageInformation.h"
+#include "../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.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 +139,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 +306,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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/ServerTranscoder.h	Thu Aug 13 14:38:28 2026 +0000
@@ -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,13 +43,39 @@
   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:
     explicit ServerTranscoder(unsigned int maxConcurrentDcmtkTranscoder);
 
@@ -51,9 +83,18 @@
     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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/Sources/main.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -53,11 +53,16 @@
 #include "ServerJobs/DicomRetrieveScuBaseJob.h"
 #include "ServerJobs/StorageCommitmentScpJob.h"
 #include "ServerToolbox.h"
+#include "ServerTranscoder.h"
 #include "StorageCommitmentReports.h"
 
+#include <PatchList.h>
+
 #include <boost/algorithm/string/predicate.hpp>
 #include <boost/program_options.hpp>
 
+#include <dcmtk/dcmnet/dimse.h>  // For STATUS_STORE_Error_CannotUnderstand
+
 #if defined(_WIN32) || defined(__CYGWIN__)
 #include <windows.h>
 #endif
@@ -1087,7 +1092,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");
@@ -1360,7 +1365,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)
@@ -1582,19 +1587,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()
@@ -1611,8 +1611,6 @@
         context_.ResetPlugins();
       }
 #endif
-
-      context_.ResetTranscoder();
     }
   };
 }
@@ -1667,7 +1665,7 @@
     maxDcmtkConcurrentTranscoders = lock.GetConfiguration().GetUnsignedIntegerParameter(KEY_MAXIMUM_CONCURRENT_DCMTK_TRANSCODERS);
     if (maxDcmtkConcurrentTranscoders == 0)
     {
-      maxDcmtkConcurrentTranscoders = static_cast<unsigned int>(boost::thread::hardware_concurrency());
+      maxDcmtkConcurrentTranscoders = SystemToolbox::GetHardwareConcurrency();
     }
 
     // Configuration of DICOM TLS for Orthanc SCU (since Orthanc 1.9.0)
@@ -1688,8 +1686,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;
@@ -1775,26 +1779,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);
-    }
   }
 
   {
@@ -2009,7 +1993,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();
@@ -2044,7 +2029,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();
@@ -2145,6 +2131,8 @@
   static const char* const OPTION_VERBOSE = "verbose";
   static const char* const OPTION_TRACE = "trace";
   static const char* const OPTION_LOGS_NO_THREAD = "logs-no-thread";
+  static const char* const OPTION_LOGS_NO_CONTEXT = "logs-no-context";
+  static const char* const OPTION_LOGS_THREAD_NAMES_IN_CONTEXT = "logs-thread-names-in-context";
 
   boost::program_options::options_description allWithoutHidden;
   std::vector<std::string> orderSensitiveArguments;
@@ -2179,7 +2167,11 @@
       (OPTION_TRACE, "highest verbosity in logs (for debug)")
 
       // New in Orthanc 1.12.2
-      (OPTION_LOGS_NO_THREAD, "remove thread names from logs");
+      (OPTION_LOGS_NO_THREAD, "remove thread names from logs")
+
+      // New in Orthanc 1.13.0
+      (OPTION_LOGS_NO_CONTEXT, "remove contexts from logs")
+      (OPTION_LOGS_THREAD_NAMES_IN_CONTEXT, "include caller thread names in log contexts");
 
     boost::program_options::options_description finetuning("Fine-tuning of log categories");
 
@@ -2409,6 +2401,16 @@
     Logging::SetThreadNamesEnabled(false);
   }
 
+  if (options.count(OPTION_LOGS_NO_CONTEXT) == 1)
+  {
+    Logging::SetThreadContextsEnabled(false);
+  }
+
+  if (options.count(OPTION_LOGS_THREAD_NAMES_IN_CONTEXT) == 1)
+  {
+    Logging::SetThreadNamesInContextsEnabled(true);
+  }
+
   if (options.count(OPTION_INPUTS) != 0)
   {
     const std::vector<std::string>& inputs = options[OPTION_INPUTS].as<std::vector<std::string> >();
@@ -2444,6 +2446,11 @@
     LOG(WARNING) << "Orthanc version: " << version;
     assert(DisplayPerformanceWarning());
 
+    for (unsigned int i = 0; i < ORTHANC_PATCHES_COUNT; i++)
+    {
+      LOG(WARNING) << "This binary includes the following patch to a third-party library: " << ORTHANC_PATCHES[i];
+    }
+
     std::string s = "Architecture: ";
     if (sizeof(void*) == 4)
     {
--- a/OrthancServer/UnitTestsSources/ServerConfigTests.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/UnitTestsSources/ServerConfigTests.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -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 Aug 12 13:21:02 2026 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,203 +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 "PrecompiledHeadersUnitTests.h"
-#include <gtest/gtest.h>
-
-#include "../../OrthancFramework/Sources/Cache/ICachePageProvider.h"
-#include "../../OrthancFramework/Sources/Cache/ICacheable.h"
-#include "../../OrthancFramework/Sources/Cache/LeastRecentlyUsedIndex.h"
-#include "../../OrthancFramework/Sources/Cache/MemoryCache.h"
-#include "../../OrthancFramework/Sources/Cache/MemoryObjectCache.h"
-#include "../../OrthancFramework/Sources/Cache/MemoryStringCache.h"
-#include "../../OrthancFramework/Sources/Cache/SharedArchive.h"
-#include "../../OrthancFramework/Sources/ChunkedBuffer.h"
-#include "../../OrthancFramework/Sources/Compatibility.h"
-#include "../../OrthancFramework/Sources/Compression/DeflateBaseCompressor.h"
-#include "../../OrthancFramework/Sources/Compression/GzipCompressor.h"
-#include "../../OrthancFramework/Sources/Compression/HierarchicalZipWriter.h"
-#include "../../OrthancFramework/Sources/Compression/IBufferCompressor.h"
-#include "../../OrthancFramework/Sources/Compression/ZipReader.h"
-#include "../../OrthancFramework/Sources/Compression/ZipWriter.h"
-#include "../../OrthancFramework/Sources/Compression/ZlibCompressor.h"
-#include "../../OrthancFramework/Sources/DicomFormat/DicomArray.h"
-#include "../../OrthancFramework/Sources/DicomFormat/DicomElement.h"
-#include "../../OrthancFramework/Sources/DicomFormat/DicomImageInformation.h"
-#include "../../OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.h"
-#include "../../OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.h"
-#include "../../OrthancFramework/Sources/DicomFormat/DicomMap.h"
-#include "../../OrthancFramework/Sources/DicomFormat/DicomStreamReader.h"
-#include "../../OrthancFramework/Sources/DicomFormat/DicomTag.h"
-#include "../../OrthancFramework/Sources/DicomFormat/DicomValue.h"
-#include "../../OrthancFramework/Sources/DicomFormat/StreamBlockReader.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/DicomAssociation.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/DicomServer.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/IApplicationEntityFilter.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/IFindRequestHandler.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/IFindRequestHandlerFactory.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/IGetRequestHandler.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/IGetRequestHandlerFactory.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/IMoveRequestHandler.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/IMoveRequestHandlerFactory.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/IStorageCommitmentRequestHandler.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/IStorageCommitmentRequestHandlerFactory.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/IStoreRequestHandler.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/IStoreRequestHandlerFactory.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/IWorklistRequestHandler.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/IWorklistRequestHandlerFactory.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/Internals/FindScp.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/Internals/GetScp.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/Internals/MoveScp.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/NetworkingCompatibility.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/TimeoutDicomConnectionManager.h"
-#include "../../OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.h"
-#include "../../OrthancFramework/Sources/DicomParsing/DicomDirWriter.h"
-#include "../../OrthancFramework/Sources/DicomParsing/DicomModification.h"
-#include "../../OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h"
-#include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
-#include "../../OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h"
-#include "../../OrthancFramework/Sources/DicomParsing/ITagVisitor.h"
-#include "../../OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.h"
-#include "../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h"
-#include "../../OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.h"
-#include "../../OrthancFramework/Sources/DicomParsing/ParsedDicomCache.h"
-#include "../../OrthancFramework/Sources/DicomParsing/ParsedDicomDir.h"
-#include "../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h"
-#include "../../OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.h"
-#include "../../OrthancFramework/Sources/Endianness.h"
-#include "../../OrthancFramework/Sources/EnumerationDictionary.h"
-#include "../../OrthancFramework/Sources/Enumerations.h"
-#include "../../OrthancFramework/Sources/FileBuffer.h"
-#include "../../OrthancFramework/Sources/FileStorage/FileInfo.h"
-#include "../../OrthancFramework/Sources/FileStorage/FilesystemStorage.h"
-#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"
-#include "../../OrthancFramework/Sources/HttpServer/FilesystemHttpSender.h"
-#include "../../OrthancFramework/Sources/HttpServer/HttpContentNegociation.h"
-#include "../../OrthancFramework/Sources/HttpServer/HttpFileSender.h"
-#include "../../OrthancFramework/Sources/HttpServer/HttpOutput.h"
-#include "../../OrthancFramework/Sources/HttpServer/HttpServer.h"
-#include "../../OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.h"
-#include "../../OrthancFramework/Sources/HttpServer/HttpToolbox.h"
-#include "../../OrthancFramework/Sources/HttpServer/IHttpHandler.h"
-#include "../../OrthancFramework/Sources/HttpServer/IHttpOutputStream.h"
-#include "../../OrthancFramework/Sources/HttpServer/IHttpStreamAnswer.h"
-#include "../../OrthancFramework/Sources/HttpServer/IIncomingHttpRequestFilter.h"
-#include "../../OrthancFramework/Sources/HttpServer/IWebDavBucket.h"
-#include "../../OrthancFramework/Sources/HttpServer/MultipartStreamReader.h"
-#include "../../OrthancFramework/Sources/HttpServer/StringHttpOutput.h"
-#include "../../OrthancFramework/Sources/HttpServer/StringMatcher.h"
-#include "../../OrthancFramework/Sources/HttpServer/WebDavStorage.h"
-#include "../../OrthancFramework/Sources/IDynamicObject.h"
-#include "../../OrthancFramework/Sources/IMemoryBuffer.h"
-#include "../../OrthancFramework/Sources/Images/Font.h"
-#include "../../OrthancFramework/Sources/Images/FontRegistry.h"
-#include "../../OrthancFramework/Sources/Images/IImageWriter.h"
-#include "../../OrthancFramework/Sources/Images/Image.h"
-#include "../../OrthancFramework/Sources/Images/ImageAccessor.h"
-#include "../../OrthancFramework/Sources/Images/ImageBuffer.h"
-#include "../../OrthancFramework/Sources/Images/ImageProcessing.h"
-#include "../../OrthancFramework/Sources/Images/ImageTraits.h"
-#include "../../OrthancFramework/Sources/Images/JpegReader.h"
-#include "../../OrthancFramework/Sources/Images/JpegWriter.h"
-#include "../../OrthancFramework/Sources/Images/NumpyWriter.h"
-#include "../../OrthancFramework/Sources/Images/PamReader.h"
-#include "../../OrthancFramework/Sources/Images/PamWriter.h"
-#include "../../OrthancFramework/Sources/Images/PixelTraits.h"
-#include "../../OrthancFramework/Sources/Images/PngReader.h"
-#include "../../OrthancFramework/Sources/Images/PngWriter.h"
-#include "../../OrthancFramework/Sources/JobsEngine/GenericJobUnserializer.h"
-#include "../../OrthancFramework/Sources/JobsEngine/IJob.h"
-#include "../../OrthancFramework/Sources/JobsEngine/IJobUnserializer.h"
-#include "../../OrthancFramework/Sources/JobsEngine/JobInfo.h"
-#include "../../OrthancFramework/Sources/JobsEngine/JobStatus.h"
-#include "../../OrthancFramework/Sources/JobsEngine/JobStepResult.h"
-#include "../../OrthancFramework/Sources/JobsEngine/JobsEngine.h"
-#include "../../OrthancFramework/Sources/JobsEngine/JobsRegistry.h"
-#include "../../OrthancFramework/Sources/JobsEngine/Operations/IJobOperation.h"
-#include "../../OrthancFramework/Sources/JobsEngine/Operations/IJobOperationValue.h"
-#include "../../OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.h"
-#include "../../OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h"
-#include "../../OrthancFramework/Sources/JobsEngine/Operations/NullOperationValue.h"
-#include "../../OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h"
-#include "../../OrthancFramework/Sources/JobsEngine/Operations/StringOperationValue.h"
-#include "../../OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h"
-#include "../../OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h"
-#include "../../OrthancFramework/Sources/Logging.h"
-#include "../../OrthancFramework/Sources/Lua/LuaContext.h"
-#include "../../OrthancFramework/Sources/Lua/LuaFunctionCall.h"
-#include "../../OrthancFramework/Sources/MallocMemoryBuffer.h"
-#include "../../OrthancFramework/Sources/MetricsRegistry.h"
-#include "../../OrthancFramework/Sources/MultiThreading/IRunnableBySteps.h"
-#include "../../OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.h"
-#include "../../OrthancFramework/Sources/MultiThreading/Semaphore.h"
-#include "../../OrthancFramework/Sources/MultiThreading/SharedMessageQueue.h"
-#include "../../OrthancFramework/Sources/OrthancException.h"
-#include "../../OrthancFramework/Sources/OrthancFramework.h"
-#include "../../OrthancFramework/Sources/RestApi/RestApi.h"
-#include "../../OrthancFramework/Sources/RestApi/RestApiCall.h"
-#include "../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.h"
-#include "../../OrthancFramework/Sources/RestApi/RestApiDeleteCall.h"
-#include "../../OrthancFramework/Sources/RestApi/RestApiGetCall.h"
-#include "../../OrthancFramework/Sources/RestApi/RestApiHierarchy.h"
-#include "../../OrthancFramework/Sources/RestApi/RestApiOutput.h"
-#include "../../OrthancFramework/Sources/RestApi/RestApiPath.h"
-#include "../../OrthancFramework/Sources/RestApi/RestApiPostCall.h"
-#include "../../OrthancFramework/Sources/RestApi/RestApiPutCall.h"
-#include "../../OrthancFramework/Sources/SQLite/Connection.h"
-#include "../../OrthancFramework/Sources/SQLite/FunctionContext.h"
-#include "../../OrthancFramework/Sources/SQLite/IScalarFunction.h"
-#include "../../OrthancFramework/Sources/SQLite/ITransaction.h"
-#include "../../OrthancFramework/Sources/SQLite/NonCopyable.h"
-#include "../../OrthancFramework/Sources/SQLite/OrthancSQLiteException.h"
-#include "../../OrthancFramework/Sources/SQLite/SQLiteTypes.h"
-#include "../../OrthancFramework/Sources/SQLite/Statement.h"
-#include "../../OrthancFramework/Sources/SQLite/StatementId.h"
-#include "../../OrthancFramework/Sources/SQLite/StatementReference.h"
-#include "../../OrthancFramework/Sources/SQLite/Transaction.h"
-#include "../../OrthancFramework/Sources/SerializationToolbox.h"
-#include "../../OrthancFramework/Sources/SharedLibrary.h"
-#include "../../OrthancFramework/Sources/StringMemoryBuffer.h"
-#include "../../OrthancFramework/Sources/SystemToolbox.h"
-#include "../../OrthancFramework/Sources/TemporaryFile.h"
-#include "../../OrthancFramework/Sources/Toolbox.h"
-#include "../../OrthancFramework/Sources/WebServiceParameters.h"
-
-
-TEST(OrthancFramework, SizeOf)
-{
-#include "SizeOfTests.impl.h"
-}
--- a/OrthancServer/UnitTestsSources/SizeOfTests.impl.h	Wed Aug 12 13:21:02 2026 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,121 +0,0 @@
-  printf("sizeof(::Orthanc::BufferHttpSender) == %d\n", static_cast<int>(sizeof(::Orthanc::BufferHttpSender)));
-  printf("sizeof(::Orthanc::CStringMatcher) == %d\n", static_cast<int>(sizeof(::Orthanc::CStringMatcher)));
-  printf("sizeof(::Orthanc::ChunkedBuffer) == %d\n", static_cast<int>(sizeof(::Orthanc::ChunkedBuffer)));
-  printf("sizeof(::Orthanc::DcmtkTranscoder) == %d\n", static_cast<int>(sizeof(::Orthanc::DcmtkTranscoder)));
-  printf("sizeof(::Orthanc::DeflateBaseCompressor) == %d\n", static_cast<int>(sizeof(::Orthanc::DeflateBaseCompressor)));
-  printf("sizeof(::Orthanc::Deprecated::MemoryCache) == %d\n", static_cast<int>(sizeof(::Orthanc::Deprecated::MemoryCache)));
-  printf("sizeof(::Orthanc::DicomArray) == %d\n", static_cast<int>(sizeof(::Orthanc::DicomArray)));
-  printf("sizeof(::Orthanc::DicomAssociationParameters) == %d\n", static_cast<int>(sizeof(::Orthanc::DicomAssociationParameters)));
-  printf("sizeof(::Orthanc::DicomElement) == %d\n", static_cast<int>(sizeof(::Orthanc::DicomElement)));
-  printf("sizeof(::Orthanc::DicomFindAnswers) == %d\n", static_cast<int>(sizeof(::Orthanc::DicomFindAnswers)));
-  printf("sizeof(::Orthanc::DicomImageDecoder) == %d\n", static_cast<int>(sizeof(::Orthanc::DicomImageDecoder)));
-  printf("sizeof(::Orthanc::DicomImageInformation) == %d\n", static_cast<int>(sizeof(::Orthanc::DicomImageInformation)));
-  printf("sizeof(::Orthanc::DicomInstanceHasher) == %d\n", static_cast<int>(sizeof(::Orthanc::DicomInstanceHasher)));
-  printf("sizeof(::Orthanc::DicomMap) == %d\n", static_cast<int>(sizeof(::Orthanc::DicomMap)));
-  printf("sizeof(::Orthanc::DicomModification) == %d\n", static_cast<int>(sizeof(::Orthanc::DicomModification)));
-  printf("sizeof(::Orthanc::DicomPath) == %d\n", static_cast<int>(sizeof(::Orthanc::DicomPath)));
-  printf("sizeof(::Orthanc::DicomStoreUserConnection) == %d\n", static_cast<int>(sizeof(::Orthanc::DicomStoreUserConnection)));
-  printf("sizeof(::Orthanc::DicomStreamReader) == %d\n", static_cast<int>(sizeof(::Orthanc::DicomStreamReader)));
-  printf("sizeof(::Orthanc::DicomTag) == %d\n", static_cast<int>(sizeof(::Orthanc::DicomTag)));
-  printf("sizeof(::Orthanc::DicomValue) == %d\n", static_cast<int>(sizeof(::Orthanc::DicomValue)));
-  printf("sizeof(::Orthanc::DicomWebJsonVisitor) == %d\n", static_cast<int>(sizeof(::Orthanc::DicomWebJsonVisitor)));
-  printf("sizeof(::Orthanc::FileBuffer) == %d\n", static_cast<int>(sizeof(::Orthanc::FileBuffer)));
-  printf("sizeof(::Orthanc::FileInfo) == %d\n", static_cast<int>(sizeof(::Orthanc::FileInfo)));
-  printf("sizeof(::Orthanc::FilesystemHttpSender) == %d\n", static_cast<int>(sizeof(::Orthanc::FilesystemHttpSender)));
-  printf("sizeof(::Orthanc::FilesystemStorage) == %d\n", static_cast<int>(sizeof(::Orthanc::FilesystemStorage)));
-  printf("sizeof(::Orthanc::Font) == %d\n", static_cast<int>(sizeof(::Orthanc::Font)));
-  printf("sizeof(::Orthanc::FontRegistry) == %d\n", static_cast<int>(sizeof(::Orthanc::FontRegistry)));
-  printf("sizeof(::Orthanc::FromDcmtkBridge) == %d\n", static_cast<int>(sizeof(::Orthanc::FromDcmtkBridge)));
-  printf("sizeof(::Orthanc::FromDcmtkBridge::IDicomPathVisitor) == %d\n", static_cast<int>(sizeof(::Orthanc::FromDcmtkBridge::IDicomPathVisitor)));
-  printf("sizeof(::Orthanc::GenericJobUnserializer) == %d\n", static_cast<int>(sizeof(::Orthanc::GenericJobUnserializer)));
-  printf("sizeof(::Orthanc::GzipCompressor) == %d\n", static_cast<int>(sizeof(::Orthanc::GzipCompressor)));
-  printf("sizeof(::Orthanc::HierarchicalZipWriter) == %d\n", static_cast<int>(sizeof(::Orthanc::HierarchicalZipWriter)));
-  printf("sizeof(::Orthanc::HttpClient) == %d\n", static_cast<int>(sizeof(::Orthanc::HttpClient)));
-  printf("sizeof(::Orthanc::HttpContentNegociation) == %d\n", static_cast<int>(sizeof(::Orthanc::HttpContentNegociation)));
-  printf("sizeof(::Orthanc::HttpFileSender) == %d\n", static_cast<int>(sizeof(::Orthanc::HttpFileSender)));
-  printf("sizeof(::Orthanc::HttpOutput) == %d\n", static_cast<int>(sizeof(::Orthanc::HttpOutput)));
-  printf("sizeof(::Orthanc::HttpServer) == %d\n", static_cast<int>(sizeof(::Orthanc::HttpServer)));
-  printf("sizeof(::Orthanc::HttpStreamTranscoder) == %d\n", static_cast<int>(sizeof(::Orthanc::HttpStreamTranscoder)));
-  printf("sizeof(::Orthanc::HttpToolbox) == %d\n", static_cast<int>(sizeof(::Orthanc::HttpToolbox)));
-  printf("sizeof(::Orthanc::IBufferCompressor) == %d\n", static_cast<int>(sizeof(::Orthanc::IBufferCompressor)));
-  printf("sizeof(::Orthanc::IDicomTranscoder) == %d\n", static_cast<int>(sizeof(::Orthanc::IDicomTranscoder)));
-  printf("sizeof(::Orthanc::IDicomTranscoder::DicomImage) == %d\n", static_cast<int>(sizeof(::Orthanc::IDicomTranscoder::DicomImage)));
-  printf("sizeof(::Orthanc::IDynamicObject) == %d\n", static_cast<int>(sizeof(::Orthanc::IDynamicObject)));
-  printf("sizeof(::Orthanc::IImageWriter) == %d\n", static_cast<int>(sizeof(::Orthanc::IImageWriter)));
-  printf("sizeof(::Orthanc::IJob) == %d\n", static_cast<int>(sizeof(::Orthanc::IJob)));
-  printf("sizeof(::Orthanc::IJobOperation) == %d\n", static_cast<int>(sizeof(::Orthanc::IJobOperation)));
-  printf("sizeof(::Orthanc::IJobOperationValue) == %d\n", static_cast<int>(sizeof(::Orthanc::IJobOperationValue)));
-  printf("sizeof(::Orthanc::IJobUnserializer) == %d\n", static_cast<int>(sizeof(::Orthanc::IJobUnserializer)));
-  printf("sizeof(::Orthanc::Image) == %d\n", static_cast<int>(sizeof(::Orthanc::Image)));
-  printf("sizeof(::Orthanc::ImageAccessor) == %d\n", static_cast<int>(sizeof(::Orthanc::ImageAccessor)));
-  printf("sizeof(::Orthanc::ImageBuffer) == %d\n", static_cast<int>(sizeof(::Orthanc::ImageBuffer)));
-  printf("sizeof(::Orthanc::ImageProcessing) == %d\n", static_cast<int>(sizeof(::Orthanc::ImageProcessing)));
-  printf("sizeof(::Orthanc::ImageProcessing::IPolygonFiller) == %d\n", static_cast<int>(sizeof(::Orthanc::ImageProcessing::IPolygonFiller)));
-  printf("sizeof(::Orthanc::ImageProcessing::ImagePoint) == %d\n", static_cast<int>(sizeof(::Orthanc::ImageProcessing::ImagePoint)));
-  printf("sizeof(::Orthanc::JobInfo) == %d\n", static_cast<int>(sizeof(::Orthanc::JobInfo)));
-  printf("sizeof(::Orthanc::JobOperationValues) == %d\n", static_cast<int>(sizeof(::Orthanc::JobOperationValues)));
-  printf("sizeof(::Orthanc::JobStepResult) == %d\n", static_cast<int>(sizeof(::Orthanc::JobStepResult)));
-  printf("sizeof(::Orthanc::JobsEngine) == %d\n", static_cast<int>(sizeof(::Orthanc::JobsEngine)));
-  printf("sizeof(::Orthanc::JobsRegistry) == %d\n", static_cast<int>(sizeof(::Orthanc::JobsRegistry)));
-  printf("sizeof(::Orthanc::JobsRegistry::IObserver) == %d\n", static_cast<int>(sizeof(::Orthanc::JobsRegistry::IObserver)));
-  printf("sizeof(::Orthanc::JobsRegistry::RunningJob) == %d\n", static_cast<int>(sizeof(::Orthanc::JobsRegistry::RunningJob)));
-  printf("sizeof(::Orthanc::JpegReader) == %d\n", static_cast<int>(sizeof(::Orthanc::JpegReader)));
-  printf("sizeof(::Orthanc::JpegWriter) == %d\n", static_cast<int>(sizeof(::Orthanc::JpegWriter)));
-  printf("sizeof(::Orthanc::LogJobOperation) == %d\n", static_cast<int>(sizeof(::Orthanc::LogJobOperation)));
-  printf("sizeof(::Orthanc::Logging::InternalLogger) == %d\n", static_cast<int>(sizeof(::Orthanc::Logging::InternalLogger)));
-  printf("sizeof(::Orthanc::LuaContext) == %d\n", static_cast<int>(sizeof(::Orthanc::LuaContext)));
-  printf("sizeof(::Orthanc::LuaFunctionCall) == %d\n", static_cast<int>(sizeof(::Orthanc::LuaFunctionCall)));
-  printf("sizeof(::Orthanc::MemoryObjectCache) == %d\n", static_cast<int>(sizeof(::Orthanc::MemoryObjectCache)));
-  printf("sizeof(::Orthanc::MemoryStringCache) == %d\n", static_cast<int>(sizeof(::Orthanc::MemoryStringCache)));
-  printf("sizeof(::Orthanc::MetricsRegistry) == %d\n", static_cast<int>(sizeof(::Orthanc::MetricsRegistry)));
-  printf("sizeof(::Orthanc::MetricsRegistry::ActiveCounter) == %d\n", static_cast<int>(sizeof(::Orthanc::MetricsRegistry::ActiveCounter)));
-  printf("sizeof(::Orthanc::MetricsRegistry::SharedMetrics) == %d\n", static_cast<int>(sizeof(::Orthanc::MetricsRegistry::SharedMetrics)));
-  printf("sizeof(::Orthanc::MetricsRegistry::Timer) == %d\n", static_cast<int>(sizeof(::Orthanc::MetricsRegistry::Timer)));
-  printf("sizeof(::Orthanc::MultipartStreamReader) == %d\n", static_cast<int>(sizeof(::Orthanc::MultipartStreamReader)));
-  printf("sizeof(::Orthanc::NullOperationValue) == %d\n", static_cast<int>(sizeof(::Orthanc::NullOperationValue)));
-  printf("sizeof(::Orthanc::NumpyWriter) == %d\n", static_cast<int>(sizeof(::Orthanc::NumpyWriter)));
-  printf("sizeof(::Orthanc::OrthancException) == %d\n", static_cast<int>(sizeof(::Orthanc::OrthancException)));
-  printf("sizeof(::Orthanc::PamReader) == %d\n", static_cast<int>(sizeof(::Orthanc::PamReader)));
-  printf("sizeof(::Orthanc::PamWriter) == %d\n", static_cast<int>(sizeof(::Orthanc::PamWriter)));
-  printf("sizeof(::Orthanc::ParsedDicomCache) == %d\n", static_cast<int>(sizeof(::Orthanc::ParsedDicomCache)));
-  printf("sizeof(::Orthanc::ParsedDicomCache::Accessor) == %d\n", static_cast<int>(sizeof(::Orthanc::ParsedDicomCache::Accessor)));
-  printf("sizeof(::Orthanc::ParsedDicomDir) == %d\n", static_cast<int>(sizeof(::Orthanc::ParsedDicomDir)));
-  printf("sizeof(::Orthanc::ParsedDicomFile) == %d\n", static_cast<int>(sizeof(::Orthanc::ParsedDicomFile)));
-  printf("sizeof(::Orthanc::PngReader) == %d\n", static_cast<int>(sizeof(::Orthanc::PngReader)));
-  printf("sizeof(::Orthanc::PngWriter) == %d\n", static_cast<int>(sizeof(::Orthanc::PngWriter)));
-  printf("sizeof(::Orthanc::RemoteModalityParameters) == %d\n", static_cast<int>(sizeof(::Orthanc::RemoteModalityParameters)));
-  printf("sizeof(::Orthanc::RestApiHierarchy) == %d\n", static_cast<int>(sizeof(::Orthanc::RestApiHierarchy)));
-  printf("sizeof(::Orthanc::RestApiHierarchy::Resource) == %d\n", static_cast<int>(sizeof(::Orthanc::RestApiHierarchy::Resource)));
-  printf("sizeof(::Orthanc::RestApiPath) == %d\n", static_cast<int>(sizeof(::Orthanc::RestApiPath)));
-  printf("sizeof(::Orthanc::SQLite::Connection) == %d\n", static_cast<int>(sizeof(::Orthanc::SQLite::Connection)));
-  printf("sizeof(::Orthanc::SQLite::FunctionContext) == %d\n", static_cast<int>(sizeof(::Orthanc::SQLite::FunctionContext)));
-  printf("sizeof(::Orthanc::SQLite::Statement) == %d\n", static_cast<int>(sizeof(::Orthanc::SQLite::Statement)));
-  printf("sizeof(::Orthanc::SQLite::StatementId) == %d\n", static_cast<int>(sizeof(::Orthanc::SQLite::StatementId)));
-  printf("sizeof(::Orthanc::SQLite::StatementReference) == %d\n", static_cast<int>(sizeof(::Orthanc::SQLite::StatementReference)));
-  printf("sizeof(::Orthanc::SQLite::Transaction) == %d\n", static_cast<int>(sizeof(::Orthanc::SQLite::Transaction)));
-  printf("sizeof(::Orthanc::Semaphore) == %d\n", static_cast<int>(sizeof(::Orthanc::Semaphore)));
-  printf("sizeof(::Orthanc::SequenceOfOperationsJob) == %d\n", static_cast<int>(sizeof(::Orthanc::SequenceOfOperationsJob)));
-  printf("sizeof(::Orthanc::SequenceOfOperationsJob::IObserver) == %d\n", static_cast<int>(sizeof(::Orthanc::SequenceOfOperationsJob::IObserver)));
-  printf("sizeof(::Orthanc::SequenceOfOperationsJob::Lock) == %d\n", static_cast<int>(sizeof(::Orthanc::SequenceOfOperationsJob::Lock)));
-  printf("sizeof(::Orthanc::SerializationToolbox) == %d\n", static_cast<int>(sizeof(::Orthanc::SerializationToolbox)));
-  printf("sizeof(::Orthanc::SetOfCommandsJob) == %d\n", static_cast<int>(sizeof(::Orthanc::SetOfCommandsJob)));
-  printf("sizeof(::Orthanc::SetOfInstancesJob) == %d\n", static_cast<int>(sizeof(::Orthanc::SetOfInstancesJob)));
-  printf("sizeof(::Orthanc::SharedArchive) == %d\n", static_cast<int>(sizeof(::Orthanc::SharedArchive)));
-  printf("sizeof(::Orthanc::SharedArchive::Accessor) == %d\n", static_cast<int>(sizeof(::Orthanc::SharedArchive::Accessor)));
-  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)));
-  printf("sizeof(::Orthanc::SystemToolbox) == %d\n", static_cast<int>(sizeof(::Orthanc::SystemToolbox)));
-  printf("sizeof(::Orthanc::TemporaryFile) == %d\n", static_cast<int>(sizeof(::Orthanc::TemporaryFile)));
-  printf("sizeof(::Orthanc::Toolbox) == %d\n", static_cast<int>(sizeof(::Orthanc::Toolbox)));
-  printf("sizeof(::Orthanc::Toolbox::LinesIterator) == %d\n", static_cast<int>(sizeof(::Orthanc::Toolbox::LinesIterator)));
-  printf("sizeof(::Orthanc::WebServiceParameters) == %d\n", static_cast<int>(sizeof(::Orthanc::WebServiceParameters)));
-  printf("sizeof(::Orthanc::ZipReader) == %d\n", static_cast<int>(sizeof(::Orthanc::ZipReader)));
-  printf("sizeof(::Orthanc::ZipWriter) == %d\n", static_cast<int>(sizeof(::Orthanc::ZipWriter)));
-  printf("sizeof(::Orthanc::ZipWriter::IOutputStream) == %d\n", static_cast<int>(sizeof(::Orthanc::ZipWriter::IOutputStream)));
-  printf("sizeof(::Orthanc::ZipWriter::MemoryStream) == %d\n", static_cast<int>(sizeof(::Orthanc::ZipWriter::MemoryStream)));
-  printf("sizeof(::Orthanc::ZlibCompressor) == %d\n", static_cast<int>(sizeof(::Orthanc::ZlibCompressor)));
--- a/OrthancServer/UnitTestsSources/UnitTestsMain.cpp	Wed Aug 12 13:21:02 2026 +0200
+++ b/OrthancServer/UnitTestsSources/UnitTestsMain.cpp	Thu Aug 13 14:38:28 2026 +0000
@@ -544,6 +544,7 @@
 #endif
 
   Logging::Initialize();
+  Logging::SetCurrentThreadName("MAIN");
   SetGlobalVerbosity(Verbosity_Verbose);
   Toolbox::DetectEndianness();
   SystemToolbox::MakeDirectory(SystemToolbox::PathFromUtf8("UnitTestsResults"));
--- a/TODO	Wed Aug 12 13:21:02 2026 +0200
+++ b/TODO	Thu Aug 13 14:38:28 2026 +0000
@@ -71,11 +71,21 @@
     "admin": {
       "Password": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", 
       "Hashing": "sha1"} }
-* Have a single instance of "ThreadedInstancesLoader" shared by all the jobs
-  in the Orthanc server to prevent resource exhaustion.
 * Unable to cancel an ArchiveJob when Orthanc does not have access to the storage anymore.
   (To reproduce: remove the cable while downloading a study from S3)
 
+* TODO-Streaming: This guideline was removed from
+  "AdvancedConfiguration.json" in the description of option
+  "SequentialDicomReaderWindowSize", as it might be incorrect in some
+  situations:
+
+  // This is kind of equivalent to the older "LoaderThreads" configuration
+  // but, furthermore, you must make sure that there are enough "StorageLoaderThreadsCount"
+  // and "SequentialDicomReaderThreadsCount" for each job ->
+  // "StorageLoaderThreadsCount" should be larger than "ConcurrentJobs" * "SequentialDicomReaderWindowSize"
+  // Together with "SequentialDicomReaderWindowCapacity", this also defines the amount of memory that is
+  // used by each job that required a SequentialDicomReader.
+
 
 ============================
 Documentation (Orthanc Book)
@@ -104,6 +114,8 @@
 * Discuss HL7 in a dedicated page:
   https://groups.google.com/d/msg/orthanc-users/4dt4992O0lQ/opTjTFU2BgAJ
   https://groups.google.com/g/orthanc-users/c/Spjtcj9vSPo/m/ktUArWxUDQAJ
+* TODO-Streaming: add a page in the book about performance (fine
+  tuning limiting memory + increasing the number of threads)
 
 
 ================