# HG changeset patch # User Sebastien Jodogne # Date 1786521289 -7200 # Node ID 126b525257e52344397b23fc5dc8dcddc7432066 # Parent 45205d0bda11a430d65f64a3633efb7d90ba4d68# Parent 2bcbb14bd94d4fd8df1a9c0b85be451768684423 integration mainline->streaming diff -r 2bcbb14bd94d -r 126b525257e5 .hgignore --- a/.hgignore Wed Aug 12 09:51:40 2026 +0200 +++ b/.hgignore Wed Aug 12 09:54:49 2026 +0200 @@ -2,8 +2,11 @@ ThirdPartyDownloads/ CMakeLists.txt.user *.cpp.orig +*.cpp.rej *.h.orig +*.h.rej *.patch.orig +*.patch.rej .vs/ .vscode/ *~ diff -r 2bcbb14bd94d -r 126b525257e5 NEWS --- a/NEWS Wed Aug 12 09:51:40 2026 +0200 +++ b/NEWS Wed Aug 12 09:54:49 2026 +0200 @@ -4,6 +4,22 @@ General ------- +* Full rewrite of access to storage area and of DICOM parsing/transcoding + to improve caching and to limit memory usage. + This change brings updated configurations (TODO: review): + - "StorageLoaderThreads" + - "StorageMemoryCapacity" + - "DicomParserThreads" + - "DicomParserMemoryCapacity" + - "DicomParserCacheSize" + - "TranscoderThreads" + - "TranscoderMemoryCapacity" + - "TranscoderCacheSize" + - "SequentialDicomReaderThreads" + - "SequentialDicomReaderWindowSize" + - "SequentialDicomReaderWindowCapacity" + And new metrics: + - TODO list * 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" @@ -23,12 +39,18 @@ - StoreKeyValue(storeId, key, value) - value = GetKeyValue(storeId, key) - DeleteKeyValue(storeId, key) -* C-Find SCP: - - When Orthanc was requested a sequence in a C-Find query, Orthanc was actually not returning +* Logs: + - Some line of log now contain additional contextual informations like the job id they relate to. + A new "--logs-no-context" command line option can be used to get back to the previous behavior to + keep backward compatibility and reduce the lines length. + Another "--logs-thread-names-in-context" command line option can be used to add more information in the + contexts wrt the thread that has requested a service. This is mainly for developers. +* C-Find SCP: + - When Orthanc was requested a sequence in a C-Find query, Orthanc was actually not returning any sequences in the C-Find answer if the matched resource did contain the requested sequence and was returning an empty sequence if the matched resource did not contain the requested sequence. Orthanc now returns the full sequence content when the matched resource contains the requested sequence. - - When Orthanc was requested to return a private tag in a C-Find query, Orthanc was actually not returning + - When Orthanc was requested to return a private tag in a C-Find query, Orthanc was actually not returning the resources that did not contain the private tag although it was not used as a filter in the query. * Fix mismatches between the configuration default values and the documentation: - "HttpTimeout" was documented as 60 while its default value is 0 (no timeout). @@ -40,7 +62,9 @@ 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) @@ -54,10 +78,10 @@ SeriesNumber at the series level and now using " - " instead of " " to separate DICOM tags used in folder names. * In "/tools/metrics-prometheus": - - Removed "orthanc_jobs_completed", "orthanc_jobs_success" and "orthanc_jobs_failed" that were + - Removed "orthanc_jobs_completed", "orthanc_jobs_success" and "orthanc_jobs_failed" that were only considering the jobs currently stored in the JobsHistory and that were not suitable for - monitoring. These metrics have been replaced by "orthanc_jobs_total_completed", - "orthanc_jobs_total_success" and "orthanc_jobs_total_failed" that are now considering all the + monitoring. These metrics have been replaced by "orthanc_jobs_total_completed", + "orthanc_jobs_total_success" and "orthanc_jobs_total_failed" that are now considering all the jobs that have run since Orthanc started. * The "/tools/bulk-delete" route now also accepts a payload of this type: {"Resources": [{"Level": "Study", "ID": "..."}]}. This payload reduces the number diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake --- a/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake Wed Aug 12 09:54:49 2026 +0200 @@ -404,7 +404,6 @@ ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Compression/HierarchicalZipWriter.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Compression/ZipWriter.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/StorageAccessor.cpp - ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/StorageCache.cpp ) endif() endif() @@ -602,7 +601,6 @@ add_definitions(-DORTHANC_ENABLE_DCMTK_TRANSCODING=1) list(APPEND ORTHANC_DICOM_SOURCES_INTERNAL ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/DcmtkTranscoder.cpp - ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/IDicomTranscoder.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/Internals/SopInstanceUidFixer.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/MemoryBufferTranscoder.cpp ) @@ -657,10 +655,12 @@ list(APPEND ORTHANC_CORE_SOURCES_INTERNAL ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Cache/MemoryStringCache.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Cache/SharedArchive.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DataSource/BaseDataIdentifier.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DataSource/DataSourceAnswer.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DataSource/DataSourceMemoryBudget.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DataSource/DataSourceReader.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DataSource/DataSourceRequest.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DataSource/DataSourceSequentialReader.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DataSource/StorageAreaDataSource.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileBuffer.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/FilesystemStorage.cpp @@ -681,6 +681,9 @@ if (ENABLE_DCMTK) list(APPEND ORTHANC_CORE_SOURCES_INTERNAL ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DataSource/DicomDataSource.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DataSource/DicomSequentialReader.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DataSource/TranscoderDataSource.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/IDicomTranscoder.cpp ) endif() diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake --- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake Wed Aug 12 09:54:49 2026 +0200 @@ -39,7 +39,7 @@ # Version of the Orthanc API, can be retrieved from "/system" URI in # order to check whether new URI endpoints are available even if using # the mainline version of Orthanc -set(ORTHANC_API_VERSION "33") +set(ORTHANC_API_VERSION "34") ##################################################################### diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/Cache/ICachePageProvider.h --- a/OrthancFramework/Sources/Cache/ICachePageProvider.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/Cache/ICachePageProvider.h Wed Aug 12 09:54:49 2026 +0200 @@ -24,14 +24,16 @@ #pragma once +#include "../Compatibility.h" +#include "../IDynamicObject.h" + #include -#include "../IDynamicObject.h" namespace Orthanc { namespace Deprecated { - class ICachePageProvider + class ORTHANC_DEPRECATED_CLASS(ICachePageProvider) { public: virtual ~ICachePageProvider() diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/Cache/ICacheable.h --- a/OrthancFramework/Sources/Cache/ICacheable.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/Cache/ICacheable.h Wed Aug 12 09:54:49 2026 +0200 @@ -24,11 +24,13 @@ #pragma once +#include "../Compatibility.h" + #include namespace Orthanc { - class ICacheable : public boost::noncopyable + class ORTHANC_DEPRECATED_CLASS(ICacheable) : public boost::noncopyable { public: virtual ~ICacheable() diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/Cache/MemoryCache.cpp --- a/OrthancFramework/Sources/Cache/MemoryCache.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/Cache/MemoryCache.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -22,11 +22,16 @@ **/ +#if defined(__GNUC__) || defined(__clang__) +# pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + #include "../PrecompiledHeaders.h" #include "MemoryCache.h" #include "../Logging.h" + namespace Orthanc { namespace Deprecated diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/Cache/MemoryCache.h --- a/OrthancFramework/Sources/Cache/MemoryCache.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/Cache/MemoryCache.h Wed Aug 12 09:54:49 2026 +0200 @@ -37,7 +37,7 @@ /** * WARNING: This class is NOT thread-safe. **/ - class ORTHANC_PUBLIC MemoryCache : public boost::noncopyable + class ORTHANC_PUBLIC ORTHANC_DEPRECATED_CLASS(MemoryCache) : public boost::noncopyable { private: struct Page diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/Cache/MemoryObjectCache.cpp --- a/OrthancFramework/Sources/Cache/MemoryObjectCache.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/Cache/MemoryObjectCache.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -22,6 +22,10 @@ **/ +#if defined(__GNUC__) || defined(__clang__) +# pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + #include "../PrecompiledHeaders.h" #include "MemoryObjectCache.h" diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/Cache/MemoryObjectCache.h --- a/OrthancFramework/Sources/Cache/MemoryObjectCache.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/Cache/MemoryObjectCache.h Wed Aug 12 09:54:49 2026 +0200 @@ -42,7 +42,7 @@ /** * Note: this class is thread safe **/ - class ORTHANC_PUBLIC MemoryObjectCache : public boost::noncopyable + class ORTHANC_PUBLIC ORTHANC_DEPRECATED_CLASS(MemoryObjectCache) : public boost::noncopyable { private: class Item; @@ -82,7 +82,7 @@ void Invalidate(const std::string& key); - class Accessor : public boost::noncopyable + class ORTHANC_PUBLIC Accessor : public boost::noncopyable { private: #if !defined(__EMSCRIPTEN__) diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/Cache/MemoryStringCache.h --- a/OrthancFramework/Sources/Cache/MemoryStringCache.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/Cache/MemoryStringCache.h Wed Aug 12 09:54:49 2026 +0200 @@ -46,7 +46,7 @@ * * Note: this class is thread safe **/ - class ORTHANC_PUBLIC MemoryStringCache : public boost::noncopyable + class ORTHANC_PUBLIC ORTHANC_DEPRECATED_CLASS(MemoryStringCache) : public boost::noncopyable { public: class ORTHANC_PUBLIC Accessor : public boost::noncopyable diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/Cache/SharedObjectCache.cpp --- a/OrthancFramework/Sources/Cache/SharedObjectCache.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/Cache/SharedObjectCache.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -189,4 +189,13 @@ assert(!lru_.Contains(id)); } + + + void SharedObjectCache::GetStatistics(size_t& currentCount, + size_t& currentSize) + { + Mutex::ScopedLock lock(mutex_); + currentCount = lru_.GetSize(); + currentSize = currentSize_; + } } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/Cache/SharedObjectCache.h --- a/OrthancFramework/Sources/Cache/SharedObjectCache.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/Cache/SharedObjectCache.h Wed Aug 12 09:54:49 2026 +0200 @@ -43,7 +43,7 @@ Mutex mutex_; LeastRecentlyUsedIndex lru_; Content content_; - size_t capacity_; + const size_t capacity_; size_t currentSize_; void MakeRoom(size_t newObjectSize); @@ -69,5 +69,13 @@ size_t size); void Invalidate(const std::string& id); + + size_t GetCapacity() const + { + return capacity_; // This is a constant value, it can be read without the mutex + } + + void GetStatistics(size_t& currentCount, + size_t& currentSize); }; } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/Compatibility.h --- a/OrthancFramework/Sources/Compatibility.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/Compatibility.h Wed Aug 12 09:54:49 2026 +0200 @@ -47,6 +47,18 @@ #endif +// Macro "ORTHANC_DEPRECATED_CLASS" tags a class as having been deprecated +#if (__cplusplus >= 201402L) // C++14 +# define ORTHANC_DEPRECATED_CLASS(f) [[deprecated]] f +#elif defined(__GNUC__) || defined(__clang__) +# define ORTHANC_DEPRECATED_CLASS(f) __attribute__((deprecated)) f +#elif defined(_MSC_VER) +# define ORTHANC_DEPRECATED_CLASS(f) __declspec(deprecated) f +#else +# define ORTHANC_DEPRECATED +#endif + + // Macros "ORTHANC_OVERRIDE" and "ORTHANC_FINAL" wrap the "override" // and "final" keywords introduced in C++11, to do compile-time // checking of virtual methods diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/Constants.h --- a/OrthancFramework/Sources/Constants.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/Constants.h Wed Aug 12 09:54:49 2026 +0200 @@ -33,6 +33,16 @@ static const uint64_t MEGABYTE = 1024ull * 1024ull; static const uint64_t GIGABYTE = 1024ull * 1024ull * 1024ull; + inline float BytesToFloatMegabytes(uint64_t size) + { + return static_cast(size) / static_cast(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 diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DataSource/BaseDataIdentifier.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/DataSource/BaseDataIdentifier.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -0,0 +1,78 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "BaseDataIdentifier.h" + +#include "../OrthancException.h" + + +namespace Orthanc +{ + void BaseDataIdentifier::SetUserData(IDynamicObject* userData) + { + std::unique_ptr 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); + } + } +} diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DataSource/BaseDataIdentifier.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/DataSource/BaseDataIdentifier.h Wed Aug 12 09:54:49 2026 +0200 @@ -0,0 +1,47 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "IDataIdentifier.h" +#include "../Compatibility.h" + + +namespace Orthanc +{ + class ORTHANC_PUBLIC BaseDataIdentifier : public IDataIdentifier + { + private: + std::unique_ptr userData_; + + public: + void SetUserData(IDynamicObject* userData); + + virtual bool HasUserData() const ORTHANC_OVERRIDE; + + virtual const IDynamicObject& GetUserData() const ORTHANC_OVERRIDE; + + virtual IDynamicObject* ReleaseUserData() ORTHANC_OVERRIDE; + }; +} diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DataSource/DataSourceAnswer.cpp --- a/OrthancFramework/Sources/DataSource/DataSourceAnswer.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/DataSource/DataSourceAnswer.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -75,10 +75,17 @@ const IDataIdentifier& DataSourceAnswer::Item::GetId() const { - assert(id_.get() != NULL); - return *id_; + if (id_.get() == NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return *id_; + } } + const boost::shared_ptr& DataSourceAnswer::Item::GetValue() const { if (error_) diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DataSource/DataSourceAnswer.h --- a/OrthancFramework/Sources/DataSource/DataSourceAnswer.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/DataSource/DataSourceAnswer.h Wed Aug 12 09:54:49 2026 +0200 @@ -75,6 +75,16 @@ const IDataIdentifier& GetId() const; const boost::shared_ptr& GetValue() const; + + bool HasUserData() const + { + return id_->HasUserData(); + } + + IDynamicObject* ReleaseUserData() + { + return id_->ReleaseUserData(); + } }; private: diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DataSource/DataSourceMemoryBudget.cpp --- a/OrthancFramework/Sources/DataSource/DataSourceMemoryBudget.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/DataSource/DataSourceMemoryBudget.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -25,11 +25,65 @@ #include "../PrecompiledHeaders.h" #include "DataSourceMemoryBudget.h" +#include "../Constants.h" +#include "../MetricsRegistry.h" +#include "../OrthancException.h" + +#include namespace Orthanc { namespace Internals { + DataSourceMemoryBudget::MetricsConfiguration::MetricsConfiguration(const boost::shared_ptr& 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_; + } } } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DataSource/DataSourceMemoryBudget.h --- a/OrthancFramework/Sources/DataSource/DataSourceMemoryBudget.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/DataSource/DataSourceMemoryBudget.h Wed Aug 12 09:54:49 2026 +0200 @@ -29,6 +29,7 @@ #endif #include "../Compatibility.h" +#include "../OrthancFramework.h" #include #include @@ -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 metrics_; + std::string capacityMaxSizeMegabytesName_; + std::string capacityCurrentSizeMegabytesName_; + std::string capacityCountName_; + std::string capacityMaxUsageSinceStartMegabytesName_; + uint64_t maxMemoryUsageSinceStart_; + + public: + MetricsConfiguration() + { + } + + MetricsConfiguration(const boost::shared_ptr& 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); }; } } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DataSource/DataSourceReader.cpp --- a/OrthancFramework/Sources/DataSource/DataSourceReader.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/DataSource/DataSourceReader.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -26,6 +26,8 @@ #include "DataSourceReader.h" #include "../Cache/SharedObjectCache.h" +#include "../Constants.h" +#include "../MetricsRegistry.h" #include "../OrthancException.h" #include "DataSourceMemoryBudget.h" @@ -36,6 +38,86 @@ namespace Orthanc { + DataSourceReader::MetricsConfiguration::MetricsConfiguration(const boost::shared_ptr& 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 id_; boost::shared_ptr cache_; boost::shared_ptr budget_; + MetricsConfiguration metricsConfiguration_; public: - DataSourceRunnable(boost::shared_ptr& answer, + DataSourceRunnable(const boost::shared_ptr& answer, IDataSource& source, IDataIdentifier* id, boost::shared_ptr& cache, - boost::shared_ptr& budget) : + boost::shared_ptr& 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 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 value; std::unique_ptr 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 lock = answer_.lock(); @@ -156,13 +261,13 @@ }; - DataSourceReader::DataSourceReader(IExecutorService* executor /* takes ownership */, + DataSourceReader::DataSourceReader(const boost::shared_ptr& executor /* takes ownership */, IDataSource* source /* takes ownership */) : executor_(executor), source_(source), budget_(new Internals::DataSourceMemoryBudget(0)) { - if (executor == NULL || + if (executor.get() == NULL || source == NULL) { throw OrthancException(ErrorCode_NullPointer); @@ -184,10 +289,38 @@ cache_ = boost::make_shared(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(maximumMemory); + budget_->SetMetricsConfiguration(Internals::DataSourceMemoryBudget::MetricsConfiguration(metricsConfiguration_.metrics_, + metricsConfiguration_.capacityMaxSizeMegabytesName_, + metricsConfiguration_.capacityCurrentSizeMegabytesName_, + metricsConfiguration_.capacityCountName_, + metricsConfiguration_.capacityMaxUsageSinceStartMegabytesName_)); + } + + uint64_t DataSourceReader::GetCapacity() const + { + uint64_t maximumMemory, currentMemory; + unsigned int currentReservations; + + GetStatistics(maximumMemory, currentMemory, currentReservations); + + return maximumMemory; } @@ -200,14 +333,14 @@ while (!protection->IsEmpty()) { std::unique_ptr 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 DataSourceReader::ReadSingle(IDataIdentifier* id) + DataSourceAnswer::Item* DataSourceReader::ReadSingle(IDataIdentifier* id /* takes ownership */) { std::unique_ptr protection(id); @@ -217,8 +350,52 @@ boost::shared_ptr answer(Submit(request.release())); std::unique_ptr 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 budgetCopy(budget_); // increment shared_ptr ref count to make sure the budget is not deleted while we use it. + budgetCopy->GetStatistics(tasksMaximumMemory, tasksCurrentMemory, tasksReservations); + } + + + size_t DataSourceReader::GetCacheCapacity() const + { + boost::shared_ptr lock(cache_); + + if (lock) + { + return lock->GetCapacity(); + } + else + { + return 0; + } + } + + + void DataSourceReader::StoreIntoCache(const std::string& key, + IDynamicObject* value /* takes ownership */) + { + boost::shared_ptr 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_); + } } } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DataSource/DataSourceReader.h --- a/OrthancFramework/Sources/DataSource/DataSourceReader.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/DataSource/DataSourceReader.h Wed Aug 12 09:54:49 2026 +0200 @@ -34,6 +34,7 @@ namespace Orthanc { + class MetricsRegistry; class SharedObjectCache; namespace Internals @@ -43,36 +44,101 @@ class ORTHANC_PUBLIC DataSourceReader : public boost::noncopyable { + public: + class MetricsConfiguration + { + protected: + boost::shared_ptr 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& 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 executor_; + boost::shared_ptr executor_; std::unique_ptr source_; + size_t cacheSize_; boost::shared_ptr cache_; boost::shared_ptr budget_; + MetricsConfiguration metricsConfiguration_; public: - DataSourceReader(IExecutorService* executor /* takes ownership */, + DataSourceReader(const boost::shared_ptr& executor, IDataSource* source /* takes ownership */); ~DataSourceReader(); - IDataSource& GetSource() const + const boost::shared_ptr& GetExecutorService() const { - return *source_; + return executor_; } + void SetMetricsConfiguration(const MetricsConfiguration& configuration); + void CreateCache(size_t capacity); - void SetMaximumMemory(uint64_t maximumMemory); + /** + * Apply backpressure by limiting memory for pending read + * tasks. Further reads block until memory is released. + **/ + void SetCapacity(uint64_t maximumMemory); + + uint64_t GetCapacity() const; + /** + * Request the data source to load a set of items. The values will + * be read in parallel by a thread pool (cf. "executor_") and will + * be answered in no specific order through a message queue + * (cf. class "DataSourceAnswer"). The user data stored in + * "IDataIdentifier" can be used to identify items. If order is + * important, use the class "DataSourceSequentialReader" instead. + **/ boost::shared_ptr Submit(DataSourceRequest* request /* takes ownership */); - boost::shared_ptr ReadSingle(IDataIdentifier* id /* takes ownership */); + DataSourceAnswer::Item* ReadSingle(IDataIdentifier* id /* takes ownership */); void Stop() { executor_->Stop(); } + + void GetStatistics(uint64_t& tasksMaximumMemory, + uint64_t& tasksCurrentMemory, + unsigned int& tasksReservations) const; + + size_t GetCacheCapacity() const; + + void StoreIntoCache(const std::string& key, + IDynamicObject* value /* takes ownership */); }; } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DataSource/DataSourceSequentialReader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/DataSource/DataSourceSequentialReader.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -0,0 +1,336 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#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 reader_; + boost::shared_ptr disconnector_; + std::unique_ptr request_; + size_t estimatedSize_; + + public: + Callable(const boost::shared_ptr& reader, + const boost::shared_ptr& 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 answer(reader_->ReadSingle(request_.release())); + + std::unique_ptr userData; + + if (answer->HasUserData()) + { + userData.reset(answer->ReleaseUserData()); + } + + std::unique_ptr item; + + try + { + std::unique_ptr 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 front(pendingRequests_.front()); + pendingRequests_.pop_front(); + + runningSize_ += estimatedSize; + + std::unique_ptr callable(new Callable(reader_, disconnector_, front.release(), estimatedSize)); + runningRequests_.push_back(executor_->Submit(callable.release())); + } + } + + + DataSourceSequentialReader::DataSourceSequentialReader(const boost::shared_ptr& executor, + const boost::shared_ptr& 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 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(runningRequests_.front()); + runningRequests_.pop_front(); + + std::unique_ptr item(dynamic_cast(future->ReleaseResult())); + runningSize_ -= item->GetEstimatedSize(); + + FillWindow(); + + return item.release(); + } + } +} diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DataSource/DataSourceSequentialReader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/DataSource/DataSourceSequentialReader.h Wed Aug 12 09:54:49 2026 +0200 @@ -0,0 +1,143 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#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 value_; + std::unique_ptr error_; + std::unique_ptr 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 PendingRequests; + typedef std::list RunningRequests; + + boost::shared_ptr executor_; + boost::shared_ptr reader_; + boost::shared_ptr disconnector_; + unsigned int windowSize_; + uint64_t windowCapacity_; + + bool started_; + PendingRequests pendingRequests_; + RunningRequests runningRequests_; + uint64_t runningSize_; + + void FillWindow(); + + public: + DataSourceSequentialReader(const boost::shared_ptr& executor, + const boost::shared_ptr& reader, + IValueDisconnector* disconnector /* takes ownership */, + unsigned int windowSize /* number of elements in the sliding window */, + uint64_t windowCapacity /* if 0, memory usage is only controlled by the size of the sliding window */); + + ~DataSourceSequentialReader(); + + void Submit(IDataIdentifier* request /* takes ownership */); + + void Start(); + + bool HasNext() const; + + Item* Next(); + }; +} diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DataSource/DicomDataSource.cpp --- a/OrthancFramework/Sources/DataSource/DicomDataSource.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/DataSource/DicomDataSource.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -26,7 +26,9 @@ #include "DicomDataSource.h" #include "../DicomParsing/ParsedDicomFile.h" +#include "../Logging.h" #include "../OrthancException.h" +#include "BaseDataIdentifier.h" #include "DataSourceReader.h" #include "StorageAreaDataSource.h" @@ -35,81 +37,60 @@ namespace Orthanc { - static size_t ComputeValueSize(size_t size, - bool isKeepRawBuffer) - { - if (isKeepRawBuffer) - { - uint64_t s = 2 * static_cast(size); - if (s != static_cast(static_cast(s))) - { - return std::numeric_limits::max(); - } - else - { - return static_cast(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(attachment_.GetContentType()) + "|" + - boost::lexical_cast(keepRawBuffer_)); + boost::lexical_cast(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 dicom_; - size_t size_; - bool hasRawBuffer_; - std::string rawBuffer_; + Mutex mutex_; + std::unique_ptr 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(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& readerCache) { - const BaseIdentifier& id = dynamic_cast(obj); + const Identifier& id = dynamic_cast(obj); std::unique_ptr 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(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(obj); - return ComputeValueSize(value.GetRawBufferSize(), value.HasRawBuffer()); + return value.GetSize(); } - DicomDataSource::Dicom::Dicom(const boost::shared_ptr& 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(*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(*that_.value_).HasRawBuffer(); - } - - - const void* DicomDataSource::Dicom::Lock::GetRawBufferData() const - { - return dynamic_cast(*that_.value_).GetRawBufferData(); + Value& value = dynamic_cast(*that_.item_->GetValue()); + lock_.reset(value.Lock()); } - size_t DicomDataSource::Dicom::Lock::GetRawBufferSize() const + ParsedDicomFile& DicomDataSource::Dicom::Lock::GetContent() const { - return dynamic_cast(*that_.value_).GetRawBufferSize(); + const Value& value = dynamic_cast(*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 id(new WholeIdentifier(attachment, keepRawBuffer, checkMD5)); - boost::shared_ptr 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(static_cast(pixelDataOffset)) != pixelDataOffset) { throw OrthancException(ErrorCode_NotEnoughMemory); } + else if (attachment.GetCompressionType() != CompressionType_None) + { + throw OrthancException(ErrorCode_BadParameterType); + } + else + { + return new BeginningIdentifier(attachment, static_cast(pixelDataOffset)); + } + } - std::unique_ptr id(new BeginningIdentifier(attachment, static_cast(pixelDataOffset))); - boost::shared_ptr 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 item(reader.ReadSingle(request)); + return new Dicom(item.release()); + } } } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DataSource/DicomDataSource.h --- a/OrthancFramework/Sources/DataSource/DicomDataSource.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/DataSource/DicomDataSource.h Wed Aug 12 09:54:49 2026 +0200 @@ -35,6 +35,7 @@ #include "../Compatibility.h" #include "../FileStorage/FileInfo.h" #include "../MultiThreading/Mutex.h" +#include "DataSourceAnswer.h" #include "IDataIdentifier.h" #include "IDataSource.h" @@ -46,10 +47,10 @@ class DataSourceReader; class ParsedDicomFile; - class DicomDataSource : public IDataSource + class ORTHANC_PUBLIC DicomDataSource : public IDataSource { private: - class BaseIdentifier; + class Identifier; class WholeIdentifier; class BeginningIdentifier; class Value; @@ -59,54 +60,45 @@ public: explicit DicomDataSource(const boost::shared_ptr& storageAreaReader); - virtual IDynamicObject* Load(const IDataIdentifier& obj) ORTHANC_OVERRIDE; + virtual IDynamicObject* Load(const IDataIdentifier& obj, + const boost::shared_ptr& 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 value_; + std::unique_ptr item_; // Holding item puts backpressure on the data source public: - explicit Dicom(const boost::shared_ptr& 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 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 */); }; } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DataSource/DicomSequentialReader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/DataSource/DicomSequentialReader.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -0,0 +1,345 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "DicomSequentialReader.h" + +#include "../DicomParsing/ParsedDicomFile.h" +#include "../OrthancException.h" +#include "../StringMemoryBuffer.h" +#include "BaseDataIdentifier.h" +#include "DataSourceReader.h" +#include "DataSourceSequentialReader.h" +#include "DicomDataSource.h" +#include "StorageAreaDataSource.h" +#include "TranscoderDataSource.h" + + +namespace Orthanc +{ + DicomSequentialReader::Item::Item(ParsedDicomFile* parsed) : + parsed_(parsed) + { + if (parsed == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + } + + + DicomSequentialReader::Item::Item(const boost::shared_ptr& 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 protection(source); + + switch (sourceType_) + { + case SourceType_StorageArea: + { + std::unique_ptr range(new StorageAreaDataSource::Range(protection.release())); + return new Item(range->GetBuffer()); // The buffer is shared through the cache + } + + case SourceType_Dicom: + { + std::unique_ptr dicom(new DicomDataSource::Dicom(protection.release())); + return new Item(dicom->Clone()); + } + + case SourceType_Transcoder: + { + std::unique_ptr 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& executor, + DicomTransferSyntax targetSyntax, + TranscodingSopInstanceUidMode mode, + bool hasLossyQuality, + unsigned int lossyQuality, + const boost::shared_ptr& 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 protection(userData); + + std::unique_ptr 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(*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 source(reader_->Next()); + + std::unique_ptr target(dynamic_cast(source->ReleaseValue())); + + if (source->HasUserData()) + { + target->SetUserData(source->ReleaseUserData()); + } + + return target.release(); + } + + + DicomSequentialReader::Factory::Factory(const boost::shared_ptr& executor, + const boost::shared_ptr& storageAreaReader, + const boost::shared_ptr& dicomReader, + const boost::shared_ptr& 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_); + } +} diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DataSource/DicomSequentialReader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/DataSource/DicomSequentialReader.h Wed Aug 12 09:54:49 2026 +0200 @@ -0,0 +1,159 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#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 parsed_; + boost::shared_ptr raw_; + std::unique_ptr userData_; + + public: + explicit Item(ParsedDicomFile* parsed /* takes ownership */); + + explicit Item(const boost::shared_ptr& 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 reader_; // boost::shared_ptr<> for PImpl + + DicomSequentialReader(SourceType sourceType, + const boost::shared_ptr& executor, + DicomTransferSyntax targetSyntax, + TranscodingSopInstanceUidMode mode, + bool hasLossyQuality, + unsigned int lossyQuality, + const boost::shared_ptr& 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 executor_; + boost::shared_ptr storageAreaReader_; + boost::shared_ptr dicomReader_; + boost::shared_ptr transcoderReader_; + unsigned int windowSize_; + uint64_t windowCapacity_; + + public: + // The readers are allowed to be NULL, for unit tests + Factory(const boost::shared_ptr& executor, + const boost::shared_ptr& storageAreaReader, + const boost::shared_ptr& dicomReader, + const boost::shared_ptr& transcoderReader, + unsigned int windowSize, + uint64_t windowCapacity); + + /** + * Methods below prioritize the downloading of the raw DICOM + * files (and avoid DICOM parsing if possible). + **/ + + DicomSequentialReader* CreateForOriginalRawMemoryBuffer() const; + + DicomSequentialReader* CreateForTranscodedRawMemoryBuffer(DicomTransferSyntax targetSyntax, + TranscodingSopInstanceUidMode mode, + bool hasLossyQuality, + unsigned int lossyQuality) const; + + /** + * Methods below prioritize the parsing of the DICOM files (and + * exploit the DICOM cache if possible). + **/ + + DicomSequentialReader* CreateForOriginalParsedDicomFile() const; + + DicomSequentialReader* CreateForTranscodedParsedDicomFile(DicomTransferSyntax targetSyntax, + TranscodingSopInstanceUidMode mode, + bool hasLossyQuality, + unsigned int lossyQuality) const; + }; + + void Submit(const FileInfo& attachment); + + void Submit(const FileInfo& attachment, + IDynamicObject* userData /* takes ownership */); + + void Start(); + + bool HasNext(); + + Item* Next(); + }; +} diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DataSource/IDataIdentifier.h --- a/OrthancFramework/Sources/DataSource/IDataIdentifier.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/DataSource/IDataIdentifier.h Wed Aug 12 09:54:49 2026 +0200 @@ -24,7 +24,7 @@ #pragma once -#include "../OrthancFramework.h" +#include "../IDynamicObject.h" #include @@ -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; }; } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DataSource/IDataSource.h --- a/OrthancFramework/Sources/DataSource/IDataSource.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/DataSource/IDataSource.h Wed Aug 12 09:54:49 2026 +0200 @@ -27,8 +27,13 @@ #include "../IDynamicObject.h" #include "IDataIdentifier.h" +#include + + 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& readerCache /* could be NULL */) = 0; virtual size_t GetValueSize(const IDynamicObject& value) const = 0; }; diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DataSource/StorageAreaDataSource.cpp --- a/OrthancFramework/Sources/DataSource/StorageAreaDataSource.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/DataSource/StorageAreaDataSource.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -25,9 +25,13 @@ #include "../PrecompiledHeaders.h" #include "StorageAreaDataSource.h" +#include "../Cache/SharedObjectCache.h" +#include "../Logging.h" +#include "../MetricsRegistry.h" #include "../OrthancException.h" #include "../StringMemoryBuffer.h" #include "../Toolbox.h" +#include "BaseDataIdentifier.h" #include "DataSourceReader.h" #if ORTHANC_ENABLE_ZLIB == 1 @@ -37,16 +41,40 @@ #include #include + namespace Orthanc { + static std::string ComputeCacheKey(std::string uuid, + FileContentType type, + uint64_t start, + uint64_t end) + { + // custom data is not part of the cache key, as it is for internal use by the plugin + // (it is opaque to the Orthanc core) + return (uuid + "|" + + boost::lexical_cast(type) + "|" + + boost::lexical_cast(start) + "|" + + boost::lexical_cast(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 buffer_; + boost::shared_ptr 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& 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(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 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(type_) + "|" + - boost::lexical_cast(start_) + "|" + - boost::lexical_cast(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 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& 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(value); - return tmp.GetBuffer().GetSize(); - } - - IDynamicObject* StorageAreaDataSource::Load(const IDataIdentifier& identifier) - { - const Identifier& id = dynamic_cast(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& readerCache) { - assert(value_.get() != NULL); - return dynamic_cast(*value_); + const Identifier& id = dynamic_cast(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 wholeCached = readerCache->GetCachedValue(id.GetWholeKey()); + if (wholeCached) + { + const Value& wholeRange = dynamic_cast(*wholeCached); + + std::string content; + wholeRange.ExtractRange(content, id.GetStart(), id.GetEnd()); + + return new Value(StringMemoryBuffer::CreateFromSwap(content), false); + } + } + + std::unique_ptr buffer; + + { + std::unique_ptr timer; + if (metrics_) + { + timer.reset(new MetricsRegistry::Timer(*metrics_, metricsReadDurationName_)); + } + + buffer.reset(id.Read(area_)); + } + + if (metrics_) + { + metrics_->IncrementIntegerValue(metricsReadBytesName_, static_cast(buffer->GetSize())); + } + + return new Value(buffer.release(), checkMD5_); } - StorageAreaDataSource::Range::Range(const boost::shared_ptr& value) : - value_(value) + const boost::shared_ptr& StorageAreaDataSource::Range::GetBuffer() const { - if (value_.get() == NULL) + if (item_.get() != NULL) + { + assert(buffer_.get() == NULL); + return dynamic_cast(*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(buffer.GetData()), buffer.GetSize()); - } - - - StorageAreaDataSource::Range* StorageAreaDataSource::Range::CreateFromSwap(std::string& content) + StorageAreaDataSource::Range::Range(DataSourceAnswer::Item* item) : + item_(item) { - boost::shared_ptr 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 id(new Identifier(uuid, type, start, end, customData)); - boost::shared_ptr 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 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(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(*item->GetValue()); + std::unique_ptr 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 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 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 uncompressed( - ReadAttachment(reader, attachment, true /* uncompress */, checkMD5)); + std::unique_ptr 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 id(new Identifier(attachment.GetUuid(), attachment.GetContentType(), + start, endExclusive, attachment.GetCustomData())); + + // Using "SetWholeKey()" allows to extract a range if the whole compressed attachment is already in the reader cache + assert(!uncompress || attachment.GetCompressionType() == CompressionType_None); + id->SetWholeKey(ComputeCacheKeyForWholeAttachment(attachment)); + + return Execute(reader, id.release()); } } } + + + StorageAreaDataSource::Range* StorageAreaDataSource::Execute(DataSourceReader& reader, + IDataIdentifier* request /* takes ownership */) + { + if (request == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else + { + return new Range(reader.ReadSingle(request)); + } + } + + + void StorageAreaDataSource::StoreIntoCache(DataSourceReader& reader, + const FileInfo& attachment, + const void* data, + size_t size) + { + if (size != attachment.GetCompressedSize()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + const size_t capacity = reader.GetCacheCapacity(); + + if (capacity != 0 && + size <= capacity) + { + const std::string key = ComputeCacheKeyForWholeAttachment(attachment); + std::unique_ptr value(new Value(StringMemoryBuffer::CreateFromBuffer(data, size), false /* no MD5 */)); + + reader.StoreIntoCache(key, value.release()); + } + } } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DataSource/StorageAreaDataSource.h --- a/OrthancFramework/Sources/DataSource/StorageAreaDataSource.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/DataSource/StorageAreaDataSource.h Wed Aug 12 09:54:49 2026 +0200 @@ -32,6 +32,8 @@ #include "../FileStorage/FileInfo.h" #include "../FileStorage/IStorageArea.h" #include "../FileStorage/StorageRange.h" +#include "../IMemoryBuffer.h" +#include "DataSourceAnswer.h" #include "IDataSource.h" #include @@ -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 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& 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& readerCache /* could be NULL */) ORTHANC_OVERRIDE; - class Range : public boost::noncopyable + class ORTHANC_PUBLIC Range : public boost::noncopyable { private: - boost::shared_ptr value_; - - const Value& GetValue() const; + std::unique_ptr item_; // Holding item puts backpressure on the data source (can be NULL) + boost::shared_ptr buffer_; // To be used if "item_ == NULL" public: - explicit Range(const boost::shared_ptr& value); + explicit Range(DataSourceAnswer::Item* item /* takes ownership */); - const void* GetData() const; + explicit Range(IMemoryBuffer* buffer /* takes ownership */); + + const boost::shared_ptr& 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); }; } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DataSource/TranscoderDataSource.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/DataSource/TranscoderDataSource.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -0,0 +1,344 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#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 +#include + + +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 parsed_; + std::unique_ptr raw_; + size_t size_; + + public: + explicit Value(IDicomTranscoder::DicomImage* transcoded) + { + std::unique_ptr 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(mode_)); + + if (hasLossyQuality_) + { + key += "|" + boost::lexical_cast(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 range( + StorageAreaDataSource::Execute( + storageAreaReader, StorageAreaDataSource::CreateAttachmentRequest(attachment_, true /* uncompress */))); + + IDicomTranscoder::DicomImage source; + source.SetExternalBuffer(range->GetData(), range->GetSize()); + + std::unique_ptr target(new IDicomTranscoder::DicomImage); + + std::set 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& transcoder, + const boost::shared_ptr& storageAreaReader) : + transcoder_(transcoder), + storageAreaReader_(storageAreaReader) + { + if (!transcoder_ || + !storageAreaReader_) + { + throw OrthancException(ErrorCode_NullPointer); + } + } + + + IDynamicObject* TranscoderDataSource::Load(const IDataIdentifier& id, + const boost::shared_ptr& readerCache) + { + return dynamic_cast(id).Load(*transcoder_, *storageAreaReader_); + } + + + size_t TranscoderDataSource::GetValueSize(const IDynamicObject& value) const + { + return dynamic_cast(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(*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(*that.item_->GetValue()); + + if (value.HasParsed()) + { + std::unique_ptr 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 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 item(reader.ReadSingle(request)); + return new Transcoded(item.release()); + } + } +} diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DataSource/TranscoderDataSource.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/DataSource/TranscoderDataSource.h Wed Aug 12 09:54:49 2026 +0200 @@ -0,0 +1,117 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../DicomParsing/IDicomTranscoder.h" +#include "../FileStorage/FileInfo.h" +#include "../MultiThreading/Mutex.h" +#include "DataSourceAnswer.h" +#include "IDataSource.h" + +#include + + +namespace Orthanc +{ + class DataSourceReader; + + class ORTHANC_PUBLIC TranscoderDataSource : public IDataSource + { + private: + class Value; + class Identifier; + + boost::shared_ptr transcoder_; + boost::shared_ptr storageAreaReader_; + + public: + TranscoderDataSource(const boost::shared_ptr& transcoder, + const boost::shared_ptr& storageAreaReader); + + virtual IDynamicObject* Load(const IDataIdentifier& id, + const boost::shared_ptr& 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 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 lock_; + std::unique_ptr 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 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 */); + }; +} diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DicomNetworking/DicomServer.cpp diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.cpp --- a/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -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) diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DicomParsing/IDicomTranscoder.cpp --- a/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -426,4 +426,17 @@ return buffer_->size(); } } + + + std::string* IDicomTranscoder::DicomImage::ReleaseInternalBuffer() + { + if (buffer_.get() == NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return buffer_.release(); + } + } } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h --- a/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h Wed Aug 12 09:54:49 2026 +0200 @@ -24,8 +24,13 @@ #pragma once +#if ORTHANC_ENABLE_DCMTK != 1 +# error The macro ORTHANC_ENABLE_DCMTK must be set to 1 +#endif + #include "../Compatibility.h" #include "../Enumerations.h" +#include "../IDynamicObject.h" #include #include @@ -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 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(); }; diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DicomParsing/ParsedDicomCache.cpp --- a/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -22,6 +22,10 @@ **/ +#if defined(__GNUC__) || defined(__clang__) +# pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + #include "../PrecompiledHeaders.h" #include "ParsedDicomCache.h" diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/DicomParsing/ParsedDicomCache.h --- a/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.h Wed Aug 12 09:54:49 2026 +0200 @@ -29,7 +29,7 @@ namespace Orthanc { - class ORTHANC_PUBLIC ParsedDicomCache : public boost::noncopyable + class ORTHANC_PUBLIC ORTHANC_DEPRECATED_CLASS(ParsedDicomCache) : public boost::noncopyable { private: class Item; diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/FileStorage/StorageAccessor.cpp --- a/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -24,7 +24,6 @@ #include "../PrecompiledHeaders.h" #include "StorageAccessor.h" -#include "StorageCache.h" #include "../Logging.h" #include "../StringMemoryBuffer.h" @@ -47,8 +46,6 @@ static const std::string METRICS_REMOVE_DURATION = "orthanc_storage_remove_duration_ms"; static const std::string METRICS_READ_BYTES = "orthanc_storage_read_bytes"; static const std::string METRICS_WRITTEN_BYTES = "orthanc_storage_written_bytes"; -static const std::string METRICS_CACHE_HIT_COUNT = "orthanc_storage_cache_hit_count"; -static const std::string METRICS_CACHE_MISS_COUNT = "orthanc_storage_cache_miss_count"; namespace Orthanc @@ -72,34 +69,14 @@ StorageAccessor::StorageAccessor(IPluginStorageArea& area) : area_(area), - cache_(NULL), metrics_(NULL) { } StorageAccessor::StorageAccessor(IPluginStorageArea& area, - StorageCache& cache) : - area_(area), - cache_(&cache), - metrics_(NULL) - { - } - - - StorageAccessor::StorageAccessor(IPluginStorageArea& area, MetricsRegistry& metrics) : area_(area), - cache_(NULL), - metrics_(&metrics) - { - } - - StorageAccessor::StorageAccessor(IPluginStorageArea& area, - StorageCache& cache, - MetricsRegistry& metrics) : - area_(area), - cache_(&cache), metrics_(&metrics) { } @@ -151,12 +128,6 @@ metrics_->IncrementIntegerValue(METRICS_WRITTEN_BYTES, static_cast(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(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 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 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); } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/FileStorage/StorageAccessor.h --- a/OrthancFramework/Sources/FileStorage/StorageAccessor.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h Wed Aug 12 09:54:49 2026 +0200 @@ -55,21 +55,19 @@ namespace Orthanc { class MetricsRegistry; - class StorageCache; /** * This class handles the compression/decompression of the raw files * contained in the storage area, and monitors timing metrics (if * enabled). **/ - class ORTHANC_PUBLIC StorageAccessor : boost::noncopyable + class ORTHANC_PUBLIC ORTHANC_DEPRECATED_CLASS(StorageAccessor) : boost::noncopyable { private: class MetricsTimer; - IPluginStorageArea& area_; - StorageCache* cache_; - MetricsRegistry* metrics_; + IPluginStorageArea& area_; + MetricsRegistry* metrics_; #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 void SetupSender(BufferHttpSender& sender, @@ -81,13 +79,6 @@ explicit StorageAccessor(IPluginStorageArea& area); StorageAccessor(IPluginStorageArea& area, - StorageCache& cache); - - StorageAccessor(IPluginStorageArea& area, - MetricsRegistry& metrics); - - StorageAccessor(IPluginStorageArea& area, - StorageCache& cache, MetricsRegistry& metrics); void Write(FileInfo& info /* out */, @@ -149,17 +140,5 @@ const std::string& mime, const std::string& contentFilename); #endif - - private: - void ReadStartRangeInternal(std::string& target, - const FileInfo& info, - uint64_t end /* exclusive */); - - void ReadWholeInternal(std::string& content, - const FileInfo& info); - - void ReadRawInternal(std::string& content, - const FileInfo& info); - }; } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/FileStorage/StorageCache.cpp --- a/OrthancFramework/Sources/FileStorage/StorageCache.cpp Wed Aug 12 09:51:40 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 - * . - **/ - - -#include "../PrecompiledHeaders.h" -#include "StorageCache.h" - -#include "../Compatibility.h" -#include "../Logging.h" -#include "../OrthancException.h" - -#include - - -namespace Orthanc -{ - static std::string GetCacheKeyFullFile(const std::string& uuid, - FileContentType contentType) - { - return uuid + ":" + boost::lexical_cast(contentType) + ":1"; - } - - - static std::string GetCacheKeyStartRange(const std::string& uuid, - FileContentType contentType) - { - return uuid + ":" + boost::lexical_cast(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 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::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(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(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(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(contentType) << " from cache"; - return true; - } - - return false; - } - - - size_t StorageCache::GetCurrentSize() const - { - return cache_.GetCurrentSize(); - } - - size_t StorageCache::GetNumberOfItems() const - { - return cache_.GetNumberOfItems(); - } - -} diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/FileStorage/StorageCache.h --- a/OrthancFramework/Sources/FileStorage/StorageCache.h Wed Aug 12 09:51:40 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 - * . - **/ - - -#pragma once - -#include "../Cache/MemoryStringCache.h" - -#include "../Compatibility.h" // For ORTHANC_OVERRIDE - -#include -#include - -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 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 */); - - }; -} diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/FileStorage/StorageRange.cpp --- a/OrthancFramework/Sources/FileStorage/StorageRange.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/FileStorage/StorageRange.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -245,6 +245,10 @@ { range.SetStartInclusive(tmp); } + else + { + throw OrthancException(ErrorCode_BadRange); + } } if (!tokens[1].empty()) @@ -253,6 +257,10 @@ { range.SetEndInclusive(tmp); } + else + { + throw OrthancException(ErrorCode_BadRange); + } } range.SanityCheck(); diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/HttpServer/HttpOutput.cpp --- a/OrthancFramework/Sources/HttpServer/HttpOutput.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/HttpServer/HttpOutput.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -377,7 +377,7 @@ void HttpOutput::SendStatus(HttpStatus status, - const char* message, + const void* message, size_t messageSize) { if (status == HttpStatus_301_MovedPermanently || diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/HttpServer/HttpOutput.h --- a/OrthancFramework/Sources/HttpServer/HttpOutput.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/HttpServer/HttpOutput.h Wed Aug 12 09:54:49 2026 +0200 @@ -158,7 +158,7 @@ } void SendStatus(HttpStatus status, - const char* message, + const void* message, size_t messageSize); void SendStatus(HttpStatus status); diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/IMemoryBuffer.h --- a/OrthancFramework/Sources/IMemoryBuffer.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/IMemoryBuffer.h Wed Aug 12 09:54:49 2026 +0200 @@ -40,6 +40,8 @@ { } + virtual void CopyToString(std::string& target) const = 0; + // The content of the memory buffer will emptied after this call virtual void MoveToString(std::string& target) = 0; diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/Images/ImageBuffer.cpp --- a/OrthancFramework/Sources/Images/ImageBuffer.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/Images/ImageBuffer.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -26,6 +26,7 @@ #include "ImageBuffer.h" #include "../OrthancException.h" +#include "../Constants.h" #include #include @@ -49,6 +50,11 @@ const uint64_t tmpPitch = static_cast(GetBytesPerPixel()) * static_cast(width_); const uint64_t size = tmpPitch * static_cast(height_); + if (size > MAX_IMAGE_FRAME_SIZE) { + throw OrthancException(ErrorCode_BadFileFormat, std::string("Trying to create an ImageBuffer whose size (") + boost::lexical_cast(size) + " bytes) is larger than the limit (" + boost::lexical_cast(MAX_IMAGE_FRAME_SIZE) + " bytes)"); + } + + if (static_cast(static_cast(tmpPitch)) != tmpPitch || static_cast(static_cast(size)) != size) { diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/JobsEngine/JobsEngine.cpp --- a/OrthancFramework/Sources/JobsEngine/JobsEngine.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/JobsEngine/JobsEngine.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -140,6 +140,8 @@ CLOG(INFO, JOBS) << "Executing " << jobType << " job with priority " << running.GetPriority() << " in worker thread " << workerIndex << ": " << running.GetId(); + Logging::ScopedCurrentThreadContextSetter logContext(std::string("job ") + running.GetId()); + while (engine->IsRunning()) { if (!engine->ExecuteStep(running, workerIndex)) diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp --- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -114,6 +114,8 @@ pauseScheduled_(false), cancelScheduled_(false) { + Logging::ScopedCurrentThreadContextSetter logContext(std::string("job ") + id_); + if (job == NULL) { throw OrthancException(ErrorCode_NullPointer); diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/Logging.cpp --- a/OrthancFramework/Sources/Logging.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/Logging.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -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 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_++; diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/Logging.h --- a/OrthancFramework/Sources/Logging.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/Logging.h Wed Aug 12 09:54:49 2026 +0200 @@ -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() : diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/MallocMemoryBuffer.cpp --- a/OrthancFramework/Sources/MallocMemoryBuffer.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/MallocMemoryBuffer.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -102,6 +102,17 @@ } + void MallocMemoryBuffer::CopyToString(std::string& target) const + { + target.resize(size_); + + if (size_ != 0) + { + memcpy(&target[0], buffer_, size_); + } + } + + void MallocMemoryBuffer::MoveToString(std::string& target) { target.resize(size_); diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/MallocMemoryBuffer.h --- a/OrthancFramework/Sources/MallocMemoryBuffer.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/MallocMemoryBuffer.h Wed Aug 12 09:54:49 2026 +0200 @@ -53,6 +53,8 @@ uint64_t size, FreeFunction freeFunction); + virtual void CopyToString(std::string& target) const ORTHANC_OVERRIDE; + virtual void MoveToString(std::string& target) ORTHANC_OVERRIDE; virtual const void* GetData() const ORTHANC_OVERRIDE diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/MultiThreading/IExecutorService.h diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/MultiThreading/Mutex.h --- a/OrthancFramework/Sources/MultiThreading/Mutex.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/MultiThreading/Mutex.h Wed Aug 12 09:54:49 2026 +0200 @@ -47,6 +47,10 @@ explicit ScopedLock(Mutex& mutex) { } + + void Unlock() + { + } }; }; @@ -68,6 +72,11 @@ lock_(mutex.mutex_) { } + + void Unlock() + { + lock_.unlock(); + } }; }; #endif diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/MultiThreading/ThreadPool.cpp --- a/OrthancFramework/Sources/MultiThreading/ThreadPool.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/MultiThreading/ThreadPool.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -27,6 +27,8 @@ #include "../Logging.h" #include "../OrthancException.h" +#include "../MetricsRegistry.h" +#include "../Toolbox.h" #include "FutureState.h" #include @@ -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 callable_; - boost::weak_ptr state_; + std::unique_ptr callable_; + boost::weak_ptr state_; + std::unique_ptr callerMemento_; public: CallableTask(ICallable* callable, boost::shared_ptr& 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 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 locked = state_.lock(); @@ -110,21 +125,24 @@ class ThreadPool::RunnableTask : public ITask { private: - std::unique_ptr runnable_; + std::unique_ptr runnable_; + std::unique_ptr 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 decounter; // mark this thread as being used in the metrics + if (availableThreadsMetrics_.get()) + { + decounter.reset(new MetricsRegistry::AvailableResourcesDecounter(*availableThreadsMetrics_)); + } + try { - dynamic_cast(*task).Execute(); + ITask& iTask = dynamic_cast(*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_; diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/MultiThreading/ThreadPool.h --- a/OrthancFramework/Sources/MultiThreading/ThreadPool.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/MultiThreading/ThreadPool.h Wed Aug 12 09:54:49 2026 +0200 @@ -27,6 +27,7 @@ #include "../Compatibility.h" #include "IExecutorService.h" #include "SharedMessageQueue.h" +#include "../MetricsRegistry.h" #include @@ -54,6 +55,8 @@ State state_; unsigned int dequeueTimeoutMilliseconds_; + std::unique_ptr availableThreadsMetrics_; + template 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; diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/StringMemoryBuffer.cpp --- a/OrthancFramework/Sources/StringMemoryBuffer.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/StringMemoryBuffer.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -25,9 +25,17 @@ #include "PrecompiledHeaders.h" #include "StringMemoryBuffer.h" +#include "OrthancException.h" + namespace Orthanc { + void StringMemoryBuffer::CopyToString(std::string& target) const + { + target = buffer_; + } + + void StringMemoryBuffer::MoveToString(std::string& target) { buffer_.swap(target); @@ -59,4 +67,21 @@ result->Copy(buffer, start, end); return result.release(); } + + + IMemoryBuffer* StringMemoryBuffer::CreateFromBuffer(const void* data, + size_t size) + { + if (size != 0 && + data == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else + { + std::string s; + s.assign(reinterpret_cast(data), size); + return CreateFromSwap(s); + } + } } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/StringMemoryBuffer.h --- a/OrthancFramework/Sources/StringMemoryBuffer.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/StringMemoryBuffer.h Wed Aug 12 09:54:49 2026 +0200 @@ -50,6 +50,8 @@ buffer_.swap(buffer); } + virtual void CopyToString(std::string& target) const ORTHANC_OVERRIDE; + virtual void MoveToString(std::string& target) ORTHANC_OVERRIDE; virtual const void* GetData() const ORTHANC_OVERRIDE @@ -67,5 +69,8 @@ static IMemoryBuffer* CreateFromCopy(const std::string& buffer); static IMemoryBuffer* CreateFromCopy(const std::string& buffer, size_t start /* inclusive */, size_t end /* exclusive */); + + static IMemoryBuffer* CreateFromBuffer(const void* data, + size_t size); }; } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/Sources/Toolbox.cpp --- a/OrthancFramework/Sources/Toolbox.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/Sources/Toolbox.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -3172,7 +3172,7 @@ if (sizeInBytes < 1024) { std::ostringstream oss; - oss << sizeInBytes << "bytes"; + oss << sizeInBytes << " bytes"; return oss.str(); } else diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/UnitTestsSources/FileStorageTests.cpp --- a/OrthancFramework/UnitTestsSources/FileStorageTests.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -29,10 +29,13 @@ #include +#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"; diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp --- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -37,6 +37,10 @@ #include +#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" diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/UnitTestsSources/JobsTests.cpp --- a/OrthancFramework/UnitTestsSources/JobsTests.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/UnitTestsSources/JobsTests.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -1843,10 +1843,10 @@ TEST(ThreadPool, Basic) { ThreadPool pool; - ASSERT_THROW(pool.SetCountThreads(0), OrthancException); + ASSERT_THROW(pool.SetThreadsCount(0), OrthancException); ASSERT_THROW(pool.SetDequeueTimeout(0), OrthancException); pool.SetDequeueTimeout(1); // Ensure fast shutdown in unit tests - ASSERT_EQ(1u, pool.GetCountThreads()); + ASSERT_EQ(1u, pool.GetThreadsCount()); ASSERT_EQ(1u, pool.GetDequeueTimeout()); pool.Start(); diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/UnitTestsSources/MemoryCacheTests.cpp --- a/OrthancFramework/UnitTestsSources/MemoryCacheTests.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/UnitTestsSources/MemoryCacheTests.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -29,6 +29,10 @@ #include +#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; diff -r 2bcbb14bd94d -r 126b525257e5 OrthancFramework/UnitTestsSources/ToolboxTests.cpp --- a/OrthancFramework/UnitTestsSources/ToolboxTests.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancFramework/UnitTestsSources/ToolboxTests.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -380,7 +380,7 @@ TEST(Toolbox, GetHumanFileSize) { - ASSERT_EQ("234bytes", Toolbox::GetHumanFileSize(234)); + ASSERT_EQ("234 bytes", Toolbox::GetHumanFileSize(234)); ASSERT_EQ("2.29KB", Toolbox::GetHumanFileSize(2345)); ASSERT_EQ("22.91KB", Toolbox::GetHumanFileSize(23456)); ASSERT_EQ("229.07KB", Toolbox::GetHumanFileSize(234567)); @@ -513,13 +513,6 @@ ASSERT_EQ(MEGABYTE * static_cast(16 * 1024), 16ull * 1024ull * 1024ull * 1024ull); ASSERT_EQ(static_cast(16 * 1024 * 1024) * KILOBYTE, 16ull * 1024ull * 1024ull * 1024ull); ASSERT_EQ(KILOBYTE * static_cast(16 * 1024 * 1024), 16ull * 1024ull * 1024ull * 1024ull); - - ASSERT_EQ(8, sizeof(static_cast(16) * GIGABYTE)); - ASSERT_EQ(8, sizeof(GIGABYTE * static_cast(16))); - ASSERT_EQ(8, sizeof(static_cast(16 * 1024) * MEGABYTE)); - ASSERT_EQ(8, sizeof(MEGABYTE * static_cast(16 * 1024))); - ASSERT_EQ(8, sizeof(static_cast(16 * 1024 * 1024) * KILOBYTE)); - ASSERT_EQ(8, sizeof(KILOBYTE * static_cast(16 * 1024 * 1024))); } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/CMakeLists.txt --- a/OrthancServer/CMakeLists.txt Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/CMakeLists.txt Wed Aug 12 09:54:49 2026 +0200 @@ -164,7 +164,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 diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Plugins/Engine/OrthancPlugins.cpp --- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -35,6 +35,8 @@ #include "../../../OrthancFramework/Sources/Compression/GzipCompressor.h" #include "../../../OrthancFramework/Sources/Compression/ZlibCompressor.h" +#include "../../../OrthancFramework/Sources/DataSource/DicomDataSource.h" +#include "../../../OrthancFramework/Sources/DataSource/StorageAreaDataSource.h" #include "../../../OrthancFramework/Sources/DicomFormat/DicomArray.h" #include "../../../OrthancFramework/Sources/DicomNetworking/DicomConnectionInfo.h" #include "../../../OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h" @@ -65,6 +67,7 @@ #include "../../Sources/Search/HierarchicalMatcher.h" #include "../../Sources/ServerContext.h" #include "../../Sources/ServerToolbox.h" +#include "../../Sources/ServerTranscoder.h" #include "OrthancPluginDatabase.h" #include "OrthancPluginDatabaseV3.h" #include "OrthancPluginDatabaseV4.h" @@ -3546,14 +3549,14 @@ const _OrthancPluginGetDicomForInstance& p = *reinterpret_cast(parameters); - std::string dicom; + std::unique_ptr 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 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 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 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 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 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 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 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 lock(pimpl_->decoderTranscoderMutex_); diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Plugins/Engine/OrthancPlugins.h --- a/OrthancServer/Plugins/Engine/OrthancPlugins.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h Wed Aug 12 09:54:49 2026 +0200 @@ -53,7 +53,6 @@ #include "../../../OrthancFramework/Sources/JobsEngine/IJob.h" #include "../../../OrthancFramework/Sources/MallocMemoryBuffer.h" #include "../../Sources/Database/IDatabaseWrapper.h" -#include "../../Sources/IDicomImageDecoder.h" #include "../../Sources/IServerListener.h" #include "../../Sources/ServerJobs/IStorageCommitmentFactory.h" #include "PluginMemoryBuffer64.h" @@ -73,7 +72,6 @@ public IPluginServiceProvider, public IServerListener, public IWorklistRequestHandlerFactory, - public IDicomImageDecoder, public IFindRequestHandlerFactory, public IMoveRequestHandlerFactory, public IStorageCommitmentFactory, @@ -394,9 +392,9 @@ bool HasCustomTranscoder(); - virtual ImageAccessor* Decode(const void* dicom, - size_t size, - unsigned int frame) ORTHANC_OVERRIDE; + ImageAccessor* Decode(const void* dicom, + size_t size, + unsigned int frame) const; bool IsAllowed(HttpMethod method, const char* uri, diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Plugins/Engine/PluginMemoryBuffer32.cpp --- a/OrthancServer/Plugins/Engine/PluginMemoryBuffer32.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer32.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -61,6 +61,19 @@ } + void PluginMemoryBuffer32::CopyToString(std::string& target) const + { + SanityCheck(); + + target.resize(buffer_.size); + + if (buffer_.size != 0) + { + memcpy(&target[0], buffer_.data, buffer_.size); + } + } + + void PluginMemoryBuffer32::MoveToString(std::string& target) { SanityCheck(); diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Plugins/Engine/PluginMemoryBuffer32.h --- a/OrthancServer/Plugins/Engine/PluginMemoryBuffer32.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer32.h Wed Aug 12 09:54:49 2026 +0200 @@ -49,6 +49,8 @@ Clear(); } + virtual void CopyToString(std::string& target) const ORTHANC_OVERRIDE; + virtual void MoveToString(std::string& target) ORTHANC_OVERRIDE; virtual const void* GetData() const ORTHANC_OVERRIDE; diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Plugins/Engine/PluginMemoryBuffer64.cpp --- a/OrthancServer/Plugins/Engine/PluginMemoryBuffer64.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer64.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -58,6 +58,19 @@ } + void PluginMemoryBuffer64::CopyToString(std::string& target) const + { + SanityCheck(); + + target.resize(buffer_.size); + + if (buffer_.size != 0) + { + memcpy(&target[0], buffer_.data, buffer_.size); + } + } + + void PluginMemoryBuffer64::MoveToString(std::string& target) { SanityCheck(); diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Plugins/Engine/PluginMemoryBuffer64.h --- a/OrthancServer/Plugins/Engine/PluginMemoryBuffer64.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer64.h Wed Aug 12 09:54:49 2026 +0200 @@ -49,6 +49,8 @@ Clear(); } + virtual void CopyToString(std::string& target) const ORTHANC_OVERRIDE; + virtual void MoveToString(std::string& target) ORTHANC_OVERRIDE; virtual const void* GetData() const ORTHANC_OVERRIDE; diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h --- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Wed Aug 12 09:54:49 2026 +0200 @@ -9997,10 +9997,15 @@ * must only be called from threads that the plugin has created * itself. * + * At the end of the lifetime of the thread, + * OrthancPluginClearCurrentThreadName() must be called to release + * the allocated resources. + * * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). * @param threadName The name of the current thread. A thread name cannot be longer than 16 characters. * @return 0 if success, other value if error. * @ingroup Toolbox + * @see OrthancPluginClearCurrentThreadName() **/ ORTHANC_PLUGIN_SINCE_SDK("1.12.2") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSetCurrentThreadName( @@ -11201,13 +11206,16 @@ /** * @brief Clear the name of the current thread. * - * This function releases the resources allocated to store the threa - * name. This function must only be called from threads that the plugin - * has created itself when the thread is reaching its end of life. + * This function releases the resources allocated to store the name + * of the current thread, after a call to + * "OrthancPluginSetCurrentThreadName()". This function must only be + * called from threads that the plugin has created itself when the + * thread is reaching its end of life. * * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). * @return 0 if success, other value if error. * @ingroup Toolbox + * @see OrthancPluginSetCurrentThreadName() **/ ORTHANC_PLUGIN_SINCE_SDK("1.13.0") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginClearCurrentThreadName( diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Resources/Configuration.json --- a/OrthancServer/Resources/Configuration.json Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Resources/Configuration.json Wed Aug 12 09:54:49 2026 +0200 @@ -52,6 +52,24 @@ // is disabled. (new in Orthanc 1.10.0) "MaximumStorageCacheSize" : 128, + // Amount of RAM (in MB) that are allocated to the storage loader threads. + // Note that this limit can be overpassed when a single file + // exceeds this size. + // (new in Orthanc 1.12.11+). + // You should monitor the "orthanc_storage_memory_usage_mb" and + // "orthanc_storage_memory_max_usage_mb" metrics to determine the + // optimal value for your setup. + "StorageMemoryCapacity": 512, + + // Number of threads that are used to read files from the storage. + // (new in Orthanc 1.12.11+) + // A value > 4 is meaningful only if the storage is a distributed + // network storage (e.g., object storage plugin). In this case, + // you can typically increase it to 20 and you should then monitor + // the "orthanc_storage_available_threads" metrics to determine + // the optimal value for your setup. + "StorageLoaderThreads": 4, + // List of paths to the custom Lua scripts that are to be loaded // into this instance of Orthanc "LuaScripts" : [ @@ -711,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, @@ -831,5 +851,85 @@ // Orthanc <= 1.12.10. This default value can be overwritten per archive, // by providing the "Utf8" field to the "{...}/archive" and // "/tools/create-archives" routes. (new in Orthanc 1.12.11) - "ZipUseUtf8" : false + "ZipUseUtf8" : false, + + + // -> TODO-Streaming: move this into AdvancedConfiguration.json + + // TODO-Streaming: add a page in the book: performance: fine tuning: limiting memory + increasing the number of threads blah blah blah + + + // Number of threads that are used to parse DICOM files (when you need + // to access the value of DICOM tags that are not store in DB). + // (new in Orthanc 1.12.11+) + // You should monitor the "orthanc_dicom_parser_available_threads" + // metrics to determine the optimal value for your setup and usage. + "DicomParserThreads": 2, + + // Amount of RAM (in MB) that are allocated to the DICOM parser threads. + // Note that this limit can be overpassed when a single file + // exceeds this size. + // (new in Orthanc 1.12.11+). + // You should monitor the "orthanc_dicom_parser_memory_usage_mb" and + // "orthanc_dicom_parser_memory_max_usage_mb" metrics to determine the + // optimal value for your setup and usage. + "DicomParserMemoryCapacity": 256, + + // Maximum size of the DICOM parser cache in MB. This cache + // is stored in RAM and contains a parsed version of recently read + // DIOCM files. A value of "0" indicates the cache + // is disabled. (new in Orthanc 1.12.11+) + "DicomParserCacheSize" : 128, + + // Number of threads that are used to parse transcode DICOM files. + // (new in Orthanc 1.12.11+) + // You should monitor the "orthanc_transcoder_available_threads" + // metrics to determine the optimal value for your setup and usage. + "TranscoderThreads": 4, + + // Amount of RAM (in MB) that are allocated to the transcoder threads. + // Note that this limit can be overpassed when a single file + // exceeds this size. + // (new in Orthanc 1.12.11+). + // You should monitor the "orthanc_transcoder_memory_usage_mb" and + // "orthanc_transcoder_memory_max_usage_mb" metrics to determine the + // optimal value for your setup and usage. + "TranscoderMemoryCapacity": 256, + + // Maximum size of the transcoder cache in MB. This cache + // is stored in RAM and contains transcoded versions of recently read + // DIOCM files. A value of "0" indicates the cache + // is disabled. (new in Orthanc 1.12.11+) + "TranscoderCacheSize" : 128, + + // Total number of threads that are used to provide DICOM files sequentially + // (currently used in Archive jobs, C-Store and C-Get). + // Each time such a job needs to get a DICOM file content, it requests it from + // a sequential DICOM reader that will itself use a s torage loader thread. + // Therefore, by default, the value of this configuration is identical to + // "StorageLoaderThreads". + // (new in Orthanc 1.13.0) + // You should monitor the "orthanc_seq_reader_available_threads" + // metrics to determine the optimal value for your setup and usage. + // "SequentialDicomReaderThreads": 4, + + // Number of files each sequential DICOM reader is allowed to buffer + // in advance before it is consumed by the related job. + // This is kind of equivalent to the older "LoaderThreads" configuration + // but, furthermore, you must make sure that there are enough "StorageLoaderThreads" + // and "SequentialDicomReaderThreads" for each job -> + // "StorageLoaderThreads" should be larger than "ConcurrentJobs" * "SequentialDicomReaderWindowSize" + // Together with "SequentialDicomReaderWindowCapacity", this also defines the amount of memory that is + // used by each job that required a SequentialDicomReader. + // By default, the value of this configuration is the same as "LoaderThreads". + // "SequentialDicomReaderWindowSize": 4, + + // Amount of RAM (in MB) that are allocated to each sequential DICOM + // Reader. The sequential DICOM reader stops pauses buffering as soon + // as one of the "SequentialDicomReaderWindowSize" or + // "SequentialDicomReaderWindowCapacity" limit is reached. + // Note that this limit can be overpassed when a single file + // exceeds this size. + // (new in Orthanc 1.13.0). + "SequentialDicomReaderWindowCapacity": 64 } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Resources/ImplementationNotes/JobsEngineClasses.txt --- a/OrthancServer/Resources/ImplementationNotes/JobsEngineClasses.txt Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Resources/ImplementationNotes/JobsEngineClasses.txt Wed Aug 12 09:54:49 2026 +0200 @@ -22,6 +22,6 @@ virtual bool HandleTrailingStep() = 0; -class StoreJob : SetOfInstancesJob (contains a ThreadedInstancesLoader) +class StoreJob : SetOfInstancesJob (contains a DicomSequentialReader) -Class ModalityStoreJob : StoreJob \ No newline at end of file +Class ModalityStoreJob : StoreJob diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Resources/RunCppCheck-2.1.sh --- a/OrthancServer/Resources/RunCppCheck-2.1.sh Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Resources/RunCppCheck-2.1.sh Wed Aug 12 09:54:49 2026 +0200 @@ -15,8 +15,8 @@ assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:279 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:292 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:391 -assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3078 -assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:287 +assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3110 +assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:293 constParameter:../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp knownArgument:../../OrthancFramework/UnitTestsSources/ImageTests.cpp knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp @@ -26,18 +26,18 @@ stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:74 stlFindInsert:../../OrthancServer/Sources/Database/MainDicomTagsRegistry.cpp:65 stlFindInsert:../../OrthancServer/Sources/Database/ResourcesContent.h:141 -stlFindInsert:../../OrthancServer/Sources/OrthancWebDav.cpp:329 +stlFindInsert:../../OrthancServer/Sources/OrthancWebDav.cpp:333 stlFindInsert:../../OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp:42 -stlFindInsert:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:362 +stlFindInsert:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:368 stlFindInsert:../../OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp:192 syntaxError:../../OrthancFramework/Sources/SQLite/FunctionContext.h:53 syntaxError:../../OrthancFramework/UnitTestsSources/DicomMapTests.cpp:74 syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:325 unreadVariable:../../OrthancFramework/Sources/FileStorage/StorageAccessor.cpp -unreadVariable:../../OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp:1173 -useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:92 +unreadVariable:../../OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp:1174 +useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:100 useInitializationList:../../OrthancFramework/Sources/Images/PngWriter.cpp:99 -useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:275 +useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:277 EOF CPPCHECK_BUILD_DIR=/tmp/cppcheck-build-dir-2.1/ diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Resources/RunCppCheck-2.17.1.sh --- a/OrthancServer/Resources/RunCppCheck-2.17.1.sh Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Resources/RunCppCheck-2.17.1.sh Wed Aug 12 09:54:49 2026 +0200 @@ -12,24 +12,20 @@ cat < /tmp/cppcheck-suppressions.txt assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:292 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:391 -assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:287 +assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:293 constParameterCallback:../../OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp:50 constParameterCallback:../../OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp:112 constParameterCallback:../../OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp:113 constParameterCallback:../../OrthancFramework/Sources/Pkcs11.cpp:125 constParameterCallback:../../OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp:3553 -constParameterCallback:../../OrthancServer/Sources/OrthancGetRequestHandler.cpp:48 +constParameterCallback:../../OrthancServer/Sources/OrthancGetRequestHandler.cpp:66 constParameterPointer:../../OrthancFramework/Sources/Logging.cpp:450 constParameterPointer:../../OrthancFramework/Sources/Logging.cpp:454 constParameterPointer:../../OrthancFramework/Sources/Toolbox.cpp:3351 knownConditionTrueFalse:../../OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp:112 -knownConditionTrueFalse:../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp:440 -knownConditionTrueFalse:../../OrthancFramework/Sources/Images/PamReader.cpp:204 -knownConditionTrueFalse:../../OrthancFramework/Sources/Images/PamReader.cpp:207 +knownConditionTrueFalse:../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp:441 +knownConditionTrueFalse:../../OrthancFramework/Sources/Images/PamReader.cpp:208 knownConditionTrueFalse:../../OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp:345 -knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2444 -knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2445 -knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2446 knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2447 knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2448 knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2449 @@ -37,6 +33,9 @@ knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2451 knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2452 knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2453 +knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2454 +knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2455 +knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2456 nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:321 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:166 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:74 @@ -45,11 +44,11 @@ syntaxError:../../OrthancFramework/UnitTestsSources/DicomMapTests.cpp:74 syntaxError:../../OrthancFramework/UnitTestsSources/ZipTests.cpp:133 syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:325 -useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:99 +useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:100 useInitializationList:../../OrthancFramework/Sources/Images/PngWriter.cpp:99 -useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:280 +useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:277 variableScope:../../OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp:230 -variableScope:../../OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp:97 +variableScope:../../OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp:98 EOF CPPCHECK_BUILD_DIR=/tmp/cppcheck-build-dir-2.17.1/ diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Resources/RunCppCheck-2.20.0.sh --- a/OrthancServer/Resources/RunCppCheck-2.20.0.sh Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Resources/RunCppCheck-2.20.0.sh Wed Aug 12 09:54:49 2026 +0200 @@ -10,10 +10,10 @@ fi cat < /tmp/cppcheck-suppressions.txt -assertWithSideEffect:../../OrthancFramework/Sources/DataSource/DataSourceReader.cpp:220 +assertWithSideEffect:../../OrthancFramework/Sources/DataSource/DataSourceReader.cpp:310 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:292 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:391 -assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:287 +assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:293 constParameterPointer:../../OrthancFramework/Sources/Logging.cpp:450 constParameterPointer:../../OrthancFramework/Sources/Logging.cpp:454 constParameterPointer:../../OrthancFramework/Sources/Toolbox.cpp:3351 @@ -25,11 +25,11 @@ syntaxError:../../OrthancFramework/Sources/SQLite/FunctionContext.h syntaxError:../../OrthancFramework/UnitTestsSources/DicomMapTests.cpp:74 syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:325 -useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:99 +useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:100 useInitializationList:../../OrthancFramework/Sources/Images/PngWriter.cpp:99 -useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:280 +useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:277 variableScope:../../OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp:230 -variableScope:../../OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp:97 +variableScope:../../OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp:98 EOF CPPCHECK_BUILD_DIR=/tmp/cppcheck-build-dir-2.20.0/ diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/IDicomImageDecoder.h --- a/OrthancServer/Sources/IDicomImageDecoder.h Wed Aug 12 09:51:40 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 . - **/ - - -#pragma once - -#include "../../OrthancFramework/Sources/Images/ImageAccessor.h" - -#include - -namespace Orthanc -{ - class IDicomImageDecoder : public boost::noncopyable - { - public: - virtual ~IDicomImageDecoder() - { - } - - virtual ImageAccessor* Decode(const void* dicom, - size_t size, - unsigned int frame) = 0; - }; -} diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/LuaScripting.cpp diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/OrthancConfiguration.cpp --- a/OrthancServer/Sources/OrthancConfiguration.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/OrthancConfiguration.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -47,8 +47,6 @@ 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 namespace Orthanc { @@ -1490,9 +1488,9 @@ // 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 - if (!LookupUnsignedIntegerParameter(loaderThreads, CONFIG_LOADER_THREADS)) + if (!LookupUnsignedIntegerParameter(loaderThreads, ORTHANC_CONFIG_LOADER_THREADS)) { - LookupUnsignedIntegerParameter(loaderThreads, CONFIG_ZIP_LOADER_THREADS); // we cannot use GetUnsignedIntegerParameter() because there is no default value for this old configuration + LookupUnsignedIntegerParameter(loaderThreads, ORTHANC_CONFIG_ZIP_LOADER_THREADS); // we cannot use GetUnsignedIntegerParameter() because there is no default value for this old configuration } if (loaderThreads <= 1) @@ -1504,4 +1502,19 @@ return loaderThreads; } } + + unsigned int OrthancConfiguration::GetConcurrentJobs() const + { + return GetUnsignedIntegerParameter(ORTHANC_CONFIG_CONCURRENT_JOBS); + } + + unsigned int OrthancConfiguration::GetHttpThreadsCount() const + { + return GetUnsignedIntegerParameter(ORTHANC_CONFIG_HTTP_THREADS_COUNT); + } + + unsigned int OrthancConfiguration::GetDicomThreadsCount() const + { + return GetUnsignedIntegerParameter(ORTHANC_CONFIG_DICOM_THREADS_COUNT); + } } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/OrthancConfiguration.h --- a/OrthancServer/Sources/OrthancConfiguration.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/OrthancConfiguration.h Wed Aug 12 09:54:49 2026 +0200 @@ -41,7 +41,6 @@ #define ORTHANC_CONFIG_DICOM_AET "DicomAet" #define ORTHANC_CONFIG_DICOM_PORT "DicomPort" #define ORTHANC_CONFIG_HTTP_PORT "HttpPort" -#define ORTHANC_CONFIG_MAXIMUM_STORAGE_CACHE_SIZE "MaximumStorageCacheSize" #define ORTHANC_CONFIG_MAXIMUM_STORAGE_SIZE "MaximumStorageSize" #define ORTHANC_CONFIG_MAXIMUM_STORAGE_MODE "MaximumStorageMode" #define ORTHANC_CONFIG_MAXIMUM_PATIENT_COUNT "MaximumPatientCount" @@ -54,8 +53,29 @@ #define ORTHANC_CONFIG_DICOM_DEFAULT_RETRIEVE_METHOD "DicomDefaultRetrieveMethod" #define ORTHANC_CONFIG_PATIENT_LEVEL_ENABLED "PatientLevelEnabled" #define ORTHANC_CONFIG_READ_ONLY "ReadOnly" +#define ORTHANC_CONFIG_CONCURRENT_JOBS "ConcurrentJobs" +#define ORTHANC_CONFIG_HTTP_THREADS_COUNT "HttpThreadsCount" +#define ORTHANC_CONFIG_DICOM_THREADS_COUNT "DicomThreadsCount" #define ORTHANC_CONFIG_STORAGE_DIRECTORY "StorageDirectory" +#define ORTHANC_CONFIG_LOADER_THREADS "LoaderThreads" +#define ORTHANC_CONFIG_ZIP_LOADER_THREADS "ZipLoaderThreads" // for backward compatibility only + + +#define ORTHANC_CONFIG_STORAGE_LOADER_THREADS "StorageLoaderThreads" +#define ORTHANC_CONFIG_STORAGE_MEMORY_CAPACITY "StorageMemoryCapacity" +#define ORTHANC_CONFIG_MAXIMUM_STORAGE_CACHE_SIZE "MaximumStorageCacheSize" +#define ORTHANC_CONFIG_DICOM_PARSER_SOURCE_THREADS "DicomParserThreads" +#define ORTHANC_CONFIG_DICOM_PARSER_MEMORY_CAPACITY "DicomParserMemoryCapacity" +#define ORTHANC_CONFIG_DICOM_PARSER_CACHE_SIZE "DicomParserCacheSize" +#define ORTHANC_CONFIG_TRANSCODER_THREADS "TranscoderThreads" +#define ORTHANC_CONFIG_TRANSCODER_MEMORY_CAPACITY "TranscoderMemoryCapacity" +#define ORTHANC_CONFIG_TRANSCODER_CACHE_SIZE "TranscoderCacheSize" +#define ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_THREADS "SequentialDicomReaderThreads" +#define ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_WINDOW_SIZE "SequentialDicomReaderWindowSize" +#define ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_WINDOW_CAPACITY "SequentialDicomReaderWindowCapacity" + + namespace Orthanc { @@ -262,6 +282,12 @@ unsigned int GetLoaderThreads() const; + unsigned int GetConcurrentJobs() const; + + unsigned int GetHttpThreadsCount() const; + + unsigned int GetDicomThreadsCount() const; + void Format(std::string& result) const; void SetDefaultEncoding(Encoding encoding); diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/OrthancGetRequestHandler.cpp --- a/OrthancServer/Sources/OrthancGetRequestHandler.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/OrthancGetRequestHandler.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -30,7 +30,7 @@ #include "OrthancConfiguration.h" #include "ServerContext.h" #include "ServerJobs/DicomModalityStoreJob.h" -#include "ServerJobs/ThreadedInstancesLoader.h" +#include "ServerTranscoder.h" #include #include @@ -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 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 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(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 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(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 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 filesInfo; for (std::list::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; diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/OrthancGetRequestHandler.h --- a/OrthancServer/Sources/OrthancGetRequestHandler.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/OrthancGetRequestHandler.h Wed Aug 12 09:54:49 2026 +0200 @@ -23,29 +23,28 @@ #pragma once #include "../../OrthancFramework/Sources/Compatibility.h" // For ORTHANC_OVERRIDE +#include "../../OrthancFramework/Sources/DataSource/DicomSequentialReader.h" #include "../../OrthancFramework/Sources/DicomNetworking/IGetRequestHandler.h" #include "../../OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h" -#include - #include -class DcmFileFormat; - namespace Orthanc { + class ParsedDicomFile; class ServerContext; - class ThreadedInstancesLoader; class OrthancGetRequestHandler : public IGetRequestHandler { private: + class ReaderUserData; + ServerContext& context_; std::string localAet_; std::vector instancesIds_; size_t position_; std::string originatorAet_; - std::unique_ptr instancesLoader_; + std::unique_ptr 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); diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/OrthancMoveRequestHandler.cpp --- a/OrthancServer/Sources/OrthancMoveRequestHandler.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/OrthancMoveRequestHandler.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -27,13 +27,13 @@ #include "../../OrthancFramework/Sources/DicomFormat/DicomArray.h" #include "../../OrthancFramework/Sources/DicomNetworking/DicomConnectionInfo.h" #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" +#include "../../OrthancFramework/Sources/DataSource/DicomSequentialReader.h" #include "../../OrthancFramework/Sources/Logging.h" #include "../../OrthancFramework/Sources/MetricsRegistry.h" #include "OrthancConfiguration.h" #include "ServerContext.h" #include "ServerJobs/DicomModalityStoreJob.h" -#include "ServerJobs/ThreadedInstancesLoader.h" namespace Orthanc @@ -42,14 +42,31 @@ { // Anonymous namespace to avoid clashes between compilation modules + class MoveReaderUserData : public IDynamicObject + { + private: + std::string instanceId_; + + public: + explicit MoveReaderUserData(const std::string& instanceId) : + instanceId_(instanceId) + { + } + + const std::string& GetInstanceId() const + { + return instanceId_; + } + }; + + class SynchronousMove : public IMoveRequestIterator { private: ServerContext& context_; std::string localAet_; std::vector instancesIds_; - // std::vector filesInfo_; - std::unique_ptr instancesLoader_; + std::unique_ptr 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 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 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(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; diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -37,6 +37,7 @@ #include #include + #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 modified; + if (transcode) { - ServerContext::DicomCacheLocker locker(context, id); - modified.reset(locker.GetDicom().Clone(true)); + std::unique_ptr transcoded( + context.ReadTranscodedDicom(id, targetSyntax, TranscodingSopInstanceUidMode_AllowNew, true, lossyQuality)); + + TranscoderDataSource::Transcoded::LockAsParsed lock(*transcoded); + modified.reset(lock.GetContent().Clone(true)); + } + else + { + std::unique_ptr 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 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()); } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -29,6 +29,7 @@ #include "../../../OrthancFramework/Sources/CompatibilityMath.h" #include "../../../OrthancFramework/Sources/Compression/GzipCompressor.h" #include "../../../OrthancFramework/Sources/Constants.h" +#include "../../../OrthancFramework/Sources/DataSource/StorageAreaDataSource.h" #include "../../../OrthancFramework/Sources/DicomFormat/DicomImageInformation.h" #include "../../../OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h" #include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" @@ -38,7 +39,6 @@ #include "../../../OrthancFramework/Sources/Images/ImageProcessing.h" #include "../../../OrthancFramework/Sources/Images/NumpyWriter.h" #include "../../../OrthancFramework/Sources/Logging.h" -#include "../../../OrthancFramework/Sources/MultiThreading/Semaphore.h" #include "../../../OrthancFramework/Sources/SerializationToolbox.h" #include "../OrthancConfiguration.h" @@ -51,15 +51,6 @@ // This "include" is mandatory for Release builds using Linux Standard Base #include -#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 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 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 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 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 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 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 dicom(context.ReadParsedDicom(publicId)); + DicomDataSource::Dicom::Lock lock(*dicom); + handler.Handle(call, decoded, &lock.GetContent(), frame); } else { @@ -1252,8 +1239,6 @@ template 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 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 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 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 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 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 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 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 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 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 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 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)); } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -177,6 +177,29 @@ result[ORTHANC_CONFIG_MAXIMUM_PATIENT_COUNT] = lock.GetConfiguration().GetMaximumPatientCount(); // New in Orthanc 1.12.4 result[ORTHANC_CONFIG_MAXIMUM_STORAGE_MODE] = lock.GetConfiguration().GetMaximumStorageMode(); // New in Orthanc 1.11.3 result[ORTHANC_CONFIG_DICOM_DEFAULT_RETRIEVE_METHOD] = lock.GetConfiguration().GetDicomDefaultRetrieveMethod(); + result[ORTHANC_CONFIG_CONCURRENT_JOBS] = lock.GetConfiguration().GetConcurrentJobs(); + result[ORTHANC_CONFIG_HTTP_THREADS_COUNT] = lock.GetConfiguration().GetHttpThreadsCount(); + result[ORTHANC_CONFIG_DICOM_THREADS_COUNT] = lock.GetConfiguration().GetDicomThreadsCount(); + } + + { + uint64_t storageMemoryCapacityMb, transcoderMemoryCapacityMb, dicomParserMemoryCapacityMb; + size_t storageMemoryCacheMb, transcoderMemoryCacheMb, dicomParserMemoryCacheMb; + unsigned int storageReaderThreadsCount, transcoderReaderThreadsCount, dicomParserThreadsCount; + + context.GetDataSourcesConfigurations(storageMemoryCapacityMb, storageMemoryCacheMb, storageReaderThreadsCount, + transcoderMemoryCapacityMb, transcoderMemoryCacheMb, transcoderReaderThreadsCount, + dicomParserMemoryCapacityMb, dicomParserMemoryCacheMb, dicomParserThreadsCount); + + result[ORTHANC_CONFIG_STORAGE_MEMORY_CAPACITY] = BytesToMegabytes(storageMemoryCapacityMb); + result[ORTHANC_CONFIG_MAXIMUM_STORAGE_CACHE_SIZE] = BytesToMegabytes(storageMemoryCacheMb); + result[ORTHANC_CONFIG_STORAGE_LOADER_THREADS] = storageReaderThreadsCount; + result[ORTHANC_CONFIG_TRANSCODER_MEMORY_CAPACITY] = BytesToMegabytes(transcoderMemoryCapacityMb); + result[ORTHANC_CONFIG_TRANSCODER_CACHE_SIZE] = BytesToMegabytes(transcoderMemoryCacheMb); + result[ORTHANC_CONFIG_TRANSCODER_THREADS] = transcoderReaderThreadsCount; + result[ORTHANC_CONFIG_DICOM_PARSER_MEMORY_CAPACITY] = BytesToMegabytes(dicomParserMemoryCapacityMb); + result[ORTHANC_CONFIG_DICOM_PARSER_CACHE_SIZE] = BytesToMegabytes(dicomParserMemoryCacheMb); + result[ORTHANC_CONFIG_DICOM_PARSER_SOURCE_THREADS] = dicomParserThreadsCount; } DicomTransferSyntax ingestTransferSyntax; @@ -1011,8 +1034,6 @@ registry.SetIntegerValue("orthanc_up_time_s", serverUpTime); registry.SetIntegerValue("orthanc_last_change", lastChange["Last"].asInt64()); - context.PublishCacheMetrics(); - std::string s; registry.ExportPrometheusText(s); diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/OrthancWebDav.cpp --- a/OrthancServer/Sources/OrthancWebDav.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/OrthancWebDav.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -226,7 +226,11 @@ time_ = GetNow(); } - context_.ReadDicom(target_, resource.GetIdentifier()); + { + std::unique_ptr 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 raw(context_.ReadRawDicom(instanceId)); + raw->Copy(content); + } + LookupTime(time, context_, instanceId, ResourceType_Instance, MetadataType_Instance_ReceptionDate); return true; } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/ServerContext.cpp --- a/OrthancServer/Sources/ServerContext.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/ServerContext.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -25,32 +25,37 @@ #include "ServerContext.h" #include "../../OrthancFramework/Sources/Cache/SharedArchive.h" +#include "../../OrthancFramework/Sources/Compression/ZlibCompressor.h" +#include "../../OrthancFramework/Sources/Constants.h" +#include "../../OrthancFramework/Sources/DataSource/DataSourceReader.h" +#include "../../OrthancFramework/Sources/DataSource/DicomSequentialReader.h" #include "../../OrthancFramework/Sources/DicomFormat/DicomElement.h" #include "../../OrthancFramework/Sources/DicomFormat/DicomImageInformation.h" #include "../../OrthancFramework/Sources/DicomFormat/DicomStreamReader.h" #include "../../OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.h" #include "../../OrthancFramework/Sources/DicomParsing/DicomModification.h" #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" -#include "../../OrthancFramework/Sources/FileStorage/StorageAccessor.h" #include "../../OrthancFramework/Sources/HttpServer/FilesystemHttpSender.h" #include "../../OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.h" #include "../../OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h" #include "../../OrthancFramework/Sources/Logging.h" #include "../../OrthancFramework/Sources/MallocMemoryBuffer.h" #include "../../OrthancFramework/Sources/MetricsRegistry.h" +#include "../../OrthancFramework/Sources/MultiThreading/ThreadPool.h" #include "../../OrthancFramework/Sources/SerializationToolbox.h" #include "../Plugins/Engine/OrthancPlugins.h" #include "DicomInstanceToStore.h" #include "OrthancConfiguration.h" #include "OrthancRestApi/OrthancRestApi.h" +#include "OutgoingDicomInstance.h" #include "ResourceFinder.h" #include "Search/DatabaseLookup.h" #include "ServerJobs/OrthancJobUnserializer.h" #include "ServerToolbox.h" +#include "ServerTranscoder.h" +#include "SimpleInstanceOrdering.h" #include "StorageCommitmentReports.h" -#include "OutgoingDicomInstance.h" -#include "SimpleInstanceOrdering.h" #include #include @@ -58,12 +63,53 @@ #include /* for variable dcmAllStorageSOPClassUIDs */ #include +#include #if HAVE_MALLOC_TRIM == 1 # include #endif -static size_t DICOM_CACHE_SIZE = static_cast(128) * 1024 * 1024; // 128 MB + +// Those metrics correspond to those found in StorageAccessor in Orthanc <= 1.12.11 +static const char* const METRICS_STORAGE_AREA_CREATE_DURATION = "orthanc_storage_create_duration_ms"; +static const char* const METRICS_STORAGE_AREA_READ_BYTES = "orthanc_storage_read_bytes"; +static const char* const METRICS_STORAGE_AREA_READ_DURATION = "orthanc_storage_read_duration_ms"; +static const char* const METRICS_STORAGE_AREA_REMOVE_DURATION = "orthanc_storage_remove_duration_ms"; +static const char* const METRICS_STORAGE_AREA_WRITTEN_BYTES = "orthanc_storage_written_bytes"; + +static const char* const METRICS_STORAGE_AREA_CACHE_COUNT = "orthanc_storage_cache_count"; +static const char* const METRICS_STORAGE_AREA_CACHE_HIT_COUNT = "orthanc_storage_cache_hit_count"; +static const char* const METRICS_STORAGE_AREA_CACHE_MISS_COUNT = "orthanc_storage_cache_miss_count"; +static const char* const METRICS_STORAGE_AREA_CACHE_SIZE_MB = "orthanc_storage_cache_size_mb"; +static const char* const METRICS_STORAGE_AREA_MEMORY_CAPACITY_MB = "orthanc_storage_memory_capacity_mb"; +static const char* const METRICS_STORAGE_AREA_MEMORY_USAGE_MB = "orthanc_storage_memory_usage_mb"; +static const char* const METRICS_STORAGE_AREA_MEMORY_MAX_USAGE_MB = "orthanc_storage_memory_max_usage_mb"; +static const char* const METRICS_STORAGE_AREA_MEMORY_COUNT = "orthanc_storage_memory_count"; +static const char* const METRICS_STORAGE_THREAD_POOL_AVAILABLE_THREADS = "orthanc_storage_available_threads"; + +static const char* const METRICS_DICOM_PARSER_CACHE_COUNT = "orthanc_dicom_parser_cache_count"; +static const char* const METRICS_DICOM_PARSER_CACHE_HIT_COUNT = "orthanc_dicom_parser_cache_hit_count"; +static const char* const METRICS_DICOM_PARSER_CACHE_MISS_COUNT = "orthanc_dicom_parser_cache_miss_count"; +static const char* const METRICS_DICOM_PARSER_CACHE_SIZE_MB = "orthanc_dicom_parser_cache_size_mb"; +static const char* const METRICS_DICOM_PARSER_MEMORY_CAPACITY_MB = "orthanc_dicom_parser_memory_capacity_mb"; +static const char* const METRICS_DICOM_PARSER_MEMORY_USAGE_MB = "orthanc_dicom_parser_memory_usage_mb"; +static const char* const METRICS_DICOM_PARSER_MEMORY_MAX_USAGE_MB = "orthanc_dicom_parser_memory_max_usage_mb"; +static const char* const METRICS_DICOM_PARSER_MEMORY_COUNT = "orthanc_dicom_parser_memory_count"; +static const char* const METRICS_DICOM_PARSER_THREAD_POOL_AVAILABLE_THREADS = "orthanc_dicom_parser_available_threads"; + +static const char* const METRICS_TRANSCODER_CACHE_COUNT = "orthanc_transcoder_cache_count"; +static const char* const METRICS_TRANSCODER_CACHE_HIT_COUNT = "orthanc_transcoder_cache_hit_count"; +static const char* const METRICS_TRANSCODER_CACHE_MISS_COUNT = "orthanc_transcoder_cache_miss_count"; +static const char* const METRICS_TRANSCODER_CACHE_SIZE_MB = "orthanc_transcoder_cache_size_mb"; +static const char* const METRICS_TRANSCODER_MEMORY_CAPACITY_MB = "orthanc_transcoder_memory_capacity_mb"; +static const char* const METRICS_TRANSCODER_MEMORY_USAGE_MB = "orthanc_transcoder_memory_usage_mb"; +static const char* const METRICS_TRANSCODER_MEMORY_MAX_USAGE_MB = "orthanc_transcoder_memory_max_usage_mb"; +static const char* const METRICS_TRANSCODER_MEMORY_COUNT = "orthanc_transcoder_memory_count"; +static const char* const METRICS_TRANSCODER_THREAD_POOL_AVAILABLE_THREADS = "orthanc_transcoder_available_threads"; + +static const char* const METRICS_SEQ_READER_THREAD_POOL_AVAILABLE_THREADS = "orthanc_seq_reader_available_threads"; + + /** @@ -105,6 +151,97 @@ } + void ServerContext::CreateFile(FileInfo& info, + const void* data, + size_t size, + FileContentType type, + CompressionType compression, + const std::string& precomputedMd5, + const DicomInstanceToStore* instance) + { + assert(metricsRegistry_.get() != NULL); + const std::string uuid = Toolbox::GenerateUuid(); + + std::string md5 = precomputedMd5; + + if (storeMD5_ && + md5.empty()) // if it has not been precomputed, compute it now + { + Toolbox::ComputeMD5(md5, data, size); + } + + std::string customData; + + switch (compression) + { + case CompressionType_None: + { + { + MetricsRegistry::Timer timer(*metricsRegistry_, METRICS_STORAGE_AREA_CREATE_DURATION); + area_.Create(customData, uuid, data, size, type, compression, instance); + } + + metricsRegistry_->IncrementIntegerValue(METRICS_STORAGE_AREA_WRITTEN_BYTES, static_cast(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(compressed.size())); + + info = FileInfo(uuid, type, size, md5, CompressionType_ZlibWithSize, compressed.size(), compressedMD5); + info.SetCustomData(customData); + + try + { + StorageAreaDataSource::StoreIntoCache(*storageAreaReader_, info, compressedData, compressed.size()); + } + catch (OrthancException& e) + { + LOG(WARNING) << "Unable to store a new attachment into the cache: " << e.What(); + } + + return; + } + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + ServerContext::StoreResult::StoreResult() : status_(StoreStatus_Failure), cstoreStatusCode_(0) @@ -361,21 +498,25 @@ } - void ServerContext::PublishCacheMetrics() + static void GetMemoryConfiguration(unsigned int& resultMb, + OrthancConfiguration::ReaderLock& lock, + const char* configurationName) { - metricsRegistry_->SetFloatValue("orthanc_dicom_cache_size_mb", - static_cast(dicomCache_.GetCurrentSize()) / static_cast(1024 * 1024)); - metricsRegistry_->SetIntegerValue("orthanc_dicom_cache_count", - static_cast(dicomCache_.GetNumberOfItems())); - metricsRegistry_->SetFloatValue("orthanc_storage_cache_size_mb", - static_cast(storageCache_.GetCurrentSize()) / static_cast(1024 * 1024)); - metricsRegistry_->SetIntegerValue("orthanc_storage_cache_count", - static_cast(storageCache_.GetNumberOfItems())); + if (!lock.GetConfiguration().LookupUnsignedIntegerParameter(resultMb, configurationName)) + { + resultMb = lock.GetConfiguration().GetUnsignedIntegerParameter(configurationName); + LOG(WARNING) << "====> '" << configurationName << "' is not defined in your configuration, setting it to " << resultMb << ". Depending on the available memory on the system, you might want to adapt this value."; + } + else + { + LOG(WARNING) << "'" << configurationName << "' is set to " << resultMb; + } } ServerContext::ServerContext(IDatabaseWrapper& database, IPluginStorageArea& area, + ServerTranscoder* transcoder /* takes ownership */, bool unitTesting, size_t maxCompletedJobs, bool readOnly) : @@ -384,7 +525,6 @@ compressionEnabled_(false), storeMD5_(true), largeDicomThrottler_(1), - dicomCache_(DICOM_CACHE_SIZE), mainLua_(*this), filterLua_(*this), luaListener_(*this), @@ -400,6 +540,8 @@ isExecuteLuaEnabled_(false), isRestApiWriteToFileSystemEnabled_(false), overwriteInstances_(OverwriteInstancesMode_Never), + transcoder_(transcoder), + transcodeDicomProtocol_(true), isIngestTranscoding_(false), ingestTranscodingOfUncompressed_(true), ingestTranscodingOfCompressed_(true), @@ -419,7 +561,7 @@ mediaArchive_.reset( new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("MediaArchiveSize"))); defaultLocalAet_ = lock.GetConfiguration().GetOrthancAET(); - jobsEngine_.SetWorkersCount(lock.GetConfiguration().GetUnsignedIntegerParameter("ConcurrentJobs")); + jobsEngine_.SetWorkersCount(lock.GetConfiguration().GetConcurrentJobs()); saveJobs_ = lock.GetConfiguration().GetBooleanParameter("SaveJobs"); if (readOnly_ && saveJobs_) @@ -550,6 +692,173 @@ #else LOG(INFO) << "Your platform does not support malloc_trim(), not starting the memory trimming thread"; #endif + + unsigned int storageLoaderThreads, transcoderThreads, dicomParserThreads; + unsigned int storageCacheSizeMb, transcoderCacheSizeMb, dicomParserCacheSizeMb; + unsigned int storageMemoryCapacityMb, transcoderMemoryCapacityMb, dicomParserMemoryCapacityMb; + unsigned int sequentialReaderThreads, sequentialReaderWindowSize, sequentialReaderWindowCapacityMb; + + { + OrthancConfiguration::ReaderLock lock; + unsigned int loaderThreads = lock.GetConfiguration().GetLoaderThreads(); + unsigned int concurrentJobs = lock.GetConfiguration().GetConcurrentJobs(); + storageCacheSizeMb = lock.GetConfiguration().GetMaximumStorageCacheSize(); + + if (!lock.GetConfiguration().LookupUnsignedIntegerParameter(storageLoaderThreads, ORTHANC_CONFIG_STORAGE_LOADER_THREADS)) + { + storageLoaderThreads = loaderThreads * concurrentJobs; + if (storageLoaderThreads < 4) + { + storageLoaderThreads = 4; + LOG(WARNING) << "'" << ORTHANC_CONFIG_STORAGE_LOADER_THREADS << "' is not defined in your configuration, setting it to " << storageLoaderThreads; + } + else + { + storageLoaderThreads = std::min(50u, storageLoaderThreads); + LOG(WARNING) << "'" << ORTHANC_CONFIG_STORAGE_LOADER_THREADS << "' is not defined in your configuration, setting it to " << storageLoaderThreads << ", based on the '" << ORTHANC_CONFIG_CONCURRENT_JOBS << "' and the '" << ORTHANC_CONFIG_LOADER_THREADS << "' configurations capped at 50."; + } + } + else + { + LOG(WARNING) << "'" << ORTHANC_CONFIG_STORAGE_LOADER_THREADS << "' is set to " << storageLoaderThreads; + } + + transcoderThreads = lock.GetConfiguration().GetUnsignedIntegerParameter(ORTHANC_CONFIG_TRANSCODER_THREADS); + LOG(WARNING) << "'" << ORTHANC_CONFIG_TRANSCODER_THREADS << "' is set to " << transcoderThreads; + + dicomParserThreads = lock.GetConfiguration().GetUnsignedIntegerParameter(ORTHANC_CONFIG_DICOM_PARSER_SOURCE_THREADS); + LOG(WARNING) << "'" << ORTHANC_CONFIG_DICOM_PARSER_SOURCE_THREADS << "' is set to " << dicomParserThreads; + + GetMemoryConfiguration(storageMemoryCapacityMb, lock, ORTHANC_CONFIG_STORAGE_MEMORY_CAPACITY); + GetMemoryConfiguration(dicomParserMemoryCapacityMb, lock, ORTHANC_CONFIG_DICOM_PARSER_MEMORY_CAPACITY); + GetMemoryConfiguration(dicomParserCacheSizeMb, lock, ORTHANC_CONFIG_DICOM_PARSER_CACHE_SIZE); + GetMemoryConfiguration(transcoderMemoryCapacityMb, lock, ORTHANC_CONFIG_TRANSCODER_MEMORY_CAPACITY); + GetMemoryConfiguration(transcoderCacheSizeMb, lock, ORTHANC_CONFIG_TRANSCODER_CACHE_SIZE); + + if (!lock.GetConfiguration().LookupUnsignedIntegerParameter(sequentialReaderThreads, ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_THREADS)) + { + LOG(WARNING) << "'" << ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_THREADS << "' is not defined in your configuration, setting it to the same value as '" << ORTHANC_CONFIG_STORAGE_LOADER_THREADS << "': " << storageLoaderThreads; + sequentialReaderThreads = storageLoaderThreads; + } + else + { + LOG(WARNING) << "'" << ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_THREADS << "' is set to " << sequentialReaderThreads; + } + + if (!lock.GetConfiguration().LookupUnsignedIntegerParameter(sequentialReaderWindowSize, ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_WINDOW_SIZE)) + { + LOG(WARNING) << "'" << ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_WINDOW_SIZE << "' is not defined in your configuration, setting it to the same value as '" << ORTHANC_CONFIG_LOADER_THREADS << "': " << loaderThreads; + sequentialReaderWindowSize = loaderThreads; + } + else + { + LOG(WARNING) << "'" << ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_WINDOW_SIZE << "' is set to " << sequentialReaderWindowSize; + } + + GetMemoryConfiguration(sequentialReaderWindowCapacityMb, lock, ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_WINDOW_CAPACITY); + } + + // For streaming + { + boost::shared_ptr 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 source(new StorageAreaDataSource(area_, IsStoreMD5ForAttachments())); + source->SetMetricsRegistry(metricsRegistry_, + METRICS_STORAGE_AREA_READ_BYTES, METRICS_STORAGE_AREA_READ_DURATION); + + storageAreaReader_.reset(new DataSourceReader(pool, source.release())); + storageAreaReader_->SetMetricsConfiguration( + DataSourceReader::MetricsConfiguration(metricsRegistry_, + METRICS_STORAGE_AREA_CACHE_SIZE_MB, + METRICS_STORAGE_AREA_CACHE_COUNT, + METRICS_STORAGE_AREA_CACHE_HIT_COUNT, + METRICS_STORAGE_AREA_CACHE_MISS_COUNT, + METRICS_STORAGE_AREA_MEMORY_CAPACITY_MB, + METRICS_STORAGE_AREA_MEMORY_USAGE_MB, + METRICS_STORAGE_AREA_MEMORY_COUNT, + METRICS_STORAGE_AREA_MEMORY_MAX_USAGE_MB)); + + storageAreaReader_->SetCapacity(storageMemoryCapacityMb * MEGABYTE); + storageAreaReader_->CreateCache(storageCacheSizeMb * MEGABYTE); + + if (storageCacheSizeMb == 0) + { + LOG(WARNING) << "Storage cache is disabled"; + } + else + { + LOG(WARNING) << "Storage cache size is " << storageCacheSizeMb << " MB"; + } + } + + { + boost::shared_ptr pool(new ThreadPool); + pool->SetThreadsCount(dicomParserThreads); + pool->SetLoggingThreadName("DICOM-SRC"); + pool->SetDequeueTimeout(100); + pool->SetMetricsConfiguration(*metricsRegistry_, METRICS_DICOM_PARSER_THREAD_POOL_AVAILABLE_THREADS); + pool->Start(); + + dicomReader_.reset(new DataSourceReader(pool, new DicomDataSource(storageAreaReader_))); + dicomReader_->SetMetricsConfiguration( + DataSourceReader::MetricsConfiguration(metricsRegistry_, + METRICS_DICOM_PARSER_CACHE_SIZE_MB, + METRICS_DICOM_PARSER_CACHE_COUNT, + METRICS_DICOM_PARSER_CACHE_HIT_COUNT, + METRICS_DICOM_PARSER_CACHE_MISS_COUNT, + METRICS_DICOM_PARSER_MEMORY_CAPACITY_MB, + METRICS_DICOM_PARSER_MEMORY_USAGE_MB, + METRICS_DICOM_PARSER_MEMORY_COUNT, + METRICS_DICOM_PARSER_MEMORY_MAX_USAGE_MB)); + + dicomReader_->SetCapacity(dicomParserMemoryCapacityMb * MEGABYTE); + dicomReader_->CreateCache(dicomParserCacheSizeMb * MEGABYTE); + } + + if (transcoder_.get() != NULL) + { + boost::shared_ptr pool(new ThreadPool); + pool->SetThreadsCount(transcoderThreads); + pool->SetLoggingThreadName("TRANSCODER"); + pool->SetDequeueTimeout(100); + pool->SetMetricsConfiguration(*metricsRegistry_, METRICS_TRANSCODER_THREAD_POOL_AVAILABLE_THREADS); + pool->Start(); + + transcoderReader_.reset(new DataSourceReader(pool, new TranscoderDataSource(transcoder_, storageAreaReader_))); + transcoderReader_->SetMetricsConfiguration( + DataSourceReader::MetricsConfiguration(metricsRegistry_, + METRICS_TRANSCODER_CACHE_SIZE_MB, + METRICS_TRANSCODER_CACHE_COUNT, + METRICS_TRANSCODER_CACHE_HIT_COUNT, + METRICS_TRANSCODER_CACHE_MISS_COUNT, + METRICS_TRANSCODER_MEMORY_CAPACITY_MB, + METRICS_TRANSCODER_MEMORY_USAGE_MB, + METRICS_TRANSCODER_MEMORY_COUNT, + METRICS_TRANSCODER_MEMORY_MAX_USAGE_MB)); + + transcoderReader_->SetCapacity(transcoderMemoryCapacityMb * MEGABYTE); + transcoderReader_->CreateCache(transcoderCacheSizeMb * MEGABYTE); + } + + { + std::unique_ptr pool(new ThreadPool); + pool->SetThreadsCount(sequentialReaderThreads); + pool->SetLoggingThreadName("SEQ-READER"); + pool->SetDequeueTimeout(100); + pool->SetMetricsConfiguration(*metricsRegistry_, METRICS_SEQ_READER_THREAD_POOL_AVAILABLE_THREADS); + pool->Start(); + + boost::shared_ptr executor(pool.release()); + dicomSequentialReaderFactory_.reset(new DicomSequentialReader::Factory( + executor, storageAreaReader_, dicomReader_, transcoderReader_, + sequentialReaderWindowSize, + sequentialReaderWindowCapacityMb * MEGABYTE)); + } } catch (OrthancException&) { @@ -656,8 +965,22 @@ FileContentType type, const std::string& customData) { - StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry()); - accessor.Remove(fileUuid, type, customData); + /** + * Note that it is not necessary to explicitly invalidate the + * cache after the removal of an attachment, as each attachment + * receives a unique UUID in ServerContext::CreateFile(), even if + * the DICOM instance is overwritten. The key of the removed + * attachment will thus never be seen again as new files are added. + **/ + + MetricsRegistry::Timer timer(*metricsRegistry_, METRICS_STORAGE_AREA_REMOVE_DURATION); + area_.Remove(fileUuid, type, customData); + } + + + void ServerContext::RemoveFile(const FileInfo& attachment) + { + RemoveFile(attachment.GetUuid(), attachment.GetContentType(), attachment.GetCustomData()); } @@ -718,7 +1041,6 @@ try { MetricsRegistry::Timer timer(GetMetricsRegistry(), "orthanc_store_dicom_duration_ms"); - StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry()); DicomInstanceHasher hasher(summary); resultPublicId = hasher.HashInstance(); @@ -809,12 +1131,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 +1139,9 @@ if (!isAdoption) { - accessor.Write(dicomInfo, dicom.GetBufferData(), dicom.GetBufferSize(), FileContentType_Dicom, compression, dicomMd5, storeMD5_, &dicom); + CreateFile(dicomInfo, dicom.GetBufferData(), dicom.GetBufferSize(), + FileContentType_Dicom, compression, dicomMd5, &dicom); + attachments.push_back(dicomInfo); } else @@ -836,7 +1154,9 @@ (!area_.HasEfficientReadRange() || compressionEnabled_)) { - accessor.Write(dicomUntilPixelData, dicom.GetBufferData(), pixelDataOffset, FileContentType_DicomUntilPixelData, compression, storeMD5_, NULL); + CreateFile(dicomUntilPixelData, dicom.GetBufferData(), pixelDataOffset, FileContentType_DicomUntilPixelData, + compression, "" /* MD5 will be computed if needed */, NULL); + attachments.push_back(dicomUntilPixelData); } @@ -860,11 +1180,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 +1203,12 @@ { if (!isAdoption) { - accessor.Remove(dicomInfo); + RemoveFile(dicomInfo); } if (dicomUntilPixelData.IsValid()) { - accessor.Remove(dicomUntilPixelData); + RemoveFile(dicomUntilPixelData); } } @@ -1092,7 +1412,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 tmp(transcoded.ReleaseAsParsedDicomFile()); @@ -1128,8 +1448,15 @@ const FileInfo& attachment, const std::string& filename) { - StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry()); - accessor.AnswerFile(output, attachment, GetFileContentMime(attachment.GetContentType()), filename); + std::unique_ptr 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 +1483,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 range(ReadAttachment(attachment, true /* uncompress */)); + + CreateFile(modified, range->GetData(), range->GetSize(), attachmentType, compression, + "" /* MD5 will be computed if needed */, NULL); + } try { @@ -1171,13 +1500,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 +1619,14 @@ if (LookupAttachment(attachment, FileContentType_DicomUntilPixelData, instanceAttachments)) { - std::string dicom; + std::unique_ptr 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 +1667,15 @@ * this case cannot be used if "StorageCompression" option is * "true". **/ - - std::string dicom; - + + std::unique_ptr 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 +1688,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 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 +1705,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 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(pixelDataOffset)); @@ -1412,8 +1740,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 +1758,103 @@ } - void ServerContext::ReadDicom(std::string& dicom, - std::string& attachmentId, - const std::string& instancePublicId) - { - std::unique_ptr 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& 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& 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 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(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 +1877,10 @@ // TODO Should we use "gzip" instead? CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None); - StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry()); - assert(attachmentType != FileContentType_Dicom && attachmentType != FileContentType_DicomUntilPixelData); // this method can not be used to store instances FileInfo attachment; - accessor.Write(attachment, data, size, attachmentType, compression, storeMD5_, NULL); + CreateFile(attachment, data, size, attachmentType, compression, "" /* MD5 will be computed if needed */, NULL); try { @@ -1686,7 +1888,7 @@ newRevision, attachment, resourceId, hasOldRevision, oldRevision, oldMD5); if (status != StoreStatus_Success) { - accessor.Remove(attachment); + RemoveFile(attachment); return false; } else @@ -1697,7 +1899,7 @@ catch (OrthancException&) { // Fixed in Orthanc 1.9.6 - accessor.Remove(attachment); + RemoveFile(attachment); throw; } } @@ -1707,24 +1909,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 +2145,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 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 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 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 lock(listenersMutex_); - std::unique_ptr outgoingInstance(OutgoingDicomInstance::CreateFromBuffer(dicom)); + std::unique_ptr outgoingInstance(OutgoingDicomInstance::CreateFromBuffer(dicomData, dicomSize)); outgoingInstance->SetDestination(DicomInstanceDestination(connection.GetParameters().GetRemoteModality().GetHost(), connection.GetParameters().GetRemoteModality().GetApplicationEntityTitle())); @@ -2042,48 +2223,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 syntaxes; - syntaxes.insert(targetSyntax); - - if (GetTranscoder().Transcode(targetDicom, sourceDicom, syntaxes, TranscodingSopInstanceUidMode_AllowNew)) - { - cacheAccessor.AddTranscodedInstance(attachmentId, targetSyntax, reinterpret_cast(targetDicom.GetBufferData()), targetDicom.GetBufferSize()); - target = std::string(reinterpret_cast(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 +2422,54 @@ } - void ServerContext::SetTranscoder(ServerTranscoder* transcoder) + const boost::shared_ptr& ServerContext::GetTranscoder() const { - std::unique_ptr protection(transcoder); - - if (transcoder == NULL) + if (transcoder_.get() != NULL) { - throw OrthancException(ErrorCode_NullPointer); - } - else if (transcoder_.get() != NULL) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); + return transcoder_; } else { - transcoder_.reset(protection.release()); + throw OrthancException(ErrorCode_BadSequenceOfCalls, "No transcoder is available"); } } - ServerTranscoder& ServerContext::GetTranscoder() const + DicomSequentialReader::Factory& ServerContext::GetDicomSequentialReaderFactory() { - if (transcoder_.get() != NULL) - { - return *transcoder_; - } - else - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } + return *dicomSequentialReaderFactory_; } + void ServerContext::GetDataSourcesConfigurations(uint64_t& storageMemoryCapacityMb, + size_t& storageMemoryCacheMb, + unsigned int& storageReaderThreadsCount, + uint64_t& transcoderMemoryCapacityMb, + size_t& transcoderMemoryCacheMb, + unsigned int& transcoderReaderThreadsCount, + uint64_t& dicomParserMemoryCapacityMb, + size_t& dicomParserMemoryCacheMb, + unsigned int& dicomParserThreadsCount) + { + storageMemoryCapacityMb = storageAreaReader_->GetCapacity(); + storageMemoryCacheMb = storageAreaReader_->GetCacheCapacity(); + transcoderMemoryCapacityMb = transcoderReader_->GetCapacity(); + transcoderMemoryCacheMb = transcoderReader_->GetCacheCapacity(); + dicomParserMemoryCapacityMb = dicomReader_->GetCapacity(); + dicomParserMemoryCacheMb = dicomReader_->GetCacheCapacity(); - void ServerContext::ResetTranscoder() - { - if (transcoder_.get() == NULL) { - throw OrthancException(ErrorCode_BadSequenceOfCalls); + boost::shared_ptr service = storageAreaReader_->GetExecutorService(); + storageReaderThreadsCount = dynamic_cast(*service).GetThreadsCount(); } - else + { - transcoder_.reset(NULL); + boost::shared_ptr service = transcoderReader_->GetExecutorService(); + transcoderReaderThreadsCount = dynamic_cast(*service).GetThreadsCount(); + } + + { + boost::shared_ptr service = dicomReader_->GetExecutorService(); + dicomParserThreadsCount = dynamic_cast(*service).GetThreadsCount(); } } } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/ServerContext.h --- a/OrthancServer/Sources/ServerContext.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/ServerContext.h Wed Aug 12 09:54:49 2026 +0200 @@ -28,13 +28,15 @@ #include "OrthancHttpHandler.h" #include "ServerIndex.h" #include "ServerJobs/IStorageCommitmentFactory.h" -#include "ServerTranscoder.h" +#include "../../OrthancFramework/Sources/DataSource/DicomDataSource.h" +#include "../../OrthancFramework/Sources/DataSource/DicomSequentialReader.h" +#include "../../OrthancFramework/Sources/DataSource/StorageAreaDataSource.h" +#include "../../OrthancFramework/Sources/DataSource/TranscoderDataSource.h" #include "../../OrthancFramework/Sources/DicomParsing/DicomModification.h" -#include "../../OrthancFramework/Sources/DicomParsing/ParsedDicomCache.h" -#include "../../OrthancFramework/Sources/FileStorage/StorageAccessor.h" -#include "../../OrthancFramework/Sources/FileStorage/StorageCache.h" #include "../../OrthancFramework/Sources/JobsEngine/JobsEngine.h" +#include "../../OrthancFramework/Sources/MetricsRegistry.h" +#include "../../OrthancFramework/Sources/MultiThreading/Future.h" #include "../../OrthancFramework/Sources/MultiThreading/Semaphore.h" @@ -42,11 +44,13 @@ { class DicomElement; class DicomStoreUserConnection; + class IExecutorService; class OrthancPlugins; + class ServerTranscoder; class SharedArchive; class StorageCommitmentReports; - + /** * This class is responsible for maintaining the storage area on the * filesystem (including compression), as well as the index of the @@ -192,13 +196,11 @@ ServerIndex index_; IPluginStorageArea& area_; - StorageCache storageCache_; bool compressionEnabled_; bool storeMD5_; Semaphore largeDicomThrottler_; // New in Orthanc 1.9.0 (notably for very large DICOM files in WSI) - ParsedDicomCache dicomCache_; LuaScripting mainLua_; LuaScripting filterLua_; @@ -238,7 +240,7 @@ unsigned int limitFindInstances_; unsigned int limitFindResults_; - std::unique_ptr metricsRegistry_; + boost::shared_ptr metricsRegistry_; bool isHttpServerSecure_; bool isExecuteLuaEnabled_; bool isRestApiWriteToFileSystemEnabled_; @@ -246,7 +248,7 @@ std::unique_ptr storageCommitmentReports_; - std::unique_ptr transcoder_; + boost::shared_ptr transcoder_; bool transcodeDicomProtocol_; bool isIngestTranscoding_; DicomTransferSyntax ingestTransferSyntax_; @@ -277,6 +279,17 @@ FileContentType type, const std::string& customData); + void RemoveFile(const FileInfo& attachment); + + // This method corresponds to StorageAccessor::Write() in Orthanc <= 1.12.11 + void CreateFile(FileInfo& info, + const void* data, + size_t size, + FileContentType type, + CompressionType compression, + const std::string& precomputedMd5, + const DicomInstanceToStore* instance); + // This DicomModification object is intended to be used as a // "rules engine" when de-identifying logs for C-Find, C-Get, and // C-Move queries (new in Orthanc 1.8.2) @@ -285,31 +298,16 @@ boost::posix_time::ptime serverStartTimeUtc_; - public: - class DicomCacheLocker : public boost::noncopyable - { - private: - ServerContext& context_; - std::string instancePublicId_; - std::unique_ptr accessor_; - std::unique_ptr dicom_; - size_t dicomSize_; - std::unique_ptr largeDicomLocker_; - std::string buffer_; + // For streaming + boost::shared_ptr storageAreaReader_; + boost::shared_ptr dicomReader_; + boost::shared_ptr transcoderReader_; + boost::shared_ptr dicomSequentialReaderFactory_; - public: - DicomCacheLocker(ServerContext& context, - const std::string& instancePublicId); - - ~DicomCacheLocker(); - - ParsedDicomFile& GetDicom() const; - - const std::string& GetBuffer(); - }; - + public: ServerContext(IDatabaseWrapper& database, IPluginStorageArea& area, + ServerTranscoder* transcoder /* takes ownership */, bool unitTesting, size_t maxCompletedJobs, bool readOnly); @@ -324,11 +322,6 @@ return index_; } - void SetMaximumStorageCacheSize(size_t size) - { - return storageCache_.SetMaximumSize(size); - } - void SetPatientLevelEnabled(bool enabled); bool IsPatientLevelEnabled() const @@ -397,42 +390,26 @@ void ReadDicomAsJson(Json::Value& result, const std::string& instancePublicId); // TODO-FIND: Can this be removed? -private: - void ReadDicomInternal(std::string& dicom, - const std::string& instancePublicId, - std::unique_ptr& 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& largeDicomLocker, - std::size_t largeDicomThreshold); + StorageAreaDataSource::Range* ReadAttachment(const FileInfo& attachment, + bool uncompress); -public: - void ReadDicom(std::string& dicom, - const std::string& instancePublicId); + StorageAreaDataSource::Range* ReadAttachment(const FileInfo& attachment, + const StorageRange& range, + bool uncompress); - void ReadDicom(std::string& dicom, - std::string& attachmentId, - const std::string& instancePublicId); + StorageAreaDataSource::Range* ReadRawDicom(const std::string& instancePublicId); - void ReadDicomForHeader(std::string& dicom, - const std::string& instancePublicId); + DicomDataSource::Dicom* ReadParsedDicom(const std::string& instancePublicId); - bool ReadDicomUntilPixelData(std::string& dicom, - const std::string& instancePublicId); + DicomDataSource::Dicom* ReadDicomUntilPixelData(const std::string& instancePublicId); - // This method is for low-level operations on "/instances/.../attachments/..." - void ReadAttachment(std::string& result, - const FileInfo& attachment, - bool uncompressIfNeeded, - bool skipCache = false); - - void ReadAttachmentRange(std::string& result, - const FileInfo& attachment, - const StorageRange& range, - bool uncompressIfNeeded); + TranscoderDataSource::Transcoded* ReadTranscodedDicom(const std::string& instancePublicId, + DicomTransferSyntax targetSyntax, + TranscodingSopInstanceUidMode mode, + bool hasLossyQuality, + unsigned int lossyQuality); void SetStoreMD5ForAttachments(bool storeMD5); @@ -585,27 +562,15 @@ ImageAccessor* DecodeDicomFrame(const std::string& publicId, unsigned int frameIndex); - ImageAccessor* DecodeDicomFrame(const DicomInstanceToStore& dicom, - unsigned int frameIndex); - - ImageAccessor* DecodeDicomFrame(const void* dicom, - size_t size, - unsigned int frameIndex); - void PerformCStoreWithTranscoding(std::string& sopClassUid, std::string& sopInstanceUid, DicomStoreUserConnection& connection, - const std::string& dicom, + const void* dicomData, + size_t dicomSize, bool hasMoveOriginator, const std::string& moveOriginatorAet, uint16_t moveOriginatorId); - virtual bool TranscodeWithCache(std::string& target, - const std::string& source, - const std::string& sourceInstanceId, - const std::string& attachmentId, // for the storage cache - DicomTransferSyntax targetSyntax); - bool IsTranscodeDicomProtocol() const { return transcodeDicomProtocol_; @@ -635,12 +600,23 @@ int64_t GetServerUpTime() const; - void PublishCacheMetrics(); + /** + * The method "GetTranscoder()" should only be used when + * necessary. The use of "TranscoderDataSource::Execute()" is + * preferred to benefit from cache and from backpressure. + **/ + const boost::shared_ptr& GetTranscoder() const; - void SetTranscoder(ServerTranscoder* transcoder /* takes ownership */); + DicomSequentialReader::Factory& GetDicomSequentialReaderFactory(); - ServerTranscoder& GetTranscoder() const; - - void ResetTranscoder(); + void GetDataSourcesConfigurations(uint64_t& storageMemoryCapacityMb, + size_t& storageMemoryCacheMb, + unsigned int& storageReaderThreadsCount, + uint64_t& transcoderMemoryCapacityMb, + size_t& transcoderMemoryCacheMb, + unsigned int& transcoderReaderThreadsCount, + uint64_t& dicomParserMemoryCapacityMb, + size_t& dicomParserMemoryCacheMb, + unsigned int& dicomParserThreadsCount); }; } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/ServerIndex.cpp diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/ServerJobs/ArchiveJob.cpp --- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -27,6 +27,7 @@ #include "../../../OrthancFramework/Sources/Cache/SharedArchive.h" #include "../../../OrthancFramework/Sources/Compression/HierarchicalZipWriter.h" #include "../../../OrthancFramework/Sources/Constants.h" +#include "../../../OrthancFramework/Sources/DataSource/DicomSequentialReader.h" #include "../../../OrthancFramework/Sources/DicomParsing/DicomDirWriter.h" #include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" #include "../../../OrthancFramework/Sources/Logging.h" @@ -37,7 +38,6 @@ #include "../OrthancConfiguration.h" #include "../ServerContext.h" #include "../SimpleInstanceOrdering.h" -#include "ThreadedInstancesLoader.h" #include #include @@ -476,12 +476,12 @@ { } - virtual void Preload(ThreadedInstancesLoader& instancesLoader) const = 0; + virtual void Preload(DicomSequentialReader& instancesLoader) const = 0; // In the "archive" flavor (without DICOMDIR), dicomDir is NULL // In the "media" flavor (with DICOMDIR), dicomDir is not NULL virtual void Execute(HierarchicalZipWriter& writer, - ThreadedInstancesLoader& instancesLoader, + DicomSequentialReader& instancesLoader, DicomDirWriter* dicomDir) const = 0; }; @@ -495,17 +495,17 @@ std::string filename_; public: - OpenDirectoryCommand(const std::string& filename) : + explicit OpenDirectoryCommand(const std::string& filename) : filename_(filename) { } - virtual void Preload(ThreadedInstancesLoader& instancesLoader) const ORTHANC_OVERRIDE + virtual void Preload(DicomSequentialReader& instancesLoader) const ORTHANC_OVERRIDE { } virtual void Execute(HierarchicalZipWriter& writer, - ThreadedInstancesLoader& instancesLoader, + DicomSequentialReader& instancesLoader, DicomDirWriter* dicomDir) const ORTHANC_OVERRIDE { writer.OpenDirectory(filename_); @@ -516,12 +516,12 @@ class CloseDirectoryCommand : public IZipCommand { public: - virtual void Preload(ThreadedInstancesLoader& instancesLoader) const ORTHANC_OVERRIDE + virtual void Preload(DicomSequentialReader& instancesLoader) const ORTHANC_OVERRIDE { } virtual void Execute(HierarchicalZipWriter& writer, - ThreadedInstancesLoader& instancesLoader, + DicomSequentialReader& instancesLoader, DicomDirWriter* dicomDir) const ORTHANC_OVERRIDE { writer.CloseDirectory(); @@ -551,36 +551,43 @@ dicomDirFolder == MEDIA_IMAGES_FOLDER); } - virtual void Preload(ThreadedInstancesLoader& instancesLoader) const ORTHANC_OVERRIDE + virtual void Preload(DicomSequentialReader& instancesLoader) const ORTHANC_OVERRIDE { - instancesLoader.PreloadDicomInstance(instanceId_, fileInfo_); + instancesLoader.Submit(fileInfo_); } virtual void Execute(HierarchicalZipWriter& writer, - ThreadedInstancesLoader& instancesLoader, + DicomSequentialReader& instancesLoader, DicomDirWriter* dicomDir) const ORTHANC_OVERRIDE { - std::string content; - - try + if (!instancesLoader.HasNext()) { - LOG(INFO) << "Adding instance " << instanceId_ << " in zip"; - instancesLoader.WaitDicomInstance(content, instanceId_); + throw OrthancException(ErrorCode_InternalError); } - catch (OrthancException& e) + else { - LOG(WARNING) << "An instance was removed after the job was issued: " << instanceId_; - return; - } - - writer.OpenFile(filename_); + std::unique_ptr 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 parsed(new ParsedDicomFile(content)); - dicomDir->Add(dicomDirFolder_, filename_, *parsed); + const IMemoryBuffer& dicom = next->GetRawMemoryBuffer(); + + writer.OpenFile(filename_); + writer.Write(dicom.GetData(), dicom.GetSize()); + + if (dicomDir != NULL) + { + dicomDir->Add(dicomDirFolder_, filename_, next->GetParsedDicomFile()); + } } } }; @@ -961,7 +968,7 @@ return commands_.GetSize() + 1; } - void PreloadAllCommands(ThreadedInstancesLoader& instancesLoader) + void PreloadAllCommands(DicomSequentialReader& instancesLoader) { for (size_t i = 0; i < commands_.GetSize(); i++) { @@ -969,7 +976,7 @@ } } - void RunStep(ThreadedInstancesLoader& instancesLoader, + void RunStep(DicomSequentialReader& instancesLoader, size_t index) { if (index > commands_.GetSize()) @@ -1223,8 +1230,19 @@ instancesCount_ = writer_->GetInstancesCount(); uncompressedSize_ = writer_->GetUncompressedSize(); - instancesLoader_.reset(new ThreadedInstancesLoader(context_, loaderThreads_, transcode_, transferSyntax_, lossyQuality_, "ARCH")); + if (transcode_) + { + instancesLoader_.reset(context_.GetDicomSequentialReaderFactory().CreateForTranscodedRawMemoryBuffer( + transferSyntax_, TranscodingSopInstanceUidMode_AllowNew, hasLossyQuality_, lossyQuality_)); + } + else + { + instancesLoader_.reset(context_.GetDicomSequentialReaderFactory().CreateForOriginalRawMemoryBuffer()); + } + writer_->PreloadAllCommands(*instancesLoader_); + + instancesLoader_->Start(); } } @@ -1264,10 +1282,7 @@ writer_.reset(); } - if (instancesLoader_.get() != NULL) - { - instancesLoader_->Clear(false); - } + instancesLoader_.reset(NULL); if (asynchronousTarget_.get() != NULL) { @@ -1336,10 +1351,7 @@ asynchronousTarget_.reset(); // clear the loader threads - if (instancesLoader_.get() != NULL) - { - instancesLoader_->Clear(true); - } + instancesLoader_.reset(NULL); } } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/ServerJobs/ArchiveJob.h --- a/OrthancServer/Sources/ServerJobs/ArchiveJob.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.h Wed Aug 12 09:54:49 2026 +0200 @@ -33,9 +33,9 @@ namespace Orthanc { + class DicomSequentialReader; class ServerContext; - class ThreadedInstancesLoader; - + class ArchiveJob : public IJob { private: @@ -51,7 +51,7 @@ std::unique_ptr synchronousTarget_; // Only valid before "Start()" std::unique_ptr asynchronousTarget_; ServerContext& context_; - std::unique_ptr instancesLoader_; + std::unique_ptr instancesLoader_; boost::shared_ptr archive_; bool isMedia_; bool enableExtendedSopClass_; diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp --- a/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -48,19 +48,14 @@ assert(IsStarted()); OpenConnection(); - if (instancesLoader_.get() == NULL) - { - StartLoaderThreads(); - } - LOG(INFO) << "Sending instance " << instance << " to modality \"" << parameters_.GetRemoteModality().GetApplicationEntityTitle() << "\""; - std::string dicom; + std::unique_ptr 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_) diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.h --- a/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.h Wed Aug 12 09:54:49 2026 +0200 @@ -56,11 +56,6 @@ virtual bool HandleTrailingStep() ORTHANC_OVERRIDE; - virtual const char* GetLoaderPrefix() const ORTHANC_OVERRIDE - { - return "CSTO"; - } - public: explicit DicomModalityStoreJob(ServerContext& context); diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp --- a/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -97,8 +97,8 @@ try { - ServerContext::DicomCacheLocker locker(GetContext(), instance); - modified.reset(locker.GetDicom().Clone(true)); + std::unique_ptr 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 loadedDicom(GetContext().ReadParsedDicom(instances.front())); + DicomDataSource::Dicom::Lock lock(*loadedDicom); + + OrthancConfiguration::DefaultExtractDicomSummary(dicom, lock.GetContent()); } const std::set moduleTags = removals_; diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/ServerJobs/Operations/DicomInstanceOperationValue.cpp --- a/OrthancServer/Sources/ServerJobs/Operations/DicomInstanceOperationValue.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/ServerJobs/Operations/DicomInstanceOperationValue.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -30,6 +30,7 @@ { void DicomInstanceOperationValue::ReadDicom(std::string& dicom) const { - context_.ReadDicom(dicom, id_); + std::unique_ptr raw(context_.ReadRawDicom(id_)); + raw->Copy(dicom); } } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/ServerJobs/Operations/ModifyInstanceOperation.cpp --- a/OrthancServer/Sources/ServerJobs/Operations/ModifyInstanceOperation.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/ServerJobs/Operations/ModifyInstanceOperation.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -87,8 +87,8 @@ std::unique_ptr modified; { - ServerContext::DicomCacheLocker lock(context_, instance.GetId()); - modified.reset(lock.GetDicom().Clone(true)); + std::unique_ptr dicom(context_.ReadParsedDicom(instance.GetId())); + modified.reset(dicom->Clone()); } try diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/ServerJobs/Operations/StoreScuOperation.cpp --- a/OrthancServer/Sources/ServerJobs/Operations/StoreScuOperation.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/ServerJobs/Operations/StoreScuOperation.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -55,7 +55,8 @@ instance.ReadDicom(dicom); std::string sopClassUid, sopInstanceUid; // Unused - context_.PerformCStoreWithTranscoding(sopClassUid, sopInstanceUid, lock.GetConnection(), dicom, + context_.PerformCStoreWithTranscoding(sopClassUid, sopInstanceUid, lock.GetConnection(), + dicom.empty() ? NULL : dicom.c_str(), dicom.size(), false /* Not a C-MOVE */, "", 0); } catch (OrthancException& e) diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp --- a/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -29,6 +29,7 @@ #include "../../../OrthancFramework/Sources/Logging.h" #include "../../../OrthancFramework/Sources/SerializationToolbox.h" #include "../ServerContext.h" +#include "../ServerTranscoder.h" #include @@ -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 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(transcoded.GetBufferData()), transcoded.GetBufferSize()); } else { - body.swap(dicom); + dicom.CopyToString(body); } } else { - body.swap(dicom); + dicom.CopyToString(body); } } catch (OrthancException& e) diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.h --- a/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.h Wed Aug 12 09:54:49 2026 +0200 @@ -49,11 +49,6 @@ virtual bool HandleTrailingStep() ORTHANC_OVERRIDE; - virtual const char* GetLoaderPrefix() const ORTHANC_OVERRIDE - { - return "PSTO"; - } - public: explicit OrthancPeerStoreJob(ServerContext& context) : StoreJob(context), diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp --- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -24,10 +24,12 @@ #include "../PrecompiledHeadersServer.h" #include "ResourceModificationJob.h" +#include "../../../OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h" #include "../../../OrthancFramework/Sources/Logging.h" #include "../../../OrthancFramework/Sources/SerializationToolbox.h" #include "../DicomInstanceToStore.h" #include "../ServerContext.h" +#include "../ServerTranscoder.h" #include #include @@ -185,8 +187,10 @@ { for (std::set::const_iterator it = instancesToReconstruct_.begin(); it != instancesToReconstruct_.end(); ++it) { - ServerContext::DicomCacheLocker locker(GetContext(), *it); - const ParsedDicomFile& modifiedDicom = locker.GetDicom(); + std::unique_ptr 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 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()); diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp --- a/OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -72,8 +72,8 @@ try { - ServerContext::DicomCacheLocker locker(GetContext(), instance); - modified.reset(locker.GetDicom().Clone(true)); + std::unique_ptr dicom(GetContext().ReadParsedDicom(instance)); + modified.reset(dicom->Clone()); } catch (OrthancException&) { diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp --- a/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -290,9 +290,12 @@ // Make sure that the DICOM file can be re-read by DCMTK // from the file storage, and that the actual SOP // class/instance UIDs do match - ServerContext::DicomCacheLocker locker(context_, orthancId[0]); - if (locker.GetDicom().GetTagValue(a, DICOM_TAG_SOP_CLASS_UID) && - locker.GetDicom().GetTagValue(b, DICOM_TAG_SOP_INSTANCE_UID) && + + std::unique_ptr 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]) diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/ServerJobs/StoreJob.cpp --- a/OrthancServer/Sources/ServerJobs/StoreJob.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/ServerJobs/StoreJob.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -35,6 +35,24 @@ namespace Orthanc { + class StoreJob::ReaderUserData : public IDynamicObject + { + private: + std::string instanceId_; + + public: + explicit ReaderUserData(const std::string& instanceId) : + instanceId_(instanceId) + { + } + + const std::string& GetInstanceId() const + { + return instanceId_; + } + }; + + StoreJob::StoreJob(ServerContext& context) : context_(context) { @@ -63,6 +81,51 @@ } + DicomSequentialReader::Item* StoreJob::WaitDicomInstance(const std::string& instanceId) + { + assert(IsStarted()); + + if (instancesLoader_.get() == NULL) + { + instancesLoader_.reset(context_.GetDicomSequentialReaderFactory().CreateForOriginalRawMemoryBuffer()); + + const size_t position = GetPosition(); // in case we reload a job in progress, we shall not preload the first instances + + if (instanceId != instancesIds_[position]) + { + throw OrthancException(ErrorCode_InternalError); + } + + for (size_t i = position; i < instancesIds_.size(); ++i) + { + instancesLoader_->Submit(filesInfo_[i], new ReaderUserData(instancesIds_[i])); + } + + instancesLoader_->Start(); + } + + if (!instancesLoader_->HasNext()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + std::unique_ptr 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(next->GetUserData()); + if (userData.GetInstanceId() != instanceId) + { + throw OrthancException(ErrorCode_InternalError); + } + } + + return next.release(); + } + + void StoreJob::AddInstances(const std::vector& instancesIds, const std::vector& 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) { diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/ServerJobs/StoreJob.h --- a/OrthancServer/Sources/ServerJobs/StoreJob.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/ServerJobs/StoreJob.h Wed Aug 12 09:54:49 2026 +0200 @@ -24,27 +24,30 @@ #pragma once #include "../../../OrthancFramework/Sources/Compatibility.h" -#include "../../../OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h" +#include "../../../OrthancFramework/Sources/DataSource/DicomSequentialReader.h" #include "../../../OrthancFramework/Sources/FileStorage/FileInfo.h" -#include "ThreadedInstancesLoader.h" +#include "../../../OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h" #include namespace Orthanc { + class DicomSequentialReader; class ServerContext; class StoreJob : public SetOfInstancesJob { + private: + class ReaderUserData; + + std::unique_ptr instancesLoader_; + std::vector instancesIds_; + std::vector filesInfo_; + protected: - ServerContext& context_; - std::unique_ptr instancesLoader_; - std::vector instancesIds_; - std::vector filesInfo_; + ServerContext& context_; - virtual const char* GetLoaderPrefix() const = 0; - - void StartLoaderThreads(); + DicomSequentialReader::Item* WaitDicomInstance(const std::string& instanceId); public: explicit StoreJob(ServerContext& context); diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/ServerJobs/ThreadedInstancesLoader.cpp --- a/OrthancServer/Sources/ServerJobs/ThreadedInstancesLoader.cpp Wed Aug 12 09:51:40 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 . - **/ - -#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(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(dynamic_cast(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 dicomContent(new std::string()); - that->context_.ReadAttachment(*dicomContent, instanceToPreload->GetFileInfo(), true); - - if (that->transcode_) - { - boost::shared_ptr 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(); - 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(); - 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 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 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(transcoded.GetBufferData()), transcoded.GetBufferSize()); - return true; - } - else - { - LOG(INFO) << "Cannot transcode instance " << instanceId - << " to transfer syntax: " << GetTransferSyntaxUid(transferSyntax_); - } - } - - return false; - } -} diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/ServerJobs/ThreadedInstancesLoader.h --- a/OrthancServer/Sources/ServerJobs/ThreadedInstancesLoader.h Wed Aug 12 09:51:40 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 . - **/ - -#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 -#include - - -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 > availableInstances_; - boost::mutex availableInstancesMutex_; - Semaphore availableInstancesSemaphore_; - BlockingSharedMessageQueue instancesToPreload_; - std::vector 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); - }; -} diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/ServerToolbox.cpp --- a/OrthancServer/Sources/ServerToolbox.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/ServerToolbox.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -24,9 +24,8 @@ #include "PrecompiledHeadersServer.h" #include "ServerToolbox.h" +#include "../../OrthancFramework/Sources/Compression/ZlibCompressor.h" #include "../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h" -#include "../../OrthancFramework/Sources/FileStorage/StorageAccessor.h" -#include "../../OrthancFramework/Sources/FileStorage/StorageCache.h" #include "../../OrthancFramework/Sources/Logging.h" #include "../../OrthancFramework/Sources/OrthancException.h" #include "Database/IDatabaseWrapper.h" @@ -96,6 +95,36 @@ } + // This corresponds to StorageAccessor::Read() in Orthanc <= 1.12.11 + static void ReadFile(std::string& content, + IPluginStorageArea& storageArea, + const FileInfo& info) + { + switch (info.GetCompressionType()) + { + case CompressionType_None: + { + std::unique_ptr 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 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 dicom(context.ReadParsedDicom(instances.front())); + DicomDataSource::Dicom::Lock lock(*dicom); + + context.GetIndex().ReconstructInstance(lock.GetContent(), true, limitToLevel); } else { for (std::list::const_iterator it = instances.begin(); it != instances.end(); ++it) { - ServerContext::DicomCacheLocker locker(context, *it); + std::unique_ptr 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 clone(lock.GetContent().Clone(true)); + std::string resultPublicId; // ignored - std::unique_ptr dicomInstancetoStore(DicomInstanceToStore::CreateFromParsedDicomFile(locker.GetDicom())); + std::unique_ptr dicomInstancetoStore(DicomInstanceToStore::CreateFromParsedDicomFile(*clone)); // TODO: TranscodeAndStore and specifically ServerIndex::Store have been "poluted" by the isReconstruct parameter // we should very likely refactor it diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/ServerTranscoder.cpp --- a/OrthancServer/Sources/ServerTranscoder.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/ServerTranscoder.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -24,22 +24,92 @@ #include "PrecompiledHeadersServer.h" #include "ServerTranscoder.h" +#include "../../OrthancFramework/Sources/Cache/LeastRecentlyUsedIndex.h" +#include "../../OrthancFramework/Sources/DataSource/DicomDataSource.h" +#include "../../OrthancFramework/Sources/DataSource/StorageAreaDataSource.h" +#include "../../OrthancFramework/Sources/DataSource/TranscoderDataSource.h" #include "../../OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.h" #include "../../OrthancFramework/Sources/DicomFormat/DicomImageInformation.h" #include "../../OrthancFramework/Sources/Logging.h" #include "../../OrthancFramework/Sources/OrthancException.h" #include "../Plugins/Engine/OrthancPlugins.h" +#include "DicomInstanceToStore.h" #include "OrthancConfiguration.h" +#include #include +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 workingSources_; + + public: + PImpl() + { + } + + WorkingSource LookupWorkingSource(const std::string& attachmentId) + { + boost::mutex::scoped_lock lock(mutex_); + + WorkingSource source; + if (workingSources_.Contains(attachmentId, source)) + { + return source; + } + else + { + return WorkingSource_Unknown; + } + } + + void SetWorkingSource(const std::string& attachmentId, + WorkingSource source) + { + if (source == WorkingSource_Unknown) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + boost::mutex::scoped_lock lock(mutex_); + + assert(MAX_CACHE_SIZE > 0); + + if (workingSources_.GetSize() >= MAX_CACHE_SIZE && + !workingSources_.Contains(attachmentId)) + { + workingSources_.RemoveOldest(); + } + + workingSources_.AddOrMakeMostRecent(attachmentId, source); + } + } + }; + ServerTranscoder::ServerTranscoder(unsigned int maxConcurrentDcmtkTranscoder) : #if ORTHANC_ENABLE_PLUGINS == 1 plugins_(NULL), #endif + pimpl_(new PImpl), dcmtkTranscoder_(new DcmtkTranscoder(maxConcurrentDcmtkTranscoder)), builtinDecoderTranscoderOrder_(BuiltinDecoderTranscoderOrder_After) { @@ -68,14 +138,164 @@ #endif - ImageAccessor* ServerTranscoder::DecodeFrame(const ParsedDicomFile& parsedDicom, - const void* buffer, - size_t size, + bool ServerTranscoder::HasPluginsDecoder() const + { +#if ORTHANC_ENABLE_PLUGINS == 1 + return (plugins_ != NULL && + plugins_->HasCustomImageDecoder()); +#else + return false; +#endif + } + + + bool ServerTranscoder::HasPluginsTranscoder() const + { +#if ORTHANC_ENABLE_PLUGINS == 1 + return (plugins_ != NULL && + plugins_->HasCustomTranscoder()); +#else + return false; +#endif + } + + + ImageAccessor* ServerTranscoder::DecodeFrameBuiltin(const ParsedDicomFile& dicom, + unsigned int frameIndex) + { + // Use Orthanc's built-in decoder + + try + { + return dicom.DecodeFrame(frameIndex); + } + catch (const OrthancException&) // NOLINT(bugprone-empty-catch) + { + } + + return NULL; + } + + + ImageAccessor* ServerTranscoder::DecodeFrameUsingPluginsDecoder(const void* buffer, + size_t size, + unsigned int frameIndex) + { +#if ORTHANC_ENABLE_PLUGINS == 1 + if (HasPluginsDecoder()) + { + try + { + return plugins_->Decode(buffer, size, frameIndex); + } + catch (const OrthancException&) // NOLINT(bugprone-empty-catch) + { + } + } +#endif + + return NULL; + } + + + ImageAccessor* ServerTranscoder::DecodeFrameUsingPluginsTranscoder(const void* buffer, + size_t size, + unsigned int frameIndex) + { + /** + * Contrarily to "ServerTranscoder::Transcode()", this method + * doesn't use "dcmtkTranscoder_", as "DecodeFrameBuiltin()" would + * have succeeded in this case. + **/ + +#if ORTHANC_ENABLE_PLUGINS == 1 + if (HasPluginsTranscoder()) + { + DicomImage transcoded; + DicomImage source; + std::set allowedSyntaxes; + + source.SetExternalBuffer(buffer, size); + allowedSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit); + + if (plugins_->Transcode(transcoded, source, allowedSyntaxes, TranscodingSopInstanceUidMode_AllowNew)) + { + try + { + std::unique_ptr dicom(transcoded.ReleaseAsParsedDicomFile()); + return DecodeFrameBuiltin(*dicom, frameIndex); + } + catch (const OrthancException&) // NOLINT(bugprone-empty-catch) + { + } + } + } +#endif + + return NULL; + } + + + ImageAccessor* ServerTranscoder::DecodeFrameBuiltin(const boost::shared_ptr& dicomReader, + const FileInfo& attachment, + unsigned int frameIndex) + { + std::unique_ptr dicom( + DicomDataSource::Execute(*dicomReader, DicomDataSource::CreateWholeRequest(attachment))); + + DicomDataSource::Dicom::Lock lock(*dicom); + + return DecodeFrameBuiltin(lock.GetContent(), frameIndex); + } + + + ImageAccessor* ServerTranscoder::DecodeFrameUsingPluginsDecoder(const boost::shared_ptr& storageAreaReader, + const FileInfo& attachment, + unsigned int frameIndex) + { + if (!HasPluginsDecoder()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + std::unique_ptr range( + StorageAreaDataSource::Execute( + *storageAreaReader, StorageAreaDataSource::CreateAttachmentRequest(attachment, true /* uncompress */))); + + return DecodeFrameUsingPluginsDecoder(range->GetData(), range->GetSize(), frameIndex); + } + } + + + ImageAccessor* ServerTranscoder::DecodeFrameUsingPluginsTranscoder(const boost::shared_ptr& transcoderReader, + const FileInfo& attachment, + unsigned int frameIndex) + { + if (!HasPluginsDecoder()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + std::unique_ptr transcoded( + TranscoderDataSource::Execute(*transcoderReader, TranscoderDataSource::CreateRequest( + attachment, DicomTransferSyntax_LittleEndianExplicit, + TranscodingSopInstanceUidMode_AllowNew, false /* no lossy quality specified */, 0))); + + TranscoderDataSource::Transcoded::LockAsParsed lock(*transcoded); + + return DecodeFrameBuiltin(lock.GetContent(), frameIndex); + } + } + + + ImageAccessor* ServerTranscoder::DecodeFrame(const DicomInstanceToStore& image, unsigned int frameIndex) { { // check that the target image has a valid/reasonable size before decoding to avoid possible crash or OOB during transcoding DicomMap summary; - parsedDicom.ExtractDicomSummary(summary, 128); + image.GetSummary(summary); DicomImageInformation imageInfo(summary); imageInfo.ThrowIfInvalidFrameSize(); @@ -85,83 +305,123 @@ if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before) { - // Use Orthanc's built-in decoder - - try + decoded.reset(DecodeFrameBuiltin(image.GetParsedDicomFile(), frameIndex)); + if (decoded.get() != NULL) { - decoded.reset(parsedDicom.DecodeFrame(frameIndex)); - if (decoded.get() != NULL) - { - return decoded.release(); - } - } - catch (const OrthancException&) // NOLINT(bugprone-empty-catch) - { // ignore, we'll try other alternatives + return decoded.release(); } } -#if ORTHANC_ENABLE_PLUGINS == 1 - if (plugins_ != NULL && - plugins_->HasCustomImageDecoder()) + decoded.reset(DecodeFrameUsingPluginsDecoder(image.GetBufferData(), image.GetBufferSize(), frameIndex)); + if (decoded.get() != NULL) { - try - { - decoded.reset(plugins_->Decode(buffer, size, frameIndex)); - } - catch (const OrthancException&) // NOLINT(bugprone-empty-catch) - { - } + return decoded.release(); + } + if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After) + { + decoded.reset(DecodeFrameBuiltin(image.GetParsedDicomFile(), frameIndex)); if (decoded.get() != NULL) { return decoded.release(); } - else if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After) + } + + decoded.reset(DecodeFrameUsingPluginsTranscoder(image.GetBufferData(), image.GetBufferSize(), frameIndex)); + if (decoded.get() != NULL) + { + return decoded.release(); + } + + return NULL; + } + + + ImageAccessor* ServerTranscoder::DecodeFrame(const boost::shared_ptr& dicomReader, // For built-in decoding + const boost::shared_ptr& storageAreaReader, // For plugin-based decoding + const boost::shared_ptr& 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 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 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 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; } diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/ServerTranscoder.h --- a/OrthancServer/Sources/ServerTranscoder.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/ServerTranscoder.h Wed Aug 12 09:54:49 2026 +0200 @@ -24,12 +24,18 @@ #pragma once #include "../../OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h" +#include "../../OrthancFramework/Sources/FileStorage/FileInfo.h" #include "../../OrthancFramework/Sources/Images/ImageAccessor.h" #include "ServerEnumerations.h" +#include + namespace Orthanc { + class DataSourceReader; + class DicomInstanceToStore; + #if ORTHANC_ENABLE_PLUGINS == 1 class OrthancPlugins; #endif @@ -37,23 +43,58 @@ class ServerTranscoder : public IDicomTranscoder { private: + class PImpl; + #if ORTHANC_ENABLE_PLUGINS == 1 OrthancPlugins* plugins_; #endif + boost::shared_ptr pimpl_; std::unique_ptr 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& dicomReader, + const FileInfo& attachment, + unsigned int frameIndex); + + ImageAccessor* DecodeFrameUsingPluginsDecoder(const boost::shared_ptr& storageAreaReader, + const FileInfo& attachment, + unsigned int frameIndex); + + ImageAccessor* DecodeFrameUsingPluginsTranscoder(const boost::shared_ptr& transcoderReader, + const FileInfo& attachment, + unsigned int frameIndex); + public: - ServerTranscoder(unsigned int maxConcurrentDcmtkTranscoder); + explicit ServerTranscoder(unsigned int maxConcurrentDcmtkTranscoder); #if ORTHANC_ENABLE_PLUGINS == 1 void SetPlugins(OrthancPlugins& plugins); #endif - ImageAccessor* DecodeFrame(const ParsedDicomFile& parsedDicom, - const void* buffer, // buffer that is the source of the ParsedDicomFile - size_t size, + bool HasPluginsDecoder() const; + + bool HasPluginsTranscoder() const; + + // This version should only be used by plugins + ImageAccessor* DecodeFrame(const DicomInstanceToStore& image, + unsigned int frameIndex); + + ImageAccessor* DecodeFrame(const boost::shared_ptr& dicomReader, // For built-in decoding + const boost::shared_ptr& storageAreaReader, // For plugin-based decoding + const boost::shared_ptr& transcoderReader, // For transcoding-based decoding + const FileInfo& attachment, unsigned int frameIndex); // This method can be used even if the global option diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/Sources/main.cpp --- a/OrthancServer/Sources/main.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/Sources/main.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -53,11 +53,14 @@ #include "ServerJobs/DicomRetrieveScuBaseJob.h" #include "ServerJobs/StorageCommitmentScpJob.h" #include "ServerToolbox.h" +#include "ServerTranscoder.h" #include "StorageCommitmentReports.h" #include #include +#include // For STATUS_STORE_Error_CannotUnderstand + #if defined(_WIN32) || defined(__CYGWIN__) #include #endif @@ -1087,7 +1090,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 httpBindAddresses; lock.GetConfiguration().GetSetOfStringsParameter(httpBindAddresses, "HttpBindAddresses"); @@ -1360,7 +1363,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 +1585,14 @@ lock.GetConfiguration().SetServerIndex(context.GetIndex()); } - std::unique_ptr 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 +1609,6 @@ context_.ResetPlugins(); } #endif - - context_.ResetTranscoder(); } }; } @@ -1688,8 +1684,14 @@ DicomAssociationParameters::SetDefaultRemoteCertificateRequired( lock.GetConfiguration().GetBooleanParameter(KEY_DICOM_TLS_REMOTE_CERTIFICATE_REQUIRED)); } + + std::unique_ptr 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 +1777,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 +1991,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 +2027,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 +2129,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 orderSensitiveArguments; @@ -2179,7 +2165,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 +2399,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& inputs = options[OPTION_INPUTS].as >(); diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/UnitTestsSources/ServerConfigTests.cpp --- a/OrthancServer/UnitTestsSources/ServerConfigTests.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/UnitTestsSources/ServerConfigTests.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -39,7 +39,7 @@ PluginStorageAreaAdapter storage(new MemoryStorageArea); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); - ServerContext context(db, storage, true /* running unit tests */, 10, false); + ServerContext context(db, storage, NULL /* no transcoder */, true /* running unit tests */, 10, false); { // default config -> all SOP Classes should be accepted std::set s; diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/UnitTestsSources/ServerIndexTests.cpp --- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -730,7 +730,7 @@ PluginStorageAreaAdapter storage(new FilesystemStorage(path)); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); - ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */); + ServerContext context(db, storage, NULL /* no transcoder */, true /* running unit tests */, 10, false /* readonly */); context.SetupJobsEngine(true, false); ServerIndex& index = context.GetIndex(); @@ -812,7 +812,7 @@ PluginStorageAreaAdapter storage(new FilesystemStorage(path)); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); - ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */); + ServerContext context(db, storage, NULL /* no transcoder */, true /* running unit tests */, 10, false /* readonly */); context.SetupJobsEngine(true, false); ServerIndex& index = context.GetIndex(); @@ -935,7 +935,7 @@ PluginStorageAreaAdapter storage(new MemoryStorageArea); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); - ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */); + ServerContext context(db, storage, NULL /* no transcoder */, true /* running unit tests */, 10, false /* readonly */); context.SetupJobsEngine(true, false); context.SetCompressionEnabled(true); @@ -1002,9 +1002,11 @@ ASSERT_EQ("name", tmp["0010,0010"]["Value"].asString()); { - ServerContext::DicomCacheLocker locker(context, id); + std::unique_ptr 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 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 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); diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/UnitTestsSources/ServerJobsTests.cpp --- a/OrthancServer/UnitTestsSources/ServerJobsTests.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -566,7 +566,7 @@ storage_(new MemoryStorageArea) { db_.Open(); - context_.reset(new ServerContext(db_, storage_, true /* running unit tests */, 10, false /* readonly */)); + context_.reset(new ServerContext(db_, storage_, NULL /* no transcoder */, true /* running unit tests */, 10, false /* readonly */)); context_->SetupJobsEngine(true, false); } @@ -1021,9 +1021,11 @@ std::string study, series; { - ServerContext::DicomCacheLocker lock(GetContext(), instance); - study = lock.GetDicom().GetHasher().HashStudy(); - series = lock.GetDicom().GetHasher().HashSeries(); + std::unique_ptr dicom(GetContext().ReadParsedDicom(instance)); + DicomDataSource::Dicom::Lock lock(*dicom); + + study = lock.GetContent().GetHasher().HashStudy(); + series = lock.GetContent().GetHasher().HashSeries(); } { diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/UnitTestsSources/SizeOfTests.cpp --- a/OrthancServer/UnitTestsSources/SizeOfTests.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/UnitTestsSources/SizeOfTests.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -24,6 +24,11 @@ #include "PrecompiledHeadersUnitTests.h" #include +#if defined(__GNUC__) || defined(__clang__) +# pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + + #include "../../OrthancFramework/Sources/Cache/ICachePageProvider.h" #include "../../OrthancFramework/Sources/Cache/ICacheable.h" #include "../../OrthancFramework/Sources/Cache/LeastRecentlyUsedIndex.h" @@ -101,7 +106,6 @@ #include "../../OrthancFramework/Sources/FileStorage/IStorageArea.h" #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h" #include "../../OrthancFramework/Sources/FileStorage/StorageAccessor.h" -#include "../../OrthancFramework/Sources/FileStorage/StorageCache.h" #include "../../OrthancFramework/Sources/HttpClient.h" #include "../../OrthancFramework/Sources/HttpServer/BufferHttpSender.h" #include "../../OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.h" diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/UnitTestsSources/SizeOfTests.impl.h --- a/OrthancServer/UnitTestsSources/SizeOfTests.impl.h Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/UnitTestsSources/SizeOfTests.impl.h Wed Aug 12 09:54:49 2026 +0200 @@ -105,7 +105,6 @@ printf("sizeof(::Orthanc::SharedLibrary) == %d\n", static_cast(sizeof(::Orthanc::SharedLibrary))); printf("sizeof(::Orthanc::SharedMessageQueue) == %d\n", static_cast(sizeof(::Orthanc::SharedMessageQueue))); printf("sizeof(::Orthanc::StorageAccessor) == %d\n", static_cast(sizeof(::Orthanc::StorageAccessor))); - printf("sizeof(::Orthanc::StorageCache) == %d\n", static_cast(sizeof(::Orthanc::StorageCache))); printf("sizeof(::Orthanc::StreamBlockReader) == %d\n", static_cast(sizeof(::Orthanc::StreamBlockReader))); printf("sizeof(::Orthanc::StringMatcher) == %d\n", static_cast(sizeof(::Orthanc::StringMatcher))); printf("sizeof(::Orthanc::StringOperationValue) == %d\n", static_cast(sizeof(::Orthanc::StringOperationValue))); diff -r 2bcbb14bd94d -r 126b525257e5 OrthancServer/UnitTestsSources/UnitTestsMain.cpp --- a/OrthancServer/UnitTestsSources/UnitTestsMain.cpp Wed Aug 12 09:51:40 2026 +0200 +++ b/OrthancServer/UnitTestsSources/UnitTestsMain.cpp Wed Aug 12 09:54:49 2026 +0200 @@ -544,6 +544,7 @@ #endif Logging::Initialize(); + Logging::SetCurrentThreadName("MAIN"); SetGlobalVerbosity(Verbosity_Verbose); Toolbox::DetectEndianness(); SystemToolbox::MakeDirectory(SystemToolbox::PathFromUtf8("UnitTestsResults")); diff -r 2bcbb14bd94d -r 126b525257e5 TODO --- a/TODO Wed Aug 12 09:51:40 2026 +0200 +++ b/TODO Wed Aug 12 09:54:49 2026 +0200 @@ -71,8 +71,6 @@ "admin": { "Password": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", "Hashing": "sha1"} } -* Have a single instance of "ThreadedInstancesLoader" shared by all the jobs - in the Orthanc server to prevent resource exhaustion. * 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)