Mercurial > hg > orthanc
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) ================
