changeset 6047:d508d2348753

integration Orthanc-1.12.4->mainline
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 18 Mar 2025 13:37:18 +0100 (2 months ago)
parents ce002301ecea (diff) 816416425f2b (current diff)
children e83414b2b98d 0a0362c50882
files
diffstat 728 files changed, 19393 insertions(+), 11805 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.clang-format	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,57 @@
+---
+Language: Cpp
+BasedOnStyle: LLVM
+AlignConsecutiveAssignments: false
+AlignConsecutiveDeclarations: false
+AlignOperands: true
+AlignTrailingComments: false
+AlwaysBreakTemplateDeclarations: Yes
+BraceWrapping: 
+  AfterCaseLabel: true
+  AfterClass: true
+  AfterControlStatement: true
+  AfterEnum: true
+  AfterFunction: true
+  AfterNamespace: true
+  AfterStruct: true
+  AfterUnion: true
+  AfterExternBlock: true
+  BeforeCatch: true
+  BeforeElse: true
+  BeforeLambdaBody: true
+  BeforeWhile: true
+  IndentBraces: false
+  SplitEmptyFunction: true
+  SplitEmptyRecord: true
+  SplitEmptyNamespace: true
+BreakBeforeBraces: Custom
+BreakBeforeTernaryOperators: false
+BreakConstructorInitializers: AfterColon
+BreakConstructorInitializersBeforeComma: false
+ColumnLimit: 200
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+ContinuationIndentWidth: 2
+IncludeCategories: 
+  - Regex: '^<.*'
+    Priority: 1
+  - Regex: '^".*'
+    Priority: 2
+  - Regex: '.*'
+    Priority: 3
+IncludeIsMainRegex: '([-_](test|unittest))?$'
+IndentCaseLabels: true
+InsertNewlineAtEOF: true
+MacroBlockBegin: ''
+MacroBlockEnd: ''
+MaxEmptyLinesToKeep: 2
+NamespaceIndentation: All
+SpaceAfterCStyleCast: true
+SpaceAfterTemplateKeyword: false
+SpaceBeforeRangeBasedForLoopColon: false
+SpaceInEmptyParentheses: false
+SpacesInAngles: false
+SpacesInConditionalStatement: false
+SpacesInCStyleCastParentheses: false
+SpacesInParentheses: false
+TabWidth: 2
+...
--- a/.hgignore	Fri Sep 20 16:07:08 2024 +0200
+++ b/.hgignore	Tue Mar 18 13:37:18 2025 +0100
@@ -7,6 +7,7 @@
 .vscode/
 *~
 *.cmake.orig
+.idea/
 
 # when opening Orthanc in VSCode, it might find a java project and create files we wan't to ignore:
 .settings/
--- a/CITATION.cff	Fri Sep 20 16:07:08 2024 +0200
+++ b/CITATION.cff	Tue Mar 18 13:37:18 2025 +0100
@@ -10,5 +10,5 @@
 doi: "10.1007/s10278-018-0082-y"
 license: "GPL-3.0-or-later"
 repository-code: "https://orthanc.uclouvain.be/hg/orthanc/"
-version: 1.12.4
-date-released: 2024-06-05
+version: 1.12.6
+date-released: 2025-01-22
--- a/INSTALL	Fri Sep 20 16:07:08 2024 +0200
+++ b/INSTALL	Tue Mar 18 13:37:18 2025 +0100
@@ -76,7 +76,7 @@
 
 # cd [...]\Orthanc\Build
 # cmake -DSTANDALONE_BUILD=ON -DSTATIC_BUILD=ON -DALLOW_DOWNLOADS=ON \
-  -DUSE_LEGACY_JSONCPP=ON -G "Visual Studio 9 2008" [...]\OrthancServer
+  -DUSE_LEGACY_JSONCPP=ON -DUSE_LEGACY_BOOST=ON -G "Visual Studio 9 2008" [...]\OrthancServer
 
 Then open the "[...]\Orthanc\Build\Orthanc.sln" with Visual Studio.
 
@@ -145,17 +145,20 @@
 Cross-Compilation for Windows under GNU/Linux
 ---------------------------------------------
 
-Some versions of MinGW-W64 might have problems with C++11 (notably
-those shipped in Ubuntu 16.04 LTS, in the "mingw-w64" package). Use
-the following command to disable C++11:
+Some versions of MinGW-W64 may have insufficient support C++11 to
+compile recent versions of Boost or ICU (notably those shipped in
+Ubuntu 22.04 LTS, in the "g++-mingw-w64-i686-win32" package). Use the
+following command to disable C++11 in Boost and ICU:
 
 # cd ~/Orthanc/Build
 # cmake ../OrthancServer \
-        -DCMAKE_BUILD_TYPE=Debug \
-        -DCMAKE_TOOLCHAIN_FILE=~/Orthanc/Resources/MinGW-W64-Toolchain32.cmake \
+        -DCMAKE_BUILD_TYPE=Release \
+        -DCMAKE_TOOLCHAIN_FILE=../OrthancFramework/Resources/Toolchains/MinGW-W64-Toolchain32.cmake \
         -DSTANDALONE_BUILD=ON \
         -DSTATIC_BUILD=ON \
-        -DUSE_LEGACY_JSONCPP=ON
+        -DBOOST_LOCALE_BACKEND=icu \
+        -DUSE_LEGACY_BOOST=ON \
+        -DUSE_LEGACY_LIBICU=ON
 # make
 
 NB: Use the toolchain "MinGW-W64-Toolchain64.cmake" to produce 64bit
@@ -170,10 +173,12 @@
 
 # cd ~/Orthanc/Build
 # cmake ../OrthancServer \
-        -DCMAKE_BUILD_TYPE=Debug \
-        -DCMAKE_TOOLCHAIN_FILE=~/Orthanc/Resources/MinGWToolchain.cmake \
+        -DCMAKE_BUILD_TYPE=Release \
+        -DCMAKE_TOOLCHAIN_FILE=../OrthancFramework/Resources/Toolchains/MinGWToolchain.cmake \
         -DSTANDALONE_BUILD=ON \
         -DSTATIC_BUILD=ON \
         -DDCMTK_STATIC_VERSION=3.6.0 \
-        -DUSE_LEGACY_JSONCPP=ON
+        -DUSE_LEGACY_JSONCPP=ON \
+        -DUSE_LEGACY_BOOST=ON \
+        -DUSE_LEGACY_LIBICU=ON
 # make
--- a/NEWS	Fri Sep 20 16:07:08 2024 +0200
+++ b/NEWS	Tue Mar 18 13:37:18 2025 +0100
@@ -1,6 +1,196 @@
 Pending changes in the mainline
 ===============================
 
+REST API
+--------
+
+* API version upgraded to 28
+* GET /studies/../archive and sibbling routes now all accept a 'filename' GET argument.
+* POST /studies/../archive and sibbling routes now all accept a 'Filename' query argument.
+* GET /instances/../file and sibbling ../attachments/../data routes now all accept a 'filename' GET argument.
+* All routes accepting a "transcode" url argument or a "Transcode" field in the payload now also
+  accepts a "lossy-quality" url argument or a "LossyQuality" field to define the compression quality factor.
+  If not specified, the "DicomLossyTranscodingQuality" configuration is taken into account.
+
+
+Maintenance
+-----------
+
+* In the "ExtendedFind" mode:
+  - optimized "tools/find" when "StorageAccessMode" is set to "Never".
+  - "tools/find" is now returning results when e.g, ordering instances against a metadata they don't have
+* Fixed interpretation of returnUnsupportedImage in /preview route. 
+* GET /series/../study now also contain LastUpdate field:
+  https://discourse.orthanc-server.org/t/lastupdate-coherency/5524
+* Recovered compatibility with Windows XP that was broken because of DCMTK 3.6.9
+* Enabled support of the 1.2.840.10008.1.2.1.99 transfer syntax
+  (Deflated Explicit VR Little Endian) in static builds + fix length of saved files.
+  https://discourse.orthanc-server.org/t/transcoding-to-deflated-transfer-syntax-fails/5489
+* Housekeeper plugin:
+  - When encountering an error, the housekeeper now skips the resource and continues processing.
+* Orthanc Explorer:
+  - Allow '-' and '_' in labels.
+
+
+Version 1.12.6 (2025-01-22)
+===========================
+
+General
+-------
+
+* DICOM: Added support for C-GET SCU.
+* Added new configuration options:
+  - "AcceptedSopClasses" and "RejectedSopClasses" to limit the SOP classes
+    accepted by Orthanc when acting as C-STORE SCP.
+  - "DicomDefaultRetrieveMethod" to define whether Orthanc uses C-MOVE or
+    C-GET to retrieve a resource after a C-FIND (if calling "/queries/.../retrieve").
+    This configuration can be overridden for each DICOM modality by setting
+    the "RetrieveMethod" field in the "DicomModalities" section.
+    Default value: "C-MOVE" to preserve backward compatibility.
+  - "MaximumConcurrentDcmtkTranscoders" to reduce CPU and memory usage by limiting
+    the number of concurrent DCMTK transcoders that are simultaneously running
+    at any given time.
+
+REST API
+--------
+
+* API version upgraded to 27
+* C-GET SCU requests can be triggered through the new route "/modalities/{id}/get"
+
+Plugins
+-------
+
+* SDK: Added "OrthancPluginStartStreamAnswer()" and "OrthancPluginSendStreamChunk()"
+  to allow the sending of HTTP responses by chunks.
+
+Maintenance
+-----------
+
+* Fix: In the "ExtendedFind" mode, using "tools/find" while querying against
+  "ModalitiesInStudy" was not compatible with pagination. This notably prevented
+  the use of the modality filter in Orthanc Explorer 2.
+* If the "HttpsCACertificates" configuration is empty, Orthanc now uses the
+  operating system native CA store (if any). This is equivalent to the "--ca-native"
+  curl option.
+* Housekeeper plugin:
+  - Fix the "Force" configuration that was ineffective.
+  - Allow transcoding to lossy transfer syntax. Orthanc will leave
+    the "SOPInstanceUID" DICOM tag untouched in this case.
+* In the "/archive" routes:
+  - The numbers in the filenames now match the "InstanceNumber" tag whenever possible.
+    When not, the files are ordered in the same order as the instances in the series.
+  - Added optimization to use the "ExtendedFind" extension, hereby reducing the number
+    of SQL queries.
+* DICOM negotiation:
+  - When opening a DICOM SCU connection, Orthanc now only proposes the contexts that it is
+    going to use in the connection, and not all the contexts as in previous versions
+    (e.g., if performing a C-ECHO, Orthanc will not propose C-MOVE or C-FIND).
+* DICOM C-GET SCP: Orthanc will not refuse anymore to send, for instance, a
+  LittleEndianExplicit file, if the accepted transfer syntax is a compressed one.
+* By default, DCMTK now uses its own "oficonv" library for character set conversion.
+  This can be tuned using the new CMake option "-DDCMTK_LOCALE_BACKEND=oficonv".
+* Improved progress reporting for DicomMoveScu jobs.
+* Upgraded dependencies for static builds:
+  - dcmtk 3.6.9
+
+
+Version 1.12.5 (2024-12-17)
+===========================
+
+General
+-------
+
+* Database:
+  - Introduced the database optimization "ExtendedFind" to replace many small SQL queries
+    by a single, large SQL query. This can greatly reduce the cost related to latency
+    when working with large databases (e.g., if using PostgreSQL).
+    Furthermore, "ExtendedFind" brings new sorting and filtering features to the
+    REST API, mainly in "/tools/find".
+  - Introduced the new database primitive "ExtendedChanges" to allow filtering on "/changes".
+  - Reduced the number of SQL queries when ingesting DICOM files.
+* Introduced a new configuration "ReadOnly" to forbid an Orthanc instance to perform
+  any modification to the index database or to the storage area.
+  
+
+REST API
+--------
+
+* API version upgraded to 26
+* Support HTTP "Range" request header on "{...}/attachments/{...}/data" and
+  "{...}/attachments/{...}/compressed-data"
+* Improved parsing of multiple numerical values in DICOM tags
+  https://discourse.orthanc-server.org/t/qido-includefield-with-sequences/4746/6
+* In "/system", added a new field "Capabilities" with new values:
+  - "HasExtendedChanges" if the index database provides this optimization
+  - "HasExtendedFind" if the index database provides this primitive
+* If the index database provides the "HasExtendedChanges" primitive, "/changes"
+  supports two additional arguments:
+  - "type" to filter the changes returned by the query
+  - "to" to possibly cycle through changes in reverse order
+  Example: "/changes?type=StableStudy&to=7584&limit=100"
+* If the index database provides the "HasExtendedFind" primitive, "/tools/find"
+  supports new options:
+  - "OrderBy" to order by DICOM tag or metadata value
+  - "ParentPatient", "ParentStudy", and "ParentSeries" to retrieve only descendants of a
+    given DICOM resource
+  - "MetadataQuery" to filter results based on metadata values
+  - "ResponseContent" to define what shall be included in the response for each returned
+    resource (e.g: Metadata, Children,...)
+* In "/tools/find", the "Limit" and "Since" arguments are not allowed anymore if the
+  query requests filtering on DICOM tags that are not stored in the index database
+* The new "/tools/count-resources" API route is similar to "tools/find" but only
+  returns the number of resources matching the criteria
+* "/studies?since=x&limit=0" and similar routes for patients, series, and instances:
+  "limit=0" now means "no limit" instead of "no results" as in previous versions of Orthanc
+* In DICOMweb JSON, the "DS - Decimal String" values were previously represented as float
+  numbers, but are now represented as strings to avoid introduction of long float representation
+  (e.g 0.1429999999999 vs "0.143") and to be compliant with the DICOMweb standard:
+  https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.3.html
+  This has no impact on the Stone Web viewer and OHIF:
+  https://discourse.orthanc-server.org/t/dicomwebplugin-does-not-return-series-metadata-properly/5195
+
+Maintenance
+-----------
+
+* DICOM TLS: "DicomTlsTrustedCertificates" is not required anymore when issuing
+  an outgoing SCU connection if "DicomTlsRemoteCertificateRequired" is set to "false"
+* Fix C-Find queries not returning computed tags such as ModalitiesInStudy,
+  NumberOfStudyRelatedSeries,... in very specific use cases
+* Fix C-Find queries not returning private tags in the modality worklist plugin
+* Fix an extremely rare error when 2 threads are trying to create the same folder
+  in the File Storage at the same time
+* Fix crashes if handling very large images
+* Fix deadlock when parsing specific invalid DICOM files
+* Loading plugins: Orthanc will now fail to start when provided with a plugin path
+  that can not be found
+* Metrics:
+  - Fix a few metrics that were not published
+  - Added 2 metrics: "orthanc_storage_cache_miss_count" and "orthanc_storage_cache_hit_count"
+* Added a new fallback when trying to decode a frame: transcode the file using the plugin
+  before decoding the frame. This solves some issues with JP2K Lossy compression:
+  https://discourse.orthanc-server.org/t/decoding-displaying-jpeg2000-lossy-images/5117
+* Added new warnings that can be disabled in the configuration: 
+  - W003_DecoderFailure
+  - W004_NoMainDicomTagsSignature
+  - W005_RequestingTagFromLowerResourceLevel
+  - W006_RequestingTagFromMetaHeader
+  - W007_MissingRequestedTagsNotReadFromDisk
+* New default MainDicomTags are now stored in the DB:
+  - At Study Level:
+    - TimezoneOffsetFromUTC (used in QIDO-RS default queries)
+  - At Series Level:
+    - TimezoneOffsetFromUTC (used in QIDO-RS default queries)
+    - PerformedProcedureStepStartDate (used in QIDO-RS default queries)
+    - PerformedProcedureStepStartTime (used in QIDO-RS default queries)
+    - RequestAttributesSequence (used in QIDO-RS default queries)
+  - Note that, in order to access these values for resources that were ingested in Orthanc
+    before this release, you will have to run the Housekeeper plugin or to call
+    "/reconstruct" on every resource
+* Upgraded dependencies for static builds:
+  - boost 1.86.0
+  - curl 8.9.0
+  - SQLite 3.46
+
 
 Version 1.12.4 (2024-06-05)
 ===========================
@@ -24,7 +214,7 @@
 * Housekeeper plugin: 
   - Added an option "LimitMainDicomTagsReconstructLevel"
     (allowed values: "Patient", "Study", "Series", "Instance"). This can greatly speed
-    up the housekeeper process, e.g. if you have only update the Study level ExtraMainDicomTags.
+    up the housekeeper process, e.g. if you have only updated the Study level ExtraMainDicomTags.
   - Fixed broken /instances/../tags route after running the Housekeeper
     after having changed the "IngestTranscoding".
 * SDK: added OrthancPluginLogMessage() as a new primitive for plugins
--- a/OrthancFramework/Resources/CMake/AutoGeneratedCode.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/AutoGeneratedCode.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/BoostConfiguration.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/BoostConfiguration.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
@@ -91,10 +91,10 @@
   ## Parameters for static compilation of Boost 
   ##
   
-  set(BOOST_NAME boost_1_85_0)
-  set(BOOST_VERSION 1.85.0)
-  set(BOOST_BCP_SUFFIX bcpdigest-1.12.4)
-  set(BOOST_MD5 "1017e9c8383efdea01c059a8d3cc4dda")
+  set(BOOST_NAME boost_1_86_0)
+  set(BOOST_VERSION 1.86.0)
+  set(BOOST_BCP_SUFFIX bcpdigest-1.12.5)
+  set(BOOST_MD5 "20b9c325c0dde830889ee75a9e64ded8")
   set(BOOST_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/${BOOST_NAME}_${BOOST_BCP_SUFFIX}.tar.gz")
   set(BOOST_SOURCES_DIR ${CMAKE_BINARY_DIR}/${BOOST_NAME})
 
@@ -115,7 +115,7 @@
   if (FirstRun)
     execute_process(
       COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
-      ${CMAKE_CURRENT_LIST_DIR}/../Patches/boost-1.85.0-emscripten.patch
+      ${CMAKE_CURRENT_LIST_DIR}/../Patches/boost-1.86.0-emscripten.patch
       WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
       RESULT_VARIABLE Failure
       )
--- a/OrthancFramework/Resources/CMake/BoostConfiguration.sh	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/BoostConfiguration.sh	Tue Mar 18 13:37:18 2025 +0100
@@ -27,9 +27,10 @@
 ##   - Orthanc 1.12.2: Boost 1.83.0
 ##   - Orthanc 1.12.3: Boost 1.84.0
 ##   - Orthanc > 1.12.3: Boost 1.85.0
+##   - Orthanc 1.12.5: Boost 1.86.0
 
-BOOST_VERSION=1_85_0
-ORTHANC_VERSION=1.12.4
+BOOST_VERSION=1_86_0
+ORTHANC_VERSION=1.12.5
 
 rm -rf /tmp/boost_${BOOST_VERSION}
 rm -rf /tmp/bcp/boost_${BOOST_VERSION}
--- a/OrthancFramework/Resources/CMake/BoostConfigurationStatic-1.69.0.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/BoostConfigurationStatic-1.69.0.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/CivetwebConfiguration.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/CivetwebConfiguration.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/Compiler.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/Compiler.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
@@ -232,10 +232,16 @@
   endif()
 
 elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")
+
+  # fix this error that appears with recent compilers on MacOS: boost/mpl/aux_/integral_wrapper.hpp:73:31: error: integer value -1 is outside the valid range of values [0, 3] for this enumeration type [-Wenum-constexpr-conversion]
+  SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-enum-constexpr-conversion")
+
   add_definitions(
     -D_XOPEN_SOURCE=1
     )
-  link_libraries(iconv)
+  
+  # Linking with iconv breaks the Universal builds on modern compilers
+  # link_libraries(iconv)
 
 elseif (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
   message("Building using Emscripten (for WebAssembly or asm.js targets)")
--- a/OrthancFramework/Resources/CMake/DcmtkConfiguration.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/DcmtkConfiguration.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
@@ -39,6 +39,8 @@
     include(${CMAKE_CURRENT_LIST_DIR}/DcmtkConfigurationStatic-3.6.7.cmake)
   elseif (DCMTK_STATIC_VERSION STREQUAL "3.6.8")
     include(${CMAKE_CURRENT_LIST_DIR}/DcmtkConfigurationStatic-3.6.8.cmake)
+  elseif (DCMTK_STATIC_VERSION STREQUAL "3.6.9")
+    include(${CMAKE_CURRENT_LIST_DIR}/DcmtkConfigurationStatic-3.6.9.cmake)
   else()
     message(FATAL_ERROR "Unsupported version of DCMTK: ${DCMTK_STATIC_VERSION}")
   endif()
@@ -58,9 +60,9 @@
     )
 
   if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
-    message(${DCMTK_SOURCES_DIR})
     list(REMOVE_ITEM DCMTK_SOURCES 
       ${DCMTK_SOURCES_DIR}/ofstd/libsrc/offilsys.cc
+      ${DCMTK_SOURCES_DIR}/ofstd/libsrc/ofwhere.c   # Needed since DCMTK 3.6.9
       )
   endif()
 
@@ -284,6 +286,7 @@
     find_path(DCMTK_DICTIONARY_DIR_AUTO dicom.dic
       /usr/share/dcmtk
       /usr/share/dcmtk-3.6.8
+      /usr/share/dcmtk-3.6.9
       /usr/share/libdcmtk1
       /usr/share/libdcmtk2
       /usr/share/libdcmtk3
--- a/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.0.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.0.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.2.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.2.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.4.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.4.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.5.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.5.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.6.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.6.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.7.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.7.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.8.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.8.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.9.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,309 @@
+# 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-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public License
+# as published by the Free Software Foundation, either version 3 of
+# the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# <http://www.gnu.org/licenses/>.
+
+
+SET(DCMTK_VERSION_NUMBER 369)
+SET(DCMTK_PACKAGE_VERSION "3.6.9")
+SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.9)
+SET(DCMTK_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/dcmtk-3.6.9.tar.gz")
+SET(DCMTK_MD5 "cb30587f8da760c832a4f19d159acda5")
+
+macro(DCMTK_UNSET)
+endmacro()
+
+macro(DCMTK_UNSET_CACHE)
+endmacro()
+
+set(DCMTK_BINARY_DIR ${DCMTK_SOURCES_DIR}/)
+set(DCMTK_CMAKE_INCLUDE ${DCMTK_SOURCES_DIR}/)
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+  set(DCMTK_WITH_THREADS OFF)  # Disable thread support in wasm/asm.js
+else()
+  set(DCMTK_WITH_THREADS ON)
+endif()
+
+add_definitions(-DDCMTK_INSIDE_LOG4CPLUS=1)
+
+if (IS_DIRECTORY "${DCMTK_SOURCES_DIR}")
+  set(FirstRun OFF)
+else()
+  set(FirstRun ON)
+endif()
+
+DownloadPackage(${DCMTK_MD5} ${DCMTK_URL} "${DCMTK_SOURCES_DIR}")
+
+
+if (FirstRun)
+  # Apply the patches
+  execute_process(
+    COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
+    ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-3.6.9.patch
+    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+    RESULT_VARIABLE Failure
+    )
+
+  if (Failure)
+    message(FATAL_ERROR "Error while patching a file")
+  endif()
+
+  if (MSVC)
+    # Older versions of Microsoft Visual Studio (notably MSVC2008)
+    # don't like void usage of function arguments in C source files,
+    # in order to avoid a warning about unused arguments. This patch
+    # removes such usages that were not present in DCMTK <= 3.6.6.
+    execute_process(
+      COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
+      ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-3.6.9-visual-studio.patch
+      WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+      RESULT_VARIABLE Failure
+      )
+
+    if (Failure)
+      message(FATAL_ERROR "Error while patching a file")
+    endif()
+  endif()
+
+  configure_file(
+    ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-dcdict_orthanc.cc
+    ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/dcdict_orthanc.cc
+    COPYONLY)
+else()
+  message("The patches for DCMTK have already been applied")
+endif()
+
+
+include_directories(
+  ${DCMTK_SOURCES_DIR}/dcmiod/include
+  ${DCMTK_SOURCES_DIR}/oficonv/include
+  )
+
+
+# C_CHAR_UNSIGNED *must* be set before calling "GenerateDCMTKConfigure.cmake"
+IF (CMAKE_CROSSCOMPILING)
+  if (CMAKE_COMPILER_IS_GNUCXX AND
+      CMAKE_SYSTEM_NAME STREQUAL "Windows")  # MinGW
+    SET(C_CHAR_UNSIGNED 1 CACHE INTERNAL "Whether char is unsigned.")
+
+  elseif(CMAKE_SYSTEM_NAME STREQUAL "Emscripten")  # WebAssembly or asm.js
+
+    # Check out "../WebAssembly/ArithmeticTests/" to regenerate the
+    # "arith.h" file
+    configure_file(
+      ${CMAKE_CURRENT_LIST_DIR}/WebAssembly/arith.h
+      ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/arith.h
+      COPYONLY)
+
+    UNSET(C_CHAR_UNSIGNED CACHE)
+    SET(C_CHAR_UNSIGNED 0 CACHE INTERNAL "")
+
+  else()
+    message(FATAL_ERROR "Support your platform here")
+  endif()
+ENDIF()
+
+
+if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
+  SET(DCMTK_ENABLE_CHARSET_CONVERSION "iconv" CACHE STRING "")
+  SET(HAVE_SYS_GETTID 0 CACHE INTERNAL "")
+endif()
+
+
+SET(DCMTK_SOURCE_DIR ${DCMTK_SOURCES_DIR})
+include(GNUInstallDirs)  # Needed since DCMTK 3.6.9
+include(${DCMTK_SOURCES_DIR}/CMake/CheckFunctionWithHeaderExists.cmake)
+include(${DCMTK_SOURCES_DIR}/CMake/GenerateDCMTKConfigure.cmake)
+
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")  # WebAssembly or
+  # asm.js The macros below are not properly discovered by DCMTK
+  # when using WebAssembly. Check out "../WebAssembly/arith.h" for
+  # how we produced these values. This step MUST be after
+  # "GenerateDCMTKConfigure" and before the generation of
+  # "osconfig.h".
+  UNSET(SIZEOF_VOID_P   CACHE)
+  UNSET(SIZEOF_CHAR     CACHE)
+  UNSET(SIZEOF_DOUBLE   CACHE)
+  UNSET(SIZEOF_FLOAT    CACHE)
+  UNSET(SIZEOF_INT      CACHE)
+  UNSET(SIZEOF_LONG     CACHE)
+  UNSET(SIZEOF_SHORT    CACHE)
+  UNSET(SIZEOF_VOID_P   CACHE)
+
+  SET(SIZEOF_VOID_P 4   CACHE INTERNAL "")
+  SET(SIZEOF_CHAR 1     CACHE INTERNAL "")
+  SET(SIZEOF_DOUBLE 8   CACHE INTERNAL "")
+  SET(SIZEOF_FLOAT 4    CACHE INTERNAL "")
+  SET(SIZEOF_INT 4      CACHE INTERNAL "")
+  SET(SIZEOF_LONG 4     CACHE INTERNAL "")
+  SET(SIZEOF_SHORT 2    CACHE INTERNAL "")
+  SET(SIZEOF_VOID_P 4   CACHE INTERNAL "")
+endif()
+
+
+set(DCMTK_PACKAGE_VERSION_SUFFIX "")
+set(DCMTK_PACKAGE_VERSION_NUMBER ${DCMTK_VERSION_NUMBER})
+
+
+# For the dcmtls module, necessary since DCMTK 3.6.7 (cf. file
+# "dcmtls/libsrc/tlslayer.cc"). This must be done before the
+# invokation of "configure_file()"!
+if (STATIC_BUILD OR NOT USE_SYSTEM_OPENSSL)
+  # The "CHECK_FUNCTIONWITHHEADER_EXISTS()" provided by DCMTK only
+  # works with the system-wide version of OpenSSL. If statically
+  # linking against OpenSSL, we manually provide information about
+  # OpenSSL 3.0.x
+  set(HAVE_OPENSSL_PROTOTYPE_DH_BITS 1)
+  set(HAVE_OPENSSL_PROTOTYPE_EVP_PKEY_BASE_ID 1)
+  set(HAVE_OPENSSL_PROTOTYPE_SSL_CTX_GET0_PARAM 1)
+  set(HAVE_OPENSSL_PROTOTYPE_SSL_CTX_GET_CERT_STORE 1)
+  set(HAVE_OPENSSL_PROTOTYPE_SSL_CTX_GET_CIPHERS 1)
+  set(HAVE_OPENSSL_PROTOTYPE_X509_GET_SIGNATURE_NID 1)
+  set(HAVE_OPENSSL_PROTOTYPE_X509_STORE_GET0_PARAM 1)
+else()
+  CHECK_FUNCTIONWITHHEADER_EXISTS("DH_bits" "openssl/dh.h" HAVE_OPENSSL_PROTOTYPE_DH_BITS)
+  CHECK_FUNCTIONWITHHEADER_EXISTS("EVP_PKEY_base_id" "openssl/evp.h" HAVE_OPENSSL_PROTOTYPE_EVP_PKEY_BASE_ID)
+  CHECK_FUNCTIONWITHHEADER_EXISTS("SSL_CTX_get0_param" "openssl/ssl.h" HAVE_OPENSSL_PROTOTYPE_SSL_CTX_GET0_PARAM)
+  CHECK_FUNCTIONWITHHEADER_EXISTS("SSL_CTX_get_cert_store" "openssl/ssl.h" HAVE_OPENSSL_PROTOTYPE_SSL_CTX_GET_CERT_STORE)
+  CHECK_FUNCTIONWITHHEADER_EXISTS("SSL_CTX_get_ciphers" "openssl/ssl.h" HAVE_OPENSSL_PROTOTYPE_SSL_CTX_GET_CIPHERS)
+  CHECK_FUNCTIONWITHHEADER_EXISTS("X509_STORE_get0_param" "openssl/x509.h" HAVE_OPENSSL_PROTOTYPE_X509_STORE_GET0_PARAM)
+  CHECK_FUNCTIONWITHHEADER_EXISTS("X509_get_signature_nid" "openssl/x509.h" HAVE_OPENSSL_PROTOTYPE_X509_GET_SIGNATURE_NID)
+endif()
+
+
+# "DCMTK_ENABLE_CHARSET_CONVERSION" is defined by "osconfig.h.in"
+if (NOT DEFINED DCMTK_LOCALE_BACKEND OR   # This is the case if locale support is disabled (e.g. in Stone)
+    DCMTK_LOCALE_BACKEND STREQUAL "gcc")
+  set(DCMTK_ENABLE_CHARSET_CONVERSION "DCMTK_CHARSET_CONVERSION_STDLIBC_ICONV" CACHE STRING "" FORCE)
+elseif (DCMTK_LOCALE_BACKEND STREQUAL "libiconv")
+  set(DCMTK_ENABLE_CHARSET_CONVERSION "DCMTK_CHARSET_CONVERSION_ICONV" CACHE STRING "" FORCE)
+elseif (DCMTK_LOCALE_BACKEND STREQUAL "icu")
+  message(FATAL_ERROR "Support for ICU has been removed since DCMTK 3.6.9")
+elseif (DCMTK_LOCALE_BACKEND STREQUAL "oficonv")
+  set(DCMTK_ENABLE_CHARSET_CONVERSION "DCMTK_CHARSET_CONVERSION_OFICONV" CACHE STRING "" FORCE)
+else()
+  message(FATAL_ERROR "Invalid value for DCMTK_LOCALE_BACKEND: ${DCMTK_LOCALE_BACKEND}")
+endif()
+
+
+# Enable support of the 1.2.840.10008.1.2.1.99 transfer syntax in
+# static builds of Orthanc (Deflated Explicit VR Little
+# Endian). Defining "WITH_ZLIB" is always OK, as zlib is part of the
+# core dependencies of the Orthanc framework.
+# https://discourse.orthanc-server.org/t/transcoding-to-deflated-transfer-syntax-fails/
+set(WITH_ZLIB ON)
+
+CONFIGURE_FILE(
+  ${DCMTK_SOURCES_DIR}/CMake/osconfig.h.in
+  ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/osconfig.h)
+
+
+
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+  link_libraries(netapi32)  # For NetWkstaUserGetInfo@12
+  link_libraries(iphlpapi)  # For GetAdaptersInfo@8
+
+  # Configure Wine if cross-compiling for Windows
+  if (CMAKE_COMPILER_IS_GNUCXX)
+    include(${DCMTK_SOURCES_DIR}/CMake/dcmtkUseWine.cmake)
+    FIND_PROGRAM(WINE_WINE_PROGRAM wine)
+    FIND_PROGRAM(WINE_WINEPATH_PROGRAM winepath)
+    list(APPEND DCMTK_TRY_COMPILE_REQUIRED_CMAKE_FLAGS "-DCMAKE_EXE_LINKER_FLAGS=-static")
+  endif()
+endif()
+
+# This step must be after the generation of "osconfig.h" => Removed since DCMTK 3.6.9
+#if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+#  INSPECT_FUNDAMENTAL_ARITHMETIC_TYPES()
+#endif()
+
+
+# Source for the logging facility of DCMTK
+AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/oflog/libsrc DCMTK_SOURCES)
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
+    ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR
+    ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
+    ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR
+    ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" OR
+    ${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten")
+  list(REMOVE_ITEM DCMTK_SOURCES
+    ${DCMTK_SOURCES_DIR}/oflog/libsrc/clfsap.cc
+    ${DCMTK_SOURCES_DIR}/oflog/libsrc/windebap.cc
+    ${DCMTK_SOURCES_DIR}/oflog/libsrc/winsock.cc
+    )
+
+elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+  list(REMOVE_ITEM DCMTK_SOURCES
+    ${DCMTK_SOURCES_DIR}/oflog/libsrc/unixsock.cc
+    ${DCMTK_SOURCES_DIR}/oflog/libsrc/clfsap.cc
+    )
+endif()
+
+
+# Starting with DCMTK 3.6.2, the Nagle algorithm is not disabled by
+# default since this does not seem to be appropriate (anymore) for
+# most modern operating systems. In order to change this default, the
+# environment variable NO_TCPDELAY can be set to "1" (see envvars.txt
+# for details). Alternatively, the macro DISABLE_NAGLE_ALGORITHM can
+# be defined to change this setting at compilation time (see
+# macros.txt for details).
+# https://forum.dcmtk.org/viewtopic.php?t=4632
+add_definitions(
+  -DDISABLE_NAGLE_ALGORITHM=1
+  )
+
+
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+  # For compatibility with Windows XP, avoid using fiber-local-storage
+  # in log4cplus, but use thread-local-storage instead. Otherwise,
+  # Windows XP complains about missing "FlsGetValue()" in KERNEL32.dll
+  add_definitions(
+    -DDCMTK_LOG4CPLUS_AVOID_WIN32_FLS
+    )
+
+  if (CMAKE_COMPILER_IS_GNUCXX OR             # MinGW
+      "${CMAKE_SIZEOF_VOID_P}" STREQUAL "4")  # MSVC for 32bit (*)
+
+    # (*) With multithreaded logging enabled, Visual Studio 2008 fails
+    # with error: ".\dcmtk-3.6.7\oflog\libsrc\globinit.cc(422) : error
+    # C2664: 'dcmtk::log4cplus::thread::impl::tls_init' : cannot
+    # convert parameter 1 from 'void (__stdcall *)(void *)' to
+    # 'dcmtk::log4cplus::thread::impl::tls_init_cleanup_func_type'"
+    #   None of the functions with this name in scope match the target type
+
+    add_definitions(
+      -DDCMTK_LOG4CPLUS_SINGLE_THREADED
+      )
+  endif()
+
+  if (CMAKE_COMPILER_IS_GNUCXX)  # MinGW
+    # Necessary since DCMTK 3.6.9
+    add_definitions(
+      -DENABLE_OLD_OFSTD_FTOA_IMPLEMENTATION
+      -DENABLE_OLD_OFSTD_ATOF_IMPLEMENTATION
+      )
+  endif()
+endif()
+
+
+if (DCMTK_LOCALE_BACKEND STREQUAL "oficonv")
+  AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/oficonv/libsrc DCMTK_SOURCES)
+endif()
--- a/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
@@ -163,6 +163,12 @@
         set(ORTHANC_FRAMEWORK_MD5 "d2476b9e796e339ac320b5333489bdb3")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.3")
         set(ORTHANC_FRAMEWORK_MD5 "975f5bf2142c22cb1777b4f6a0a614c5")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.4")
+        set(ORTHANC_FRAMEWORK_MD5 "1e61779ea4a7cd705720bdcfed8a6a73")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.5")
+        set(ORTHANC_FRAMEWORK_MD5 "5bb69f092981fdcfc11dec0a0f9a7db3")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.6")
+        set(ORTHANC_FRAMEWORK_MD5 "0e971f32f4f3e4951e0f3b5de49a3da6")
 
       # Below this point are development snapshots that were used to
       # release some plugin, before an official release of the Orthanc
--- a/OrthancFramework/Resources/CMake/DownloadPackage.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/DownloadPackage.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/EmscriptenParameters.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/EmscriptenParameters.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/GoogleTestConfiguration.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/GoogleTestConfiguration.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/JsonCppConfiguration.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/JsonCppConfiguration.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/LibCurlConfiguration.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/LibCurlConfiguration.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
@@ -21,9 +21,9 @@
 
 
 if (STATIC_BUILD OR NOT USE_SYSTEM_CURL)
-  SET(CURL_SOURCES_DIR ${CMAKE_BINARY_DIR}/curl-8.5.0)
-  SET(CURL_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/curl-8.5.0.tar.gz")
-  SET(CURL_MD5 "0bc69288b20ae165ff4b7d6d7bbe70d2")
+  SET(CURL_SOURCES_DIR ${CMAKE_BINARY_DIR}/curl-8.9.0)
+  SET(CURL_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/curl-8.9.0.tar.gz")
+  SET(CURL_MD5 "f9bca5d4d5bac1f04e6c5eb4d0418618")
 
   if (IS_DIRECTORY "${CURL_SOURCES_DIR}")
     set(FirstRun OFF)
@@ -36,7 +36,7 @@
   if (FirstRun)
     execute_process(
       COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
-      ${CMAKE_CURRENT_LIST_DIR}/../Patches/curl-8.5.0.patch
+      ${CMAKE_CURRENT_LIST_DIR}/../Patches/curl-8.9.0.patch
       WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
       RESULT_VARIABLE Failure
       )
--- a/OrthancFramework/Resources/CMake/LibIconvConfiguration.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/LibIconvConfiguration.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/LibIcuConfiguration.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/LibIcuConfiguration.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/LibJpegConfiguration.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/LibJpegConfiguration.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/LibP11Configuration.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/LibP11Configuration.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/LibPngConfiguration.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/LibPngConfiguration.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/LuaConfiguration.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/LuaConfiguration.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/MongooseConfiguration.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/MongooseConfiguration.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/OpenSslConfiguration.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/OpenSslConfiguration.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/OpenSslConfigurationStatic-1.1.1.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/OpenSslConfigurationStatic-1.1.1.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/OpenSslConfigurationStatic-3.0.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/OpenSslConfigurationStatic-3.0.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
@@ -456,7 +456,8 @@
     # is shipped with the stdlib
     unset(BOOST_LOCALE_BACKEND CACHE)
   else()
-    if (BOOST_LOCALE_BACKEND STREQUAL "gcc")
+    if (BOOST_LOCALE_BACKEND STREQUAL "gcc" OR
+        BOOST_LOCALE_BACKEND STREQUAL "oficonv")
     elseif (BOOST_LOCALE_BACKEND STREQUAL "libiconv")
       include(${CMAKE_CURRENT_LIST_DIR}/LibIconvConfiguration.cmake)
     elseif (BOOST_LOCALE_BACKEND STREQUAL "icu")
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
@@ -25,7 +25,7 @@
 #####################################################################
 
 # Version of the build, should always be "mainline" except in release branches
-set(ORTHANC_VERSION "1.12.4")
+set(ORTHANC_VERSION "mainline")
 
 # Version of the database schema. History:
 #   * Orthanc 0.1.0 -> Orthanc 0.3.0 = no versioning
@@ -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 "24")
+set(ORTHANC_API_VERSION "27")
 
 
 #####################################################################
@@ -79,7 +79,7 @@
 
 # Parameters specific to DCMTK
 set(DCMTK_DICTIONARY_DIR "" CACHE PATH "Directory containing the DCMTK dictionaries \"dicom.dic\" and \"private.dic\" (only when using system version of DCMTK)")
-set(DCMTK_STATIC_VERSION "3.6.8" CACHE STRING "Version of DCMTK to be used in static builds (can be \"3.6.0\", \"3.6.2\", \"3.6.4\", \"3.6.5\", \"3.6.6\", \"3.6.7\", or \"3.6.8\")")
+set(DCMTK_STATIC_VERSION "3.6.9" CACHE STRING "Version of DCMTK to be used in static builds (can be \"3.6.0\", \"3.6.2\", \"3.6.4\", \"3.6.5\", \"3.6.6\", \"3.6.7\", \"3.6.8\", or \"3.6.9\")")
 set(USE_DCMTK_362_PRIVATE_DIC ON CACHE BOOL "Use the dictionary of private tags from DCMTK 3.6.2 if using DCMTK 3.6.0")
 set(USE_SYSTEM_DCMTK ON CACHE BOOL "Use the system version of DCMTK")
 set(ENABLE_DCMTK_LOG ON CACHE BOOL "Enable logging internal to DCMTK")
@@ -90,6 +90,7 @@
 set(USE_GOOGLE_TEST_DEBIAN_PACKAGE OFF CACHE BOOL "Use the sources of Google Test shipped with libgtest-dev (Debian only)")
 set(SYSTEM_MONGOOSE_USE_CALLBACKS ON CACHE BOOL "The system version of Mongoose uses callbacks (version >= 3.7)")
 set(BOOST_LOCALE_BACKEND "libiconv" CACHE STRING "Back-end for locales that is used by Boost (can be \"gcc\", \"libiconv\", \"icu\", or \"wconv\" on Windows)")
+set(DCMTK_LOCALE_BACKEND "oficonv" CACHE STRING "Back-end for locales that is used by DCMTK (can be \"gcc\", \"libiconv\", \"icu\" (only up to DCMTK 3.6.8), \"oficonv\")")
 set(USE_PUGIXML ON CACHE BOOL "Use the Pugixml parser (turn off only for debug)")
 set(USE_LEGACY_JSONCPP OFF CACHE BOOL "Use the old branch 0.x.y of JsonCpp, that does not require a C++11 compiler (for LSB and old versions of Visual Studio)")
 set(USE_LEGACY_LIBICU OFF CACHE BOOL "Use icu icu4c-58_2, latest version not requiring a C++11 compiler (for LSB and old versions of Visual Studio)")
--- a/OrthancFramework/Resources/CMake/ProtobufConfiguration.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/ProtobufConfiguration.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/PugixmlConfiguration.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/PugixmlConfiguration.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/SQLiteConfiguration.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/SQLiteConfiguration.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
@@ -20,16 +20,7 @@
 # <http://www.gnu.org/licenses/>.
 
 
-if (APPLE)
-  # Under OS X, the binaries must always be linked against the
-  # system-wide version of SQLite. Otherwise, if some Orthanc plugin
-  # also uses its own version of SQLite (such as orthanc-webviewer),
-  # this results in a crash in "sqlite3_mutex_enter(db->mutex);" (the
-  # mutex is not initialized), probably because the EXE and the DYNLIB
-  # share the same memory location for this mutex.
-  set(SQLITE_STATIC OFF)
-
-elseif (STATIC_BUILD OR NOT USE_SYSTEM_SQLITE)
+if (STATIC_BUILD OR NOT USE_SYSTEM_SQLITE)
   set(SQLITE_STATIC ON)
 else()
   set(SQLITE_STATIC OFF)
@@ -37,11 +28,11 @@
 
 
 if (SQLITE_STATIC)
-  SET(SQLITE_SOURCES_DIR ${CMAKE_BINARY_DIR}/sqlite-amalgamation-3270100)
-  SET(SQLITE_MD5 "16717b26358ba81f0bfdac07addc77da")
-  SET(SQLITE_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/sqlite-amalgamation-3270100.zip")
+  SET(SQLITE_SOURCES_DIR ${CMAKE_BINARY_DIR}/sqlite-amalgamation-3460100)
+  SET(SQLITE_MD5 "1fb0f7ebbee45752098cf453b6dffff3")
+  SET(SQLITE_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/sqlite-amalgamation-3460100.zip")
 
-  set(ORTHANC_SQLITE_VERSION 3027001)
+  set(ORTHANC_SQLITE_VERSION 3046001)
 
   DownloadPackage(${SQLITE_MD5} ${SQLITE_URL} "${SQLITE_SOURCES_DIR}")
 
--- a/OrthancFramework/Resources/CMake/UuidConfiguration.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/UuidConfiguration.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/VisualStudioPrecompiledHeaders.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/VisualStudioPrecompiledHeaders.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/CMakeLists.txt	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/CMakeLists.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CMake/ZlibConfiguration.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CMake/ZlibConfiguration.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CheckOrthancFrameworkSymbols.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CheckOrthancFrameworkSymbols.py	Tue Mar 18 13:37:18 2025 +0100
@@ -4,8 +4,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CodeGeneration/CheckDcmtkTransferSyntaxes.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CodeGeneration/CheckDcmtkTransferSyntaxes.py	Tue Mar 18 13:37:18 2025 +0100
@@ -4,8 +4,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json	Tue Mar 18 13:37:18 2025 +0100
@@ -262,6 +262,11 @@
     "Name": "DuplicateResource", 
     "Description": "Duplicate resource"
   }, 
+  {
+    "Code": 47,
+    "Name": "IncompatibleConfigurations", 
+    "Description": "Your configuration file contains configuration that are mutually incompatible"
+  },
 
 
 
@@ -337,7 +342,7 @@
   {
     "Code": 1011, 
     "Name": "SQLiteBindOutOfRange", 
-    "Description": "SQLite: Bing a value while out of range (serious error)",
+    "Description": "SQLite: Bind a value while out of range (serious error)",
     "SQLite": true
   },
   {
@@ -602,6 +607,11 @@
     "Name": "NoCGetHandler", 
     "Description": "No request handler factory for DICOM C-GET SCP"
   },
+  {
+    "Code": 2045, 
+    "Name": "DicomGetUnavailable", 
+    "Description": "DicomUserConnection: The C-GET command is not supported by the remote SCP"
+  },
 
 
 
--- a/OrthancFramework/Resources/CodeGeneration/GenerateErrorCodes.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CodeGeneration/GenerateErrorCodes.py	Tue Mar 18 13:37:18 2025 +0100
@@ -4,8 +4,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxes.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxes.py	Tue Mar 18 13:37:18 2025 +0100
@@ -4,8 +4,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxesDcmtk.mustache	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxesDcmtk.mustache	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxesEnumerations.mustache	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxesEnumerations.mustache	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/DcmtkTools/CMakeLists.txt	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/DcmtkTools/CMakeLists.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/DcmtkTools/dummy.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/DcmtkTools/dummy.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/EmbedResources.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/EmbedResources.py	Tue Mar 18 13:37:18 2025 +0100
@@ -4,8 +4,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/Graveyard/EclipseCodingStyle.xml	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,167 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<profiles version="1">
-<profile kind="CodeFormatterProfile" name="Orthanc" version="1">
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_method_declaration" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_new_line_in_empty_block" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.lineSplit" value="80"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_member_access" value="0"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_base_types" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.keep_else_statement_on_same_line" value="false"/>
-<setting id="org.eclipse.cdt.core.formatter.indent_switchstatements_compare_to_switch" value="false"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_constructor_initializer_list" value="0"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_brace_in_array_initializer" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_exception_specification" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_base_types" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.indent_body_declarations_compare_to_access_specifier" value="true"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_exception_specification" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_template_arguments" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_method_declaration" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.use_tabs_only_for_leading_indentations" value="false"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_colon_in_labeled_statement" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_colon_in_case" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.comment.min_distance_between_code_and_line_comment" value="1"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_expressions_in_array_initializer" value="18"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_declarator_list" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_bracket" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.tabulation.size" value="2"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_new_line_before_else_in_if_statement" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_enumerator_list" value="49"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_between_empty_parens_in_method_declaration" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_declarator_list" value="16"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.indent_empty_lines" value="false"/>
-<setting id="org.eclipse.cdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
-<setting id="org.eclipse.cdt.core.formatter.keep_empty_array_initializer_on_one_line" value="false"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_method_declaration" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.put_empty_statement_on_new_line" value="true"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.brace_position_for_method_declaration" value="next_line"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_semicolon" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_closing_angle_bracket_in_template_arguments" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_colon_in_base_clause" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.indent_breaks_compare_to_cases" value="true"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.join_wrapped_lines" value="true"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_declarator_list" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_arguments_in_method_invocation" value="18"/>
-<setting id="org.eclipse.cdt.core.formatter.comment.never_indent_line_comments_on_first_column" value="true"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_between_empty_brackets" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_bracket" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_parameters_in_method_declaration" value="18"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.number_of_empty_lines_to_preserve" value="1"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_method_invocation" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.brace_position_for_block" value="next_line"/>
-<setting id="org.eclipse.cdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments" value="true"/>
-<setting id="org.eclipse.cdt.core.formatter.brace_position_for_type_declaration" value="next_line"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_assignment_operator" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_angle_bracket_in_template_arguments" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_expression_list" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_angle_bracket_in_template_parameters" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.continuation_indentation" value="2"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_expression_list" value="0"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_template_parameters" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_binary_operator" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_conditional_expression" value="34"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_between_empty_parens_in_method_invocation" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_array_initializer" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.format_guardian_clause_on_one_line" value="false"/>
-<setting id="org.eclipse.cdt.core.formatter.indent_access_specifier_extra_spaces" value="0"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.indent_access_specifier_compare_to_type_header" value="false"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_type_declaration" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.continuation_indentation_for_array_initializer" value="2"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_method_invocation" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.indent_body_declarations_compare_to_namespace_header" value="true"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_compact_if" value="16"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_assignment_operator" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_array_initializer" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_assignment" value="16"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_conditional_expression_chain" value="18"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_template_parameters" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_expression_list" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_exception_specification" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_binary_operator" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_new_line_before_identifier_in_function_declaration" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_base_clause_in_type_declaration" value="80"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_method_declaration_throws" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_between_empty_parens_in_exception_specification" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.indent_declaration_compare_to_template_header" value="false"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.indent_statements_compare_to_body" value="true"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_method_declaration_throws" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_binary_expression" value="18"/>
-<setting id="org.eclipse.cdt.core.formatter.indent_statements_compare_to_block" value="true"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_template_arguments" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_method_invocation" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_closing_paren_in_cast" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_angle_bracket_in_template_parameters" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.tabulation.char" value="space"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_angle_bracket_in_template_parameters" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_new_line_before_colon_in_constructor_initializer_list" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.brace_position_for_block_in_case" value="next_line"/>
-<setting id="org.eclipse.cdt.core.formatter.compact_else_if" value="true"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_new_line_after_template_declaration" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_colon_in_base_clause" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.keep_then_statement_on_same_line" value="false"/>
-<setting id="org.eclipse.cdt.core.formatter.brace_position_for_switch" value="next_line"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_overloaded_left_shift_chain" value="18"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.keep_imple_if_on_one_line" value="false"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.indentation.size" value="2"/>
-<setting id="org.eclipse.cdt.core.formatter.brace_position_for_namespace_declaration" value="next_line"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_enum_declarations" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_angle_bracket_in_template_arguments" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.brace_position_for_array_initializer" value="next_line"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_namespace_declaration" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_bracket" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_new_line_before_while_in_do_statement" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_closing_angle_bracket_in_template_parameters" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_angle_bracket_in_template_arguments" value="do not insert"/>
-</profile>
-</profiles>
--- a/OrthancFramework/Resources/Graveyard/FromDcmtkBridge.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,192 +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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
- **/
-
-
-  DcmElement* FromDcmtkBridge::CreateElementForTag(const DicomTag& tag)
-  {
-    DcmTag key(tag.GetGroup(), tag.GetElement());
-
-    if (tag.IsPrivate())
-    {
-      // This raises BitBucket issue 140 (Modifying private tags with
-      // REST API changes VR from LO to UN)
-      // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=140
-      LOG(WARNING) << "You are using DCMTK < 3.6.1: All the private tags "
-        "are considered as having a binary value representation";
-      return new DcmOtherByteOtherWord(key);
-    }
-    else if (IsBinaryTag(key))
-    {
-      return new DcmOtherByteOtherWord(key);
-    }
-
-    switch (key.getEVR())
-    {
-      // http://support.dcmtk.org/docs/dcvr_8h-source.html
-
-      /**
-       * Binary types, handled above
-       **/
-    
-#if DCMTK_VERSION_NUMBER >= 361
-      case EVR_OD:
-#endif            
-
-#if DCMTK_VERSION_NUMBER >= 362
-      case EVR_OL:
-#endif            
-
-      case EVR_OB:  // other byte
-      case EVR_OF:  // other float
-      case EVR_OW:  // other word
-      case EVR_UN:  // unknown value representation
-      case EVR_ox:  // OB or OW depending on context
-        throw OrthancException(ErrorCode_InternalError);
-
-
-      /**
-       * String types.
-       * http://support.dcmtk.org/docs/classDcmByteString.html
-       **/
-      
-      case EVR_AS:  // age string
-        return new DcmAgeString(key);
-
-      case EVR_AE:  // application entity title
-        return new DcmApplicationEntity(key);
-
-      case EVR_CS:  // code string
-        return new DcmCodeString(key);        
-
-      case EVR_DA:  // date string
-        return new DcmDate(key);
-        
-      case EVR_DT:  // date time string
-        return new DcmDateTime(key);
-
-      case EVR_DS:  // decimal string
-        return new DcmDecimalString(key);
-
-      case EVR_IS:  // integer string
-        return new DcmIntegerString(key);
-
-      case EVR_TM:  // time string
-        return new DcmTime(key);
-
-      case EVR_UI:  // unique identifier
-        return new DcmUniqueIdentifier(key);
-
-      case EVR_ST:  // short text
-        return new DcmShortText(key);
-
-      case EVR_LO:  // long string
-        return new DcmLongString(key);
-
-      case EVR_LT:  // long text
-        return new DcmLongText(key);
-
-      case EVR_UT:  // unlimited text
-        return new DcmUnlimitedText(key);
-
-      case EVR_SH:  // short string
-        return new DcmShortString(key);
-
-      case EVR_PN:  // person name
-        return new DcmPersonName(key);
-
-#if DCMTK_VERSION_NUMBER >= 361
-      case EVR_UC:  // unlimited characters
-        return new DcmUnlimitedCharacters(key);
-#endif
-
-#if DCMTK_VERSION_NUMBER >= 361
-      case EVR_UR:  // URI/URL
-        return new DcmUniversalResourceIdentifierOrLocator(key);
-#endif
-          
-        
-      /**
-       * Numerical types
-       **/ 
-      
-      case EVR_SL:  // signed long
-        return new DcmSignedLong(key);
-
-      case EVR_SS:  // signed short
-        return new DcmSignedShort(key);
-
-      case EVR_UL:  // unsigned long
-        return new DcmUnsignedLong(key);
-
-      case EVR_US:  // unsigned short
-        return new DcmUnsignedShort(key);
-
-      case EVR_FL:  // float single-precision
-        return new DcmFloatingPointSingle(key);
-
-      case EVR_FD:  // float double-precision
-        return new DcmFloatingPointDouble(key);
-
-
-      /**
-       * Sequence types, should never occur at this point.
-       **/
-
-      case EVR_SQ:  // sequence of items
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-
-
-      /**
-       * TODO
-       **/
-
-      case EVR_AT:  // attribute tag
-        throw OrthancException(ErrorCode_NotImplemented);
-
-
-      /**
-       * Internal to DCMTK.
-       **/ 
-
-      case EVR_xs:  // SS or US depending on context
-      case EVR_lt:  // US, SS or OW depending on context, used for LUT Data (thus the name)
-      case EVR_na:  // na="not applicable", for data which has no VR
-      case EVR_up:  // up="unsigned pointer", used internally for DICOMDIR suppor
-      case EVR_item:  // used internally for items
-      case EVR_metainfo:  // used internally for meta info datasets
-      case EVR_dataset:  // used internally for datasets
-      case EVR_fileFormat:  // used internally for DICOM files
-      case EVR_dicomDir:  // used internally for DICOMDIR objects
-      case EVR_dirRecord:  // used internally for DICOMDIR records
-      case EVR_pixelSQ:  // used internally for pixel sequences in a compressed image
-      case EVR_pixelItem:  // used internally for pixel items in a compressed image
-      case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR)
-      case EVR_PixelData:  // used internally for uncompressed pixeld data
-      case EVR_OverlayData:  // used internally for overlay data
-      case EVR_UNKNOWN2B:  // used internally for elements with unknown VR with 2-byte length field in explicit VR
-      default:
-        break;
-    }
-
-    throw OrthancException(ErrorCode_InternalError);
-  }
--- a/OrthancFramework/Resources/Graveyard/Multithreading/BagOfTasks.h	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../ICommand.h"
-
-#include <list>
-#include <cstddef>
-
-namespace Orthanc
-{
-  class BagOfTasks : public boost::noncopyable
-  {
-  private:
-    typedef std::list<ICommand*>  Tasks;
-
-    Tasks  tasks_;
-
-  public:
-    ~BagOfTasks()
-    {
-      for (Tasks::iterator it = tasks_.begin(); it != tasks_.end(); ++it)
-      {
-        delete *it;
-      }
-    }
-
-    ICommand* Pop()
-    {
-      ICommand* task = tasks_.front();
-      tasks_.pop_front();
-      return task;
-    }
-
-    void Push(ICommand* task)   // Takes ownership
-    {
-      if (task != NULL)
-      {
-        tasks_.push_back(task);
-      }
-    }
-
-    size_t GetSize() const
-    {
-      return tasks_.size();
-    }
-
-    bool IsEmpty() const
-    {
-      return tasks_.empty();
-    }
-  };
-}
--- a/OrthancFramework/Resources/Graveyard/Multithreading/BagOfTasksProcessor.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,268 +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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeaders.h"
-#include "BagOfTasksProcessor.h"
-
-#include "../Logging.h"
-#include "../OrthancException.h"
-
-#include <stdio.h>
-
-namespace Orthanc
-{
-  class BagOfTasksProcessor::Task : public IDynamicObject
-  {
-  private:
-    uint64_t                 bag_;
-    std::auto_ptr<ICommand>  command_;
-
-  public:
-    Task(uint64_t  bag,
-         ICommand* command) :
-      bag_(bag),
-      command_(command)
-    {
-    }
-
-    bool Execute()
-    {
-      try
-      {
-        return command_->Execute();
-      }
-      catch (OrthancException& e)
-      {
-        LOG(ERROR) << "Exception while processing a bag of tasks: " << e.What();
-        return false;
-      }
-      catch (std::runtime_error& e)
-      {
-        LOG(ERROR) << "Runtime exception while processing a bag of tasks: " << e.what();
-        return false;
-      }
-      catch (...)
-      {
-        LOG(ERROR) << "Native exception while processing a bag of tasks";
-        return false;
-      }
-    }
-
-    uint64_t GetBag()
-    {
-      return bag_;
-    }
-  };
-
-
-  void BagOfTasksProcessor::SignalProgress(Task& task,
-                                           Bag& bag)
-  {
-    assert(bag.done_ < bag.size_);
-
-    bag.done_ += 1;
-
-    if (bag.done_ == bag.size_)
-    {
-      exitStatus_[task.GetBag()] = (bag.status_ == BagStatus_Running);
-      bagFinished_.notify_all();
-    }
-  }
-
-  void BagOfTasksProcessor::Worker(BagOfTasksProcessor* that)
-  {
-    while (that->continue_)
-    {
-      std::auto_ptr<IDynamicObject> obj(that->queue_.Dequeue(100));
-      if (obj.get() != NULL)
-      {
-        Task& task = *dynamic_cast<Task*>(obj.get());
-
-        {
-          boost::mutex::scoped_lock lock(that->mutex_);
-
-          Bags::iterator bag = that->bags_.find(task.GetBag());
-          assert(bag != that->bags_.end());
-          assert(bag->second.done_ < bag->second.size_);
-
-          if (bag->second.status_ != BagStatus_Running)
-          {
-            // Do not execute this task, as its parent bag of tasks
-            // has failed or is tagged as canceled
-            that->SignalProgress(task, bag->second);
-            continue;
-          }
-        }
-
-        bool success = task.Execute();
-
-        {
-          boost::mutex::scoped_lock lock(that->mutex_);
-
-          Bags::iterator bag = that->bags_.find(task.GetBag());
-          assert(bag != that->bags_.end());
-
-          if (!success)
-          {
-            bag->second.status_ = BagStatus_Failed;
-          }
-
-          that->SignalProgress(task, bag->second);
-        }
-      }
-    }
-  }
-
-
-  void BagOfTasksProcessor::Cancel(int64_t bag)
-  {
-    boost::mutex::scoped_lock  lock(mutex_);
-
-    Bags::iterator it = bags_.find(bag);
-    if (it != bags_.end())
-    {
-      it->second.status_ = BagStatus_Canceled;
-    }
-  }
-
-
-  bool BagOfTasksProcessor::Join(int64_t bag)
-  {
-    boost::mutex::scoped_lock  lock(mutex_);
-
-    while (continue_)
-    {
-      ExitStatus::iterator it = exitStatus_.find(bag);
-      if (it == exitStatus_.end())  // The bag is still running
-      {
-        bagFinished_.wait(lock);
-      }
-      else
-      {
-        bool status = it->second;
-        exitStatus_.erase(it);
-        return status;
-      }
-    }
-
-    return false;   // The processor is stopping
-  }
-
-
-  float BagOfTasksProcessor::GetProgress(int64_t bag)
-  {
-    boost::mutex::scoped_lock  lock(mutex_);
-
-    Bags::const_iterator it = bags_.find(bag);
-    if (it == bags_.end())
-    {
-      // The bag of tasks has finished
-      return 1.0f;
-    }
-    else
-    {
-      return (static_cast<float>(it->second.done_) / 
-              static_cast<float>(it->second.size_));
-    }
-  }
-
-
-  bool BagOfTasksProcessor::Handle::Join()
-  {
-    if (hasJoined_)
-    {
-      return status_;
-    }
-    else
-    {
-      status_ = that_.Join(bag_);
-      hasJoined_ = true;
-      return status_;
-    }
-  }
-
-
-  BagOfTasksProcessor::BagOfTasksProcessor(size_t countThreads) : 
-    countBags_(0),
-    continue_(true)
-  {
-    if (countThreads == 0)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    threads_.resize(countThreads);
-
-    for (size_t i = 0; i < threads_.size(); i++)
-    {
-      threads_[i] = new boost::thread(Worker, this);
-    }
-  }
-
-
-  BagOfTasksProcessor::~BagOfTasksProcessor()
-  {
-    continue_ = false;
-
-    bagFinished_.notify_all();   // Wakes up all the pending "Join()"
-
-    for (size_t i = 0; i < threads_.size(); i++)
-    {
-      if (threads_[i])
-      {
-        if (threads_[i]->joinable())
-        {
-          threads_[i]->join();
-        }
-
-        delete threads_[i];
-        threads_[i] = NULL;
-      }
-    }
-  }
-
-
-  BagOfTasksProcessor::Handle* BagOfTasksProcessor::Submit(BagOfTasks& tasks)
-  {
-    if (tasks.GetSize() == 0)
-    {
-      return new Handle(*this, 0, true);
-    }
-
-    boost::mutex::scoped_lock lock(mutex_);
-
-    uint64_t id = countBags_;
-    countBags_ += 1;
-
-    Bag bag(tasks.GetSize());
-    bags_[id] = bag;
-
-    while (!tasks.IsEmpty())
-    {
-      queue_.Enqueue(new Task(id, tasks.Pop()));
-    }
-
-    return new Handle(*this, id, false);
-  }
-}
--- a/OrthancFramework/Resources/Graveyard/Multithreading/BagOfTasksProcessor.h	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,141 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "BagOfTasks.h"
-#include "SharedMessageQueue.h"
-
-#include <stdint.h>
-#include <map>
-
-namespace Orthanc
-{
-  class BagOfTasksProcessor : public boost::noncopyable
-  {
-  private:
-    enum BagStatus
-    {
-      BagStatus_Running,
-      BagStatus_Canceled,
-      BagStatus_Failed
-    };
-
-
-    struct Bag
-    {
-      size_t    size_;
-      size_t    done_;
-      BagStatus status_;
-
-      Bag() :
-        size_(0),
-        done_(0),
-        status_(BagStatus_Failed)
-      {
-      }
-
-      explicit Bag(size_t size) : 
-        size_(size),
-        done_(0),
-        status_(BagStatus_Running)
-      {
-      }
-    };
-
-    class Task;
-
-
-    typedef std::map<uint64_t, Bag>   Bags;
-    typedef std::map<uint64_t, bool>  ExitStatus;
-
-    SharedMessageQueue  queue_;
-
-    boost::mutex  mutex_;
-    uint64_t  countBags_;
-    Bags bags_;
-    std::vector<boost::thread*>   threads_;
-    ExitStatus  exitStatus_;
-    bool continue_;
-
-    boost::condition_variable  bagFinished_;
-
-    static void Worker(BagOfTasksProcessor* that);
-
-    void Cancel(int64_t bag);
-
-    bool Join(int64_t bag);
-
-    float GetProgress(int64_t bag);
-
-    void SignalProgress(Task& task,
-                        Bag& bag);
-
-  public:
-    class Handle : public boost::noncopyable
-    {
-      friend class BagOfTasksProcessor;
-
-    private:
-      BagOfTasksProcessor&  that_;
-      uint64_t              bag_;
-      bool                  hasJoined_;
-      bool                  status_;
- 
-      Handle(BagOfTasksProcessor&  that,
-             uint64_t bag,
-             bool empty) : 
-        that_(that),
-        bag_(bag),
-        hasJoined_(empty)
-      {
-      }
-
-    public:
-      ~Handle()
-      {
-        Join();
-      }
-
-      void Cancel()
-      {
-        that_.Cancel(bag_);
-      }
-
-      bool Join();
-
-      float GetProgress()
-      {
-        return that_.GetProgress(bag_);
-      }
-    };
-  
-
-    explicit BagOfTasksProcessor(size_t countThreads);
-
-    ~BagOfTasksProcessor();
-
-    Handle* Submit(BagOfTasks& tasks);
-  };
-}
--- a/OrthancFramework/Resources/Graveyard/Multithreading/ICommand.h	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,40 +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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "IDynamicObject.h"
-
-namespace Orthanc
-{
-  /**
-   * This class is the base class for the "Command" design pattern.
-   * http://en.wikipedia.org/wiki/Command_pattern
-   **/
-  class ICommand : public IDynamicObject
-  {
-  public:
-    virtual bool Execute() = 0;
-  };
-}
--- a/OrthancFramework/Resources/Graveyard/Multithreading/ILockable.h	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,45 +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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  class ILockable : public boost::noncopyable
-  {
-    friend class Locker;
-
-  protected:
-    virtual void Lock() = 0;
-
-    virtual void Unlock() = 0;
-
-  public:
-    virtual ~ILockable()
-    {
-    }
-  };
-}
--- a/OrthancFramework/Resources/Graveyard/Multithreading/Locker.h	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,47 +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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "ILockable.h"
-
-namespace Orthanc
-{
-  class Locker : public boost::noncopyable
-  {
-  private:
-    ILockable& lockable_;
-
-  public:
-    Locker(ILockable& lockable) : lockable_(lockable)
-    {
-      lockable_.Lock();
-    }
-
-    virtual ~Locker()
-    {
-      lockable_.Unlock();
-    }
-  };
-}
--- a/OrthancFramework/Resources/Graveyard/Multithreading/Mutex.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,113 +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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeaders.h"
-#include "Mutex.h"
-
-#include "../OrthancException.h"
-
-#if defined(_WIN32)
-#include <windows.h>
-#elif defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__)
-#include <pthread.h>
-#else
-#error Support your platform here
-#endif
-
-namespace Orthanc
-{
-#if defined (_WIN32)
-
-  struct Mutex::PImpl
-  {
-    CRITICAL_SECTION criticalSection_;
-  };
-
-  Mutex::Mutex()
-  {
-    pimpl_ = new PImpl;
-    ::InitializeCriticalSection(&pimpl_->criticalSection_);
-  }
-
-  Mutex::~Mutex()
-  {
-    ::DeleteCriticalSection(&pimpl_->criticalSection_);
-    delete pimpl_;
-  }
-
-  void Mutex::Lock()
-  {
-    ::EnterCriticalSection(&pimpl_->criticalSection_);
-  }
-
-  void Mutex::Unlock()
-  {
-    ::LeaveCriticalSection(&pimpl_->criticalSection_);
-  }
-
-
-#elif defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__)
-
-  struct Mutex::PImpl
-  {
-    pthread_mutex_t mutex_;
-  };
-
-  Mutex::Mutex()
-  {
-    pimpl_ = new PImpl;
-
-    if (pthread_mutex_init(&pimpl_->mutex_, NULL) != 0)
-    {
-      delete pimpl_;
-      throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-
-  Mutex::~Mutex()
-  {
-    pthread_mutex_destroy(&pimpl_->mutex_);
-    delete pimpl_;
-  }
-
-  void Mutex::Lock()
-  {
-    if (pthread_mutex_lock(&pimpl_->mutex_) != 0)
-    {
-      throw OrthancException(ErrorCode_InternalError);    
-    }
-  }
-
-  void Mutex::Unlock()
-  {
-    if (pthread_mutex_unlock(&pimpl_->mutex_) != 0)
-    {
-      throw OrthancException(ErrorCode_InternalError);    
-    }
-  }
-
-#else
-#error Support your plateform here
-#endif
-}
--- a/OrthancFramework/Resources/Graveyard/Multithreading/Mutex.h	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "ILockable.h"
-
-namespace Orthanc
-{
-  class Mutex : public ILockable
-  {
-  private:
-    struct PImpl;
-
-    PImpl *pimpl_;
-
-  protected:
-    virtual void Lock();
-
-    virtual void Unlock();
-    
-  public:
-    Mutex();
-
-    ~Mutex();
-  };
-}
--- a/OrthancFramework/Resources/Graveyard/Multithreading/ReaderWriterLock.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,117 +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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeaders.h"
-#include "ReaderWriterLock.h"
-
-#include <boost/thread/shared_mutex.hpp>
-
-namespace Orthanc
-{
-  namespace
-  {
-    // Anonymous namespace to avoid clashes between compilation
-    // modules.
-
-    class ReaderLockable : public ILockable
-    {
-    private:
-      boost::shared_mutex& lock_;
-
-    protected:
-      virtual void Lock()
-      {
-        lock_.lock_shared();
-      }
-
-      virtual void Unlock()
-      {
-        lock_.unlock_shared();        
-      }
-
-    public:
-      explicit ReaderLockable(boost::shared_mutex& lock) : lock_(lock)
-      {
-      }
-    };
-
-
-    class WriterLockable : public ILockable
-    {
-    private:
-      boost::shared_mutex& lock_;
-
-    protected:
-      virtual void Lock()
-      {
-        lock_.lock();
-      }
-
-      virtual void Unlock()
-      {
-        lock_.unlock();        
-      }
-
-    public:
-      explicit WriterLockable(boost::shared_mutex& lock) : lock_(lock)
-      {
-      }
-    };
-  }
-
-  struct ReaderWriterLock::PImpl
-  {
-    boost::shared_mutex lock_;
-    ReaderLockable reader_;
-    WriterLockable writer_;
-
-    PImpl() : reader_(lock_), writer_(lock_)
-    {
-    }
-  };
-
-
-  ReaderWriterLock::ReaderWriterLock()
-  {
-    pimpl_ = new PImpl;
-  }
-
-
-  ReaderWriterLock::~ReaderWriterLock()
-  {
-    delete pimpl_;
-  }
-
-
-  ILockable&  ReaderWriterLock::ForReader()
-  {
-    return pimpl_->reader_;
-  }
-
-
-  ILockable&  ReaderWriterLock::ForWriter()
-  {
-    return pimpl_->writer_;
-  }
-}
--- a/OrthancFramework/Resources/Graveyard/Multithreading/ReaderWriterLock.h	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "ILockable.h"
-
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  class ReaderWriterLock : public boost::noncopyable
-  {
-  private:
-    struct PImpl;
-
-    PImpl *pimpl_;
-
-  public:
-    ReaderWriterLock();
-
-    virtual ~ReaderWriterLock();
-
-    ILockable& ForReader();
-
-    ILockable& ForWriter();
-  };
-}
--- a/OrthancFramework/Resources/Graveyard/TestTranscoding.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,991 +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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this program. If not, see
- * <http://www.gnu.org/licenses/>.
- **/
-
-
-  bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer,
-                                           DcmFileFormat& dicom,
-                                           DicomTransferSyntax syntax)
-  {
-    E_TransferSyntax xfer;
-    if (!LookupDcmtkTransferSyntax(xfer, syntax))
-    {
-      return false;
-    }
-    else if (!dicom.validateMetaInfo(xfer).good())
-    {
-      throw OrthancException(ErrorCode_InternalError,
-                             "Cannot setup the transfer syntax to write a DICOM instance");
-    }
-    else
-    {
-      return SaveToMemoryBufferInternal(buffer, dicom, xfer);
-    }
-  }
-
-
-  bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer,
-                                           DcmFileFormat& dicom)
-  {
-    E_TransferSyntax xfer = dicom.getDataset()->getCurrentXfer();
-    if (xfer == EXS_Unknown)
-    {
-      throw OrthancException(ErrorCode_InternalError,
-                             "Cannot write a DICOM instance with unknown transfer syntax");
-    }
-    else if (!dicom.validateMetaInfo(xfer).good())
-    {
-      throw OrthancException(ErrorCode_InternalError,
-                             "Cannot setup the transfer syntax to write a DICOM instance");
-    }
-    else
-    {
-      return SaveToMemoryBufferInternal(buffer, dicom, xfer);
-    }
-  }
-
-
-
-
-
-#include <dcmtk/dcmdata/dcostrmb.h>
-#include <dcmtk/dcmdata/dcpixel.h>
-#include <dcmtk/dcmdata/dcpxitem.h>
-
-#include "../Core/DicomParsing/Internals/DicomFrameIndex.h"
-
-namespace Orthanc
-{
-  class IParsedDicomImage : public boost::noncopyable
-  {
-  public:
-    virtual ~IParsedDicomImage()
-    {
-    }
-
-    virtual DicomTransferSyntax GetTransferSyntax() = 0;
-
-    virtual std::string GetSopClassUid() = 0;
-
-    virtual std::string GetSopInstanceUid() = 0;
-
-    virtual unsigned int GetFramesCount() = 0;
-
-    // Can return NULL, for compressed transfer syntaxes
-    virtual ImageAccessor* GetUncompressedFrame(unsigned int frame) = 0;
-    
-    virtual void GetCompressedFrame(std::string& target,
-                                    unsigned int frame) = 0;
-    
-    virtual void WriteToMemoryBuffer(std::string& target) = 0;
-  };
-
-
-  class IDicomImageReader : public boost::noncopyable
-  {
-  public:
-    virtual ~IDicomImageReader()
-    {
-    }
-
-    virtual IParsedDicomImage* Read(const void* data,
-                                    size_t size) = 0;
-
-    virtual IParsedDicomImage* Transcode(const void* data,
-                                         size_t size,
-                                         DicomTransferSyntax syntax,
-                                         bool allowNewSopInstanceUid) = 0;
-  };
-
-
-  class DcmtkImageReader : public IDicomImageReader
-  {
-  private:
-    class Image : public IParsedDicomImage
-    {
-    private:
-      std::unique_ptr<DcmFileFormat>    dicom_;
-      std::unique_ptr<DicomFrameIndex>  index_;
-      DicomTransferSyntax               transferSyntax_;
-      std::string                       sopClassUid_;
-      std::string                       sopInstanceUid_;
-
-      static std::string GetStringTag(DcmDataset& dataset,
-                                      const DcmTagKey& tag)
-      {
-        const char* value = NULL;
-
-        if (!dataset.findAndGetString(tag, value).good() ||
-            value == NULL)
-        {
-          throw OrthancException(ErrorCode_BadFileFormat,
-                                 "Missing SOP class/instance UID in DICOM instance");
-        }
-        else
-        {
-          return std::string(value);
-        }
-      }
-
-    public:
-      Image(DcmFileFormat* dicom,
-            DicomTransferSyntax syntax) :
-        dicom_(dicom),
-        transferSyntax_(syntax)
-      {
-        if (dicom == NULL ||
-            dicom_->getDataset() == NULL)
-        {
-          throw OrthancException(ErrorCode_NullPointer);
-        }
-
-        DcmDataset& dataset = *dicom_->getDataset();
-        index_.reset(new DicomFrameIndex(dataset));
-
-        sopClassUid_ = GetStringTag(dataset, DCM_SOPClassUID);
-        sopInstanceUid_ = GetStringTag(dataset, DCM_SOPInstanceUID);
-      }
-
-      virtual DicomTransferSyntax GetTransferSyntax() ORTHANC_OVERRIDE
-      {
-        return transferSyntax_;
-      }
-
-      virtual std::string GetSopClassUid() ORTHANC_OVERRIDE
-      {
-        return sopClassUid_;
-      }
-    
-      virtual std::string GetSopInstanceUid() ORTHANC_OVERRIDE
-      {
-        return sopInstanceUid_;
-      }
-
-      virtual unsigned int GetFramesCount() ORTHANC_OVERRIDE
-      {
-        return index_->GetFramesCount();
-      }
-
-      virtual void WriteToMemoryBuffer(std::string& target) ORTHANC_OVERRIDE
-      {
-        assert(dicom_.get() != NULL);
-        if (!FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, transferSyntax_))
-        {
-          throw OrthancException(ErrorCode_InternalError,
-                                 "Cannot write the DICOM instance to a memory buffer");
-        }
-      }
-
-      virtual ImageAccessor* GetUncompressedFrame(unsigned int frame) ORTHANC_OVERRIDE
-      {
-        assert(dicom_.get() != NULL &&
-               dicom_->getDataset() != NULL);
-        return DicomImageDecoder::Decode(*dicom_->getDataset(), frame);
-      }
-
-      virtual void GetCompressedFrame(std::string& target,
-                                      unsigned int frame) ORTHANC_OVERRIDE
-      {
-        assert(index_.get() != NULL);
-        index_->GetRawFrame(target, frame);
-      }
-    };
-
-    unsigned int lossyQuality_;
-
-    static DicomTransferSyntax DetectTransferSyntax(DcmFileFormat& dicom)
-    {
-      if (dicom.getDataset() == NULL)
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-        
-      DcmDataset& dataset = *dicom.getDataset();
-
-      E_TransferSyntax xfer = dataset.getCurrentXfer();
-      if (xfer == EXS_Unknown)
-      {
-        dataset.updateOriginalXfer();
-        xfer = dataset.getCurrentXfer();
-        if (xfer == EXS_Unknown)
-        {
-          throw OrthancException(ErrorCode_BadFileFormat,
-                                 "Cannot determine the transfer syntax of the DICOM instance");
-        }
-      }
-
-      DicomTransferSyntax syntax;
-      if (FromDcmtkBridge::LookupOrthancTransferSyntax(syntax, xfer))
-      {
-        return syntax;
-      }
-      else
-      {
-        throw OrthancException(
-          ErrorCode_BadFileFormat,
-          "Unsupported transfer syntax: " + boost::lexical_cast<std::string>(xfer));
-      }
-    }
-
-
-    static uint16_t GetBitsStored(DcmFileFormat& dicom)
-    {
-      if (dicom.getDataset() == NULL)
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      uint16_t bitsStored;
-      if (dicom.getDataset()->findAndGetUint16(DCM_BitsStored, bitsStored).good())
-      {
-        return bitsStored;
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_BadFileFormat,
-                               "Missing \"Bits Stored\" tag in DICOM instance");
-      }      
-    }
-    
-      
-  public:
-    DcmtkImageReader() :
-      lossyQuality_(90)
-    {
-    }
-
-    void SetLossyQuality(unsigned int quality)
-    {
-      if (quality <= 0 ||
-          quality > 100)
-      {
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-      else
-      {
-        lossyQuality_ = quality;
-      }
-    }
-
-    unsigned int GetLossyQuality() const
-    {
-      return lossyQuality_;
-    }
-
-    virtual IParsedDicomImage* Read(const void* data,
-                                    size_t size)
-    {
-      std::unique_ptr<DcmFileFormat> dicom(FromDcmtkBridge::LoadFromMemoryBuffer(data, size));
-      if (dicom.get() == NULL)
-      {
-        throw OrthancException(ErrorCode_BadFileFormat);
-      }
-
-      DicomTransferSyntax transferSyntax = DetectTransferSyntax(*dicom);
-
-      return new Image(dicom.release(), transferSyntax);
-    }
-
-    virtual IParsedDicomImage* Transcode(const void* data,
-                                         size_t size,
-                                         DicomTransferSyntax syntax,
-                                         bool allowNewSopInstanceUid)
-    {
-      std::unique_ptr<DcmFileFormat> dicom(FromDcmtkBridge::LoadFromMemoryBuffer(data, size));
-      if (dicom.get() == NULL)
-      {
-        throw OrthancException(ErrorCode_BadFileFormat);
-      }
-
-      const uint16_t bitsStored = GetBitsStored(*dicom);
-
-      if (syntax == DetectTransferSyntax(*dicom))
-      {
-        // No transcoding is needed
-        return new Image(dicom.release(), syntax);
-      }
-      
-      if (syntax == DicomTransferSyntax_LittleEndianImplicit &&
-          FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_LittleEndianImplicit, NULL))
-      {
-        return new Image(dicom.release(), syntax);
-      }
-
-      if (syntax == DicomTransferSyntax_LittleEndianExplicit &&
-          FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_LittleEndianExplicit, NULL))
-      {
-        return new Image(dicom.release(), syntax);
-      }
-      
-      if (syntax == DicomTransferSyntax_BigEndianExplicit &&
-          FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_BigEndianExplicit, NULL))
-      {
-        return new Image(dicom.release(), syntax);
-      }
-
-      if (syntax == DicomTransferSyntax_DeflatedLittleEndianExplicit &&
-          FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_DeflatedLittleEndianExplicit, NULL))
-      {
-        return new Image(dicom.release(), syntax);
-      }
-
-#if ORTHANC_ENABLE_JPEG == 1
-      if (syntax == DicomTransferSyntax_JPEGProcess1 &&
-          allowNewSopInstanceUid &&
-          bitsStored == 8)
-      {
-        DJ_RPLossy rpLossy(lossyQuality_);
-        
-        if (FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_JPEGProcess1, &rpLossy))
-        {
-          return new Image(dicom.release(), syntax);
-        }
-      }
-#endif
-      
-#if ORTHANC_ENABLE_JPEG == 1
-      if (syntax == DicomTransferSyntax_JPEGProcess2_4 &&
-          allowNewSopInstanceUid &&
-          bitsStored <= 12)
-      {
-        DJ_RPLossy rpLossy(lossyQuality_);
-        if (FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_JPEGProcess2_4, &rpLossy))
-        {
-          return new Image(dicom.release(), syntax);
-        }
-      }
-#endif
-
-      //LOG(INFO) << "Unable to transcode DICOM image using the built-in reader";
-      return NULL;
-    }
-  };
-  
-
-  
-  class IDicomTranscoder1 : public boost::noncopyable
-  {
-  public:
-    virtual ~IDicomTranscoder1()
-    {
-    }
-
-    virtual DcmFileFormat& GetDicom() = 0;
-
-    virtual DicomTransferSyntax GetTransferSyntax() = 0;
-
-    virtual std::string GetSopClassUid() = 0;
-
-    virtual std::string GetSopInstanceUid() = 0;
-
-    virtual unsigned int GetFramesCount() = 0;
-
-    virtual ImageAccessor* DecodeFrame(unsigned int frame) = 0;
-
-    virtual void GetCompressedFrame(std::string& target,
-                                    unsigned int frame) = 0;
-
-    // NB: Transcoding can change the value of "GetSopInstanceUid()"
-    // and "GetTransferSyntax()" if lossy compression is applied
-    virtual bool Transcode(std::string& target,
-                           DicomTransferSyntax syntax,
-                           bool allowNewSopInstanceUid) = 0;
-
-    virtual void WriteToMemoryBuffer(std::string& target) = 0;
-  };
-
-
-  class DcmtkTranscoder2 : public IDicomTranscoder1
-  {
-  private:
-    std::unique_ptr<DcmFileFormat>    dicom_;
-    std::unique_ptr<DicomFrameIndex>  index_;
-    DicomTransferSyntax               transferSyntax_;
-    std::string                       sopClassUid_;
-    std::string                       sopInstanceUid_;
-    uint16_t                          bitsStored_;
-    unsigned int                      lossyQuality_;
-
-    static std::string GetStringTag(DcmDataset& dataset,
-                                    const DcmTagKey& tag)
-    {
-      const char* value = NULL;
-
-      if (!dataset.findAndGetString(tag, value).good() ||
-          value == NULL)
-      {
-        throw OrthancException(ErrorCode_BadFileFormat,
-                               "Missing SOP class/instance UID in DICOM instance");
-      }
-      else
-      {
-        return std::string(value);
-      }
-    }
-
-    void Setup(DcmFileFormat* dicom)
-    {
-      lossyQuality_ = 90;
-      
-      dicom_.reset(dicom);
-      
-      if (dicom == NULL ||
-          dicom_->getDataset() == NULL)
-      {
-        throw OrthancException(ErrorCode_NullPointer);
-      }
-
-      DcmDataset& dataset = *dicom_->getDataset();
-      index_.reset(new DicomFrameIndex(dataset));
-
-      E_TransferSyntax xfer = dataset.getCurrentXfer();
-      if (xfer == EXS_Unknown)
-      {
-        dataset.updateOriginalXfer();
-        xfer = dataset.getCurrentXfer();
-        if (xfer == EXS_Unknown)
-        {
-          throw OrthancException(ErrorCode_BadFileFormat,
-                                 "Cannot determine the transfer syntax of the DICOM instance");
-        }
-      }
-
-      if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transferSyntax_, xfer))
-      {
-        throw OrthancException(
-          ErrorCode_BadFileFormat,
-          "Unsupported transfer syntax: " + boost::lexical_cast<std::string>(xfer));
-      }
-
-      if (!dataset.findAndGetUint16(DCM_BitsStored, bitsStored_).good())
-      {
-        throw OrthancException(ErrorCode_BadFileFormat,
-                               "Missing \"Bits Stored\" tag in DICOM instance");
-      }      
-
-      sopClassUid_ = GetStringTag(dataset, DCM_SOPClassUID);
-      sopInstanceUid_ = GetStringTag(dataset, DCM_SOPInstanceUID);
-    }
-    
-  public:
-    DcmtkTranscoder2(DcmFileFormat* dicom)  // Takes ownership
-    {
-      Setup(dicom);
-    }
-
-    DcmtkTranscoder2(const void* dicom,
-                    size_t size)
-    {
-      Setup(FromDcmtkBridge::LoadFromMemoryBuffer(dicom, size));
-    }
-
-    void SetLossyQuality(unsigned int quality)
-    {
-      if (quality <= 0 ||
-          quality > 100)
-      {
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-      else
-      {
-        lossyQuality_ = quality;
-      }
-    }
-
-    unsigned int GetLossyQuality() const
-    {
-      return lossyQuality_;
-    }
-
-    unsigned int GetBitsStored() const
-    {
-      return bitsStored_;
-    }
-
-    virtual DcmFileFormat& GetDicom()
-    {
-      assert(dicom_ != NULL);
-      return *dicom_;
-    }
-
-    virtual DicomTransferSyntax GetTransferSyntax() ORTHANC_OVERRIDE
-    {
-      return transferSyntax_;
-    }
-
-    virtual std::string GetSopClassUid() ORTHANC_OVERRIDE
-    {
-      return sopClassUid_;
-    }
-    
-    virtual std::string GetSopInstanceUid() ORTHANC_OVERRIDE
-    {
-      return sopInstanceUid_;
-    }
-
-    virtual unsigned int GetFramesCount() ORTHANC_OVERRIDE
-    {
-      return index_->GetFramesCount();
-    }
-
-    virtual void WriteToMemoryBuffer(std::string& target) ORTHANC_OVERRIDE
-    {
-      if (!FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_))
-      {
-        throw OrthancException(ErrorCode_InternalError,
-                               "Cannot write the DICOM instance to a memory buffer");
-      }
-    }
-
-    virtual ImageAccessor* DecodeFrame(unsigned int frame) ORTHANC_OVERRIDE
-    {
-      assert(dicom_->getDataset() != NULL);
-      return DicomImageDecoder::Decode(*dicom_->getDataset(), frame);
-    }
-
-    virtual void GetCompressedFrame(std::string& target,
-                                    unsigned int frame) ORTHANC_OVERRIDE
-    {
-      index_->GetRawFrame(target, frame);
-    }
-
-    virtual bool Transcode(std::string& target,
-                           DicomTransferSyntax syntax,
-                           bool allowNewSopInstanceUid) ORTHANC_OVERRIDE
-    {
-      assert(dicom_ != NULL &&
-             dicom_->getDataset() != NULL);
-      
-      if (syntax == GetTransferSyntax())
-      {
-        printf("NO TRANSCODING\n");
-        
-        // No change in the transfer syntax => simply serialize the current dataset
-        WriteToMemoryBuffer(target);
-        return true;
-      }
-      
-      printf(">> %d\n", bitsStored_);
-
-      if (syntax == DicomTransferSyntax_LittleEndianImplicit &&
-          FromDcmtkBridge::Transcode(*dicom_, syntax, NULL) &&
-          FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax))
-      {
-        transferSyntax_ = DicomTransferSyntax_LittleEndianImplicit;
-        return true;
-      }
-
-      if (syntax == DicomTransferSyntax_LittleEndianExplicit &&
-          FromDcmtkBridge::Transcode(*dicom_, syntax, NULL) &&
-          FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax))
-      {
-        transferSyntax_ = DicomTransferSyntax_LittleEndianExplicit;
-        return true;
-      }
-      
-      if (syntax == DicomTransferSyntax_BigEndianExplicit &&
-          FromDcmtkBridge::Transcode(*dicom_, syntax, NULL) &&
-          FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax))
-      {
-        transferSyntax_ = DicomTransferSyntax_BigEndianExplicit;
-        return true;
-      }
-
-      if (syntax == DicomTransferSyntax_DeflatedLittleEndianExplicit &&
-          FromDcmtkBridge::Transcode(*dicom_, syntax, NULL) &&
-          FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax))
-      {
-        transferSyntax_ = DicomTransferSyntax_DeflatedLittleEndianExplicit;
-        return true;
-      }
-
-#if ORTHANC_ENABLE_JPEG == 1
-      if (syntax == DicomTransferSyntax_JPEGProcess1 &&
-          allowNewSopInstanceUid &&
-          GetBitsStored() == 8)
-      {
-        DJ_RPLossy rpLossy(lossyQuality_);
-        
-        if (FromDcmtkBridge::Transcode(*dicom_, syntax, &rpLossy) &&
-            FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax))
-        {
-          transferSyntax_ = DicomTransferSyntax_JPEGProcess1;
-          sopInstanceUid_ = GetStringTag(*dicom_->getDataset(), DCM_SOPInstanceUID);
-          return true;
-        }
-      }
-#endif
-      
-#if ORTHANC_ENABLE_JPEG == 1
-      if (syntax == DicomTransferSyntax_JPEGProcess2_4 &&
-          allowNewSopInstanceUid &&
-          GetBitsStored() <= 12)
-      {
-        DJ_RPLossy rpLossy(lossyQuality_);
-        if (FromDcmtkBridge::Transcode(*dicom_, syntax, &rpLossy) &&
-            FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax))
-        {
-          transferSyntax_ = DicomTransferSyntax_JPEGProcess2_4;
-          sopInstanceUid_ = GetStringTag(*dicom_->getDataset(), DCM_SOPInstanceUID);
-          return true;
-        }
-      }
-#endif
-
-      return false;
-    }
-  };
-}
-
-
-
-
-#include <boost/filesystem.hpp>
-
-
-static void TestFile(const std::string& path)
-{
-  static unsigned int count = 0;
-  count++;
-  
-
-  printf("** %s\n", path.c_str());
-
-  std::string s;
-  SystemToolbox::ReadFile(s, path);
-
-  Orthanc::DcmtkTranscoder2 transcoder(s.c_str(), s.size());
-
-  /*if (transcoder.GetBitsStored() != 8)  // TODO
-    return; */
-
-  {
-    char buf[1024];
-    sprintf(buf, "/tmp/source-%06d.dcm", count);
-    printf(">> %s\n", buf);
-    Orthanc::SystemToolbox::WriteFile(s, buf);
-  }
-
-  printf("[%s] [%s] [%s] %d %d\n", GetTransferSyntaxUid(transcoder.GetTransferSyntax()),
-         transcoder.GetSopClassUid().c_str(), transcoder.GetSopInstanceUid().c_str(),
-         transcoder.GetFramesCount(), transcoder.GetTransferSyntax());
-
-  for (size_t i = 0; i < transcoder.GetFramesCount(); i++)
-  {
-    std::string f;
-    transcoder.GetCompressedFrame(f, i);
-
-    if (i == 0)
-    {
-      char buf[1024];
-      sprintf(buf, "/tmp/frame-%06d.raw", count);
-      printf(">> %s\n", buf);
-      Orthanc::SystemToolbox::WriteFile(f, buf);
-    }
-  }
-
-  {
-    std::string t;
-    transcoder.WriteToMemoryBuffer(t);
-
-    Orthanc::DcmtkTranscoder2 transcoder2(t.c_str(), t.size());
-    printf(">> %d %d ; %lu bytes\n", transcoder.GetTransferSyntax(), transcoder2.GetTransferSyntax(), t.size());
-  }
-
-  {
-    std::string a = transcoder.GetSopInstanceUid();
-    DicomTransferSyntax b = transcoder.GetTransferSyntax();
-    
-    DicomTransferSyntax syntax = DicomTransferSyntax_JPEGProcess2_4;
-    //DicomTransferSyntax syntax = DicomTransferSyntax_LittleEndianExplicit;
-
-    std::string t;
-    bool ok = transcoder.Transcode(t, syntax, true);
-    printf("Transcoding: %d\n", ok);
-
-    if (ok)
-    {
-      printf("[%s] => [%s]\n", a.c_str(), transcoder.GetSopInstanceUid().c_str());
-      printf("[%s] => [%s]\n", GetTransferSyntaxUid(b),
-             GetTransferSyntaxUid(transcoder.GetTransferSyntax()));
-      
-      {
-        char buf[1024];
-        sprintf(buf, "/tmp/transcoded-%06d.dcm", count);
-        printf(">> %s\n", buf);
-        Orthanc::SystemToolbox::WriteFile(t, buf);
-      }
-
-      Orthanc::DcmtkTranscoder2 transcoder2(t.c_str(), t.size());
-      printf("  => transcoded transfer syntax %d ; %lu bytes\n", transcoder2.GetTransferSyntax(), t.size());
-    }
-  }
-  
-  printf("\n");
-}
-
-TEST(Toto, DISABLED_Transcode)
-{
-  //OFLog::configure(OFLogger::DEBUG_LOG_LEVEL);
-
-  if (1)
-  {
-    const char* const PATH = "/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes";
-    
-    for (boost::filesystem::directory_iterator it(PATH);
-         it != boost::filesystem::directory_iterator(); ++it)
-    {
-      if (boost::filesystem::is_regular_file(it->status()))
-      {
-        TestFile(it->path().string());
-      }
-    }
-  }
-
-  if (0)
-  {
-    TestFile("/home/jodogne/Subversion/orthanc-tests/Database/Multiframe.dcm");
-    TestFile("/home/jodogne/Subversion/orthanc-tests/Database/Issue44/Monochrome1-Jpeg.dcm");
-  }
-
-  if (0)
-  {
-    TestFile("/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/1.2.840.10008.1.2.1.dcm");
-  }
-}
-
-
-TEST(Toto, DISABLED_Transcode2)
-{
-  for (int i = 0; i <= DicomTransferSyntax_XML; i++)
-  {
-    DicomTransferSyntax a = (DicomTransferSyntax) i;
-
-    std::string path = ("/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/" +
-                        std::string(GetTransferSyntaxUid(a)) + ".dcm");
-    if (Orthanc::SystemToolbox::IsRegularFile(path))
-    {
-      printf("\n======= %s\n", GetTransferSyntaxUid(a));
-
-      std::string source;
-      Orthanc::SystemToolbox::ReadFile(source, path);
-
-      DcmtkImageReader reader;
-
-      {
-        std::unique_ptr<IParsedDicomImage> image(
-          reader.Read(source.c_str(), source.size()));
-        ASSERT_TRUE(image.get() != NULL);
-        ASSERT_EQ(a, image->GetTransferSyntax());
-
-        std::string target;
-        image->WriteToMemoryBuffer(target);
-      }
-
-      for (int j = 0; j <= DicomTransferSyntax_XML; j++)
-      {
-        DicomTransferSyntax b = (DicomTransferSyntax) j;
-        //if (a == b) continue;
-
-        std::unique_ptr<IParsedDicomImage> image(
-          reader.Transcode(source.c_str(), source.size(), b, true));
-        if (image.get() != NULL)
-        {
-          printf("[%s] -> [%s]\n", GetTransferSyntaxUid(a), GetTransferSyntaxUid(b));
-
-          std::string target;
-          image->WriteToMemoryBuffer(target);
-
-          char buf[1024];
-          sprintf(buf, "/tmp/%s-%s.dcm", GetTransferSyntaxUid(a), GetTransferSyntaxUid(b));
-          
-          SystemToolbox::WriteFile(target, buf);
-        }
-        else if (a != DicomTransferSyntax_JPEG2000 &&
-                 a != DicomTransferSyntax_JPEG2000LosslessOnly)
-        {
-          ASSERT_TRUE(b != DicomTransferSyntax_LittleEndianImplicit &&
-                      b != DicomTransferSyntax_LittleEndianExplicit &&
-                      b != DicomTransferSyntax_BigEndianExplicit &&
-                      b != DicomTransferSyntax_DeflatedLittleEndianExplicit);
-        }
-      }
-    }
-  }
-}
-
-
-#include "../Core/DicomNetworking/DicomAssociation.h"
-#include "../Core/DicomNetworking/DicomControlUserConnection.h"
-#include "../Core/DicomNetworking/DicomStoreUserConnection.h"
-
-TEST(Toto, DISABLED_DicomAssociation)
-{
-  DicomAssociationParameters params;
-  params.SetLocalApplicationEntityTitle("ORTHANC");
-  params.SetRemoteApplicationEntityTitle("PACS");
-  params.SetRemotePort(2001);
-
-#if 0
-  DicomAssociation assoc;
-  assoc.ProposeGenericPresentationContext(UID_StorageCommitmentPushModelSOPClass);
-  assoc.ProposeGenericPresentationContext(UID_VerificationSOPClass);
-  assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage,
-                                   DicomTransferSyntax_JPEGProcess1);
-  assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage,
-                                   DicomTransferSyntax_JPEGProcess2_4);
-  assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage,
-                                   DicomTransferSyntax_JPEG2000);
-  
-  assoc.Open(params);
-
-  int presID = ASC_findAcceptedPresentationContextID(&assoc.GetDcmtkAssociation(), UID_ComputedRadiographyImageStorage);
-  printf(">> %d\n", presID);
-    
-  std::map<DicomTransferSyntax, uint8_t> pc;
-  printf(">> %d\n", assoc.LookupAcceptedPresentationContext(pc, UID_ComputedRadiographyImageStorage));
-  
-  for (std::map<DicomTransferSyntax, uint8_t>::const_iterator
-         it = pc.begin(); it != pc.end(); ++it)
-  {
-    printf("[%s] => %d\n", GetTransferSyntaxUid(it->first), it->second);
-  }
-#else
-  {
-    DicomControlUserConnection assoc(params);
-
-    try
-    {
-      printf(">> %d\n", assoc.Echo());
-    }
-    catch (OrthancException&)
-    {
-    }
-  }
-    
-  params.SetRemoteApplicationEntityTitle("PACS");
-  params.SetRemotePort(2000);
-
-  {
-    DicomControlUserConnection assoc(params);
-    printf(">> %d\n", assoc.Echo());
-  }
-
-#endif
-}
-
-static void TestTranscode(DicomStoreUserConnection& scu,
-                          const std::string& sopClassUid,
-                          DicomTransferSyntax transferSyntax)
-{
-  std::set<DicomTransferSyntax> accepted;
-
-  scu.LookupTranscoding(accepted, sopClassUid, transferSyntax);
-  if (accepted.empty())
-  {
-    throw OrthancException(ErrorCode_NetworkProtocol,
-                           "The SOP class is not supported by the remote modality");
-  }
-
-  {
-    unsigned int count = 0;
-    for (std::set<DicomTransferSyntax>::const_iterator
-           it = accepted.begin(); it != accepted.end(); ++it)
-    {
-      LOG(INFO) << "available for transcoding " << (count++) << ": " << sopClassUid
-                << " / " << GetTransferSyntaxUid(*it);
-    }
-  }
-  
-  if (accepted.find(transferSyntax) != accepted.end())
-  {
-    printf("**** OK, without transcoding !! [%s]\n", GetTransferSyntaxUid(transferSyntax));
-  }
-  else
-  {
-    // Transcoding - only in Orthanc >= 1.7.0
-
-    const DicomTransferSyntax uncompressed[] = {
-      DicomTransferSyntax_LittleEndianImplicit,  // Default transfer syntax
-      DicomTransferSyntax_LittleEndianExplicit,
-      DicomTransferSyntax_BigEndianExplicit
-    };
-
-    bool found = false;
-    for (size_t i = 0; i < 3; i++)
-    {
-      if (accepted.find(uncompressed[i]) != accepted.end())
-      {
-        printf("**** TRANSCODING to %s\n", GetTransferSyntaxUid(uncompressed[i]));
-        found = true;
-        break;
-      }
-    }
-
-    if (!found)
-    {
-      printf("**** KO KO KO\n");
-    }
-  }
-}
-
-
-TEST(Toto, DISABLED_Store)
-{
-  DicomAssociationParameters params;
-  params.SetLocalApplicationEntityTitle("ORTHANC");
-  params.SetRemoteApplicationEntityTitle("STORESCP");
-  params.SetRemotePort(2000);
-
-  DicomStoreUserConnection assoc(params);
-  assoc.RegisterStorageClass(UID_MRImageStorage, DicomTransferSyntax_JPEGProcess1);
-  assoc.RegisterStorageClass(UID_MRImageStorage, DicomTransferSyntax_JPEGProcess2_4);
-  //assoc.RegisterStorageClass(UID_MRImageStorage, DicomTransferSyntax_LittleEndianExplicit);
-
-  //assoc.SetUncompressedSyntaxesProposed(false);  // Necessary for transcoding
-  assoc.SetCommonClassesProposed(false);
-  assoc.SetRetiredBigEndianProposed(true);
-  TestTranscode(assoc, UID_MRImageStorage, DicomTransferSyntax_LittleEndianExplicit);
-  TestTranscode(assoc, UID_MRImageStorage, DicomTransferSyntax_JPEG2000);
-  TestTranscode(assoc, UID_MRImageStorage, DicomTransferSyntax_JPEG2000);
-}
-
-
-TEST(Toto, DISABLED_Store2)
-{
-  DicomAssociationParameters params;
-  params.SetLocalApplicationEntityTitle("ORTHANC");
-  params.SetRemoteApplicationEntityTitle("STORESCP");
-  params.SetRemotePort(2000);
-
-  DicomStoreUserConnection assoc(params);
-  //assoc.SetCommonClassesProposed(false);
-  assoc.SetRetiredBigEndianProposed(true);
-
-  std::string s;
-  Orthanc::SystemToolbox::ReadFile(s, "/tmp/i/" + std::string(GetTransferSyntaxUid(DicomTransferSyntax_BigEndianExplicit)) +".dcm");
-
-  std::string c, i;
-  assoc.Store(c, i, s.c_str(), s.size());
-  printf("[%s] [%s]\n", c.c_str(), i.c_str());
-}
-
--- a/OrthancFramework/Resources/Patches/OpenSSL-ConfigureHeaders.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/Patches/OpenSSL-ConfigureHeaders.py	Tue Mar 18 13:37:18 2025 +0100
@@ -4,8 +4,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/Patches/OpenSSL-ExtractProvidersOIDs.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/Patches/OpenSSL-ExtractProvidersOIDs.py	Tue Mar 18 13:37:18 2025 +0100
@@ -4,8 +4,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/boost-1.86.0-emscripten.patch	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,127 @@
+diff -urEb boost_1_86_0.orig/libs/locale/src/boost/locale/shared/date_time.cpp boost_1_86_0/libs/locale/src/boost/locale/shared/date_time.cpp
+--- boost_1_86_0.orig/libs/locale/src/boost/locale/shared/date_time.cpp	2024-09-25 15:46:01.000000000 +0200
++++ boost_1_86_0/libs/locale/src/boost/locale/shared/date_time.cpp	2024-09-25 15:58:51.306131987 +0200
+@@ -12,8 +12,10 @@
+ #include <boost/locale/date_time.hpp>
+ #include <boost/locale/formatting.hpp>
+ #include <boost/core/exchange.hpp>
+-#include <boost/thread/locks.hpp>
+-#include <boost/thread/mutex.hpp>
++#if !defined(__EMSCRIPTEN__)
++#  include <boost/thread/locks.hpp>
++#  include <boost/thread/mutex.hpp>
++#endif
+ #include <cmath>
+ 
+ namespace boost { namespace locale {
+@@ -400,6 +402,7 @@
+         return impl_->get_option(abstract_calendar::is_dst) != 0;
+     }
+ 
++#if !defined(__EMSCRIPTEN__)
+     namespace time_zone {
+         boost::mutex& tz_mutex()
+         {
+@@ -422,7 +425,7 @@
+             return boost::exchange(tz_id(), new_id);
+         }
+     } // namespace time_zone
+-
++#endif
+ }} // namespace boost::locale
+ 
+ // boostinspect:nominmax
+diff -urEb boost_1_86_0.orig/libs/locale/src/boost/locale/shared/generator.cpp boost_1_86_0/libs/locale/src/boost/locale/shared/generator.cpp
+--- boost_1_86_0.orig/libs/locale/src/boost/locale/shared/generator.cpp	2024-09-25 15:46:01.000000000 +0200
++++ boost_1_86_0/libs/locale/src/boost/locale/shared/generator.cpp	2024-09-25 16:00:07.756233916 +0200
+@@ -7,8 +7,10 @@
+ #include <boost/locale/encoding.hpp>
+ #include <boost/locale/generator.hpp>
+ #include <boost/locale/localization_backend.hpp>
+-#include <boost/thread/locks.hpp>
+-#include <boost/thread/mutex.hpp>
++#if !defined(__EMSCRIPTEN__)
++#  include <boost/thread/locks.hpp>
++#  include <boost/thread/mutex.hpp>
++#endif
+ #include <algorithm>
+ #include <map>
+ #include <vector>
+@@ -21,8 +23,9 @@
+         {}
+ 
+         mutable std::map<std::string, std::locale> cached;
++#if !defined(__EMSCRIPTEN__)
+         mutable boost::mutex cached_lock;
+-
++#endif
+         category_t cats;
+         char_facet_t chars;
+ 
+@@ -101,7 +104,9 @@
+     std::locale generator::generate(const std::locale& base, const std::string& id) const
+     {
+         if(d->caching_enabled) {
++#if !defined(__EMSCRIPTEN__)
+             boost::unique_lock<boost::mutex> guard(d->cached_lock);
++#endif
+             const auto p = d->cached.find(id);
+             if(p != d->cached.end())
+                 return p->second;
+@@ -126,7 +131,9 @@
+                 result = backend->install(result, facet, char_facet_t::nochar);
+         }
+         if(d->caching_enabled) {
++#if !defined(__EMSCRIPTEN__)
+             boost::unique_lock<boost::mutex> guard(d->cached_lock);
++#endif
+             const auto p = d->cached.find(id);
+             if(p == d->cached.end())
+                 d->cached[id] = result;
+diff -urEb boost_1_86_0.orig/libs/locale/src/boost/locale/shared/localization_backend.cpp boost_1_86_0/libs/locale/src/boost/locale/shared/localization_backend.cpp
+--- boost_1_86_0.orig/libs/locale/src/boost/locale/shared/localization_backend.cpp	2024-09-25 15:46:01.000000000 +0200
++++ boost_1_86_0/libs/locale/src/boost/locale/shared/localization_backend.cpp	2024-09-25 16:01:09.196820495 +0200
+@@ -5,8 +5,10 @@
+ // https://www.boost.org/LICENSE_1_0.txt
+ 
+ #include <boost/locale/localization_backend.hpp>
+-#include <boost/thread/locks.hpp>
+-#include <boost/thread/mutex.hpp>
++#if !defined(__EMSCRIPTEN__)
++#  include <boost/thread/locks.hpp>
++#  include <boost/thread/mutex.hpp>
++#endif
+ #include <functional>
+ #include <memory>
+ #include <vector>
+@@ -211,11 +213,13 @@
+             return mgr;
+         }
+ 
++#if !defined(__EMSCRIPTEN__)
+         boost::mutex& localization_backend_manager_mutex()
+         {
+             static boost::mutex the_mutex;
+             return the_mutex;
+         }
++#endif
+         localization_backend_manager& localization_backend_manager_global()
+         {
+             static localization_backend_manager the_manager = make_default_backend_mgr();
+@@ -225,12 +229,16 @@
+ 
+     localization_backend_manager localization_backend_manager::global()
+     {
++#if !defined(__EMSCRIPTEN__)
+         boost::unique_lock<boost::mutex> lock(localization_backend_manager_mutex());
++#endif
+         return localization_backend_manager_global();
+     }
+     localization_backend_manager localization_backend_manager::global(const localization_backend_manager& in)
+     {
++#if !defined(__EMSCRIPTEN__)
+         boost::unique_lock<boost::mutex> lock(localization_backend_manager_mutex());
++#endif
+         return exchange(localization_backend_manager_global(), in);
+     }
+ 
--- a/OrthancFramework/Resources/Patches/curl-8.5.0.patch	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-diff -urEb curl-8.5.0.orig/CMake/Macros.cmake curl-8.5.0/CMake/Macros.cmake
---- curl-8.5.0.orig/CMake/Macros.cmake	2024-01-24 17:21:21.387965189 +0100
-+++ curl-8.5.0/CMake/Macros.cmake	2024-01-24 17:21:48.523719072 +0100
-@@ -48,7 +48,7 @@
-     message(STATUS "Performing Curl Test ${CURL_TEST}")
-     try_compile(${CURL_TEST}
-       ${CMAKE_BINARY_DIR}
--      ${CMAKE_CURRENT_SOURCE_DIR}/CMake/CurlTests.c
-+      ${CURL_SOURCES_DIR}/CMake/CurlTests.c
-       CMAKE_FLAGS -DCOMPILE_DEFINITIONS:STRING=${MACRO_CHECK_FUNCTION_DEFINITIONS}
-       "${CURL_TEST_ADD_LIBRARIES}"
-       OUTPUT_VARIABLE OUTPUT)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/curl-8.9.0.patch	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,24 @@
+diff -urEb curl-8.9.0.orig/CMake/Macros.cmake curl-8.9.0/CMake/Macros.cmake
+--- curl-8.9.0.orig/CMake/Macros.cmake	2025-02-18 16:04:59.818585107 +0100
++++ curl-8.9.0/CMake/Macros.cmake	2025-02-18 16:05:16.867458366 +0100
+@@ -48,7 +48,7 @@
+     message(STATUS "Performing Test ${CURL_TEST}")
+     try_compile(${CURL_TEST}
+       ${CMAKE_BINARY_DIR}
+-      ${CMAKE_CURRENT_SOURCE_DIR}/CMake/CurlTests.c
++      ${CURL_SOURCES_DIR}/CMake/CurlTests.c
+       CMAKE_FLAGS -DCOMPILE_DEFINITIONS:STRING=${MACRO_CHECK_FUNCTION_DEFINITIONS}
+       "${CURL_TEST_ADD_LIBRARIES}"
+       OUTPUT_VARIABLE OUTPUT)
+diff -urEb curl-8.9.0.orig/lib/system_win32.c curl-8.9.0/lib/system_win32.c
+--- curl-8.9.0.orig/lib/system_win32.c	2025-02-18 16:04:59.834584988 +0100
++++ curl-8.9.0/lib/system_win32.c	2025-02-18 16:06:26.448941452 +0100
+@@ -273,7 +273,7 @@
+ 
+ bool Curl_win32_impersonating(void)
+ {
+-#ifndef CURL_WINDOWS_APP
++#if !defined(CURL_WINDOWS_APP) && !defined(__MINGW32__)
+   HANDLE token = NULL;
+   if(OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, TRUE, &token)) {
+     CloseHandle(token);
--- a/OrthancFramework/Resources/Patches/dcmtk-3.6.8.patch	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/Patches/dcmtk-3.6.8.patch	Tue Mar 18 13:37:18 2025 +0100
@@ -1,6 +1,6 @@
 diff -urEb dcmtk-DCMTK-3.6.8.orig/CMake/GenerateDCMTKConfigure.cmake dcmtk-DCMTK-3.6.8/CMake/GenerateDCMTKConfigure.cmake
---- dcmtk-DCMTK-3.6.8.orig/CMake/GenerateDCMTKConfigure.cmake	2024-01-09 17:13:10.329673608 +0100
-+++ dcmtk-DCMTK-3.6.8/CMake/GenerateDCMTKConfigure.cmake	2024-01-09 18:21:52.568142681 +0100
+--- dcmtk-DCMTK-3.6.8.orig/CMake/GenerateDCMTKConfigure.cmake	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/CMake/GenerateDCMTKConfigure.cmake	2024-11-25 16:54:59.036009112 +0100
 @@ -224,6 +224,8 @@
  
  # Check the sizes of various types
@@ -18,9 +18,11 @@
  
  # Check for include files, libraries, and functions
  include("${DCMTK_CMAKE_INCLUDE}CMake/dcmtkTryCompile.cmake")
+Only in dcmtk-DCMTK-3.6.8/config/include/dcmtk/config: arith.h
+Only in dcmtk-DCMTK-3.6.8/config/include/dcmtk/config: osconfig.h
 diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h dcmtk-DCMTK-3.6.8/dcmdata/include/dcmtk/dcmdata/dcdict.h
---- dcmtk-DCMTK-3.6.8.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h	2024-01-09 17:13:10.337673529 +0100
-+++ dcmtk-DCMTK-3.6.8/dcmdata/include/dcmtk/dcmdata/dcdict.h	2024-01-09 18:21:52.568142681 +0100
+--- dcmtk-DCMTK-3.6.8.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/dcmdata/include/dcmtk/dcmdata/dcdict.h	2024-11-25 16:54:59.036009112 +0100
 @@ -162,6 +162,12 @@
      /// returns an iterator to the end of the repeating tag dictionary
      DcmDictEntryListIterator repeatingEnd() { return repDict.end(); }
@@ -35,17 +37,18 @@
  
      /** private undefined assignment operator
 diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmdata/libsrc/dcdict.cc dcmtk-DCMTK-3.6.8/dcmdata/libsrc/dcdict.cc
---- dcmtk-DCMTK-3.6.8.orig/dcmdata/libsrc/dcdict.cc	2024-01-09 17:13:10.337673529 +0100
-+++ dcmtk-DCMTK-3.6.8/dcmdata/libsrc/dcdict.cc	2024-01-09 18:21:52.568142681 +0100
+--- dcmtk-DCMTK-3.6.8.orig/dcmdata/libsrc/dcdict.cc	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/dcmdata/libsrc/dcdict.cc	2024-11-25 16:54:59.036009112 +0100
 @@ -914,3 +914,5 @@
    wrlock().clear();
    wrunlock();
  }
 +
 +#include "dcdict_orthanc.cc"
+Only in dcmtk-DCMTK-3.6.8/dcmdata/libsrc: dcdict_orthanc.cc
 diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmdata/libsrc/dcpxitem.cc dcmtk-DCMTK-3.6.8/dcmdata/libsrc/dcpxitem.cc
---- dcmtk-DCMTK-3.6.8.orig/dcmdata/libsrc/dcpxitem.cc	2024-01-09 17:13:10.337673529 +0100
-+++ dcmtk-DCMTK-3.6.8/dcmdata/libsrc/dcpxitem.cc	2024-01-09 18:21:52.568142681 +0100
+--- dcmtk-DCMTK-3.6.8.orig/dcmdata/libsrc/dcpxitem.cc	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/dcmdata/libsrc/dcpxitem.cc	2024-11-25 16:54:59.036009112 +0100
 @@ -31,6 +31,9 @@
  #include "dcmtk/dcmdata/dcostrma.h"    /* for class DcmOutputStream */
  #include "dcmtk/dcmdata/dcwcache.h"    /* for class DcmWriteCache */
@@ -57,8 +60,8 @@
  // ********************************
  
 diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmnet/libsrc/scu.cc dcmtk-DCMTK-3.6.8/dcmnet/libsrc/scu.cc
---- dcmtk-DCMTK-3.6.8.orig/dcmnet/libsrc/scu.cc	2024-01-09 17:13:10.349673411 +0100
-+++ dcmtk-DCMTK-3.6.8/dcmnet/libsrc/scu.cc	2024-01-09 18:23:08.723435667 +0100
+--- dcmtk-DCMTK-3.6.8.orig/dcmnet/libsrc/scu.cc	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/dcmnet/libsrc/scu.cc	2024-11-25 16:54:59.036009112 +0100
 @@ -19,6 +19,11 @@
   *
   */
@@ -72,8 +75,8 @@
  
  #include "dcmtk/dcmdata/dcostrmf.h" /* for class DcmOutputFileStream */
 diff -urEb dcmtk-DCMTK-3.6.8.orig/oflog/include/dcmtk/oflog/thread/syncpub.h dcmtk-DCMTK-3.6.8/oflog/include/dcmtk/oflog/thread/syncpub.h
---- dcmtk-DCMTK-3.6.8.orig/oflog/include/dcmtk/oflog/thread/syncpub.h	2024-01-09 17:13:10.389673016 +0100
-+++ dcmtk-DCMTK-3.6.8/oflog/include/dcmtk/oflog/thread/syncpub.h	2024-01-09 18:21:52.568142681 +0100
+--- dcmtk-DCMTK-3.6.8.orig/oflog/include/dcmtk/oflog/thread/syncpub.h	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/oflog/include/dcmtk/oflog/thread/syncpub.h	2024-11-25 16:54:59.037009100 +0100
 @@ -63,7 +63,7 @@
  
  DCMTK_LOG4CPLUS_INLINE_EXPORT
@@ -111,8 +114,8 @@
  
  
 diff -urEb dcmtk-DCMTK-3.6.8.orig/oflog/libsrc/oflog.cc dcmtk-DCMTK-3.6.8/oflog/libsrc/oflog.cc
---- dcmtk-DCMTK-3.6.8.orig/oflog/libsrc/oflog.cc	2024-01-09 17:13:10.389673016 +0100
-+++ dcmtk-DCMTK-3.6.8/oflog/libsrc/oflog.cc	2024-01-09 18:21:52.568142681 +0100
+--- dcmtk-DCMTK-3.6.8.orig/oflog/libsrc/oflog.cc	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/oflog/libsrc/oflog.cc	2024-11-25 16:54:59.037009100 +0100
 @@ -19,6 +19,11 @@
   *
   */
@@ -126,8 +129,8 @@
  #include "dcmtk/oflog/oflog.h"
  
 diff -urEb dcmtk-DCMTK-3.6.8.orig/ofstd/include/dcmtk/ofstd/offile.h dcmtk-DCMTK-3.6.8/ofstd/include/dcmtk/ofstd/offile.h
---- dcmtk-DCMTK-3.6.8.orig/ofstd/include/dcmtk/ofstd/offile.h	2024-01-09 17:13:10.389673016 +0100
-+++ dcmtk-DCMTK-3.6.8/ofstd/include/dcmtk/ofstd/offile.h	2024-01-09 18:21:52.568142681 +0100
+--- dcmtk-DCMTK-3.6.8.orig/ofstd/include/dcmtk/ofstd/offile.h	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/ofstd/include/dcmtk/ofstd/offile.h	2024-11-25 16:54:59.037009100 +0100
 @@ -570,7 +570,7 @@
     */
    void setlinebuf()
@@ -137,3 +140,18 @@
      this->setvbuf(NULL, _IOLBF, 0);
  #else
      :: setlinebuf(file_);
+diff -urEb dcmtk-DCMTK-3.6.8.orig/ofstd/include/dcmtk/ofstd/ofutil.h dcmtk-DCMTK-3.6.8/ofstd/include/dcmtk/ofstd/ofutil.h
+--- dcmtk-DCMTK-3.6.8.orig/ofstd/include/dcmtk/ofstd/ofutil.h	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/ofstd/include/dcmtk/ofstd/ofutil.h	2024-11-25 17:00:27.525244000 +0100
+@@ -75,8 +75,8 @@
+         // copy constructor should be fine for primitive types.
+         inline type(const T& pt)
+         : t( pt ) {}
+-        inline type(const OFrvalue_storage& rhs)
+-        : t( rhs.pt ) {}
++        inline type(const type& rhs)
++        : t( rhs.t ) {}
+ 
+         // automatic conversion to the underlying type
+         inline operator T&() const { return OFconst_cast( T&, t ); }
+Only in dcmtk-DCMTK-3.6.8/ofstd/include/dcmtk/ofstd: ofutil.h~
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/dcmtk-3.6.9-visual-studio.patch	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,1576 @@
+diff -urEb dcmtk-3.6.9.orig/config/math.cc dcmtk-3.6.9/config/math.cc
+--- dcmtk-3.6.9.orig/config/math.cc	2025-02-18 18:03:13.501406015 +0100
++++ dcmtk-3.6.9/config/math.cc	2025-02-18 18:05:52.950086576 +0100
+@@ -42,11 +42,17 @@
+ #include <windows.h>
+ #endif
+ 
++#if defined(_MSC_VER)
++#include <float.h>
++#endif
++
+ struct dcmtk_config_math
+ {
+   static inline OFBool isnan( float f )
+   {
+-#ifdef HAVE_PROTOTYPE_STD__ISNAN
++#if defined(_MSC_VER)
++    return _isnan(static_cast<double>(f)) != 0;
++#elif defined(HAVE_PROTOTYPE_STD__ISNAN)
+     return STD_NAMESPACE isnan(f);
+ #else
+     return ::isnan(f);
+@@ -55,7 +61,9 @@
+ 
+   static inline OFBool isnan( double d )
+   {
+-#ifdef HAVE_PROTOTYPE_STD__ISNAN
++#if defined(_MSC_VER)
++    return _isnan(d) != 0;
++#elif defined(HAVE_PROTOTYPE_STD__ISNAN)
+     return STD_NAMESPACE isnan(d);
+ #else
+     return ::isnan(d);
+@@ -64,7 +72,9 @@
+ 
+   static inline OFBool isinf( float f )
+   {
+-#ifdef HAVE_PROTOTYPE_STD__ISINF
++#if defined(_MSC_VER)
++    return _finite(static_cast<double>(f)) != 0;
++#elif defined(HAVE_PROTOTYPE_STD__ISINF)
+     return STD_NAMESPACE isinf( f );
+ #else
+     return ::isinf( f );
+@@ -73,7 +83,9 @@
+ 
+   static inline OFBool isinf( double d )
+   {
+-#ifdef HAVE_PROTOTYPE_STD__ISINF
++#if defined(_MSC_VER)
++    return _finite(d) != 0;
++#elif defined(HAVE_PROTOTYPE_STD__ISINF)
+     return STD_NAMESPACE isinf( d );
+ #else
+     return ::isinf( d );
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg12/jccoefct.c dcmtk-3.6.9/dcmjpeg/libijg12/jccoefct.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg12/jccoefct.c	2025-02-18 18:03:13.530405562 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg12/jccoefct.c	2025-02-18 18:05:52.950086576 +0100
+@@ -343,7 +343,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossy_c_ptr lossyc = (j_lossy_c_ptr) cinfo->codec;
+   c_coef_ptr coef = (c_coef_ptr) lossyc->coef_private;
+   JDIMENSION MCU_col_num;   /* index of current MCU within row */
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg12/jcdiffct.c dcmtk-3.6.9/dcmjpeg/libijg12/jcdiffct.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg12/jcdiffct.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg12/jcdiffct.c	2025-02-18 18:05:52.951086562 +0100
+@@ -302,7 +302,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossless_c_ptr losslsc = (j_lossless_c_ptr) cinfo->codec;
+   c_diff_ptr diff = (c_diff_ptr) losslsc->diff_private;
+   /* JDIMENSION MCU_col_num; */ /* index of current MCU within row */
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg12/jcpred.c dcmtk-3.6.9/dcmjpeg/libijg12/jcpred.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg12/jcpred.c	2025-02-18 18:03:13.530405562 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg12/jcpred.c	2025-02-18 18:05:52.951086562 +0100
+@@ -213,7 +213,7 @@
+          const JSAMPROW input_buf, JSAMPROW prev_row,
+          JDIFFROW diff_buf, JDIMENSION width)
+ {
+-  (void)prev_row;
++  //(void)prev_row;
+   DIFFERENCE_1D(INITIAL_PREDICTORx);
+ 
+   /*
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg12/jctrans.c dcmtk-3.6.9/dcmjpeg/libijg12/jctrans.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg12/jctrans.c	2025-02-18 18:03:13.530405562 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg12/jctrans.c	2025-02-18 18:05:52.951086562 +0100
+@@ -267,7 +267,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossy_c_ptr lossyc = (j_lossy_c_ptr) cinfo->codec;
+   c_coef_ptr coef = (c_coef_ptr) lossyc->coef_private;
+   JDIMENSION MCU_col_num;   /* index of current MCU within row */
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg12/jdmerge.c dcmtk-3.6.9/dcmjpeg/libijg12/jdmerge.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg12/jdmerge.c	2025-02-18 18:03:13.530405562 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg12/jdmerge.c	2025-02-18 18:05:52.951086562 +0100
+@@ -148,7 +148,7 @@
+             JDIMENSION out_rows_avail)
+ /* 2:1 vertical sampling case: may need a spare row. */
+ {
+-  (void) in_row_groups_avail;
++  //(void) in_row_groups_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+   JSAMPROW work_ptrs[2];
+   JDIMENSION num_rows;      /* number of rows returned to caller */
+@@ -198,8 +198,8 @@
+             JDIMENSION out_rows_avail)
+ /* 1:1 vertical sampling case: much easier, never need a spare row. */
+ {
+-  (void) in_row_groups_avail;
+-  (void) out_rows_avail;
++  //(void) in_row_groups_avail;
++  //(void) out_rows_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+ 
+   /* Just do the upsampling. */
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg12/jdpostct.c dcmtk-3.6.9/dcmjpeg/libijg12/jdpostct.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg12/jdpostct.c	2025-02-18 18:03:13.530405562 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg12/jdpostct.c	2025-02-18 18:05:52.952086549 +0100
+@@ -161,8 +161,8 @@
+               JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+               JDIMENSION out_rows_avail)
+ {
+-  (void) output_buf;
+-  (void) out_rows_avail;
++  //(void) output_buf;
++  //(void) out_rows_avail;
+   my_post_ptr post = (my_post_ptr) cinfo->post;
+   JDIMENSION old_next_row, num_rows;
+ 
+@@ -207,9 +207,9 @@
+             JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+             JDIMENSION out_rows_avail)
+ {
+-  (void) input_buf;
+-  (void) in_row_group_ctr;
+-  (void) in_row_groups_avail;
++  //(void) input_buf;
++  //(void) in_row_group_ctr;
++  //(void) in_row_groups_avail;
+ 
+   my_post_ptr post = (my_post_ptr) cinfo->post;
+   JDIMENSION num_rows, max_rows;
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg12/jdpred.c dcmtk-3.6.9/dcmjpeg/libijg12/jdpred.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg12/jdpred.c	2025-02-18 18:03:13.530405562 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg12/jdpred.c	2025-02-18 18:05:52.952086549 +0100
+@@ -101,8 +101,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_1D(INITIAL_PREDICTOR2);
+ }
+ 
+@@ -111,8 +111,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR2);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -123,8 +123,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR3);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -135,8 +135,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR4);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -147,8 +147,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR5);
+   JPEG_UNUSED(Rc);
+@@ -160,8 +160,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR6);
+   JPEG_UNUSED(Rc);
+@@ -173,8 +173,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR7);
+   JPEG_UNUSED(Rc);
+@@ -195,7 +195,7 @@
+                 JDIFFROW undiff_buf, JDIMENSION width)
+ {
+ 
+-  (void)prev_row;
++  //(void)prev_row;
+   j_lossless_d_ptr losslsd = (j_lossless_d_ptr) cinfo->codec;
+ 
+   UNDIFFERENCE_1D(INITIAL_PREDICTORx);
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg12/jdsample.c dcmtk-3.6.9/dcmjpeg/libijg12/jdsample.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg12/jdsample.c	2025-02-18 18:03:13.530405562 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg12/jdsample.c	2025-02-18 18:05:52.952086549 +0100
+@@ -92,7 +92,7 @@
+           JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+           JDIMENSION out_rows_avail)
+ {
+-  (void)in_row_groups_avail;
++  //(void)in_row_groups_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+   int ci;
+   jpeg_component_info * compptr;
+@@ -158,8 +158,8 @@
+ fullsize_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)cinfo;
+-  (void)compptr;
++  //(void)cinfo;
++  //(void)compptr;
+   *output_data_ptr = input_data;
+ }
+ 
+@@ -173,9 +173,9 @@
+ noop_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)cinfo;
+-  (void)compptr;
+-  (void)input_data;
++  //(void)cinfo;
++  //(void)compptr;
++  //(void)input_data;
+   *output_data_ptr = NULL;  /* safety check */
+ }
+ 
+@@ -239,7 +239,7 @@
+ h2v1_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)compptr;
++  //(void)compptr;
+   JSAMPARRAY output_data = *output_data_ptr;
+   register JSAMPROW inptr, outptr;
+   register JSAMPLE invalue;
+@@ -268,7 +268,7 @@
+ h2v2_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)compptr;
++  //(void)compptr;
+   JSAMPARRAY output_data = *output_data_ptr;
+   register JSAMPROW inptr, outptr;
+   register JSAMPLE invalue;
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg12/jdscale.c dcmtk-3.6.9/dcmjpeg/libijg12/jdscale.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg12/jdscale.c	2025-02-18 18:03:13.530405562 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg12/jdscale.c	2025-02-18 18:05:52.953086535 +0100
+@@ -67,7 +67,7 @@
+ 	const JDIFFROW diff_buf, JSAMPROW output_buf,
+ 	JDIMENSION width)
+ {
+-  (void)cinfo;
++  //(void)cinfo;
+   unsigned int xindex;
+ 
+   for (xindex = 0; xindex < width; xindex++)
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg12/jerror.c dcmtk-3.6.9/dcmjpeg/libijg12/jerror.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg12/jerror.c	2025-02-18 18:03:13.530405562 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg12/jerror.c	2025-02-18 18:05:52.953086535 +0100
+@@ -34,6 +34,10 @@
+ #define EXIT_FAILURE  1
+ #endif
+ 
++#if defined(_MSC_VER) && _MSC_VER < 1900
++#define snprintf _snprintf
++#endif
++
+ 
+ /*
+  * Create the message string table.
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg12/jquant1.c dcmtk-3.6.9/dcmjpeg/libijg12/jquant1.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg12/jquant1.c	2025-02-18 18:03:13.530405562 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg12/jquant1.c	2025-02-18 18:05:52.953086535 +0100
+@@ -251,8 +251,8 @@
+    * (Forcing the upper and lower values to the limits ensures that
+    * dithering can't produce a color outside the selected gamut.)
+    */
+-  (void) cinfo;
+-  (void) ci;
++  //(void) cinfo;
++  //(void) ci;
+   return (int) (((IJG_INT32) j * MAXJSAMPLE + maxj/2) / maxj);
+ }
+ 
+@@ -262,8 +262,8 @@
+ /* Return largest input value that should map to j'th output value */
+ /* Must have largest(j=0) >= 0, and largest(j=maxj) >= MAXJSAMPLE */
+ {
+-  (void) cinfo;
+-  (void) ci;
++  //(void) cinfo;
++  //(void) ci;
+   /* Breakpoints are halfway between values returned by output_value */
+   return (int) (((IJG_INT32) (2*j + 1) * MAXJSAMPLE + maxj) / (2*maxj));
+ }
+@@ -744,7 +744,7 @@
+ METHODDEF(void)
+ start_pass_1_quant (j_decompress_ptr cinfo, boolean is_pre_scan)
+ {
+-  (void) is_pre_scan;
++  //(void) is_pre_scan;
+   my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+   size_t arraysize;
+   int i;
+@@ -802,7 +802,7 @@
+ METHODDEF(void)
+ finish_pass_1_quant (j_decompress_ptr cinfo)
+ {
+-  (void) cinfo;
++  //(void) cinfo;
+   /* no work in 1-pass case */
+ }
+ 
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg12/jquant2.c dcmtk-3.6.9/dcmjpeg/libijg12/jquant2.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg12/jquant2.c	2025-02-18 18:03:13.530405562 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg12/jquant2.c	2025-02-18 18:05:52.954086521 +0100
+@@ -224,7 +224,7 @@
+ prescan_quantize (j_decompress_ptr cinfo, JSAMPARRAY input_buf,
+           JSAMPARRAY output_buf, int num_rows)
+ {
+-  (void) output_buf;
++  //(void) output_buf;
+   my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+   register JSAMPROW ptr;
+   register histptr histp;
+@@ -1156,7 +1156,7 @@
+ METHODDEF(void)
+ finish_pass2 (j_decompress_ptr cinfo)
+ {
+-  (void) cinfo;
++  //(void) cinfo;
+   /* no work */
+ }
+ 
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg16/jccoefct.c dcmtk-3.6.9/dcmjpeg/libijg16/jccoefct.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg16/jccoefct.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg16/jccoefct.c	2025-02-18 18:05:52.954086521 +0100
+@@ -343,7 +343,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossy_c_ptr lossyc = (j_lossy_c_ptr) cinfo->codec;
+   c_coef_ptr coef = (c_coef_ptr) lossyc->coef_private;
+   JDIMENSION MCU_col_num;   /* index of current MCU within row */
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg16/jcdiffct.c dcmtk-3.6.9/dcmjpeg/libijg16/jcdiffct.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg16/jcdiffct.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg16/jcdiffct.c	2025-02-18 18:05:52.954086521 +0100
+@@ -302,7 +302,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossless_c_ptr losslsc = (j_lossless_c_ptr) cinfo->codec;
+   c_diff_ptr diff = (c_diff_ptr) losslsc->diff_private;
+   /* JDIMENSION MCU_col_num; */ /* index of current MCU within row */
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg16/jcpred.c dcmtk-3.6.9/dcmjpeg/libijg16/jcpred.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg16/jcpred.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg16/jcpred.c	2025-02-18 18:05:52.954086521 +0100
+@@ -213,7 +213,7 @@
+          const JSAMPROW input_buf, JSAMPROW prev_row,
+          JDIFFROW diff_buf, JDIMENSION width)
+ {
+-  (void)prev_row;
++  //(void)prev_row;
+   DIFFERENCE_1D(INITIAL_PREDICTORx);
+ 
+   /*
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg16/jctrans.c dcmtk-3.6.9/dcmjpeg/libijg16/jctrans.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg16/jctrans.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg16/jctrans.c	2025-02-18 18:05:52.954086521 +0100
+@@ -267,7 +267,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossy_c_ptr lossyc = (j_lossy_c_ptr) cinfo->codec;
+   c_coef_ptr coef = (c_coef_ptr) lossyc->coef_private;
+   JDIMENSION MCU_col_num;   /* index of current MCU within row */
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg16/jdmerge.c dcmtk-3.6.9/dcmjpeg/libijg16/jdmerge.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg16/jdmerge.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg16/jdmerge.c	2025-02-18 18:05:52.955086508 +0100
+@@ -169,7 +169,7 @@
+             JDIMENSION out_rows_avail)
+ /* 2:1 vertical sampling case: may need a spare row. */
+ {
+-  (void) in_row_groups_avail;
++  //(void) in_row_groups_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+   JSAMPROW work_ptrs[2];
+   JDIMENSION num_rows;      /* number of rows returned to caller */
+@@ -219,8 +219,8 @@
+             JDIMENSION out_rows_avail)
+ /* 1:1 vertical sampling case: much easier, never need a spare row. */
+ {
+-  (void) in_row_groups_avail;
+-  (void) out_rows_avail;
++  //(void) in_row_groups_avail;
++  //(void) out_rows_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+ 
+   /* Just do the upsampling. */
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg16/jdpostct.c dcmtk-3.6.9/dcmjpeg/libijg16/jdpostct.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg16/jdpostct.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg16/jdpostct.c	2025-02-18 18:05:52.955086508 +0100
+@@ -161,8 +161,8 @@
+               JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+               JDIMENSION out_rows_avail)
+ {
+-  (void) output_buf;
+-  (void) out_rows_avail;
++  //(void) output_buf;
++  //(void) out_rows_avail;
+   my_post_ptr post = (my_post_ptr) cinfo->post;
+   JDIMENSION old_next_row, num_rows;
+ 
+@@ -207,9 +207,9 @@
+             JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+             JDIMENSION out_rows_avail)
+ {
+-  (void) input_buf;
+-  (void) in_row_group_ctr;
+-  (void) in_row_groups_avail;
++  //(void) input_buf;
++  //(void) in_row_group_ctr;
++  //(void) in_row_groups_avail;
+   my_post_ptr post = (my_post_ptr) cinfo->post;
+   JDIMENSION num_rows, max_rows;
+ 
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg16/jdpred.c dcmtk-3.6.9/dcmjpeg/libijg16/jdpred.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg16/jdpred.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg16/jdpred.c	2025-02-18 18:05:52.955086508 +0100
+@@ -101,8 +101,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_1D(INITIAL_PREDICTOR2);
+ }
+ 
+@@ -111,8 +111,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-   (void)cinfo;
+-  (void)comp_index;
++   //(void)cinfo;
++  //(void)comp_index;
+  UNDIFFERENCE_2D(PREDICTOR2);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -123,8 +123,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR3);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -135,8 +135,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR4A);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -147,8 +147,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR4);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -159,8 +159,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR5);
+   JPEG_UNUSED(Rc);
+@@ -172,8 +172,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR5A);
+   JPEG_UNUSED(Rc);
+@@ -185,8 +185,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR6);
+   JPEG_UNUSED(Rc);
+@@ -198,8 +198,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR6A);
+   JPEG_UNUSED(Rc);
+@@ -211,8 +211,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR7);
+   JPEG_UNUSED(Rc);
+@@ -224,8 +224,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR7A);
+   JPEG_UNUSED(Rc);
+@@ -245,7 +245,7 @@
+                 const JDIFFROW diff_buf, JDIFFROW prev_row,
+                 JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)prev_row;
++  //(void)prev_row;
+   j_lossless_d_ptr losslsd = (j_lossless_d_ptr) cinfo->codec;
+ 
+   UNDIFFERENCE_1D(INITIAL_PREDICTORx);
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg16/jdsample.c dcmtk-3.6.9/dcmjpeg/libijg16/jdsample.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg16/jdsample.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg16/jdsample.c	2025-02-18 18:05:52.955086508 +0100
+@@ -92,7 +92,7 @@
+           JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+           JDIMENSION out_rows_avail)
+ {
+-  (void)in_row_groups_avail;
++  //(void)in_row_groups_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+   int ci;
+   jpeg_component_info * compptr;
+@@ -158,8 +158,8 @@
+ fullsize_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)cinfo;
+-  (void)compptr;
++  //(void)cinfo;
++  //(void)compptr;
+   *output_data_ptr = input_data;
+ }
+ 
+@@ -173,9 +173,9 @@
+ noop_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)cinfo;
+-  (void)compptr;
+-  (void)input_data;
++  //(void)cinfo;
++  //(void)compptr;
++  //(void)input_data;
+   *output_data_ptr = NULL;  /* safety check */
+ }
+ 
+@@ -239,7 +239,7 @@
+ h2v1_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)compptr;
++  //(void)compptr;
+   JSAMPARRAY output_data = *output_data_ptr;
+   register JSAMPROW inptr, outptr;
+   register JSAMPLE invalue;
+@@ -268,7 +268,7 @@
+ h2v2_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)compptr;
++  //(void)compptr;
+   JSAMPARRAY output_data = *output_data_ptr;
+   register JSAMPROW inptr, outptr;
+   register JSAMPLE invalue;
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg16/jdscale.c dcmtk-3.6.9/dcmjpeg/libijg16/jdscale.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg16/jdscale.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg16/jdscale.c	2025-02-18 18:05:52.955086508 +0100
+@@ -67,7 +67,7 @@
+ 	const JDIFFROW diff_buf, JSAMPROW output_buf,
+ 	JDIMENSION width)
+ {
+-  (void)cinfo;
++  //(void)cinfo;
+   unsigned int xindex;
+ 
+   for (xindex = 0; xindex < width; xindex++)
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg16/jerror.c dcmtk-3.6.9/dcmjpeg/libijg16/jerror.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg16/jerror.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg16/jerror.c	2025-02-18 18:05:52.955086508 +0100
+@@ -34,6 +34,10 @@
+ #define EXIT_FAILURE  1
+ #endif
+ 
++#if defined(_MSC_VER) && _MSC_VER < 1900
++#define snprintf _snprintf
++#endif
++
+ 
+ /*
+  * Create the message string table.
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg16/jquant1.c dcmtk-3.6.9/dcmjpeg/libijg16/jquant1.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg16/jquant1.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg16/jquant1.c	2025-02-18 18:05:52.956086494 +0100
+@@ -251,8 +251,8 @@
+    * (Forcing the upper and lower values to the limits ensures that
+    * dithering can't produce a color outside the selected gamut.)
+    */
+-  (void) cinfo;
+-  (void) ci;
++  //(void) cinfo;
++  //(void) ci;
+   return (int) (((IJG_INT32) j * MAXJSAMPLE + maxj/2) / maxj);
+ }
+ 
+@@ -262,8 +262,8 @@
+ /* Return largest input value that should map to j'th output value */
+ /* Must have largest(j=0) >= 0, and largest(j=maxj) >= MAXJSAMPLE */
+ {
+-  (void) cinfo;
+-  (void) ci;
++  //(void) cinfo;
++  //(void) ci;
+   /* Breakpoints are halfway between values returned by output_value */
+   return (int) (((IJG_INT32) (2*j + 1) * MAXJSAMPLE + maxj) / (2*maxj));
+ }
+@@ -744,7 +744,7 @@
+ METHODDEF(void)
+ start_pass_1_quant (j_decompress_ptr cinfo, boolean is_pre_scan)
+ {
+-  (void) is_pre_scan;
++  //(void) is_pre_scan;
+   my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+   size_t arraysize;
+   int i;
+@@ -802,7 +802,7 @@
+ METHODDEF(void)
+ finish_pass_1_quant (j_decompress_ptr cinfo)
+ {
+-  (void) cinfo;
++  //(void) cinfo;
+   /* no work in 1-pass case */
+ }
+ 
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg16/jquant2.c dcmtk-3.6.9/dcmjpeg/libijg16/jquant2.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg16/jquant2.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg16/jquant2.c	2025-02-18 18:05:52.956086494 +0100
+@@ -224,7 +224,7 @@
+ prescan_quantize (j_decompress_ptr cinfo, JSAMPARRAY input_buf,
+           JSAMPARRAY output_buf, int num_rows)
+ {
+-  (void) output_buf;
++  //(void) output_buf;
+   my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+   register JSAMPROW ptr;
+   register histptr histp;
+@@ -1156,7 +1156,7 @@
+ METHODDEF(void)
+ finish_pass2 (j_decompress_ptr cinfo)
+ {
+-  (void) cinfo;
++  //(void) cinfo;
+   /* no work */
+ }
+ 
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg8/jccoefct.c dcmtk-3.6.9/dcmjpeg/libijg8/jccoefct.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg8/jccoefct.c	2025-02-18 18:03:13.533405515 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg8/jccoefct.c	2025-02-18 18:05:52.956086494 +0100
+@@ -343,7 +343,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossy_c_ptr lossyc = (j_lossy_c_ptr) cinfo->codec;
+   c_coef_ptr coef = (c_coef_ptr) lossyc->coef_private;
+   JDIMENSION MCU_col_num;   /* index of current MCU within row */
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg8/jcdiffct.c dcmtk-3.6.9/dcmjpeg/libijg8/jcdiffct.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg8/jcdiffct.c	2025-02-18 18:03:13.533405515 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg8/jcdiffct.c	2025-02-18 18:05:52.956086494 +0100
+@@ -302,7 +302,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossless_c_ptr losslsc = (j_lossless_c_ptr) cinfo->codec;
+   c_diff_ptr diff = (c_diff_ptr) losslsc->diff_private;
+   /* JDIMENSION MCU_col_num; */ /* index of current MCU within row */
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg8/jcpred.c dcmtk-3.6.9/dcmjpeg/libijg8/jcpred.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg8/jcpred.c	2025-02-18 18:03:13.533405515 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg8/jcpred.c	2025-02-18 18:05:52.957086481 +0100
+@@ -213,7 +213,7 @@
+          const JSAMPROW input_buf, JSAMPROW prev_row,
+          JDIFFROW diff_buf, JDIMENSION width)
+ {
+-  (void)prev_row;
++  //(void)prev_row;
+   DIFFERENCE_1D(INITIAL_PREDICTORx);
+ 
+   /*
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg8/jctrans.c dcmtk-3.6.9/dcmjpeg/libijg8/jctrans.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg8/jctrans.c	2025-02-18 18:03:13.533405515 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg8/jctrans.c	2025-02-18 18:05:52.957086481 +0100
+@@ -267,7 +267,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossy_c_ptr lossyc = (j_lossy_c_ptr) cinfo->codec;
+   c_coef_ptr coef = (c_coef_ptr) lossyc->coef_private;
+   JDIMENSION MCU_col_num;   /* index of current MCU within row */
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg8/jdmerge.c dcmtk-3.6.9/dcmjpeg/libijg8/jdmerge.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg8/jdmerge.c	2025-02-18 18:03:13.533405515 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg8/jdmerge.c	2025-02-18 18:05:52.957086481 +0100
+@@ -148,7 +148,7 @@
+             JDIMENSION out_rows_avail)
+ /* 2:1 vertical sampling case: may need a spare row. */
+ {
+-  (void) in_row_groups_avail;
++  //(void) in_row_groups_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+   JSAMPROW work_ptrs[2];
+   JDIMENSION num_rows;      /* number of rows returned to caller */
+@@ -198,8 +198,8 @@
+             JDIMENSION out_rows_avail)
+ /* 1:1 vertical sampling case: much easier, never need a spare row. */
+ {
+-  (void) in_row_groups_avail;
+-  (void) out_rows_avail;
++  //(void) in_row_groups_avail;
++  //(void) out_rows_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+ 
+   /* Just do the upsampling. */
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg8/jdpostct.c dcmtk-3.6.9/dcmjpeg/libijg8/jdpostct.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg8/jdpostct.c	2025-02-18 18:03:13.533405515 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg8/jdpostct.c	2025-02-18 18:05:52.957086481 +0100
+@@ -161,8 +161,8 @@
+               JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+               JDIMENSION out_rows_avail)
+ {
+-  (void) output_buf;
+-  (void) out_rows_avail;
++  //(void) output_buf;
++  //(void) out_rows_avail;
+   my_post_ptr post = (my_post_ptr) cinfo->post;
+   JDIMENSION old_next_row, num_rows;
+ 
+@@ -207,9 +207,9 @@
+             JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+             JDIMENSION out_rows_avail)
+ {
+-  (void) input_buf;
+-  (void) in_row_group_ctr;
+-  (void) in_row_groups_avail;
++  //(void) input_buf;
++  //(void) in_row_group_ctr;
++  //(void) in_row_groups_avail;
+   my_post_ptr post = (my_post_ptr) cinfo->post;
+   JDIMENSION num_rows, max_rows;
+ 
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg8/jdpred.c dcmtk-3.6.9/dcmjpeg/libijg8/jdpred.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg8/jdpred.c	2025-02-18 18:03:13.533405515 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg8/jdpred.c	2025-02-18 18:05:52.957086481 +0100
+@@ -101,8 +101,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_1D(INITIAL_PREDICTOR2);
+ }
+ 
+@@ -111,8 +111,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR2);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -123,8 +123,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR3);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -135,8 +135,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR4);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -147,8 +147,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR5);
+   JPEG_UNUSED(Rc);
+@@ -160,8 +160,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR6);
+   JPEG_UNUSED(Rc);
+@@ -173,8 +173,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR7);
+   JPEG_UNUSED(Rc);
+@@ -194,7 +194,7 @@
+                 const JDIFFROW diff_buf, JDIFFROW prev_row,
+                 JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)prev_row;
++  //(void)prev_row;
+   j_lossless_d_ptr losslsd = (j_lossless_d_ptr) cinfo->codec;
+ 
+   UNDIFFERENCE_1D(INITIAL_PREDICTORx);
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg8/jdsample.c dcmtk-3.6.9/dcmjpeg/libijg8/jdsample.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg8/jdsample.c	2025-02-18 18:03:13.533405515 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg8/jdsample.c	2025-02-18 18:05:52.957086481 +0100
+@@ -92,7 +92,7 @@
+           JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+           JDIMENSION out_rows_avail)
+ {
+-  (void)in_row_groups_avail;
++  //(void)in_row_groups_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+   int ci;
+   jpeg_component_info * compptr;
+@@ -158,8 +158,8 @@
+ fullsize_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)cinfo;
+-  (void)compptr;
++  //(void)cinfo;
++  //(void)compptr;
+   *output_data_ptr = input_data;
+ }
+ 
+@@ -173,9 +173,9 @@
+ noop_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)cinfo;
+-  (void)compptr;
+-  (void)input_data;
++  //(void)cinfo;
++  //(void)compptr;
++  //(void)input_data;
+   *output_data_ptr = NULL;  /* safety check */
+ }
+ 
+@@ -239,7 +239,7 @@
+ h2v1_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)compptr;
++  //(void)compptr;
+   JSAMPARRAY output_data = *output_data_ptr;
+   register JSAMPROW inptr, outptr;
+   register JSAMPLE invalue;
+@@ -268,7 +268,7 @@
+ h2v2_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)compptr;
++  //(void)compptr;
+   JSAMPARRAY output_data = *output_data_ptr;
+   register JSAMPROW inptr, outptr;
+   register JSAMPLE invalue;
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg8/jdscale.c dcmtk-3.6.9/dcmjpeg/libijg8/jdscale.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg8/jdscale.c	2025-02-18 18:03:13.532405530 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg8/jdscale.c	2025-02-18 18:05:52.958086467 +0100
+@@ -67,7 +67,7 @@
+ 	const JDIFFROW diff_buf, JSAMPROW output_buf,
+ 	JDIMENSION width)
+ {
+-  (void)cinfo;
++  //(void)cinfo;
+   unsigned int xindex;
+ 
+   for (xindex = 0; xindex < width; xindex++)
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg8/jerror.c dcmtk-3.6.9/dcmjpeg/libijg8/jerror.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg8/jerror.c	2025-02-18 18:03:13.533405515 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg8/jerror.c	2025-02-18 18:05:52.958086467 +0100
+@@ -34,6 +34,10 @@
+ #define EXIT_FAILURE  1
+ #endif
+ 
++#if defined(_MSC_VER) && _MSC_VER < 1900
++#define snprintf _snprintf
++#endif
++
+ 
+ /*
+  * Create the message string table.
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg8/jquant1.c dcmtk-3.6.9/dcmjpeg/libijg8/jquant1.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg8/jquant1.c	2025-02-18 18:03:13.532405530 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg8/jquant1.c	2025-02-18 18:05:52.958086467 +0100
+@@ -251,8 +251,8 @@
+    * (Forcing the upper and lower values to the limits ensures that
+    * dithering can't produce a color outside the selected gamut.)
+    */
+-  (void) cinfo;
+-  (void) ci;
++  //(void) cinfo;
++  //(void) ci;
+   return (int) (((IJG_INT32) j * MAXJSAMPLE + maxj/2) / maxj);
+ }
+ 
+@@ -262,8 +262,8 @@
+ /* Return largest input value that should map to j'th output value */
+ /* Must have largest(j=0) >= 0, and largest(j=maxj) >= MAXJSAMPLE */
+ {
+-  (void) cinfo;
+-  (void) ci;
++  //(void) cinfo;
++  //(void) ci;
+   /* Breakpoints are halfway between values returned by output_value */
+   return (int) (((IJG_INT32) (2*j + 1) * MAXJSAMPLE + maxj) / (2*maxj));
+ }
+@@ -744,7 +744,7 @@
+ METHODDEF(void)
+ start_pass_1_quant (j_decompress_ptr cinfo, boolean is_pre_scan)
+ {
+-  (void) is_pre_scan;
++  //(void) is_pre_scan;
+   my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+   size_t arraysize;
+   int i;
+@@ -802,7 +802,7 @@
+ METHODDEF(void)
+ finish_pass_1_quant (j_decompress_ptr cinfo)
+ {
+-  (void) cinfo;
++  //(void) cinfo;
+   /* no work in 1-pass case */
+ }
+ 
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg8/jquant2.c dcmtk-3.6.9/dcmjpeg/libijg8/jquant2.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg8/jquant2.c	2025-02-18 18:03:13.532405530 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg8/jquant2.c	2025-02-18 18:05:52.958086467 +0100
+@@ -224,7 +224,7 @@
+ prescan_quantize (j_decompress_ptr cinfo, JSAMPARRAY input_buf,
+           JSAMPARRAY output_buf, int num_rows)
+ {
+-  (void) output_buf;
++  //(void) output_buf;
+   my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+   register JSAMPROW ptr;
+   register histptr histp;
+@@ -1156,7 +1156,7 @@
+ METHODDEF(void)
+ finish_pass2 (j_decompress_ptr cinfo)
+ {
+-  (void) cinfo;
++  //(void) cinfo;
+   /* no work */
+ }
+ 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_bcs.h dcmtk-3.6.9/oficonv/libsrc/citrus_bcs.h
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_bcs.h	2025-02-18 18:03:13.519405733 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_bcs.h	2025-02-18 18:05:52.958086467 +0100
+@@ -39,7 +39,21 @@
+ #include <errno.h>
+ #include <stdint.h>
+ #include <limits.h>
++
++#undef EOPNOTSUPP
++#define EOPNOTSUPP 130   // https://learn.microsoft.com/fr-fr/cpp/c-runtime-library/errno-constants
++
++#if (_MSC_VER >= 1900)
+ #include <stdbool.h>
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
++#if defined(_MSC_VER) && _MSC_VER < 1900
++#define snprintf _snprintf
++#endif
+ 
+ #define CITRUS_DECONST(type, var)    ((type)(uintptr_t)(const void *)(var))
+ 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_big5.c dcmtk-3.6.9/oficonv/libsrc/citrus_big5.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_big5.c	2025-02-18 18:03:13.511405858 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_big5.c	2025-02-18 18:05:52.959086454 +0100
+@@ -218,13 +218,6 @@
+     return (0);
+ }
+ 
+-static const _citrus_prop_hint_t root_hints[] = {
+-    _CITRUS_PROP_HINT_NUM("row", &_citrus_BIG5_fill_rowcol),
+-    _CITRUS_PROP_HINT_NUM("col", &_citrus_BIG5_fill_rowcol),
+-    _CITRUS_PROP_HINT_NUM("excludes", &_citrus_BIG5_fill_excludes),
+-    _CITRUS_PROP_HINT_END
+-};
+-
+ static void
+ /*ARGSUSED*/
+ _citrus_BIG5_encoding_module_uninit(_BIG5EncodingInfo *ei)
+@@ -245,6 +238,18 @@
+     const char *s;
+     int err;
+ 
++    _citrus_prop_hint_t root_hints[4];
++    root_hints[0].name = "row";
++    root_hints[0].type = _CITRUS_PROP_NUM;
++    root_hints[0].cb.num.func = _citrus_BIG5_fill_rowcol;
++    root_hints[1].name = "col";
++    root_hints[1].type = _CITRUS_PROP_NUM;
++    root_hints[1].cb.num.func = _citrus_BIG5_fill_rowcol;
++    root_hints[2].name = "excludes";
++    root_hints[2].type = _CITRUS_PROP_NUM;
++    root_hints[2].cb.num.func = _citrus_BIG5_fill_excludes;
++    root_hints[3].name = NULL;  // _CITRUS_PROP_HINT_END
++
+     memset((void *)ei, 0, sizeof(*ei));
+     TAILQ_INIT(&ei->excludes);
+ 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_db_hash.h dcmtk-3.6.9/oficonv/libsrc/citrus_db_hash.h
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_db_hash.h	2025-02-18 18:03:13.510405874 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_db_hash.h	2025-02-18 18:05:52.959086454 +0100
+@@ -29,7 +29,8 @@
+ 
+ #include "dcmtk/config/osconfig.h"
+ #include "dcmtk/oficonv/oidefine.h"
+-#include <inttypes.h>
++
++#include <stdint.h>
+ 
+ struct _citrus_region;
+ 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_esdb.c dcmtk-3.6.9/oficonv/libsrc/citrus_esdb.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_esdb.c	2025-02-18 18:03:13.511405858 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_esdb.c	2025-02-18 18:05:52.959086454 +0100
+@@ -34,7 +34,15 @@
+ 
+ #include <errno.h>
+ #include <limits.h>
++
++#if (_MSC_VER >= 1900)
+ #include <stdbool.h>
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include <stdio.h>
+ #include <stdlib.h>
+ #include <string.h>
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_esdb.h dcmtk-3.6.9/oficonv/libsrc/citrus_esdb.h
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_esdb.h	2025-02-18 18:03:13.512405843 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_esdb.h	2025-02-18 18:05:52.959086454 +0100
+@@ -29,7 +29,14 @@
+ 
+ #include "dcmtk/config/osconfig.h"
+ #include "citrus_types.h"
++
++#if (_MSC_VER >= 1900)
+ #include <stdbool.h>
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
+ 
+ struct _citrus_esdb_charset {
+     _citrus_csid_t           ec_csid;
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_gbk2k.c dcmtk-3.6.9/oficonv/libsrc/citrus_gbk2k.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_gbk2k.c	2025-02-18 18:03:13.519405733 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_gbk2k.c	2025-02-18 18:05:52.959086454 +0100
+@@ -34,7 +34,15 @@
+ 
+ #include <errno.h>
+ #include <limits.h>
++
++#if (_MSC_VER >= 1900)
+ #include <stdbool.h>
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include <stddef.h>
+ #include <stdio.h>
+ #include <stdlib.h>
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_hz.c dcmtk-3.6.9/oficonv/libsrc/citrus_hz.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_hz.c	2025-02-18 18:03:13.518405749 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_hz.c	2025-02-18 18:05:52.959086454 +0100
+@@ -571,13 +571,6 @@
+     return (0);
+ }
+ 
+-static const _citrus_prop_hint_t escape_hints[] = {
+-_CITRUS_PROP_HINT_STR("CH", &_citrus_HZ_parse_char),
+-_CITRUS_PROP_HINT_STR("GL", &_citrus_HZ_parse_graphic),
+-_CITRUS_PROP_HINT_STR("GR", &_citrus_HZ_parse_graphic),
+-_CITRUS_PROP_HINT_END
+-};
+-
+ static int
+ _citrus_HZ_parse_escape(void *context, const char *name, const char *s)
+ {
+@@ -585,6 +578,18 @@
+     escape_t *escape;
+     void *p[2];
+ 
++    _citrus_prop_hint_t escape_hints[4];
++    escape_hints[0].name = "CH";
++    escape_hints[0].type = _CITRUS_PROP_STR;
++    escape_hints[0].cb.str.func = _citrus_HZ_parse_char;
++    escape_hints[1].name = "GL";
++    escape_hints[1].type = _CITRUS_PROP_STR;
++    escape_hints[1].cb.str.func = _citrus_HZ_parse_graphic;
++    escape_hints[2].name = "GR";
++    escape_hints[2].type = _CITRUS_PROP_STR;
++    escape_hints[2].cb.str.func = _citrus_HZ_parse_graphic;
++    escape_hints[3].name = NULL;  // _CITRUS_PROP_HINT_END
++
+     ei = (_HZEncodingInfo *)context;
+     escape = calloc(1, sizeof(*escape));
+     if (escape == NULL)
+@@ -605,18 +610,21 @@
+         escape_hints, (void *)&p[0], s, strlen(s)));
+ }
+ 
+-static const _citrus_prop_hint_t root_hints[] = {
+-_CITRUS_PROP_HINT_STR("0", &_citrus_HZ_parse_escape),
+-_CITRUS_PROP_HINT_STR("1", &_citrus_HZ_parse_escape),
+-_CITRUS_PROP_HINT_END
+-};
+-
+ static int
+ _citrus_HZ_encoding_module_init(_HZEncodingInfo * ei,
+     const void * var, size_t lenvar)
+ {
+     int errnum;
+ 
++    _citrus_prop_hint_t root_hints[3];
++    root_hints[0].name = "0";
++    root_hints[0].type = _CITRUS_PROP_STR;
++    root_hints[0].cb.str.func = _citrus_HZ_parse_escape;
++    root_hints[1].name = "1";
++    root_hints[1].type = _CITRUS_PROP_STR;
++    root_hints[1].cb.str.func = _citrus_HZ_parse_escape;
++    root_hints[2].name = NULL;  // _CITRUS_PROP_HINT_END
++
+     memset(ei, 0, sizeof(*ei));
+     TAILQ_INIT(E0SET(ei));
+     TAILQ_INIT(E1SET(ei));
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_iconv_local.h dcmtk-3.6.9/oficonv/libsrc/citrus_iconv_local.h
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_iconv_local.h	2025-02-18 18:03:13.520405718 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_iconv_local.h	2025-02-18 18:05:52.960086440 +0100
+@@ -29,7 +29,15 @@
+ 
+ #include "dcmtk/config/osconfig.h"
+ #include "dcmtk/oficonv/iconv.h"
++
++#if (_MSC_VER >= 1900)
+ #include <stdbool.h>
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include <stdint.h>
+ 
+ #ifdef HAVE_SYS_QUEUE_H
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_iconv_none.c dcmtk-3.6.9/oficonv/libsrc/citrus_iconv_none.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_iconv_none.c	2025-02-18 18:03:13.516405780 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_iconv_none.c	2025-02-18 18:05:52.960086440 +0100
+@@ -35,7 +35,15 @@
+ 
+ 
+ #include <errno.h>
++
++#if (_MSC_VER >= 1900)
+ #include <stdbool.h>
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include <stdlib.h>
+ #include <string.h>
+ 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_iconv_std.c dcmtk-3.6.9/oficonv/libsrc/citrus_iconv_std.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_iconv_std.c	2025-02-18 18:03:13.516405780 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_iconv_std.c	2025-02-18 18:05:52.960086440 +0100
+@@ -36,7 +36,15 @@
+ 
+ #include <errno.h>
+ #include <limits.h>
++
++#if (_MSC_VER >= 1900)
+ #include <stdbool.h>
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include <stdio.h>
+ #include <stdlib.h>
+ #include <string.h>
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_iso2022.c dcmtk-3.6.9/oficonv/libsrc/citrus_iso2022.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_iso2022.c	2025-02-18 18:03:13.516405780 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_iso2022.c	2025-02-18 18:05:52.960086440 +0100
+@@ -35,7 +35,15 @@
+ 
+ #include <errno.h>
+ #include <limits.h>
++
++#if (_MSC_VER >= 1900)
+ #include <stdbool.h>
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include <stddef.h>
+ #include <stdio.h>
+ #include <stdlib.h>
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_johab.c dcmtk-3.6.9/oficonv/libsrc/citrus_johab.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_johab.c	2025-02-18 18:03:13.518405749 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_johab.c	2025-02-18 18:05:52.961086427 +0100
+@@ -34,7 +34,15 @@
+ 
+ #include <errno.h>
+ #include <limits.h>
++
++#if (_MSC_VER >= 1900)
+ #include <stdbool.h>
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include <stddef.h>
+ #include <stdint.h>
+ #include <stdio.h>
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_module.c dcmtk-3.6.9/oficonv/libsrc/citrus_module.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_module.c	2025-02-18 18:03:13.520405718 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_module.c	2025-02-18 18:05:52.961086427 +0100
+@@ -98,7 +98,15 @@
+ #endif
+ #include <errno.h>
+ #include <limits.h>
++
++#if (_MSC_VER >= 1900)
+ #include <stdbool.h>
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include <stddef.h>
+ #include <stdio.h>
+ #include <stdlib.h>
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_mskanji.c dcmtk-3.6.9/oficonv/libsrc/citrus_mskanji.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_mskanji.c	2025-02-18 18:03:13.519405733 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_mskanji.c	2025-02-18 18:05:52.961086427 +0100
+@@ -67,7 +67,15 @@
+ 
+ #include <errno.h>
+ #include <limits.h>
++
++#if (_MSC_VER >= 1900)
+ #include <stdbool.h>
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include <stddef.h>
+ #include <stdio.h>
+ #include <stdlib.h>
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_prop.c dcmtk-3.6.9/oficonv/libsrc/citrus_prop.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_prop.c	2025-02-18 18:03:13.514405812 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_prop.c	2025-02-18 18:05:52.961086427 +0100
+@@ -30,7 +30,15 @@
+ 
+ #include <errno.h>
+ #include <limits.h>
++
++#if (_MSC_VER >= 1900)
+ #include <stdbool.h>
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include <stdio.h>
+ #include <stdlib.h>
+ #include <string.h>
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_region.h dcmtk-3.6.9/oficonv/libsrc/citrus_region.h
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_region.h	2025-02-18 18:03:13.516405780 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_region.h	2025-02-18 18:05:52.961086427 +0100
+@@ -31,7 +31,14 @@
+ #include "dcmtk/config/osconfig.h"
+ #include <stdint.h>
+ #include <string.h>
++
++#if (_MSC_VER >= 1900)
+ #include <stdbool.h>
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
+ 
+ #ifdef HAVE_SYS_TYPES_H
+ #include <sys/types.h>
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_utf8.c dcmtk-3.6.9/oficonv/libsrc/citrus_utf8.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_utf8.c	2025-02-18 18:03:13.519405733 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_utf8.c	2025-02-18 18:05:52.961086427 +0100
+@@ -66,7 +66,15 @@
+ 
+ #include <errno.h>
+ #include <limits.h>
++
++#if (_MSC_VER >= 1900)
+ #include <stdbool.h>
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include <stddef.h>
+ #include <stdio.h>
+ #include <stdlib.h>
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/oficonv_iconv.c dcmtk-3.6.9/oficonv/libsrc/oficonv_iconv.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/oficonv_iconv.c	2025-02-18 18:03:13.512405843 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/oficonv_iconv.c	2025-02-18 18:05:52.962086413 +0100
+@@ -40,7 +40,15 @@
+ 
+ #include <errno.h>
+ #include <limits.h>
++
++#if (_MSC_VER >= 1900)
+ #include <stdbool.h>
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include <stdlib.h>
+ #include <string.h>
+ #include <stdio.h>
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/windows_mmap.h dcmtk-3.6.9/oficonv/libsrc/windows_mmap.h
+--- dcmtk-3.6.9.orig/oficonv/libsrc/windows_mmap.h	2025-02-18 18:03:13.511405858 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/windows_mmap.h	2025-02-18 18:05:52.962086413 +0100
+@@ -74,6 +74,12 @@
+ 
+ static void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)
+ {
++  DWORD flProtect;
++  DWORD dwDesiredAccess;
++  HANDLE mmap_fd, h;
++  off_t end;
++  void *ret;
++
+     (void) start;
+ 
+ 	if (prot & ~(PROT_READ | PROT_WRITE | PROT_EXEC))
+@@ -84,7 +90,6 @@
+ 	} else if (flags & MAP_ANON)
+ 		return MAP_FAILED;
+ 
+-	DWORD flProtect;
+     flProtect = 0;
+ 	if (prot & PROT_WRITE) {
+ 		if (prot & PROT_EXEC)
+@@ -99,8 +104,7 @@
+ 	} else
+ 		flProtect = PAGE_READONLY;
+ 
+-	off_t end = (off_t)(length + offset);
+-	HANDLE mmap_fd, h;
++	end = (off_t)(length + offset);
+ 	if (fd == -1)
+ 		mmap_fd = INVALID_HANDLE_VALUE;
+ 	else
+@@ -109,7 +113,6 @@
+ 	if (h == NULL)
+ 		return MAP_FAILED;
+ 
+-	DWORD dwDesiredAccess;
+ 	if (prot & PROT_WRITE)
+ 		dwDesiredAccess = FILE_MAP_WRITE;
+ 	else
+@@ -118,7 +121,7 @@
+ 		dwDesiredAccess |= FILE_MAP_EXECUTE;
+ 	if (flags & MAP_PRIVATE)
+ 		dwDesiredAccess |= FILE_MAP_COPY;
+-	void *ret = MapViewOfFile(h, dwDesiredAccess, MM_DWORD_HI(offset), MM_DWORD_LO(offset), length);
++	ret = MapViewOfFile(h, dwDesiredAccess, MM_DWORD_HI(offset), MM_DWORD_LO(offset), length);
+ 	if (ret == NULL) {
+ 		CloseHandle(h);
+ 		ret = MAP_FAILED;
+@@ -140,11 +143,13 @@
+ 
+ static void munmap(void *addr, size_t length)
+ {
++  mmap_cleanup_t **prevPtr;
++  mmap_cleanup_t *mc;
++
+     (void) length;
+ 	UnmapViewOfFile(addr);
+ 	// Look up through the tracking elements to close the handle
+-	mmap_cleanup_t **prevPtr = &mmap_cleanup;
+-	mmap_cleanup_t *mc;
++	prevPtr = &mmap_cleanup;
+ 	for (mc = *prevPtr; mc != NULL; prevPtr = &mc->next, mc = *prevPtr)
+ 	{
+ 		if (mc->addr == addr)
+diff -urEb dcmtk-3.6.9.orig/ofstd/include/dcmtk/ofstd/oftypes.h dcmtk-3.6.9/ofstd/include/dcmtk/ofstd/oftypes.h
+--- dcmtk-3.6.9.orig/ofstd/include/dcmtk/ofstd/oftypes.h	2025-02-18 18:03:13.523405671 +0100
++++ dcmtk-3.6.9/ofstd/include/dcmtk/ofstd/oftypes.h	2025-02-18 18:05:52.962086413 +0100
+@@ -79,10 +79,9 @@
+ 
+ #include <cstddef>
+ BEGIN_EXTERN_C
+-#ifdef HAVE_STDINT_H
++#if defined(HAVE_STDINT_H) || _MSC_VER >= 1600
+ #include <stdint.h>
+ #endif
+-#include <inttypes.h>
+ END_EXTERN_C
+ 
+ #include "dcmtk/ofstd/ofstream.h"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/dcmtk-3.6.9.patch	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,296 @@
+diff -urEb dcmtk-3.6.9.orig/CMake/GenerateDCMTKConfigure.cmake dcmtk-3.6.9/CMake/GenerateDCMTKConfigure.cmake
+--- dcmtk-3.6.9.orig/CMake/GenerateDCMTKConfigure.cmake	2025-02-18 18:03:13.505405952 +0100
++++ dcmtk-3.6.9/CMake/GenerateDCMTKConfigure.cmake	2025-02-18 18:06:53.925278621 +0100
+@@ -227,12 +227,15 @@
+ 
+ # Check the sizes of various types
+ include (CheckTypeSize)
++if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
++  # This doesn't work for wasm, Orthanc defines the macros manually
+ CHECK_TYPE_SIZE("double" SIZEOF_DOUBLE)
+ CHECK_TYPE_SIZE("float" SIZEOF_FLOAT)
+ CHECK_TYPE_SIZE("int" SIZEOF_INT)
+ CHECK_TYPE_SIZE("long" SIZEOF_LONG)
+ CHECK_TYPE_SIZE("short" SIZEOF_SHORT)
+ CHECK_TYPE_SIZE("void*" SIZEOF_VOID_P)
++endif()
+ 
+ # Check for include files, libraries, and functions
+ include("${DCMTK_CMAKE_INCLUDE}CMake/dcmtkTryCompile.cmake")
+diff -urEb dcmtk-3.6.9.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h dcmtk-3.6.9/dcmdata/include/dcmtk/dcmdata/dcdict.h
+--- dcmtk-3.6.9.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h	2025-02-18 18:03:13.497406077 +0100
++++ dcmtk-3.6.9/dcmdata/include/dcmtk/dcmdata/dcdict.h	2025-02-18 18:06:53.925278621 +0100
+@@ -163,6 +163,12 @@
+     /// returns an iterator to the end of the repeating groups data dictionary
+     DcmDictEntryListIterator repeatingEnd() { return repDict.end(); }
+ 
++    // Function by the Orthanc project to load a dictionary from a
++    // memory buffer, which is necessary in sandboxed
++    // environments. This is an adapted version of
++    // DcmDataDictionary::loadDictionary().
++    OFBool loadFromMemory(const std::string& content, OFBool errorIfAbsent = OFTrue);
++    
+ private:
+ 
+     /** private undefined assignment operator
+diff -urEb dcmtk-3.6.9.orig/dcmdata/libsrc/dcdict.cc dcmtk-3.6.9/dcmdata/libsrc/dcdict.cc
+--- dcmtk-3.6.9.orig/dcmdata/libsrc/dcdict.cc	2025-02-18 18:03:13.499406046 +0100
++++ dcmtk-3.6.9/dcmdata/libsrc/dcdict.cc	2025-02-18 18:06:53.926278608 +0100
+@@ -904,3 +904,5 @@
+   wrlock().clear();
+   wrunlock();
+ }
++
++#include "dcdict_orthanc.cc"
+diff -urEb dcmtk-3.6.9.orig/dcmdata/libsrc/dcpxitem.cc dcmtk-3.6.9/dcmdata/libsrc/dcpxitem.cc
+--- dcmtk-3.6.9.orig/dcmdata/libsrc/dcpxitem.cc	2025-02-18 18:03:13.497406077 +0100
++++ dcmtk-3.6.9/dcmdata/libsrc/dcpxitem.cc	2025-02-18 18:06:53.926278608 +0100
+@@ -31,6 +31,8 @@
+ #include "dcmtk/dcmdata/dcostrma.h"    /* for class DcmOutputStream */
+ #include "dcmtk/dcmdata/dcwcache.h"    /* for class DcmWriteCache */
+ 
++#undef max
++#include "dcmtk/ofstd/oflimits.h"
+ 
+ // ********************************
+ 
+diff -urEb dcmtk-3.6.9.orig/dcmnet/libsrc/scu.cc dcmtk-3.6.9/dcmnet/libsrc/scu.cc
+--- dcmtk-3.6.9.orig/dcmnet/libsrc/scu.cc	2025-02-18 18:03:13.525405640 +0100
++++ dcmtk-3.6.9/dcmnet/libsrc/scu.cc	2025-02-18 18:06:53.927278595 +0100
+@@ -19,6 +19,11 @@
+  *
+  */
+ 
++#if defined(_WIN32)
++#  define __STDC_LIMIT_MACROS   // Get access to UINT16_MAX
++#  include <stdint.h>
++#endif
++
+ #include "dcmtk/config/osconfig.h" /* make sure OS specific configuration is included first */
+ 
+ #include "dcmtk/dcmdata/dcostrmf.h" /* for class DcmOutputFileStream */
+diff -urEb dcmtk-3.6.9.orig/oficonv/include/dcmtk/oficonv/iconv.h dcmtk-3.6.9/oficonv/include/dcmtk/oficonv/iconv.h
+--- dcmtk-3.6.9.orig/oficonv/include/dcmtk/oficonv/iconv.h	2025-02-18 18:03:13.510405874 +0100
++++ dcmtk-3.6.9/oficonv/include/dcmtk/oficonv/iconv.h	2025-02-18 18:06:53.927278595 +0100
+@@ -55,7 +55,12 @@
+ #endif
+ 
+ struct __tag_iconv_t;
++
++#if defined(__LSB_VERSION__)
++typedef void *iconv_t;
++#else
+ typedef struct __tag_iconv_t *iconv_t;
++#endif
+ 
+ #ifndef OFICONV_CITRUS_WC_T_DEFINED
+ #define OFICONV_CITRUS_WC_T_DEFINED
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_csmapper.c dcmtk-3.6.9/oficonv/libsrc/citrus_csmapper.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_csmapper.c	2025-02-18 18:03:13.510405874 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_csmapper.c	2025-02-18 18:06:53.927278595 +0100
+@@ -63,7 +63,8 @@
+ 
+ #ifdef WITH_THREADS
+ #ifdef HAVE_WINDOWS_H
+-static SRWLOCK ma_lock = SRWLOCK_INIT;
++static int ma_lock_initialized = 0;
++static CRITICAL_SECTION ma_lock;
+ #elif defined(HAVE_PTHREAD_H)
+ static pthread_rwlock_t ma_lock = PTHREAD_RWLOCK_INITIALIZER;
+ #endif
+@@ -382,6 +383,14 @@
+     char mapper_path[OFICONV_PATH_MAX];
+     unsigned long norm;
+     int ret;
++
++#if defined(WITH_THREADS) && defined(HAVE_WINDOWS_H)
++    if (ma_lock_initialized == 0) { /* Very minor risk of race condition here */
++      InitializeCriticalSection(&ma_lock);
++      ma_lock_initialized = 1;
++    }
++#endif
++
+     norm = 0;
+ 
+     getCSMapperPath(mapper_path, sizeof(mapper_path), NULL);
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_iconv.c dcmtk-3.6.9/oficonv/libsrc/citrus_iconv.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_iconv.c	2025-02-18 18:03:13.520405718 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_iconv.c	2025-02-18 18:10:35.928614598 +0100
+@@ -49,7 +49,15 @@
+ #endif
+ 
+ #include <limits.h>
++
++#if (_MSC_VER >= 1900)
+ #include <stdbool.h>
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include <stdio.h>
+ #include <stdlib.h>
+ #include <string.h>
+@@ -80,7 +88,8 @@
+ 
+ #ifdef WITH_THREADS
+ #ifdef HAVE_WINDOWS_H
+-static SRWLOCK ci_lock = SRWLOCK_INIT;
++static int ci_lock_initialized = 0;
++static CRITICAL_SECTION ci_lock;
+ #elif defined(HAVE_PTHREAD_H)
+ static pthread_rwlock_t ci_lock = PTHREAD_RWLOCK_INITIALIZER;
+ #endif
+@@ -299,14 +308,24 @@
+ _citrus_iconv_open(struct _citrus_iconv * * rcv,
+     const char * src, const char * dst)
+ {
+-struct _citrus_iconv *cv = NULL;
++#ifdef HAVE_WINDOWS_H
++    char current_codepage[20];
++#endif
++
++    struct _citrus_iconv *cv = NULL;
+     struct _citrus_iconv_shared *ci = NULL;
+     char realdst[OFICONV_PATH_MAX], realsrc[OFICONV_PATH_MAX];
+     int ret;
+ 
++#if defined(WITH_THREADS) && defined(HAVE_WINDOWS_H)
++    if (ci_lock_initialized == 0) { /* Very minor risk of race condition here */
++      InitializeCriticalSection(&ci_lock);
++      ci_lock_initialized = 1;
++    }
++#endif
++
+     init_cache();
+ #ifdef HAVE_WINDOWS_H
+-    char current_codepage[20];
+     snprintf(current_codepage, sizeof(current_codepage), "%lu", (unsigned long) GetConsoleOutputCP());
+ #endif
+ 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_lock.h dcmtk-3.6.9/oficonv/libsrc/citrus_lock.h
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_lock.h	2025-02-18 18:03:13.518405749 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_lock.h	2025-02-18 18:06:53.927278595 +0100
+@@ -31,11 +31,11 @@
+ 
+ #ifdef WITH_THREADS
+ 
+-#ifdef HAVE_WINDOWS_H
++#if defined(HAVE_WINDOWS_H)
+ 
+ #include <windows.h>
+-#define WLOCK(lock)  AcquireSRWLockExclusive(lock);
+-#define UNLOCK(lock) ReleaseSRWLockExclusive(lock);
++#define WLOCK(lock)  EnterCriticalSection(lock);
++#define UNLOCK(lock) LeaveCriticalSection(lock);
+ 
+ #else /* HAVE_WINDOWS_H */
+ 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_mapper.c dcmtk-3.6.9/oficonv/libsrc/citrus_mapper.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_mapper.c	2025-02-18 18:03:13.516405780 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_mapper.c	2025-02-18 18:06:53.928278582 +0100
+@@ -64,7 +64,8 @@
+ 
+ #ifdef WITH_THREADS
+ #ifdef HAVE_WINDOWS_H
+-static SRWLOCK cm_lock = SRWLOCK_INIT;
++static int cm_lock_initialized = 0;
++static CRITICAL_SECTION cm_lock;
+ #elif defined(HAVE_PTHREAD_H)
+ static pthread_rwlock_t cm_lock = PTHREAD_RWLOCK_INITIALIZER;
+ #endif
+@@ -355,6 +356,13 @@
+     const char *module, *variable;
+     int hashval, ret;
+ 
++#if defined(WITH_THREADS) && defined(HAVE_WINDOWS_H)
++    if (cm_lock_initialized == 0) { /* Very minor risk of race condition here */
++      InitializeCriticalSection(&cm_lock);
++      cm_lock_initialized = 1;
++    }
++#endif
++
+     variable = NULL;
+ 
+     WLOCK(&cm_lock);
+diff -urEb dcmtk-3.6.9.orig/oflog/include/dcmtk/oflog/thread/syncpub.h dcmtk-3.6.9/oflog/include/dcmtk/oflog/thread/syncpub.h
+--- dcmtk-3.6.9.orig/oflog/include/dcmtk/oflog/thread/syncpub.h	2025-02-18 18:03:13.473406452 +0100
++++ dcmtk-3.6.9/oflog/include/dcmtk/oflog/thread/syncpub.h	2025-02-18 18:06:53.928278582 +0100
+@@ -63,7 +63,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ Mutex::Mutex (Mutex::Type t)
+-    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::Mutex (t) + 0))
++    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::Mutex (t)))
+ { }
+ 
+ 
+@@ -106,7 +106,7 @@
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ Semaphore::Semaphore (unsigned DCMTK_LOG4CPLUS_THREADED (max),
+     unsigned DCMTK_LOG4CPLUS_THREADED (initial))
+-    : sem (DCMTK_LOG4CPLUS_THREADED (new impl::Semaphore (max, initial) + 0))
++    : sem (DCMTK_LOG4CPLUS_THREADED (new impl::Semaphore (max, initial)))
+ { }
+ 
+ 
+@@ -190,7 +190,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ ManualResetEvent::ManualResetEvent (bool DCMTK_LOG4CPLUS_THREADED (sig))
+-    : ev (DCMTK_LOG4CPLUS_THREADED (new impl::ManualResetEvent (sig) + 0))
++    : ev (DCMTK_LOG4CPLUS_THREADED (new impl::ManualResetEvent (sig)))
+ { }
+ 
+ 
+@@ -252,7 +252,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ SharedMutex::SharedMutex ()
+-    : sm (DCMTK_LOG4CPLUS_THREADED (new impl::SharedMutex + 0))
++    : sm (DCMTK_LOG4CPLUS_THREADED (new impl::SharedMutex))
+ { }
+ 
+ 
+diff -urEb dcmtk-3.6.9.orig/oflog/libsrc/oflog.cc dcmtk-3.6.9/oflog/libsrc/oflog.cc
+--- dcmtk-3.6.9.orig/oflog/libsrc/oflog.cc	2025-02-18 18:03:13.475406421 +0100
++++ dcmtk-3.6.9/oflog/libsrc/oflog.cc	2025-02-18 18:06:53.928278582 +0100
+@@ -19,6 +19,11 @@
+  *
+  */
+ 
++
++#if defined(_WIN32)
++#  include <winsock2.h>
++#endif
++
+ #include "dcmtk/config/osconfig.h"    /* make sure OS specific configuration is included first */
+ #include "dcmtk/oflog/oflog.h"
+ 
+diff -urEb dcmtk-3.6.9.orig/ofstd/include/dcmtk/ofstd/offile.h dcmtk-3.6.9/ofstd/include/dcmtk/ofstd/offile.h
+--- dcmtk-3.6.9.orig/ofstd/include/dcmtk/ofstd/offile.h	2025-02-18 18:03:13.523405671 +0100
++++ dcmtk-3.6.9/ofstd/include/dcmtk/ofstd/offile.h	2025-02-18 18:06:53.929278570 +0100
+@@ -569,7 +569,7 @@
+    */
+   void setlinebuf()
+   {
+-#if defined(_WIN32) || defined(__hpux)
++#if defined(_WIN32) || defined(__hpux) || defined(__LSB_VERSION__)
+     this->setvbuf(NULL, _IOLBF, 0);
+ #else
+     :: setlinebuf(file_);
+diff -urEb dcmtk-3.6.9.orig/ofstd/libsrc/ofstub.cc dcmtk-3.6.9/ofstd/libsrc/ofstub.cc
+--- dcmtk-3.6.9.orig/ofstd/libsrc/ofstub.cc	2025-02-18 18:03:13.523405671 +0100
++++ dcmtk-3.6.9/ofstd/libsrc/ofstub.cc	2025-02-18 18:06:53.929278570 +0100
+@@ -35,6 +35,10 @@
+ #include <windows.h>
+ #endif /* HAVE_WINDOWS_H */
+ 
++#if defined(__LSB_VERSION__)
++#include <errno.h>
++#endif
++
+ #define EXITCODE_CANNOT_DETERMINE_DIR        90
+ #define EXITCODE_EXEC_FAILED                 91
+ #define EXITCODE_ILLEGAL_PARAMS              92
--- a/OrthancFramework/Resources/Patches/protobuf-3.5.1.patch	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/Patches/protobuf-3.5.1.patch	Tue Mar 18 13:37:18 2025 +0100
@@ -2,7 +2,7 @@
 --- protobuf-3.5.1.orig/src/google/protobuf/stubs/io_win32.cc	2023-03-26 20:13:45.095021011 +0200
 +++ protobuf-3.5.1/src/google/protobuf/stubs/io_win32.cc	2023-03-26 20:19:19.932920102 +0200
 @@ -91,7 +91,12 @@
- 
+
  template <typename char_type>
  bool null_or_empty(const char_type* s) {
 -  return s == nullptr || *s == 0;
@@ -13,5 +13,18 @@
 +   **/
 +  return s == NULL || *s == 0;
  }
- 
+
  // Returns true if the path starts with a drive letter, e.g. "c:".
+diff -urEb protobuf-3.5.1.orig/src/google/protobuf/stubs/hash.h protobuf-3.5.1/src/google/protobuf/stubs/hash.h
+--- protobuf-3.5.1.orig/src/google/protobuf/stubs/hash.h	2023-03-26 20:13:45.095021011 +0200
++++ protobuf-3.5.1/src/google/protobuf/stubs/hash.h	2023-03-26 20:19:19.932920102 +0200
+@@ -1,3 +1,9 @@
++#if _MSC_VER >= 1930       // Since Visual Studio 2022
++#define _SILENCE_STDEXT_HASH_DEPRECATION_WARNINGS
++#include <unordered_map>
++#include <hash_map>
++#endif
++
+ // Protocol Buffers - Google's data interchange format
+ // Copyright 2008 Google Inc.  All rights reserved.
+ // https://developers.google.com/protocol-buffers/
\ No newline at end of file
--- a/OrthancFramework/Resources/ProtocolBuffers/CMakeLists.txt	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/ProtocolBuffers/CMakeLists.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/ProtocolBuffers/ProtobufLibrary.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/ProtocolBuffers/ProtobufLibrary.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/RetrieveCACertificates.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/RetrieveCACertificates.py	Tue Mar 18 13:37:18 2025 +0100
@@ -4,8 +4,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
@@ -32,7 +32,7 @@
     print('Download a set of CA certificates, convert them to PEM, then format them as a C macro')
     print('Usage: %s [Macro] [Certificate1] <Certificate2>...' % sys.argv[0])
     print('')
-    print('Example: %s BITBUCKET_CERTIFICATES https://cacerts.digicert.com/DigiCertSHA2HighAssuranceServerCA.crt' % sys.argv[0])
+    print('Example: %s GITHUB_CERTIFICATES https://cacerts.digicert.com/DigiCertSHA2HighAssuranceServerCA.crt' % sys.argv[0])
     print('')
     sys.exit(-1)
 
--- a/OrthancFramework/Resources/Samples/MicroService/CMakeLists.txt	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/Samples/MicroService/CMakeLists.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/Samples/MicroService/Sample.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/Samples/MicroService/Sample.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/ThirdParty/icu/CMakeLists.txt	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/ThirdParty/icu/CMakeLists.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/ThirdParty/icu/Version.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/ThirdParty/icu/Version.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/Toolchains/CrossToolchain.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/Toolchains/CrossToolchain.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/Toolchains/MinGW-W64-Toolchain32.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/Toolchains/MinGW-W64-Toolchain32.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/Toolchains/MinGW-W64-Toolchain64.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/Toolchains/MinGW-W64-Toolchain64.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/Toolchains/MinGWToolchain.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/Toolchains/MinGWToolchain.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/Resources/WindowsResources.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Resources/WindowsResources.py	Tue Mar 18 13:37:18 2025 +0100
@@ -4,8 +4,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/SharedLibrary/CMakeLists.txt	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/SharedLibrary/CMakeLists.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/SharedLibrary/DllMain.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/SharedLibrary/DllMain.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/SharedLibrary/OrthancFramework.h.in	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/SharedLibrary/OrthancFramework.h.in	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Cache/ICachePageProvider.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Cache/ICachePageProvider.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Cache/ICacheable.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Cache/ICacheable.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Cache/LeastRecentlyUsedIndex.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Cache/LeastRecentlyUsedIndex.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Cache/MemoryCache.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryCache.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Cache/MemoryCache.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryCache.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Cache/MemoryObjectCache.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryObjectCache.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Cache/MemoryObjectCache.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryObjectCache.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Cache/MemoryStringCache.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryStringCache.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Cache/MemoryStringCache.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryStringCache.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Cache/SharedArchive.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Cache/SharedArchive.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Cache/SharedArchive.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Cache/SharedArchive.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/ChunkedBuffer.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/ChunkedBuffer.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/ChunkedBuffer.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/ChunkedBuffer.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Compatibility.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Compatibility.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Compression/DeflateBaseCompressor.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Compression/DeflateBaseCompressor.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Compression/DeflateBaseCompressor.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Compression/DeflateBaseCompressor.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Compression/GzipCompressor.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Compression/GzipCompressor.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Compression/GzipCompressor.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Compression/GzipCompressor.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Compression/HierarchicalZipWriter.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Compression/HierarchicalZipWriter.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Compression/HierarchicalZipWriter.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Compression/HierarchicalZipWriter.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Compression/IBufferCompressor.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Compression/IBufferCompressor.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Compression/IBufferCompressor.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Compression/IBufferCompressor.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Compression/ZipReader.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Compression/ZipReader.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Compression/ZipReader.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Compression/ZipReader.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Compression/ZipWriter.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Compression/ZipWriter.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Compression/ZipWriter.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Compression/ZipWriter.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Compression/ZlibCompressor.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Compression/ZlibCompressor.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Compression/ZlibCompressor.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Compression/ZlibCompressor.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomFormat/DicomArray.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomArray.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -95,7 +95,8 @@
       }
       else if (v.IsSequence())
       {
-        s = "(sequence)";
+        //s = "(sequence)";
+        s = "(sequence) " + v.GetSequenceContent().toStyledString();
       }
       else
       {
--- a/OrthancFramework/Sources/DicomFormat/DicomArray.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomArray.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomFormat/DicomElement.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomElement.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomFormat/DicomElement.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomElement.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomFormat/DicomImageInformation.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomImageInformation.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -55,9 +55,9 @@
   // These lists have a specific signature.  When a resource does not have
   // the metadata "MainDicomTagsSignature", we'll assume that they were stored
   // with an Orthanc prior to 1.11.  It is therefore very important that you never
-  // change these lists !
+  // change these lists !  Update ResetDefaultMainDicomTags instead.
 
-  static const DicomTag DEFAULT_PATIENT_MAIN_DICOM_TAGS[] =
+  static const DicomTag DEFAULT_1_11_PATIENT_MAIN_DICOM_TAGS[] =
   {
     // { DicomTag(0x0010, 0x1010), "PatientAge" },
     // { DicomTag(0x0010, 0x1040), "PatientAddress" },
@@ -66,9 +66,11 @@
     DICOM_TAG_PATIENT_SEX,
     DICOM_TAG_OTHER_PATIENT_IDS,
     DICOM_TAG_PATIENT_ID
+
+    // don't add tags here, check ResetDefaultMainDicomTags instead
   };
   
-  static const DicomTag DEFAULT_STUDY_MAIN_DICOM_TAGS[] =
+  static const DicomTag DEFAULT_1_11_STUDY_MAIN_DICOM_TAGS[] =
   {
     // { DicomTag(0x0010, 0x1020), "PatientSize" },
     // { DicomTag(0x0010, 0x1030), "PatientWeight" },
@@ -84,9 +86,11 @@
     DICOM_TAG_INSTITUTION_NAME,
     DICOM_TAG_REQUESTING_PHYSICIAN,
     DICOM_TAG_REFERRING_PHYSICIAN_NAME
+
+    // don't add tags here, check ResetDefaultMainDicomTags instead
   };
 
-  static const DicomTag DEFAULT_SERIES_MAIN_DICOM_TAGS[] =
+  static const DicomTag DEFAULT_1_11_SERIES_MAIN_DICOM_TAGS[] =
   {
     // { DicomTag(0x0010, 0x1080), "MilitaryRank" },
     DICOM_TAG_SERIES_DATE,
@@ -113,9 +117,11 @@
     DICOM_TAG_PERFORMED_PROCEDURE_STEP_DESCRIPTION,
     DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_DESCRIPTION,
     DICOM_TAG_CONTRAST_BOLUS_AGENT
+
+    // don't add tags here, check ResetDefaultMainDicomTags instead
   };
 
-  static const DicomTag DEFAULT_INSTANCE_MAIN_DICOM_TAGS[] =
+  static const DicomTag DEFAULT_1_11_INSTANCE_MAIN_DICOM_TAGS[] =
   {
     DICOM_TAG_INSTANCE_CREATION_DATE,
     DICOM_TAG_INSTANCE_CREATION_TIME,
@@ -138,6 +144,8 @@
      * indexed in the database by an older version of Orthanc.
      **/
     DICOM_TAG_IMAGE_ORIENTATION_PATIENT  // New in Orthanc 1.4.2
+
+    // don't add tags here, check ResetDefaultMainDicomTags instead
   };
 
   class DicomMap::MainDicomTagsConfiguration : public boost::noncopyable
@@ -187,23 +195,23 @@
       switch (level)
       {
         case ResourceType_Patient:
-          tags = DEFAULT_PATIENT_MAIN_DICOM_TAGS;
-          size = sizeof(DEFAULT_PATIENT_MAIN_DICOM_TAGS) / sizeof(DicomTag);
+          tags = DEFAULT_1_11_PATIENT_MAIN_DICOM_TAGS;
+          size = sizeof(DEFAULT_1_11_PATIENT_MAIN_DICOM_TAGS) / sizeof(DicomTag);
           break;
 
         case ResourceType_Study:
-          tags = DEFAULT_STUDY_MAIN_DICOM_TAGS;
-          size = sizeof(DEFAULT_STUDY_MAIN_DICOM_TAGS) / sizeof(DicomTag);
+          tags = DEFAULT_1_11_STUDY_MAIN_DICOM_TAGS;
+          size = sizeof(DEFAULT_1_11_STUDY_MAIN_DICOM_TAGS) / sizeof(DicomTag);
           break;
 
         case ResourceType_Series:
-          tags = DEFAULT_SERIES_MAIN_DICOM_TAGS;
-          size = sizeof(DEFAULT_SERIES_MAIN_DICOM_TAGS) / sizeof(DicomTag);
+          tags = DEFAULT_1_11_SERIES_MAIN_DICOM_TAGS;
+          size = sizeof(DEFAULT_1_11_SERIES_MAIN_DICOM_TAGS) / sizeof(DicomTag);
           break;
 
         case ResourceType_Instance:
-          tags = DEFAULT_INSTANCE_MAIN_DICOM_TAGS;
-          size = sizeof(DEFAULT_INSTANCE_MAIN_DICOM_TAGS) / sizeof(DicomTag);
+          tags = DEFAULT_1_11_INSTANCE_MAIN_DICOM_TAGS;
+          size = sizeof(DEFAULT_1_11_INSTANCE_MAIN_DICOM_TAGS) / sizeof(DicomTag);
           break;
 
         default:
@@ -247,7 +255,7 @@
       
       if (existingLevelTags.find(tag) != existingLevelTags.end())
       {
-        throw OrthancException(ErrorCode_MainDicomTagsMultiplyDefined, tag.Format() + " is already defined");
+        throw OrthancException(ErrorCode_MainDicomTagsMultiplyDefined, tag.Format() + " is already defined", false);
       }
 
       existingLevelTags.insert(tag);
@@ -287,6 +295,17 @@
       defaultSignatures_[ResourceType_Study] = signatures_[ResourceType_Study];
       defaultSignatures_[ResourceType_Series] = signatures_[ResourceType_Series];
       defaultSignatures_[ResourceType_Instance] = signatures_[ResourceType_Instance];
+
+      // only add new tags here !
+      // introduced in v 1.12.5
+      AddMainDicomTagInternal(DICOM_TAG_TIMEZONE_OFFSET_FROM_UTC, ResourceType_Study);  // used in default QIDO-RS queries
+      
+      AddMainDicomTagInternal(DICOM_TAG_TIMEZONE_OFFSET_FROM_UTC, ResourceType_Series);  // used in default QIDO-RS queries
+      AddMainDicomTagInternal(DICOM_TAG_PERFORMED_PROCEDURE_STEP_START_DATE, ResourceType_Series);  // used in default QIDO-RS queries
+      AddMainDicomTagInternal(DICOM_TAG_PERFORMED_PROCEDURE_STEP_START_TIME, ResourceType_Series);  // used in default QIDO-RS queries
+      AddMainDicomTagInternal(DICOM_TAG_REQUEST_ATTRIBUTES_SEQUENCE, ResourceType_Series);  // used in default QIDO-RS queries
+
+      // TODO-FIND: remove it from metadata when adding it ! AddMainDicomTagInternal(DICOM_TAG_SOP_CLASS_UID, ResourceType_Instance);  // previously saved in a metadata; makes more sense to store it in a DICOM tag
     }
 
     void AddMainDicomTag(const DicomTag& tag,
@@ -328,7 +347,7 @@
       return signatures_[level];
     }
 
-    std::string GetDefaultMainDicomTagsSignature(ResourceType level)
+    std::string GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType level)
     {
 #if !defined(__EMSCRIPTEN__)
       ReaderLock lock(mutex_);
@@ -667,13 +686,16 @@
   }
 
 
-  void DicomMap::CopyTagIfExists(const DicomMap& source,
+  bool DicomMap::CopyTagIfExists(const DicomMap& source,
                                  const DicomTag& tag)
   {
     if (source.HasTag(tag))
     {
       SetValue(tag, source.GetValue(tag));
+      return true;
     }
+
+    return false;
   }
 
 
@@ -778,6 +800,17 @@
     return false;
   }
 
+  bool DicomMap::HasMetaInformationTags(const std::set<DicomTag>& tags)
+  {
+    for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
+    {
+      if (it->GetGroup() == 0x0002)
+      {
+        return true;
+      }
+    }
+    return false;
+  }
 
   void DicomMap::GetMainDicomTags(std::set<DicomTag>& target,
                                   ResourceType level)
@@ -805,9 +838,9 @@
     return DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsSignature(level);
   }
 
-  std::string DicomMap::GetDefaultMainDicomTagsSignature(ResourceType level)
+  std::string DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType level)
   {
-    return DicomMap::MainDicomTagsConfiguration::GetInstance().GetDefaultMainDicomTagsSignature(level);
+    return DicomMap::MainDicomTagsConfiguration::GetInstance().GetDefaultMainDicomTagsSignatureFrom1_11(level);
   }
 
   void DicomMap::GetTags(std::set<DicomTag>& tags) const
@@ -1300,7 +1333,22 @@
       return value->CopyToString(result, allowBinary);
     }
   }
-    
+
+  bool DicomMap::LookupStringValues(std::set<std::string>& results,
+                                    const DicomTag& tag,
+                                    bool allowBinary) const
+  {
+    std::string tmp;
+    if (LookupStringValue(tmp, tag, allowBinary))
+    {
+      Toolbox::SplitString(results, tmp, '\\');
+      return true;
+    }
+
+    return false;
+  }
+
+
   bool DicomMap::ParseInteger32(int32_t& result,
                                 const DicomTag& tag) const
   {
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -129,7 +129,7 @@
 
     static void SetupFindInstanceTemplate(DicomMap& result);
 
-    void CopyTagIfExists(const DicomMap& source,
+    bool CopyTagIfExists(const DicomMap& source,
                          const DicomTag& tag);
 
     static bool IsMainDicomTag(const DicomTag& tag, ResourceType level);
@@ -146,13 +146,15 @@
 
     static bool HasComputedTags(const std::set<DicomTag>& tags);
 
+    static bool HasMetaInformationTags(const std::set<DicomTag>& tags);
+
     static void GetMainDicomTags(std::set<DicomTag>& target,
                                  ResourceType level);
 
     // returns a string uniquely identifying the list of main dicom tags for a level
     static std::string GetMainDicomTagsSignature(ResourceType level);
 
-    static std::string GetDefaultMainDicomTagsSignature(ResourceType level);
+    static std::string GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType level);
 
     static void GetAllMainDicomTags(std::set<DicomTag>& target);
 
@@ -179,7 +181,11 @@
     bool LookupStringValue(std::string& result,
                            const DicomTag& tag,
                            bool allowBinary) const;
-    
+
+    bool LookupStringValues(std::set<std::string>& results,
+                           const DicomTag& tag,
+                           bool allowBinary) const;
+
     bool ParseInteger32(int32_t& result,
                         const DicomTag& tag) const;
 
--- a/OrthancFramework/Sources/DicomFormat/DicomPath.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomPath.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomFormat/DicomPath.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomPath.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomFormat/DicomStreamReader.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomStreamReader.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -247,6 +247,10 @@
           
         pos += length + 12;
       }
+      else
+      {
+        throw OrthancException(ErrorCode_BadFileFormat, "Invalid DICOM File: Unable to parse Meta Header");
+      }
     }
 
     if (pos != block.size())
--- a/OrthancFramework/Sources/DicomFormat/DicomStreamReader.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomStreamReader.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomFormat/DicomTag.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomTag.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomFormat/DicomTag.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomTag.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -157,7 +157,10 @@
   static const DicomTag DICOM_TAG_REQUESTING_PHYSICIAN(0x0032, 0x1032);
   static const DicomTag DICOM_TAG_REFERRING_PHYSICIAN_NAME(0x0008, 0x0090);
   static const DicomTag DICOM_TAG_OPERATOR_NAME(0x0008, 0x1070);
+  static const DicomTag DICOM_TAG_PERFORMED_PROCEDURE_STEP_START_DATE(0x0040, 0x0244);
+  static const DicomTag DICOM_TAG_PERFORMED_PROCEDURE_STEP_START_TIME(0x0040, 0x0245);
   static const DicomTag DICOM_TAG_PERFORMED_PROCEDURE_STEP_DESCRIPTION(0x0040, 0x0254);
+  static const DicomTag DICOM_TAG_REQUEST_ATTRIBUTES_SEQUENCE(0x0040, 0x0275);
   static const DicomTag DICOM_TAG_IMAGE_COMMENTS(0x0020, 0x4000);
   static const DicomTag DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_DESCRIPTION(0x0018, 0x1400);
   static const DicomTag DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_CODE(0x0018, 0x1401);
--- a/OrthancFramework/Sources/DicomFormat/DicomValue.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomValue.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomFormat/DicomValue.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomValue.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomFormat/StreamBlockReader.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/StreamBlockReader.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomFormat/StreamBlockReader.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/StreamBlockReader.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -173,7 +173,6 @@
 
   DicomAssociation::DicomAssociation()
   {
-    role_ = DicomAssociationRole_Default;
     isOpen_ = false;
     net_ = NULL; 
     params_ = NULL;
@@ -198,16 +197,6 @@
   }
 
 
-  void DicomAssociation::SetRole(DicomAssociationRole role)
-  {
-    if (role_ != role)
-    {
-      Close();
-      role_ = role;
-    }
-  }
-
-  
   void DicomAssociation::ClearPresentationContexts()
   {
     Close();
@@ -215,7 +204,26 @@
     proposed_.reserve(MAX_PROPOSED_PRESENTATIONS);
   }
 
-  
+
+  static T_ASC_SC_ROLE GetDcmtkRole(DicomAssociationRole role)
+  {
+    switch (role)
+    {
+      case DicomAssociationRole_Default:
+        return ASC_SC_ROLE_DEFAULT;
+
+      case DicomAssociationRole_Scu:
+        return ASC_SC_ROLE_SCU;
+
+      case DicomAssociationRole_Scp:
+        return ASC_SC_ROLE_SCP;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
   void DicomAssociation::Open(const DicomAssociationParameters& parameters)
   {
     if (isOpen_)
@@ -240,24 +248,6 @@
       dcmConnectionTimeout.set(acseTimeout);
     }
       
-    T_ASC_SC_ROLE dcmtkRole;
-    switch (role_)
-    {
-      case DicomAssociationRole_Default:
-        dcmtkRole = ASC_SC_ROLE_DEFAULT;
-        break;
-
-      case DicomAssociationRole_Scu:
-        dcmtkRole = ASC_SC_ROLE_SCU;
-        break;
-
-      case DicomAssociationRole_Scp:
-        dcmtkRole = ASC_SC_ROLE_SCP;
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
 
     assert(net_ == NULL &&
            params_ == NULL &&
@@ -291,7 +281,12 @@
                                   "no timeout") << ")";
 
     CheckConnecting(parameters, ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ acseTimeout, &net_));
+#if DCMTK_VERSION_NUMBER >= 368
+    CheckConnecting(parameters, ASC_createAssociationParameters(&params_, parameters.GetMaximumPduLength(), acseTimeout));
+#else
+    // from 3.6.8, this version is obsolete
     CheckConnecting(parameters, ASC_createAssociationParameters(&params_, parameters.GetMaximumPduLength()));
+#endif
 
 #if ORTHANC_ENABLE_SSL == 1
     if (parameters.GetRemoteModality().IsDicomTlsEnabled())
@@ -351,12 +346,12 @@
       assert(presentationContextId <= 255);
       const char* abstractSyntax = proposed_[i].abstractSyntax_.c_str();
 
-      const std::set<DicomTransferSyntax>& source = proposed_[i].transferSyntaxes_;
+      const std::list<DicomTransferSyntax>& source = proposed_[i].transferSyntaxes_;
           
       std::vector<const char*> transferSyntaxes;
       transferSyntaxes.reserve(source.size());
           
-      for (std::set<DicomTransferSyntax>::const_iterator
+      for (std::list<DicomTransferSyntax>::const_iterator
              it = source.begin(); it != source.end(); ++it)
       {
         transferSyntaxes.push_back(GetTransferSyntaxUid(*it));
@@ -365,7 +360,7 @@
       assert(!transferSyntaxes.empty());
       CheckConnecting(parameters, ASC_addPresentationContext(
                         params_, presentationContextId, abstractSyntax,
-                        &transferSyntaxes[0], transferSyntaxes.size(), dcmtkRole));
+                        &transferSyntaxes[0], transferSyntaxes.size(), GetDcmtkRole(proposed_[i].role_)));
 
       presentationContextId += 2;
     }
@@ -456,36 +451,56 @@
     }
   }
 
+  void DicomAssociation::ProposeGenericPresentationContext(const std::string& abstractSyntax,
+                                                           DicomAssociationRole role)
+  {
+    std::list<DicomTransferSyntax> ts;
+    ts.push_back(DicomTransferSyntax_LittleEndianExplicit); // the most standard one first !
+    ts.push_back(DicomTransferSyntax_LittleEndianImplicit);
+    ts.push_back(DicomTransferSyntax_BigEndianExplicit);  // Retired but was historicaly proposed by Orthanc
+    ProposePresentationContext(abstractSyntax, ts, role);
+  }
     
   void DicomAssociation::ProposeGenericPresentationContext(const std::string& abstractSyntax)
   {
-    std::set<DicomTransferSyntax> ts;
-    ts.insert(DicomTransferSyntax_LittleEndianImplicit);
-    ts.insert(DicomTransferSyntax_LittleEndianExplicit);
-    ts.insert(DicomTransferSyntax_BigEndianExplicit);  // Retired
-    ProposePresentationContext(abstractSyntax, ts);
+    ProposeGenericPresentationContext(abstractSyntax, DicomAssociationRole_Default);
   }
 
     
   void DicomAssociation::ProposePresentationContext(const std::string& abstractSyntax,
                                                     DicomTransferSyntax transferSyntax)
   {
-    std::set<DicomTransferSyntax> ts;
-    ts.insert(transferSyntax);
-    ProposePresentationContext(abstractSyntax, ts);
+    ProposePresentationContext(abstractSyntax, transferSyntax, DicomAssociationRole_Default);
   }
 
-    
+
+  void DicomAssociation::ProposePresentationContext(const std::string& abstractSyntax,
+                                                    DicomTransferSyntax transferSyntax,
+                                                    DicomAssociationRole role)
+  {
+    std::list<DicomTransferSyntax> ts;
+    ts.push_back(transferSyntax);
+    ProposePresentationContext(abstractSyntax, ts, role);
+  }
+
   size_t DicomAssociation::GetRemainingPropositions() const
   {
     assert(proposed_.size() <= MAX_PROPOSED_PRESENTATIONS);
     return MAX_PROPOSED_PRESENTATIONS - proposed_.size();
   }
     
+  void DicomAssociation::ProposePresentationContext(
+    const std::string& abstractSyntax,
+    const std::list<DicomTransferSyntax>& transferSyntaxes)
+  {
+    ProposePresentationContext(abstractSyntax, transferSyntaxes, DicomAssociationRole_Default);
+  }
+
 
   void DicomAssociation::ProposePresentationContext(
     const std::string& abstractSyntax,
-    const std::set<DicomTransferSyntax>& transferSyntaxes)
+    const std::list<DicomTransferSyntax>& transferSyntaxes,
+    DicomAssociationRole role)
   {
     if (transferSyntaxes.empty())
     {
@@ -507,6 +522,7 @@
     ProposedPresentationContext context;
     context.abstractSyntax_ = abstractSyntax;
     context.transferSyntaxes_ = transferSyntaxes;
+    context.role_ = role;
 
     proposed_.push_back(context);
   }
@@ -526,6 +542,35 @@
     }
   }
 
+  bool DicomAssociation::GetAssociationParameters(std::string& remoteAet,
+                                                  std::string& remoteIp,
+                                                  std::string& calledAet) const
+  {
+    T_ASC_Association& dcmtkAssoc = GetDcmtkAssociation();
+
+    DIC_AE remoteAet_C;
+    DIC_AE calledAet_C;
+    DIC_AE remoteIp_C;
+    DIC_AE calledIP_C;
+
+    if (
+#if DCMTK_VERSION_NUMBER >= 364
+      ASC_getAPTitles(dcmtkAssoc.params, remoteAet_C, sizeof(remoteAet_C), calledAet_C, sizeof(calledAet_C), NULL, 0).good() &&
+      ASC_getPresentationAddresses(dcmtkAssoc.params, remoteIp_C, sizeof(remoteIp_C), calledIP_C, sizeof(calledIP_C)).good()
+#else
+      ASC_getAPTitles(dcmtkAssoc.params, remoteAet_C, calledAet_C, NULL).good() &&
+      ASC_getPresentationAddresses(dcmtkAssoc.params, remoteIp_C, calledIP_C).good()
+#endif
+      )
+    {
+      remoteIp = std::string(/*OFSTRING_GUARD*/(remoteIp_C));
+      remoteAet = std::string(/*OFSTRING_GUARD*/(remoteAet_C));
+      calledAet = (/*OFSTRING_GUARD*/(calledAet_C));
+      return true;
+    }
+
+    return false;
+  }
     
   T_ASC_Network& DicomAssociation::GetDcmtkNetwork() const
   {
@@ -649,13 +694,12 @@
     DicomAssociation association;
 
     {
-      std::set<DicomTransferSyntax> transferSyntaxes;
-      transferSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
-      transferSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit);
+      std::list<DicomTransferSyntax> transferSyntaxes;
+      transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianExplicit);
+      transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianImplicit);
 
-      association.SetRole(DicomAssociationRole_Scp);
       association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass,
-                                             transferSyntaxes);
+                                             transferSyntaxes, DicomAssociationRole_Scp);
     }
       
     association.Open(parameters);
@@ -828,13 +872,13 @@
     DicomAssociation association;
 
     {
-      std::set<DicomTransferSyntax> transferSyntaxes;
-      transferSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
-      transferSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit);
+      std::list<DicomTransferSyntax> transferSyntaxes;
+      transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianExplicit);
+      transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianImplicit);
       
-      association.SetRole(DicomAssociationRole_Default);
+      // association.SetRole(DicomAssociationRole_Default);
       association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass,
-                                             transferSyntaxes);
+                                             transferSyntaxes, DicomAssociationRole_Default);
     }
       
     association.Open(parameters);
--- a/OrthancFramework/Sources/DicomNetworking/DicomAssociation.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociation.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -44,6 +44,7 @@
 #include <stdint.h>   // For uint8_t
 #include <boost/noncopyable.hpp>
 #include <set>
+#include <list>
 
 namespace Orthanc
 {
@@ -58,13 +59,13 @@
     struct ProposedPresentationContext
     {
       std::string                    abstractSyntax_;
-      std::set<DicomTransferSyntax>  transferSyntaxes_;
+      std::list<DicomTransferSyntax> transferSyntaxes_;
+      DicomAssociationRole           role_;
     };
 
     typedef std::map<std::string, std::map<DicomTransferSyntax, uint8_t> >
     AcceptedPresentationContexts;
 
-    DicomAssociationRole                      role_;
     bool                                      isOpen_;
     std::vector<ProposedPresentationContext>  proposed_;
     AcceptedPresentationContexts              accepted_;
@@ -95,8 +96,6 @@
       return isOpen_;
     }
 
-    void SetRole(DicomAssociationRole role);
-
     void ClearPresentationContexts();
 
     void Open(const DicomAssociationParameters& parameters);
@@ -109,6 +108,13 @@
 
     void ProposeGenericPresentationContext(const std::string& abstractSyntax);
 
+    void ProposeGenericPresentationContext(const std::string& abstractSyntax,
+                                           DicomAssociationRole role);
+
+    void ProposePresentationContext(const std::string& abstractSyntax,
+                                    DicomTransferSyntax transferSyntax,
+                                    DicomAssociationRole role);
+
     void ProposePresentationContext(const std::string& abstractSyntax,
                                     DicomTransferSyntax transferSyntax);
 
@@ -116,12 +122,21 @@
 
     void ProposePresentationContext(
       const std::string& abstractSyntax,
-      const std::set<DicomTransferSyntax>& transferSyntaxes);
-    
+      const std::list<DicomTransferSyntax>& transferSyntaxes);
+
+    void ProposePresentationContext(
+      const std::string& abstractSyntax,
+      const std::list<DicomTransferSyntax>& transferSyntaxes,
+      DicomAssociationRole role);
+
     T_ASC_Association& GetDcmtkAssociation() const;
 
     T_ASC_Network& GetDcmtkNetwork() const;
 
+    bool GetAssociationParameters(std::string& remoteAet,
+                                  std::string& remoteIp,
+                                  std::string& calledAet) const;
+
     static void CheckCondition(const OFCondition& cond,
                                const DicomAssociationParameters& parameters,
                                const std::string& command);
--- a/OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -195,7 +195,7 @@
       throw OrthancException(ErrorCode_BadSequenceOfCalls,
                              "DICOM TLS - No path to the local certificate was provided");
     }
-    else if (trustedCertificatesPath_.empty())
+    else if (remoteCertificateRequired_ && trustedCertificatesPath_.empty())
     {
       throw OrthancException(ErrorCode_BadSequenceOfCalls,
                              "DICOM TLS - No path to the trusted remote certificates was provided");
--- a/OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -234,15 +234,58 @@
 
 
 
-  void DicomControlUserConnection::SetupPresentationContexts()
+  void DicomControlUserConnection::SetupPresentationContexts(
+    ScuOperationFlags scuOperation,
+    const std::set<std::string>& acceptedStorageSopClasses,
+    const std::list<DicomTransferSyntax>& proposedStorageTransferSyntaxes)
   {
     assert(association_.get() != NULL);
-    association_->ProposeGenericPresentationContext(UID_VerificationSOPClass);
-    association_->ProposeGenericPresentationContext(UID_FINDPatientRootQueryRetrieveInformationModel);
-    association_->ProposeGenericPresentationContext(UID_MOVEPatientRootQueryRetrieveInformationModel);
-    association_->ProposeGenericPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel);
-    association_->ProposeGenericPresentationContext(UID_MOVEStudyRootQueryRetrieveInformationModel);
-    association_->ProposeGenericPresentationContext(UID_FINDModalityWorklistInformationModel);
+
+    if ((scuOperation & ScuOperationFlags_Echo) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_VerificationSOPClass);
+    }
+
+    if ((scuOperation & ScuOperationFlags_FindPatient) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_FINDPatientRootQueryRetrieveInformationModel);
+    }
+
+    if ((scuOperation & ScuOperationFlags_FindStudy) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel);
+    }
+
+    if ((scuOperation & ScuOperationFlags_FindWorklist) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_FINDModalityWorklistInformationModel);
+    }
+
+    if ((scuOperation & ScuOperationFlags_MovePatient) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_MOVEPatientRootQueryRetrieveInformationModel);
+    }
+
+    if ((scuOperation & ScuOperationFlags_MoveStudy) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_MOVEStudyRootQueryRetrieveInformationModel);
+    }
+
+    if ((scuOperation & ScuOperationFlags_Get) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_GETStudyRootQueryRetrieveInformationModel);
+      association_->ProposeGenericPresentationContext(UID_GETPatientRootQueryRetrieveInformationModel);
+
+      if (acceptedStorageSopClasses.size() == 0)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls); // the acceptedStorageSopClassUids should always be defined for a C-Get
+      }
+
+      for (std::set<std::string>::const_iterator it = acceptedStorageSopClasses.begin(); it != acceptedStorageSopClasses.end(); ++it)
+      {
+        association_->ProposePresentationContext(*it, proposedStorageTransferSyntaxes, DicomAssociationRole_Scp);
+      }
+    }
   }
     
 
@@ -350,6 +393,21 @@
     }
   }
 
+  void MoveProgressCallback(void *callbackData,
+                            T_DIMSE_C_MoveRQ *request,
+                            int responseCount, 
+                            T_DIMSE_C_MoveRSP *response)
+  {
+    DicomControlUserConnection::IProgressListener* listener = reinterpret_cast<DicomControlUserConnection::IProgressListener*>(callbackData);
+    if (listener)
+    {
+      listener->OnProgressUpdated(response->NumberOfRemainingSubOperations,
+                                  response->NumberOfCompletedSubOperations,
+                                  response->NumberOfFailedSubOperations,
+                                  response->NumberOfWarningSubOperations);
+    }
+  }
+
     
   void DicomControlUserConnection::MoveInternal(const std::string& targetAet,
                                                 ResourceType level,
@@ -391,7 +449,8 @@
     DcmDataset* statusDetail = NULL;
     DcmDataset* responseIdentifiers = NULL;
     OFCondition cond = DIMSE_moveUser(
-      &association_->GetDcmtkAssociation(), presID, &request, dataset, /*moveCallback*/ NULL, NULL,
+      &association_->GetDcmtkAssociation(), presID, &request, dataset, 
+      (progressListener_ != NULL ? MoveProgressCallback : NULL), progressListener_,
       /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
       /*opt_dimse_timeout*/ parameters_.GetTimeout(),
       &association_->GetDcmtkNetwork(), /*subOpCallback*/ NULL, NULL,
@@ -413,6 +472,14 @@
       OFString str;
       CLOG(TRACE, DICOM) << "Received Final Move Response:" << std::endl
                          << DIMSE_dumpMessage(str, response, DIMSE_INCOMING);
+
+      if (progressListener_ != NULL)
+      {
+        progressListener_->OnProgressUpdated(response.NumberOfRemainingSubOperations,
+                                             response.NumberOfCompletedSubOperations,
+                                             response.NumberOfFailedSubOperations,
+                                             response.NumberOfWarningSubOperations);
+      }
     }
     
     /**
@@ -445,11 +512,240 @@
   }
     
 
-  DicomControlUserConnection::DicomControlUserConnection(const DicomAssociationParameters& params) :
+  void DicomControlUserConnection::Get(const DicomMap& findResult,
+                                       CGetInstanceReceivedCallback instanceReceivedCallback,
+                                       void* callbackContext)
+  {
+    assert(association_.get() != NULL);
+    association_->Open(parameters_);
+
+    std::unique_ptr<ParsedDicomFile> query(
+      ConvertQueryFields(findResult, parameters_.GetRemoteModality().GetManufacturer()));
+    DcmDataset* queryDataset = query->GetDcmtkObject().getDataset();
+
+    std::string remoteAet;
+    std::string remoteIp;
+    std::string calledAet;
+
+    association_->GetAssociationParameters(remoteAet, remoteIp, calledAet);
+
+    const char* sopClass = NULL;
+    const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent();
+    ResourceType level = StringToResourceType(tmp.c_str());
+    switch (level)
+    {
+      case ResourceType_Patient:
+        sopClass = UID_GETPatientRootQueryRetrieveInformationModel;
+        break;
+      case ResourceType_Study:
+      case ResourceType_Series:
+      case ResourceType_Instance:
+        sopClass = UID_GETStudyRootQueryRetrieveInformationModel;
+        break;
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    // Figure out which of the accepted presentation contexts should be used
+    int cgetPresID = ASC_findAcceptedPresentationContextID(&association_->GetDcmtkAssociation(), sopClass);
+    if (cgetPresID == 0)
+    {
+      throw OrthancException(ErrorCode_DicomGetUnavailable,
+                             "Remote AET is " + parameters_.GetRemoteModality().GetApplicationEntityTitle());
+    }
+
+    T_DIMSE_Message msgGetRequest;
+    memset((char*)&msgGetRequest, 0, sizeof(msgGetRequest));
+    msgGetRequest.CommandField = DIMSE_C_GET_RQ;
+
+    T_DIMSE_C_GetRQ* request = &(msgGetRequest.msg.CGetRQ);
+    request->MessageID = association_->GetDcmtkAssociation().nextMsgID++;
+    strncpy(request->AffectedSOPClassUID, sopClass, DIC_UI_LEN);
+    request->Priority = DIMSE_PRIORITY_MEDIUM;
+    request->DataSetType = DIMSE_DATASET_PRESENT;
+
+    {
+      OFString str;
+      CLOG(TRACE, DICOM) << "Sending Get Request:" << std::endl
+                         << DIMSE_dumpMessage(str, *request, DIMSE_OUTGOING, NULL, cgetPresID);
+    }
+    
+    OFCondition cond = DIMSE_sendMessageUsingMemoryData(
+          &(association_->GetDcmtkAssociation()), cgetPresID, &msgGetRequest, NULL /* statusDetail */, queryDataset,
+          NULL, NULL, NULL /* commandSet */);
+      
+    if (cond.bad())
+    {
+        OFString tempStr;
+        CLOG(TRACE, DICOM) << "Failed sending C-GET request: " << DimseCondition::dump(tempStr, cond);
+        // return cond;
+    }
+
+    // equivalent to handleCGETSession in DCMTK
+    bool continueSession = true;
+
+    // As long we want to continue (usually, as long as we receive more objects,
+    // i.e. the final C-GET response has not arrived yet)
+    while (continueSession)
+    {
+        T_DIMSE_Message rsp;
+        // Make sure everything is zeroed (especially options)
+        memset((char*)&rsp, 0, sizeof(rsp));
+
+        // DcmDataset* statusDetail = NULL;
+        T_ASC_PresentationContextID cmdPresId = 0;
+
+        OFCondition result = DIMSE_receiveCommand(&(association_->GetDcmtkAssociation()),
+                                                  (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                                                  parameters_.GetTimeout(),
+                                                  &cmdPresId,
+                                                  &rsp,
+                                                  NULL /* statusDetail */,
+                                                  NULL /* not interested in the command set */);
+
+        if (result.bad())
+        {
+          OFString tempStr;
+          CLOG(TRACE, DICOM) << "Failed receiving DIMSE command: " << DimseCondition::dump(tempStr, result);
+          // delete statusDetail;
+          break;  // TODO: return value
+        }
+        // Handle C-GET Response
+        if (rsp.CommandField == DIMSE_C_GET_RSP)
+        {
+          {
+            OFString tempStr;
+            CLOG(TRACE, DICOM) << "Received C-GET Response: " << std::endl
+              << DIMSE_dumpMessage(tempStr, rsp, DIMSE_INCOMING, NULL, cmdPresId);
+          }
+
+          if (progressListener_ != NULL)
+          {
+            progressListener_->OnProgressUpdated(rsp.msg.CGetRSP.NumberOfRemainingSubOperations,
+                                                  rsp.msg.CGetRSP.NumberOfCompletedSubOperations,
+                                                  rsp.msg.CGetRSP.NumberOfFailedSubOperations,
+                                                  rsp.msg.CGetRSP.NumberOfWarningSubOperations);
+          }
+
+          if (rsp.msg.CGetRSP.DimseStatus == 0x0000)  // final success message
+          {
+            continueSession = false;
+          }
+        }
+        // Handle C-STORE Request
+        else if (rsp.CommandField == DIMSE_C_STORE_RQ)
+        {
+          {
+            OFString tempStr;
+            CLOG(TRACE, DICOM) << "Received C-STORE Request: " << std::endl
+              << DIMSE_dumpMessage(tempStr, rsp, DIMSE_INCOMING, NULL, cmdPresId);
+          }
+
+          T_DIMSE_C_StoreRQ* storeRequest = &(rsp.msg.CStoreRQ);
+
+          // Check if dataset is announced correctly
+          if (rsp.msg.CStoreRQ.DataSetType == DIMSE_DATASET_NULL)
+          {
+            CLOG(WARNING, DICOM) << "C-GET SCU handler: Incoming C-STORE with no dataset";
+          }
+
+          Uint16 desiredCStoreReturnStatus = 0;
+          DcmDataset* dataObject = NULL;
+
+          // Receive dataset
+          result = DIMSE_receiveDataSetInMemory(&(association_->GetDcmtkAssociation()),
+                                                  (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                                                  parameters_.GetTimeout(),
+                                                  &cmdPresId,
+                                                  &dataObject,
+                                                  NULL, NULL);
+
+          if (result.bad())
+          {
+            LOG(WARNING) << "C-GET SCU handler: Failed to receive dataset: " << result.text();
+            desiredCStoreReturnStatus = STATUS_STORE_Error_CannotUnderstand;
+          }
+          else
+          {
+            // callback the OrthancServer with the received data
+            if (instanceReceivedCallback != NULL)
+            {
+              desiredCStoreReturnStatus = instanceReceivedCallback(callbackContext, *dataObject, remoteAet, remoteIp, calledAet);
+            }
+
+            // send the Store response
+            T_DIMSE_Message storeResponse;
+            memset((char*)&storeResponse, 0, sizeof(storeResponse));
+            storeResponse.CommandField         = DIMSE_C_STORE_RSP;
+
+            T_DIMSE_C_StoreRSP& storeRsp       = storeResponse.msg.CStoreRSP;
+            storeRsp.MessageIDBeingRespondedTo = storeRequest->MessageID;
+            storeRsp.DimseStatus               = desiredCStoreReturnStatus;
+            storeRsp.DataSetType               = DIMSE_DATASET_NULL;
+
+            OFStandard::strlcpy(
+                storeRsp.AffectedSOPClassUID, storeRequest->AffectedSOPClassUID, sizeof(storeRsp.AffectedSOPClassUID));
+            OFStandard::strlcpy(
+                storeRsp.AffectedSOPInstanceUID, storeRequest->AffectedSOPInstanceUID, sizeof(storeRsp.AffectedSOPInstanceUID));
+            storeRsp.opts = O_STORE_AFFECTEDSOPCLASSUID | O_STORE_AFFECTEDSOPINSTANCEUID;
+
+            result = DIMSE_sendMessageUsingMemoryData(&(association_->GetDcmtkAssociation()), 
+                                                      cmdPresId, 
+                                                      &storeResponse, NULL /* statusDetail */, NULL /* dataObject */,
+                                                      NULL, NULL, NULL /* commandSet */);
+            if (result.bad())
+            {
+              continueSession = false;
+            }
+            else
+            {
+              OFString tempStr;
+              CLOG(TRACE, DICOM) << "Sent C-STORE Response: " << std::endl
+                << DIMSE_dumpMessage(tempStr, storeResponse, DIMSE_OUTGOING, NULL, cmdPresId);
+            }
+          }
+        }
+        // Handle other DIMSE command (error since other command than GET/STORE not expected)
+        else
+        {
+          CLOG(WARNING, DICOM) << "Expected C-GET response or C-STORE request but received DIMSE command 0x"
+                               << std::hex << std::setfill('0') << std::setw(4)
+                               << static_cast<unsigned int>(rsp.CommandField);
+          
+          result          = DIMSE_BADCOMMANDTYPE;
+          continueSession = false;
+        }
+
+        // delete statusDetail; // should be NULL if not existing or added to response list
+        // statusDetail = NULL;
+    }
+    /* All responses received or break signal occurred */
+
+    // return result;
+}
+
+
+  DicomControlUserConnection::DicomControlUserConnection(const DicomAssociationParameters& params, ScuOperationFlags scuOperation) :
     parameters_(params),
-    association_(new DicomAssociation)
+    association_(new DicomAssociation),
+    progressListener_(NULL)
   {
-    SetupPresentationContexts();
+    assert((scuOperation & ScuOperationFlags_Get) == 0);  // you must provide acceptedStorageSopClassUids for Get SCU
+    std::set<std::string> emptyStorageSopClasses;
+    std::list<DicomTransferSyntax> emptyStorageTransferSyntaxes;
+
+    SetupPresentationContexts(scuOperation, emptyStorageSopClasses, emptyStorageTransferSyntaxes);
+  }
+    
+  DicomControlUserConnection::DicomControlUserConnection(const DicomAssociationParameters& params, 
+                                                         ScuOperationFlags scuOperation,
+                                                         const std::set<std::string>& acceptedStorageSopClasses,
+                                                         const std::list<DicomTransferSyntax>& proposedStorageTransferSyntaxes) :
+    parameters_(params),
+    association_(new DicomAssociation),
+    progressListener_(NULL)
+  {
+    SetupPresentationContexts(scuOperation, acceptedStorageSopClasses, proposedStorageTransferSyntaxes);
   }
     
 
--- a/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -32,18 +32,55 @@
 #include "DicomFindAnswers.h"
 
 #include <boost/noncopyable.hpp>
+#include <list>
 
 namespace Orthanc
 {
   class DicomAssociation;  // Forward declaration for PImpl design pattern
   
+  typedef uint16_t (*CGetInstanceReceivedCallback) (void *callbackContext,
+                                                    DcmDataset& dataset,
+                                                    const std::string& remoteAet,
+                                                    const std::string& remoteIp,
+                                                    const std::string& calledAet);
+
+
+  enum ScuOperationFlags
+  {
+    ScuOperationFlags_Echo = 1 << 0,
+    ScuOperationFlags_FindPatient = 1 << 1,
+    ScuOperationFlags_FindStudy = 1 << 2,
+    ScuOperationFlags_FindWorklist = 1 << 3,
+    ScuOperationFlags_MoveStudy = 1 << 4,
+    ScuOperationFlags_MovePatient = 1 << 5,
+    // C-Store is not using DicomControlUserConnection but DicomStoreUserConnection
+    ScuOperationFlags_Get = 1 << 6,
+
+    ScuOperationFlags_Find = ScuOperationFlags_FindPatient | ScuOperationFlags_FindStudy | ScuOperationFlags_FindWorklist,
+    ScuOperationFlags_Move = ScuOperationFlags_MoveStudy | ScuOperationFlags_MovePatient,
+    ScuOperationFlags_All = ScuOperationFlags_Echo | ScuOperationFlags_Find | ScuOperationFlags_Move | ScuOperationFlags_Get
+  };
+
   class DicomControlUserConnection : public boost::noncopyable
   {
+  public:
+    class IProgressListener
+    {
+    public:
+      virtual void OnProgressUpdated(uint16_t nbRemainingSubOperations,
+                                     uint16_t nbCompletedSubOperations,
+                                     uint16_t nbFailedSubOperations,
+                                     uint16_t nbWarningSubOperations) = 0;
+    };
+
   private:
     DicomAssociationParameters           parameters_;
     boost::shared_ptr<DicomAssociation>  association_;
+    IProgressListener*                   progressListener_;
 
-    void SetupPresentationContexts();
+    void SetupPresentationContexts(ScuOperationFlags scuOperation,
+                                   const std::set<std::string>& acceptedStorageSopClasses,
+                                   const std::list<DicomTransferSyntax>& proposedStorageTransferSyntaxes);
 
     void FindInternal(DicomFindAnswers& answers,
                       DcmDataset* dataset,
@@ -56,8 +93,14 @@
                       const DicomMap& fields);
     
   public:
-    explicit DicomControlUserConnection(const DicomAssociationParameters& params);
-    
+    explicit DicomControlUserConnection(const DicomAssociationParameters& params, ScuOperationFlags scuOperation);
+
+    // specific constructor for CGet SCU
+    explicit DicomControlUserConnection(const DicomAssociationParameters& params, 
+                                        ScuOperationFlags scuOperation,
+                                        const std::set<std::string>& acceptedStorageSopClasses,
+                                        const std::list<DicomTransferSyntax>& proposedStorageTransferSyntaxes);
+
     const DicomAssociationParameters& GetParameters() const
     {
       return parameters_;
@@ -67,11 +110,20 @@
 
     bool Echo();
 
+    void SetProgressListener(IProgressListener* progressListener)
+    {
+      progressListener_ = progressListener;
+    }
+
     void Find(DicomFindAnswers& result,
               ResourceType level,
               const DicomMap& originalFields,
               bool normalize);
 
+    void Get(const DicomMap& getQuery,
+             CGetInstanceReceivedCallback instanceReceivedCallback,
+             void* callbackContext);
+
     void Move(const std::string& targetAet,
               ResourceType level,
               const DicomMap& findResult);
--- a/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -202,7 +202,7 @@
                                 DicomToJsonFormat format) const
   {
     const ParsedDicomFile& answer = GetAnswer(index);
-    answer.DatasetToJson(target, format, DicomToJsonFlags_None, 0);
+    answer.DatasetToJson(target, format, DicomToJsonFlags_IncludePrivateTags, 0);
   }
 
 
--- a/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/DicomServer.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomServer.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/DicomServer.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomServer.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -57,7 +57,7 @@
                                                      bool hasPreferred,
                                                      DicomTransferSyntax preferred)
   {
-    typedef std::list< std::set<DicomTransferSyntax> >  GroupsOfSyntaxes;
+    typedef std::list< std::list<DicomTransferSyntax> >  GroupsOfSyntaxes;
 
     GroupsOfSyntaxes  groups;
 
@@ -65,8 +65,8 @@
     for (std::set<DicomTransferSyntax>::const_iterator
            it = sourceSyntaxes.begin(); it != sourceSyntaxes.end(); ++it)
     {
-      std::set<DicomTransferSyntax> group;
-      group.insert(*it);
+      std::list<DicomTransferSyntax> group;
+      group.push_back(*it);
       groups.push_back(group);
     }
 
@@ -74,8 +74,8 @@
     if (hasPreferred &&
         sourceSyntaxes.find(preferred) == sourceSyntaxes.end())
     {
-      std::set<DicomTransferSyntax> group;
-      group.insert(preferred);
+      std::list<DicomTransferSyntax> group;
+      group.push_back(preferred);
       groups.push_back(group);
     }
 
@@ -89,7 +89,7 @@
         DicomTransferSyntax_BigEndianExplicit
       };
 
-      std::set<DicomTransferSyntax> group;
+      std::list<DicomTransferSyntax> group;
 
       for (size_t i = 0; i < N; i++)
       {
@@ -97,7 +97,7 @@
         if (sourceSyntaxes.find(syntax) == sourceSyntaxes.end() &&
             (!hasPreferred || preferred != syntax))
         {
-          group.insert(syntax);
+          group.push_back(syntax);
         }
       }
 
@@ -651,7 +651,7 @@
           s += " " + std::string(GetTransferSyntaxUid(*it));
         }
         
-        throw OrthancException(ErrorCode_NotImplemented, "Cannot transcode instance of SOPClassUID " + 
+        throw OrthancException(ErrorCode_InternalError, "Cannot transcode instance of SOPClassUID " + 
                                sopClassUid + " from " +
                                std::string(GetTransferSyntaxUid(sourceSyntax)) +
                                " to one of [" + s + " ]");
--- a/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/IApplicationEntityFilter.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IApplicationEntityFilter.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -28,6 +28,7 @@
 
 #include <boost/noncopyable.hpp>
 #include <string>
+#include <list>
 
 namespace Orthanc
 {
@@ -47,13 +48,23 @@
                                   const std::string& calledAet,
                                   DicomRequestType type) = 0;
 
+    // Get the set of TransferSyntaxes that are accepted when negotiation a C-Store association, acting as SCP when it has been initiated by the C-Store SCU.
     virtual void GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax>& target,
                                              const std::string& remoteIp,
                                              const std::string& remoteAet,
                                              const std::string& calledAet) = 0;
-    
+
+    // Get the list of TransferSyntaxes that are proposed when initiating a C-Store SCP which actually only happens in a C-Get SCU
+    virtual void GetProposedStorageTransferSyntaxes(std::list<DicomTransferSyntax>& target,
+                                                    const std::string& remoteIp,
+                                                    const std::string& remoteAet,
+                                                    const std::string& calledAet) = 0;
+
     virtual bool IsUnknownSopClassAccepted(const std::string& remoteIp,
                                            const std::string& remoteAet,
                                            const std::string& calledAet) = 0;
+
+    virtual void GetAcceptedSopClasses(std::set<std::string>& sopClasses,
+                                       size_t maxCount) = 0;
   };
 }
--- a/OrthancFramework/Sources/DicomNetworking/IFindRequestHandler.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IFindRequestHandler.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/IFindRequestHandlerFactory.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IFindRequestHandlerFactory.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/IGetRequestHandler.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IGetRequestHandler.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/IGetRequestHandlerFactory.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IGetRequestHandlerFactory.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/IMoveRequestHandler.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IMoveRequestHandler.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/IMoveRequestHandlerFactory.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IMoveRequestHandlerFactory.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/IStorageCommitmentRequestHandler.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IStorageCommitmentRequestHandler.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/IStorageCommitmentRequestHandlerFactory.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IStorageCommitmentRequestHandlerFactory.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandler.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandler.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandlerFactory.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandlerFactory.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/IWorklistRequestHandler.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IWorklistRequestHandler.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/IWorklistRequestHandlerFactory.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IWorklistRequestHandlerFactory.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -502,13 +502,21 @@
           }
           else                                         // see dcmqrsrv.cc lines 839 - 876
           {
+            std::set<std::string> acceptedStorageClasses;
+
+            if (server.HasApplicationEntityFilter())
+            {
+              server.GetApplicationEntityFilter().GetAcceptedSopClasses(acceptedStorageClasses, 0);
+            }
+
             /* accept storage syntaxes with proposed role */
             int npc = ASC_countPresentationContexts(assoc->params);
             for (int i = 0; i < npc; i++)
             {
               T_ASC_PresentationContext pc;
               ASC_getPresentationContext(assoc->params, i, &pc);
-              if (dcmIsaStorageSOPClassUID(pc.abstractSyntax))
+              if (acceptedStorageClasses.find(pc.abstractSyntax) != acceptedStorageClasses.end()
+                 || (!server.HasApplicationEntityFilter() && dcmIsaStorageSOPClassUID(pc.abstractSyntax)))  // previous behavior kept for compatibility in case the server does not have an ApplicationEntityFilter
               {
                 /**
                  * We are prepared to accept whatever role the caller
--- a/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -44,6 +44,16 @@
 #endif
 
 
+#if ORTHANC_ENABLE_PLUGINS == 1
+#  if defined(__ORTHANC_FILE__)
+//   Prevents the system-wide DCMTK library from leaking the
+//   full path of this source file in "DCMTLS_ERROR()"
+#    undef __FILE__
+#    define __FILE__ __ORTHANC_FILE__
+#  endif
+#endif
+
+
 namespace Orthanc
 {
   namespace Internals
--- a/OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/Internals/FindScp.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/FindScp.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/Internals/FindScp.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/FindScp.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/Internals/GetScp.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/GetScp.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/Internals/GetScp.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/GetScp.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/Internals/MoveScp.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/MoveScp.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/Internals/MoveScp.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/MoveScp.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/NetworkingCompatibility.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/NetworkingCompatibility.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -50,6 +50,7 @@
 static const char* KEY_USE_DICOM_TLS = "UseDicomTls";
 static const char* KEY_LOCAL_AET = "LocalAet";
 static const char* KEY_TIMEOUT = "Timeout";
+static const char* KEY_RETRIEVE_METHOD = "RetrieveMethod";
 
 
 namespace Orthanc
@@ -72,6 +73,7 @@
     useDicomTls_ = false;
     localAet_.clear();
     timeout_ = 0;
+    retrieveMethod_ = RetrieveMethod_SystemDefault;
   }
 
 
@@ -308,6 +310,17 @@
     {
       timeout_ = SerializationToolbox::ReadUnsignedInteger(serialized, KEY_TIMEOUT);
     }
+
+    if (serialized.isMember(KEY_RETRIEVE_METHOD))
+    {
+      retrieveMethod_ = StringToRetrieveMethod
+        (SerializationToolbox::ReadString(serialized, KEY_RETRIEVE_METHOD));
+    }   
+    else
+    {
+      retrieveMethod_ = RetrieveMethod_SystemDefault;
+    }
+
   }
 
 
@@ -427,6 +440,7 @@
       target[KEY_USE_DICOM_TLS] = useDicomTls_;
       target[KEY_LOCAL_AET] = localAet_;
       target[KEY_TIMEOUT] = timeout_;
+      target[KEY_RETRIEVE_METHOD] = EnumerationToString(retrieveMethod_);
     }
     else
     {
@@ -521,4 +535,14 @@
   {
     return timeout_ != 0;
   }
+
+  RetrieveMethod RemoteModalityParameters::GetRetrieveMethod() const
+  {
+    return retrieveMethod_;
+  }
+
+  void RemoteModalityParameters::SetRetrieveMethod(RetrieveMethod retrieveMethod)
+  {
+    retrieveMethod_ = retrieveMethod;
+  }
 }
--- a/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -51,6 +51,7 @@
     bool                  useDicomTls_;
     std::string           localAet_;
     uint32_t              timeout_;
+    RetrieveMethod        retrieveMethod_;   // New in Orthanc 1.12.6
     
     void Clear();
 
@@ -118,5 +119,10 @@
     uint32_t GetTimeout() const;
 
     bool HasTimeout() const;    
+
+    RetrieveMethod GetRetrieveMethod() const;
+
+    void SetRetrieveMethod(RetrieveMethod retrieveMethod);
+
   };
 }
--- a/OrthancFramework/Sources/DicomNetworking/TimeoutDicomConnectionManager.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/TimeoutDicomConnectionManager.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomNetworking/TimeoutDicomConnectionManager.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/TimeoutDicomConnectionManager.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -50,8 +50,9 @@
 
 namespace Orthanc
 {
-  DcmtkTranscoder::DcmtkTranscoder() :
-    lossyQuality_(90)
+  DcmtkTranscoder::DcmtkTranscoder(unsigned int maxConcurrentExecutions) :
+  defaultLossyQuality_(90),
+    maxConcurrentExecutionsSemaphore_(maxConcurrentExecutions)
   {
   }
 
@@ -63,26 +64,26 @@
   }
 
   
-  void DcmtkTranscoder::SetLossyQuality(unsigned int quality)
+  void DcmtkTranscoder::SetDefaultLossyQuality(unsigned int quality)
   {
     if (quality == 0 ||
         quality > 100)
     {
       throw OrthancException(
         ErrorCode_ParameterOutOfRange,
-        "The quality for lossy transcoding must be an integer between 1 and 100, received: " +
+        "The default quality for lossy transcoding must be an integer between 1 and 100, received: " +
         boost::lexical_cast<std::string>(quality));
     }
     else
     {
-      LOG(INFO) << "Quality for lossy transcoding using DCMTK is set to: " << quality;
-      lossyQuality_ = quality;
+      LOG(INFO) << "Default quality for lossy transcoding using DCMTK is set to: " << quality;
+      defaultLossyQuality_ = quality;
     }
   }
 
-  unsigned int DcmtkTranscoder::GetLossyQuality() const
+  unsigned int DcmtkTranscoder::GetDefaultLossyQuality() const
   {
-    return lossyQuality_;
+    return defaultLossyQuality_;
   }
 
   bool TryTranscode(std::vector<std::string>& failureReasons, /* out */
@@ -108,7 +109,8 @@
                                          std::string& failureReason /* out */,
                                          DcmFileFormat& dicom, /* in/out */
                                          const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                                         bool allowNewSopInstanceUid) 
+                                         bool allowNewSopInstanceUid,
+                                         unsigned int lossyQuality) 
   {
     std::vector<std::string> failureReasons;
 
@@ -168,7 +170,7 @@
       else
       {
         // Check out "dcmjpeg/apps/dcmcjpeg.cc"
-        DJ_RPLossy parameters(lossyQuality_);
+        DJ_RPLossy parameters(lossyQuality);
           
         if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess1, &parameters))
         {
@@ -194,7 +196,7 @@
       else
       {
         // Check out "dcmjpeg/apps/dcmcjpeg.cc"
-        DJ_RPLossy parameters(lossyQuality_);
+        DJ_RPLossy parameters(lossyQuality);
         if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess2_4, &parameters))
         {
           selectedSyntax = DicomTransferSyntax_JPEGProcess2_4;
@@ -311,12 +313,23 @@
     return false;
   }
 
-
   bool DcmtkTranscoder::Transcode(DicomImage& target,
                                   DicomImage& source /* in, "GetParsed()" possibly modified */,
                                   const std::set<DicomTransferSyntax>& allowedSyntaxes,
                                   bool allowNewSopInstanceUid)
   {
+    return Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid, defaultLossyQuality_);
+  }
+
+
+  bool DcmtkTranscoder::Transcode(DicomImage& target,
+                                  DicomImage& source /* in, "GetParsed()" possibly modified */,
+                                  const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                                  bool allowNewSopInstanceUid,
+                                  unsigned int lossyQuality)
+  {
+    Semaphore::Locker lock(maxConcurrentExecutionsSemaphore_); // limit the number of concurrent executions
+
     target.Clear();
     
     DicomTransferSyntax sourceSyntax;
@@ -360,7 +373,7 @@
       return true;
     }
     else if (InplaceTranscode(targetSyntax, failureReason, source.GetParsed(),
-                              allowedSyntaxes, allowNewSopInstanceUid))
+                              allowedSyntaxes, allowNewSopInstanceUid, lossyQuality))
     {   
       // Sanity check
       DicomTransferSyntax targetSyntax2;
--- a/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -33,26 +33,30 @@
 #endif
 
 #include "IDicomTranscoder.h"
+#include "../MultiThreading/Semaphore.h"
+
 
 namespace Orthanc
 {
   class ORTHANC_PUBLIC DcmtkTranscoder : public IDicomTranscoder
   {
   private:
-    unsigned int  lossyQuality_;
-    
+    unsigned int  defaultLossyQuality_;
+    Semaphore maxConcurrentExecutionsSemaphore_;
+
     bool InplaceTranscode(DicomTransferSyntax& selectedSyntax /* out */,
                           std::string& failureReason /* out */,
                           DcmFileFormat& dicom,
                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                          bool allowNewSopInstanceUid);
+                          bool allowNewSopInstanceUid,
+                          unsigned int lossyQuality);
     
   public:
-    DcmtkTranscoder();
+    explicit DcmtkTranscoder(unsigned int maxConcurrentExecutions);
 
-    void SetLossyQuality(unsigned int quality);
+    void SetDefaultLossyQuality(unsigned int quality);
 
-    unsigned int GetLossyQuality() const;
+    unsigned int GetDefaultLossyQuality() const;
     
     static bool IsSupported(DicomTransferSyntax syntax);
 
@@ -60,5 +64,11 @@
                            DicomImage& source /* in, "GetParsed()" possibly modified */,
                            const std::set<DicomTransferSyntax>& allowedSyntaxes,
                            bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
+
+    virtual bool Transcode(DicomImage& target,
+                           DicomImage& source /* in, "GetParsed()" possibly modified */,
+                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                           bool allowNewSopInstanceUid,
+                           unsigned int lossyQuality) ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancFramework/Sources/DicomParsing/DicomDirWriter.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DicomDirWriter.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomParsing/DicomDirWriter.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DicomDirWriter.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomParsing/DicomModification.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DicomModification.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomParsing/DicomModification.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DicomModification.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -28,6 +28,7 @@
 #include "../Logging.h"
 #include "../OrthancException.h"
 #include "../Toolbox.h"
+#include "../SerializationToolbox.h"
 #include "FromDcmtkBridge.h"
 
 #include <boost/math/special_functions/round.hpp>
@@ -341,6 +342,31 @@
     }
   }
 
+  Json::Value DicomWebJsonVisitor::FormatDecimalString(double value, const std::string& originalString)
+  {
+    try
+    {
+      long long a = boost::math::llround<double>(value);
+
+      double d = fabs(value - static_cast<double>(a));
+
+      if (d <= std::numeric_limits<double>::epsilon() * 100.0)
+      {
+        return FormatInteger(a);  // if the decimal number is an integer, you can represent it as an integer  
+      }
+      else
+      {
+        return Json::Value(originalString);  // keep the original string to avoid rounding errors e.g, transforming "0.143" into 0.14299999999999
+      }
+    }
+    catch (boost::math::rounding_error&)
+    {
+      // Can occur if "long long" is too small to receive this value
+      // (e.g. infinity)
+      return Json::Value(originalString);
+    }
+  }
+
   DicomWebJsonVisitor::DicomWebJsonVisitor() :
     formatter_(NULL)
   {
@@ -677,14 +703,33 @@
                 case ValueRepresentation_DecimalString:
                 {
                   std::string t = Toolbox::StripSpaces(tokens[i]);
+                  boost::replace_all(t, ",", "."); // some invalid files uses "," instead of "."
+                  
+                  // remove invalid/useless trailing decimal separator
+                  if (t.size() > 0 && t[t.size()-1] == '.')
+                  {
+                    t.resize(t.size() -1);
+                  }
+
                   if (t.empty())
                   {
                     node[KEY_VALUE].append(Json::nullValue);
                   }
                   else
                   {
-                    double tmp = boost::lexical_cast<double>(t);
-                    node[KEY_VALUE].append(FormatDouble(tmp));
+                    // https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.3.html
+                    // DS values can be represented as String or Number in Json.
+                    // For IS, DS, SV and UV, a JSON String representation can be used to preserve the original format during transformation of the representation, or if needed to avoid losing precision of a decimal string.
+                    // Since 1.12.5, always use the string repesentation.  Before, decimal numbers were represented as double which led to loss of precision (e.g: 0.143 represented as 0.1429999999)
+                    double tmp;
+                    if (SerializationToolbox::ParseDouble(tmp, t)) // make sure that the string contains a valid decimal number
+                    {
+                      node[KEY_VALUE].append(t);
+                    }
+                    else
+                    {
+                      throw boost::bad_lexical_cast();
+                    }
                   }
 
                   break;
--- a/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -74,6 +74,8 @@
 
     static Json::Value FormatDouble(double value);
 
+    static Json::Value FormatDecimalString(double value, const std::string& originalString);
+
   public:
     DicomWebJsonVisitor();
 
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -1603,7 +1603,7 @@
       // buffer if its size was overestimated by (*)
       ob.flush();
 
-      size_t effectiveSize = static_cast<size_t>(ob.tell());
+      size_t effectiveSize = static_cast<size_t>(ob.filled());
       if (effectiveSize < buffer.size())
       {
         buffer.resize(effectiveSize);
@@ -1978,13 +1978,27 @@
       
         case EVR_SL:  // signed long
         {
-          ok = element.putSint32(boost::lexical_cast<Sint32>(*decoded)).good();
+          if (decoded->find('\\') != std::string::npos)
+          {
+            ok = element.putString(decoded->c_str()).good();
+          }
+          else
+          {
+            ok = element.putSint32(boost::lexical_cast<Sint32>(*decoded)).good();
+          }
           break;
         }
 
         case EVR_SS:  // signed short
         {
-          ok = element.putSint16(boost::lexical_cast<Sint16>(*decoded)).good();
+          if (decoded->find('\\') != std::string::npos)
+          {
+            ok = element.putString(decoded->c_str()).good();
+          }
+          else
+          {
+            ok = element.putSint16(boost::lexical_cast<Sint16>(*decoded)).good();
+          }
           break;
         }
 
@@ -2023,14 +2037,28 @@
 
         case EVR_US:  // unsigned short
         {
-          ok = element.putUint16(boost::lexical_cast<Uint16>(*decoded)).good();
+          if (decoded->find('\\') != std::string::npos)
+          {
+            ok = element.putString(decoded->c_str()).good();
+          }
+          else
+          {
+            ok = element.putUint16(boost::lexical_cast<Uint16>(*decoded)).good();
+          }
           break;
         }
 
         case EVR_FL:  // float single-precision
         case EVR_OF:  // other float (requires byte swapping)
         {
-          ok = element.putFloat32(boost::lexical_cast<float>(*decoded)).good();
+          if (decoded->find('\\') != std::string::npos)
+          {
+            ok = element.putString(decoded->c_str()).good();
+          }
+          else
+          {
+            ok = element.putFloat32(boost::lexical_cast<float>(*decoded)).good();
+          }
           break;
         }
 
@@ -2039,7 +2067,14 @@
         case EVR_OD:  // other double (requires byte-swapping)
 #endif
         {
-          ok = element.putFloat64(boost::lexical_cast<double>(*decoded)).good();
+          if (decoded->find('\\') != std::string::npos)
+          {
+            ok = element.putString(decoded->c_str()).good();
+          }
+          else
+          {
+            ok = element.putFloat64(boost::lexical_cast<double>(*decoded)).good();
+          }
           break;
         }
 
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge_TransferSyntaxes.impl.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge_TransferSyntaxes.impl.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -116,6 +116,12 @@
                            const std::set<DicomTransferSyntax>& allowedSyntaxes,
                            bool allowNewSopInstanceUid) = 0;
 
+    virtual bool Transcode(DicomImage& target,
+                           DicomImage& source /* in, "GetParsed()" possibly modified */,
+                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                           bool allowNewSopInstanceUid,
+                           unsigned int lossyQuality) = 0;
+
     static std::string GetSopInstanceUid(DcmFileFormat& dicom);
   };
 }
--- a/OrthancFramework/Sources/DicomParsing/ITagVisitor.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ITagVisitor.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -949,12 +949,12 @@
     {
       throw OrthancException(ErrorCode_NotImplemented,
                              "The built-in DCMTK decoder cannot decode some DICOM instance "
-                             "whose transfer syntax is: " + std::string(GetTransferSyntaxUid(s)));
+                             "whose transfer syntax is: " + std::string(GetTransferSyntaxUid(s)), false /* don't log here*/);
     }
     else
     {
       throw OrthancException(ErrorCode_NotImplemented,
-                             "The built-in DCMTK decoder cannot decode some DICOM instance");
+                             "The built-in DCMTK decoder cannot decode some DICOM instance", false /* don't log here*/);
     }
   }
 
--- a/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -56,6 +56,14 @@
 #endif
   }
     
+  bool MemoryBufferTranscoder::Transcode(DicomImage& target,
+                                         DicomImage& source,
+                                         const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                                         bool allowNewSopInstanceUid,
+                                         unsigned int lossyQualityNotUsed)
+  {
+    return Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid);
+  }
 
   bool MemoryBufferTranscoder::Transcode(DicomImage& target,
                                          DicomImage& source,
--- a/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -43,5 +43,11 @@
                            DicomImage& source,
                            const std::set<DicomTransferSyntax>& allowedSyntaxes,
                            bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
+
+    virtual bool Transcode(DicomImage& target /* out */,
+                           DicomImage& source,
+                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                           bool allowNewSopInstanceUid,
+                           unsigned int lossyQualityNotUsed) ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomDir.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomDir.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomDir.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomDir.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Endianness.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Endianness.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/EnumerationDictionary.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/EnumerationDictionary.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Enumerations.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Enumerations.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -186,6 +186,9 @@
       case ErrorCode_DuplicateResource:
         return "Duplicate resource";
 
+      case ErrorCode_IncompatibleConfigurations:
+        return "Your configuration file contains configuration that are mutually incompatible";
+
       case ErrorCode_SQLiteNotOpened:
         return "SQLite: The database is not opened";
 
@@ -220,7 +223,7 @@
         return "SQLite: Cannot step over a cached statement";
 
       case ErrorCode_SQLiteBindOutOfRange:
-        return "SQLite: Bing a value while out of range (serious error)";
+        return "SQLite: Bind a value while out of range (serious error)";
 
       case ErrorCode_SQLitePrepareStatement:
         return "SQLite: Cannot prepare a cached statement";
@@ -369,6 +372,9 @@
       case ErrorCode_NoCGetHandler:
         return "No request handler factory for DICOM C-GET SCP";
 
+      case ErrorCode_DicomGetUnavailable:
+        return "DicomUserConnection: The C-GET command is not supported by the remote SCP";
+
       case ErrorCode_UnsupportedMediaType:
         return "Unsupported media type";
 
@@ -2490,6 +2496,44 @@
     }
   }
 
+  RetrieveMethod StringToRetrieveMethod(const std::string& str)
+  {
+    if (str == "C-MOVE")
+    {
+      return RetrieveMethod_Move;
+    }
+    else if (str == "C-GET")
+    {
+      return RetrieveMethod_Get;
+    }
+    else if (str == "SystemDefault")
+    {
+      return RetrieveMethod_SystemDefault;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "RetrieveMethod can be \"C-MOVE\", \"C-GET\" or \"SystemDefault\": " + str);
+    }    
+  }
+
+  const char* EnumerationToString(RetrieveMethod method)
+  {
+    switch (method)
+    {
+      case RetrieveMethod_Get:
+        return "C-GET";
+
+      case RetrieveMethod_Move:
+        return "C-MOVE";
+
+      case RetrieveMethod_SystemDefault:
+        return "SystemDefault";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
 }
 
 
--- a/OrthancFramework/Sources/Enumerations.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Enumerations.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -172,6 +172,7 @@
     ErrorCode_MainDicomTagsMultiplyDefined = 44    /*!< A main DICOM Tag has been defined multiple times for the same resource level */,
     ErrorCode_ForbiddenAccess = 45    /*!< Access to a resource is forbidden */,
     ErrorCode_DuplicateResource = 46    /*!< Duplicate resource */,
+    ErrorCode_IncompatibleConfigurations = 47    /*!< Your configuration file contains configuration that are mutually incompatible */,
     ErrorCode_SQLiteNotOpened = 1000    /*!< SQLite: The database is not opened */,
     ErrorCode_SQLiteAlreadyOpened = 1001    /*!< SQLite: Connection is already open */,
     ErrorCode_SQLiteCannotOpen = 1002    /*!< SQLite: Unable to open the database */,
@@ -183,7 +184,7 @@
     ErrorCode_SQLiteFlush = 1008    /*!< SQLite: Unable to flush the database */,
     ErrorCode_SQLiteCannotRun = 1009    /*!< SQLite: Cannot run a cached statement */,
     ErrorCode_SQLiteCannotStep = 1010    /*!< SQLite: Cannot step over a cached statement */,
-    ErrorCode_SQLiteBindOutOfRange = 1011    /*!< SQLite: Bing a value while out of range (serious error) */,
+    ErrorCode_SQLiteBindOutOfRange = 1011    /*!< SQLite: Bind a value while out of range (serious error) */,
     ErrorCode_SQLitePrepareStatement = 1012    /*!< SQLite: Cannot prepare a cached statement */,
     ErrorCode_SQLiteTransactionAlreadyStarted = 1013    /*!< SQLite: Beginning the same transaction twice */,
     ErrorCode_SQLiteTransactionCommit = 1014    /*!< SQLite: Failure when committing the transaction */,
@@ -233,6 +234,7 @@
     ErrorCode_AlreadyExistingTag = 2042    /*!< Cannot override the value of a tag that already exists */,
     ErrorCode_NoStorageCommitmentHandler = 2043    /*!< No request handler factory for DICOM N-ACTION SCP (storage commitment) */,
     ErrorCode_NoCGetHandler = 2044    /*!< No request handler factory for DICOM C-GET SCP */,
+    ErrorCode_DicomGetUnavailable = 2045    /*!< DicomUserConnection: The C-GET command is not supported by the remote SCP */,
     ErrorCode_UnsupportedMediaType = 3000    /*!< Unsupported media type */,
     ErrorCode_START_PLUGINS = 1000000
   };
@@ -792,6 +794,14 @@
     ResourceType_Instance = 4
   };
 
+  enum RetrieveMethod                         // new in Orthanc 1.12.6
+  {
+    RetrieveMethod_Move = 1,
+    RetrieveMethod_Get = 2,
+
+    RetrieveMethod_SystemDefault = 65535
+  };
+
 
   ORTHANC_PUBLIC
   const char* EnumerationToString(ErrorCode code);
@@ -848,6 +858,9 @@
   const char* EnumerationToString(DicomToJsonFormat format);
 
   ORTHANC_PUBLIC
+  const char* EnumerationToString(RetrieveMethod method);
+
+  ORTHANC_PUBLIC
   Encoding StringToEncoding(const char* encoding);
 
   ORTHANC_PUBLIC
@@ -946,4 +959,7 @@
 
   ORTHANC_PUBLIC
   void GetAllDicomTransferSyntaxes(std::set<DicomTransferSyntax>& target);
+
+  ORTHANC_PUBLIC 
+  RetrieveMethod StringToRetrieveMethod(const std::string& str);
 }
--- a/OrthancFramework/Sources/Enumerations_TransferSyntaxes.impl.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Enumerations_TransferSyntaxes.impl.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/FileBuffer.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/FileBuffer.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/FileBuffer.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/FileBuffer.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/FileStorage/FileInfo.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/FileStorage/FileInfo.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/FileStorage/FileInfo.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/FileStorage/FileInfo.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -24,6 +24,7 @@
 
 #include "../PrecompiledHeaders.h"
 #include "FilesystemStorage.h"
+#include <boost/thread.hpp>
 
 // http://stackoverflow.com/questions/1576272/storing-large-number-of-files-in-file-system
 // http://stackoverflow.com/questions/446358/storing-a-large-number-of-images
@@ -135,26 +136,54 @@
     {
       // Extremely unlikely case: This Uuid has already been created
       // in the past.
-      throw OrthancException(ErrorCode_InternalError);
+      throw OrthancException(ErrorCode_InternalError, "This file UUID already exists");
     }
 
-    if (boost::filesystem::exists(path.parent_path()))
+    // In very unlikely cases, a thread could be deleting a
+    // directory while another thread needs it -> introduce 3 retries at 1 ms interval
+    int retryCount = 0;
+    const int maxRetryCount = 3;
+    
+    while (retryCount < maxRetryCount)
     {
-      if (!boost::filesystem::is_directory(path.parent_path()))
+      retryCount++;
+      if (retryCount > 1)
+      {
+        boost::this_thread::sleep(boost::posix_time::milliseconds(2 * retryCount + (rand() % 10)));
+        LOG(INFO) << "Retrying to create attachment \"" << uuid << "\" of \"" << GetDescriptionInternal(type) 
+                  << "\" type";
+      }
+
+      try 
+      {
+        boost::filesystem::create_directories(path.parent_path());  // the function ensures that the directory exists or throws
+      }
+      catch (boost::filesystem::filesystem_error& er)
       {
-        throw OrthancException(ErrorCode_DirectoryOverFile);
+        if (er.code() == boost::system::errc::file_exists  // the last element of the parent_path is a file
+          || er.code() == boost::system::errc::not_a_directory) // one of the element of the parent_path is not a directory 
+        {
+          throw OrthancException(ErrorCode_DirectoryOverFile, "One of the element of the path is a file");  // no need to retry this error
+        }
+
+        // ignore other errors and retry
+      }
+
+      try 
+      {
+        SystemToolbox::WriteFile(content, size, path.string(), fsyncOnWrite_);
+        
+        LOG(INFO) << "Created attachment \"" << uuid << "\" (" << timer.GetHumanTransferSpeed(true, size) << ")";
+        return;
+      }
+      catch (OrthancException& e)
+      {
+        if (retryCount >= maxRetryCount)
+        {
+          throw;
+        }
       }
     }
-    else
-    {
-      if (!boost::filesystem::create_directories(path.parent_path()))
-      {
-        throw OrthancException(ErrorCode_FileStorageCannotWrite);
-      }
-    }
-
-    SystemToolbox::WriteFile(content, size, path.string(), fsyncOnWrite_);
-    LOG(INFO) << "Created attachment \"" << uuid << "\" (" << timer.GetHumanTransferSpeed(true, size) << ")";
   }
 
 
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/FileStorage/IStorageArea.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/FileStorage/IStorageArea.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/FileStorage/MemoryStorageArea.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -28,26 +28,236 @@
 
 #include "../Logging.h"
 #include "../StringMemoryBuffer.h"
-#include "../Compatibility.h"
 #include "../Compression/ZlibCompressor.h"
 #include "../MetricsRegistry.h"
 #include "../OrthancException.h"
+#include "../SerializationToolbox.h"
 #include "../Toolbox.h"
 
 #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
 #  include "../HttpServer/HttpStreamTranscoder.h"
 #endif
 
+#include <boost/algorithm/string.hpp>
+
 
 static const std::string METRICS_CREATE_DURATION = "orthanc_storage_create_duration_ms";
 static const std::string METRICS_READ_DURATION = "orthanc_storage_read_duration_ms";
 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
 {
+  void StorageAccessor::Range::SanityCheck() const
+  {
+    if (hasStart_ && hasEnd_ && start_ > end_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+  StorageAccessor::Range::Range():
+    hasStart_(false),
+    start_(0),
+    hasEnd_(false),
+    end_(0)
+  {
+  }
+
+  void StorageAccessor::Range::SetStartInclusive(uint64_t start)
+  {
+    hasStart_ = true;
+    start_ = start;
+  }
+
+  void StorageAccessor::Range::SetEndInclusive(uint64_t end)
+  {
+    hasEnd_ = true;
+    end_ = end;
+  }
+
+  uint64_t StorageAccessor::Range::GetStartInclusive() const
+  {
+    if (!hasStart_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (hasEnd_ && start_ > end_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return start_;
+    }
+  }
+
+  uint64_t StorageAccessor::Range::GetEndInclusive() const
+  {
+    if (!hasEnd_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (hasStart_ && start_ > end_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return end_;
+    }
+  }
+
+  std::string StorageAccessor::Range::FormatHttpContentRange(uint64_t fullSize) const
+  {
+    SanityCheck();
+
+    if (fullSize == 0 ||
+        (hasStart_ && start_ >= fullSize) ||
+        (hasEnd_ && end_ >= fullSize))
+    {
+      throw OrthancException(ErrorCode_BadRange);
+    }
+
+    std::string s = "bytes ";
+
+    if (hasStart_)
+    {
+      s += boost::lexical_cast<std::string>(start_);
+    }
+    else
+    {
+      s += "0";
+    }
+
+    s += "-";
+
+    if (hasEnd_)
+    {
+      s += boost::lexical_cast<std::string>(end_);
+    }
+    else
+    {
+      s += boost::lexical_cast<std::string>(fullSize - 1);
+    }
+
+    return s + "/" + boost::lexical_cast<std::string>(fullSize);
+  }
+
+  void StorageAccessor::Range::Extract(std::string &target,
+                                       const std::string &source) const
+  {
+    SanityCheck();
+
+    if (hasStart_ && start_ >= source.size())
+    {
+      throw OrthancException(ErrorCode_BadRange);
+    }
+
+    if (hasEnd_ && end_ >= source.size())
+    {
+      throw OrthancException(ErrorCode_BadRange);
+    }
+
+    if (hasStart_ && hasEnd_)
+    {
+      target = source.substr(start_, end_ - start_ + 1);
+    }
+    else if (hasStart_)
+    {
+      target = source.substr(start_, source.size() - start_);
+    }
+    else if (hasEnd_)
+    {
+      target = source.substr(0, end_ + 1);
+    }
+    else
+    {
+      target = source;
+    }
+  }
+
+  uint64_t StorageAccessor::Range::GetContentLength(uint64_t fullSize) const
+  {
+    SanityCheck();
+
+    if (fullSize == 0)
+    {
+      throw OrthancException(ErrorCode_BadRange);
+    }
+
+    if (hasStart_ && start_ >= fullSize)
+    {
+      throw OrthancException(ErrorCode_BadRange);
+    }
+
+    if (hasEnd_ && end_ >= fullSize)
+    {
+      throw OrthancException(ErrorCode_BadRange);
+    }
+
+    if (hasStart_ && hasEnd_)
+    {
+      return end_ - start_ + 1;
+    }
+    else if (hasStart_)
+    {
+      return fullSize - start_;
+    }
+    else if (hasEnd_)
+    {
+      return end_ + 1;
+    }
+    else
+    {
+      return fullSize;
+    }
+  }
+
+  StorageAccessor::Range StorageAccessor::Range::ParseHttpRange(const std::string& s)
+  {
+    static const std::string BYTES = "bytes=";
+
+    if (!boost::starts_with(s, BYTES))
+    {
+      throw OrthancException(ErrorCode_BadRange);  // Range not satisfiable
+    }
+
+    std::vector<std::string> tokens;
+    Orthanc::Toolbox::TokenizeString(tokens, s.substr(BYTES.length()), '-');
+
+    if (tokens.size() != 2)
+    {
+      throw OrthancException(ErrorCode_BadRange);
+    }
+
+    Range range;
+
+    uint64_t tmp;
+    if (!tokens[0].empty())
+    {
+      if (SerializationToolbox::ParseUnsignedInteger64(tmp, tokens[0]))
+      {
+        range.SetStartInclusive(tmp);
+      }
+    }
+
+    if (!tokens[1].empty())
+    {
+      if (SerializationToolbox::ParseUnsignedInteger64(tmp, tokens[1]))
+      {
+        range.SetEndInclusive(tmp);
+      }
+    }
+
+    range.SanityCheck();
+    return range;
+  }
+
   class StorageAccessor::MetricsTimer : public boost::noncopyable
   {
   private:
@@ -208,10 +418,19 @@
 
       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);
       }
     }
   }
@@ -284,10 +503,19 @@
 
       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);
+      }
     }
   }
 
@@ -331,15 +559,6 @@
   }
 
 
-  void ReadStartRangeFromAreaInternal(std::string& target,
-                                      IStorageArea& area,
-                                      const std::string& fileUuid,
-                                      FileContentType contentType,
-                                      uint64_t end /* exclusive */)
-  {
-
-  }
-
   void StorageAccessor::ReadStartRange(std::string& target,
                                        const FileInfo& info,
                                        uint64_t end /* exclusive */)
@@ -353,24 +572,38 @@
       StorageCache::Accessor accessorStartRange(*cache_);
       if (!accessorStartRange.FetchStartRange(target, info.GetUuid(), info.GetContentType(), end))
       {
-        ReadStartRangeInternal(target, info, end);
-        accessorStartRange.AddStartRange(info.GetUuid(), info.GetContentType(), target);
-      }
-      else
-      {
+        // 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()))
         {
-          ReadWholeInternal(target, info);
-          accessorWhole.Add(info.GetUuid(), info.GetContentType(), target);
-        }
+          if (metrics_ != NULL)
+          {
+            metrics_->IncrementIntegerValue(METRICS_CACHE_MISS_COUNT, 1);
+          }
 
-        if (target.size() < end)
+          // 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
         {
-          throw OrthancException(ErrorCode_CorruptedFile);
+          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);
         }
-
-        target.resize(end);
+      }
+      else if (metrics_ != NULL)
+      {
+        metrics_->IncrementIntegerValue(METRICS_CACHE_HIT_COUNT, 1);
       }
     }
   }
@@ -396,6 +629,79 @@
   }
 
 
+  void StorageAccessor::ReadRange(std::string &target,
+                                  const FileInfo &info,
+                                  const Range &range,
+                                  bool uncompressIfNeeded)
+  {
+    if (uncompressIfNeeded &&
+        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())
+      {
+        throw OrthancException(ErrorCode_BadRange);
+      }
+
+      std::unique_ptr<IMemoryBuffer> buffer;
+
+      if (range.HasStart() &&
+          range.HasEnd())
+      {
+        buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), range.GetStartInclusive(), range.GetEndInclusive() + 1));
+      }
+      else if (range.HasStart())
+      {
+        buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), range.GetStartInclusive(), info.GetCompressedSize()));
+      }
+      else if (range.HasEnd())
+      {
+        buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, range.GetEndInclusive() + 1));
+      }
+      else
+      {
+        buffer.reset(area_.Read(info.GetUuid(), info.GetContentType()));
+      }
+
+      buffer->MoveToString(target);
+    }
+  }
+
+
 #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
   void StorageAccessor::SetupSender(BufferHttpSender& sender,
                                     const FileInfo& info,
@@ -430,9 +736,10 @@
 #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
   void StorageAccessor::AnswerFile(HttpOutput& output,
                                    const FileInfo& info,
-                                   MimeType mime)
+                                   MimeType mime,
+                                   const std::string& contentFilename)
   {
-    AnswerFile(output, info, EnumerationToString(mime));
+    AnswerFile(output, info, EnumerationToString(mime), contentFilename);
   }
 #endif
 
@@ -440,10 +747,12 @@
 #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
   void StorageAccessor::AnswerFile(HttpOutput& output,
                                    const FileInfo& info,
-                                   const std::string& mime)
+                                   const std::string& mime,
+                                   const std::string& contentFilename)
   {
     BufferHttpSender sender;
     SetupSender(sender, info, mime);
+    sender.SetContentFilename(contentFilename);
   
     HttpStreamTranscoder transcoder(sender, CompressionType_None); // since 1.11.2, the storage accessor only returns uncompressed buffers
     output.Answer(transcoder);
@@ -454,9 +763,10 @@
 #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
   void StorageAccessor::AnswerFile(RestApiOutput& output,
                                    const FileInfo& info,
-                                   MimeType mime)
+                                   MimeType mime,
+                                   const std::string& contentFilename)
   {
-    AnswerFile(output, info, EnumerationToString(mime));
+    AnswerFile(output, info, EnumerationToString(mime), contentFilename);
   }
 #endif
 
@@ -464,11 +774,13 @@
 #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
   void StorageAccessor::AnswerFile(RestApiOutput& output,
                                    const FileInfo& info,
-                                   const std::string& mime)
+                                   const std::string& mime,
+                                   const std::string& contentFilename)
   {
     BufferHttpSender sender;
     SetupSender(sender, info, mime);
-  
+    sender.SetContentFilename(contentFilename);
+
     HttpStreamTranscoder transcoder(sender, CompressionType_None); // since 1.11.2, the storage accessor only returns uncompressed buffers
     output.AnswerStream(transcoder);
   }
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -65,6 +65,48 @@
    **/
   class ORTHANC_PUBLIC StorageAccessor : boost::noncopyable
   {
+  public:
+    class ORTHANC_PUBLIC Range
+    {
+    private:
+      bool      hasStart_;
+      uint64_t  start_;
+      bool      hasEnd_;
+      uint64_t  end_;
+
+      void SanityCheck() const;
+
+    public:
+      Range();
+
+      void SetStartInclusive(uint64_t start);
+
+      void SetEndInclusive(uint64_t end);
+
+      bool HasStart() const
+      {
+        return hasStart_;
+      }
+
+      bool HasEnd() const
+      {
+        return hasEnd_;
+      }
+
+      uint64_t GetStartInclusive() const;
+
+      uint64_t GetEndInclusive() const;
+
+      std::string FormatHttpContentRange(uint64_t fullSize) const;
+
+      void Extract(std::string& target,
+                   const std::string& source) const;
+
+      uint64_t GetContentLength(uint64_t fullSize) const;
+
+      static Range ParseHttpRange(const std::string& s);
+    };
+
   private:
     class MetricsTimer;
 
@@ -117,22 +159,31 @@
 
     void Remove(const FileInfo& info);
 
+    void ReadRange(std::string& target,
+                   const FileInfo& info,
+                   const Range& range,
+                   bool uncompressIfNeeded);
+
 #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
     void AnswerFile(HttpOutput& output,
                     const FileInfo& info,
-                    MimeType mime);
+                    MimeType mime,
+                    const std::string& contentFilename);
 
     void AnswerFile(HttpOutput& output,
                     const FileInfo& info,
-                    const std::string& mime);
+                    const std::string& mime,
+                    const std::string& contentFilename);
 
     void AnswerFile(RestApiOutput& output,
                     const FileInfo& info,
-                    MimeType mime);
+                    MimeType mime,
+                    const std::string& contentFilename);
 
     void AnswerFile(RestApiOutput& output,
                     const FileInfo& info,
-                    const std::string& mime);
+                    const std::string& mime,
+                    const std::string& contentFilename);
 #endif
   private:
     void ReadStartRangeInternal(std::string& target,
--- a/OrthancFramework/Sources/FileStorage/StorageCache.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageCache.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/FileStorage/StorageCache.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageCache.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpClient.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpClient.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -860,7 +860,18 @@
 
     if (verifyPeers_)
     {
-      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CAINFO, caCertificates_.c_str()));
+#if defined(CURLSSLOPT_NATIVE_CA)   // from curl v 8.2.0     
+      if (caCertificates_.empty())  // use native CA store (equivalent to --ca-native)
+      {
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA));
+      }
+      else 
+#endif
+      {
+        // use provided CA file (equivalent to --cacert)
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CAINFO, caCertificates_.c_str()));
+      }
+      
       CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYHOST, 2));  // libcurl default is strict verifyhost
       CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYPEER, 1)); 
     }
@@ -1187,8 +1198,8 @@
     {
       if (httpsVerifyCertificates.empty())
       {
-        LOG(WARNING) << "No certificates are provided to validate peers, "
-                     << "set \"HttpsCACertificates\" if you need to do HTTPS requests";
+        LOG(WARNING) << "No certificates are provided to validate peers.  Orthanc will use the native CA store. "
+                     << "Set \"HttpsCACertificates\" if you need to do HTTPS requests and use custom CAs.";
       }
       else
       {
--- a/OrthancFramework/Sources/HttpClient.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpClient.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/BufferHttpSender.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/BufferHttpSender.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/BufferHttpSender.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/BufferHttpSender.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/CStringMatcher.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/CStringMatcher.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/CStringMatcher.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/CStringMatcher.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/FilesystemHttpSender.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/FilesystemHttpSender.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/FilesystemHttpSender.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/FilesystemHttpSender.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/HttpContentNegociation.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpContentNegociation.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/HttpContentNegociation.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpContentNegociation.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/HttpFileSender.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpFileSender.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/HttpFileSender.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpFileSender.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/HttpOutput.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpOutput.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -62,7 +62,8 @@
     contentPosition_(0),
     keepAlive_(isKeepAlive),
     keepAliveTimeout_(keepAliveTimeout),
-    hasXContentTypeOptions_(false)
+    hasXContentTypeOptions_(false),
+    hasContentType_(false)
   {
   }
 
@@ -105,6 +106,7 @@
 
   void HttpOutput::StateMachine::SetContentType(const char* contentType)
   {
+    hasContentType_ = true;
     AddHeader("Content-Type", contentType);
   }
 
@@ -380,7 +382,8 @@
     
     stateMachine_.SetHttpStatus(status);
 
-    if (messageSize > 0)
+    if (messageSize > 0 &&
+      !stateMachine_.HasContentType())
     {
       // Assume that the body always contains a textual description of the error
       stateMachine_.SetContentType("text/plain");
@@ -471,6 +474,10 @@
     return stateMachine_.GetState() == StateMachine::State_WritingMultipart;
   }
 
+  bool HttpOutput::IsWritingStream() const
+  {
+    return stateMachine_.GetState() == StateMachine::State_WritingStream;
+  }
   
   void HttpOutput::Answer(const void* buffer,
                           size_t length)
@@ -974,4 +981,21 @@
 
     stateMachine_.CloseStream();
   }
+
+  void HttpOutput::StartStream(const std::string& contentType)
+  {
+    stateMachine_.StartStream(contentType.c_str());
+  }
+
+  void HttpOutput::SendStreamItem(const void* data,
+                                  size_t size)
+  {
+    stateMachine_.SendStreamItem(data, size);
+  }
+
+  void HttpOutput::CloseStream()
+  {
+    stateMachine_.CloseStream();
+  }
+
 }
--- a/OrthancFramework/Sources/HttpServer/HttpOutput.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpOutput.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -66,6 +66,7 @@
       unsigned int keepAliveTimeout_;
       std::list<std::string> headers_;
       bool hasXContentTypeOptions_;
+      bool hasContentType_;
 
       std::string multipartBoundary_;
       std::string multipartContentType_;
@@ -125,6 +126,11 @@
                           size_t size);
 
       void CloseStream();
+
+      bool HasContentType() const
+      {
+        return hasContentType_;
+      }
     };
 
     StateMachine stateMachine_;
@@ -218,5 +224,14 @@
      * used to handle compression using "Content-Encoding".
      **/
     void AnswerWithoutBuffering(IHttpStreamAnswer& stream);
+
+    void StartStream(const std::string& contentType);
+
+    void SendStreamItem(const void* data,
+                        size_t size);
+
+    void CloseStream();
+
+    bool IsWritingStream() const;
   };
 }
--- a/OrthancFramework/Sources/HttpServer/HttpServer.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpServer.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -699,7 +699,7 @@
                             const HttpToolbox::Arguments& headers,
                             const HttpToolbox::GetArguments& argumentsGET)
   {
-    std::string overriden;
+    std::string overridden;
 
     // Check whether some PUT/DELETE faking is done
 
@@ -709,7 +709,7 @@
 
     if (methodOverride != headers.end())
     {
-      overriden = methodOverride->second;
+      overridden = methodOverride->second;
     }
     else if (!strcmp(request->request_method, "GET"))
     {
@@ -719,25 +719,25 @@
       {
         if (argumentsGET[i].first == "_method")
         {
-          overriden = argumentsGET[i].second;
+          overridden = argumentsGET[i].second;
           break;
         }
       }
     }
 
-    if (overriden.size() > 0)
+    if (overridden.size() > 0)
     {
       // A faking has been done within this request
-      Toolbox::ToUpperCase(overriden);
+      Toolbox::ToUpperCase(overridden);
 
-      CLOG(INFO, HTTP) << "HTTP method faking has been detected for " << overriden;
+      CLOG(INFO, HTTP) << "HTTP method faking has been detected for " << overridden;
 
-      if (overriden == "PUT")
+      if (overridden == "PUT")
       {
         method = HttpMethod_Put;
         return true;
       }
-      else if (overriden == "DELETE")
+      else if (overridden == "DELETE")
       {
         method = HttpMethod_Delete;
         return true;
--- a/OrthancFramework/Sources/HttpServer/HttpServer.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpServer.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/HttpToolbox.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpToolbox.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/HttpToolbox.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpToolbox.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/IHttpHandler.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/IHttpHandler.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/IHttpHandler.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/IHttpHandler.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/IHttpOutputStream.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/IHttpOutputStream.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/IHttpStreamAnswer.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/IHttpStreamAnswer.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/IIncomingHttpRequestFilter.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/IIncomingHttpRequestFilter.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/IWebDavBucket.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/IWebDavBucket.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/IWebDavBucket.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/IWebDavBucket.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/MultipartStreamReader.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/MultipartStreamReader.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/MultipartStreamReader.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/MultipartStreamReader.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/StringHttpOutput.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/StringHttpOutput.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/StringHttpOutput.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/StringHttpOutput.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/StringMatcher.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/StringMatcher.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/StringMatcher.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/StringMatcher.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/WebDavStorage.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/WebDavStorage.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/HttpServer/WebDavStorage.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/HttpServer/WebDavStorage.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/IDynamicObject.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/IDynamicObject.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/IMemoryBuffer.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/IMemoryBuffer.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/Font.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/Font.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/Font.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/Font.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/FontRegistry.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/FontRegistry.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/FontRegistry.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/FontRegistry.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/IImageWriter.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/IImageWriter.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/IImageWriter.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/IImageWriter.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/Image.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/Image.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/Image.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/Image.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/ImageAccessor.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/ImageAccessor.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -139,7 +139,7 @@
     return pitch_;
   }
 
-  unsigned int ImageAccessor::GetSize() const
+  size_t ImageAccessor::GetSize() const
   {
     return GetHeight() * GetPitch();
   }
@@ -165,7 +165,7 @@
   {
     if (buffer_ != NULL)
     {
-      return buffer_ + y * pitch_;
+      return buffer_ + static_cast<size_t>(y) * static_cast<size_t>(pitch_);
     }
     else
     {
@@ -184,7 +184,7 @@
 
     if (buffer_ != NULL)
     {
-      return buffer_ + y * pitch_;
+      return buffer_ + static_cast<size_t>(y) * static_cast<size_t>(pitch_);
     }
     else
     {
@@ -325,8 +325,8 @@
     else
     {
       uint8_t* p = (buffer_ + 
-                    y * pitch_ + 
-                    x * GetBytesPerPixel());
+                    static_cast<size_t>(y) * static_cast<size_t>(pitch_) +
+                    static_cast<size_t>(x) * static_cast<size_t>(GetBytesPerPixel()));
 
       if (readOnly_)
       {
--- a/OrthancFramework/Sources/Images/ImageAccessor.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/ImageAccessor.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -86,7 +86,7 @@
 
     unsigned int GetPitch() const;
 
-    unsigned int GetSize() const;
+    size_t GetSize() const;
 
     const void* GetConstBuffer() const;
 
--- a/OrthancFramework/Sources/Images/ImageBuffer.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/ImageBuffer.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -47,7 +47,7 @@
       */
 
       pitch_ = GetBytesPerPixel() * width_;
-      size_t size = pitch_ * height_;
+      size_t size = static_cast<size_t>(pitch_) * static_cast<size_t>(height_);
 
       if (size == 0)
       {
--- a/OrthancFramework/Sources/Images/ImageBuffer.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/ImageBuffer.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/ImageProcessing.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/ImageProcessing.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -2888,8 +2888,7 @@
     const unsigned int width = image.GetWidth();
     const unsigned int height = image.GetHeight();
     const unsigned int pitch = image.GetPitch();
-    uint8_t* buffer = reinterpret_cast<uint8_t*>(image.GetBuffer());
-        
+
     if (image.GetFormat() != PixelFormat_RGB24 ||
         pitch < 3 * width)
     {
@@ -2898,7 +2897,7 @@
 
     for (unsigned int y = 0; y < height; y++)
     {
-      uint8_t* p = buffer + y * pitch;
+      uint8_t* p = reinterpret_cast<uint8_t*>(image.GetRow(y));
           
       for (unsigned int x = 0; x < width; x++, p += 3)
       {
--- a/OrthancFramework/Sources/Images/ImageProcessing.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/ImageProcessing.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/ImageTraits.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/ImageTraits.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/JpegErrorManager.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/JpegErrorManager.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/JpegErrorManager.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/JpegErrorManager.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/JpegReader.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/JpegReader.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/JpegReader.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/JpegReader.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/JpegWriter.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/JpegWriter.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/JpegWriter.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/JpegWriter.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/NumpyWriter.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/NumpyWriter.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/NumpyWriter.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/NumpyWriter.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/PamReader.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/PamReader.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/PamReader.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/PamReader.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/PamWriter.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/PamWriter.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/PamWriter.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/PamWriter.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/PixelTraits.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/PixelTraits.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/PngReader.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/PngReader.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/PngReader.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/PngReader.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/PngWriter.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/PngWriter.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Images/PngWriter.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Images/PngWriter.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/JobsEngine/GenericJobUnserializer.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/GenericJobUnserializer.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/JobsEngine/GenericJobUnserializer.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/GenericJobUnserializer.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/JobsEngine/IJob.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/IJob.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -49,13 +49,18 @@
     // For pausing/canceling/ending jobs: This method must release allocated resources
     virtual void Stop(JobStopReason reason) = 0;
 
-    virtual float GetProgress() = 0;
+    virtual float GetProgress() const = 0;
 
-    virtual void GetJobType(std::string& target) = 0;
+    virtual bool NeedsProgressUpdateBetweenSteps() const // only for jobs whose progress is updated by outside events (like C-Move and C-Get)
+    {
+      return false;
+    }
+
+    virtual void GetJobType(std::string& target) const = 0;
     
-    virtual void GetPublicContent(Json::Value& value) = 0;
+    virtual void GetPublicContent(Json::Value& value) const = 0;
 
-    virtual bool Serialize(Json::Value& value) = 0;
+    virtual bool Serialize(Json::Value& value) const = 0;
 
     // This function can only be called if the job has reached its
     // "success" state
--- a/OrthancFramework/Sources/JobsEngine/IJobUnserializer.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/IJobUnserializer.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/JobsEngine/JobInfo.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobInfo.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -54,7 +54,8 @@
                    const JobStatus& status,
                    const boost::posix_time::ptime& creationTime,
                    const boost::posix_time::ptime& lastStateChangeTime,
-                   const boost::posix_time::time_duration& runtime) :
+                   const boost::posix_time::time_duration& runtime,
+                   const IJob& job) :
     id_(id),
     priority_(priority),
     state_(state),
@@ -68,11 +69,16 @@
     if (state_ == JobState_Running)
     {
       float ms = static_cast<float>(runtime_.total_milliseconds());
+      if (job.NeedsProgressUpdateBetweenSteps())
+      {
+        status_.UpdateProgress(job);
+      }
 
-      if (status_.GetProgress() > 0.01f &&
+      float progress = status_.GetProgress();
+
+      if (progress > 0.01f &&
           ms > 0.01f)
       {
-        float progress = status_.GetProgress();
         long long remaining = boost::math::llround(ms / progress * (1.0f - progress));
         eta_ = timestamp_ + boost::posix_time::milliseconds(remaining);
         hasEta_ = true;
--- a/OrthancFramework/Sources/JobsEngine/JobInfo.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobInfo.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -51,7 +51,8 @@
             const JobStatus& status,
             const boost::posix_time::ptime& creationTime,
             const boost::posix_time::ptime& lastStateChangeTime,
-            const boost::posix_time::time_duration& runtime) ORTHANC_LOCAL;
+            const boost::posix_time::time_duration& runtime,
+            const IJob& job) ORTHANC_LOCAL;
 
     JobInfo();
 
--- a/OrthancFramework/Sources/JobsEngine/JobStatus.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobStatus.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -41,7 +41,7 @@
   
   JobStatus::JobStatus(ErrorCode code,
                        const std::string& details,
-                       IJob& job) :
+                       const IJob& job) :
     errorCode_(code),
     progress_(job.GetProgress()),
     publicContent_(Json::objectValue),
--- a/OrthancFramework/Sources/JobsEngine/JobStatus.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobStatus.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -44,7 +44,7 @@
 
     JobStatus(ErrorCode code,
               const std::string& details,
-              IJob& job);
+              const IJob& job);
 
     ErrorCode GetErrorCode() const
     {
@@ -61,6 +61,11 @@
       return progress_;
     }
 
+    void UpdateProgress(const IJob& job)
+    {
+      progress_ = job.GetProgress();
+    }
+
     const std::string& GetJobType() const
     {
       return jobType_;
--- a/OrthancFramework/Sources/JobsEngine/JobStepResult.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobStepResult.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/JobsEngine/JobStepResult.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobStepResult.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/JobsEngine/JobsEngine.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobsEngine.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -132,7 +132,10 @@
 
       if (running.IsValid())
       {
-        CLOG(INFO, JOBS) << "Executing job with priority " << running.GetPriority()
+        std::string jobType;
+        running.GetJob().GetJobType(jobType);
+
+        CLOG(INFO, JOBS) << "Executing " << jobType << " job with priority " << running.GetPriority()
                          << " in worker thread " << workerIndex << ": " << running.GetId();
 
         while (engine->IsRunning())
--- a/OrthancFramework/Sources/JobsEngine/JobsEngine.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobsEngine.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -223,8 +223,14 @@
       lastStateChangeTime_ = time;
     }
 
-    const boost::posix_time::time_duration& GetRuntime() const
+    boost::posix_time::time_duration GetRuntime() const
     {
+      if (state_ == JobState_Running)
+      {
+        const boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time();
+        return now - lastStateChangeTime_;
+      }
+
       return runtime_;
     }
 
@@ -644,7 +650,8 @@
                        handler.GetLastStatus(),
                        handler.GetCreationTime(),
                        handler.GetLastStateChangeTime(),
-                       handler.GetRuntime());
+                       handler.GetRuntime(),
+                       handler.GetJob());
       return true;
     }
   }
@@ -792,7 +799,10 @@
         }
       }
 
-      LOG(INFO) << "New job submitted with priority " << priority << ": " << id;
+      std::string jobType;
+      handler->GetJob().GetJobType(jobType);
+
+      LOG(INFO) << "New " << jobType << " job submitted with priority " << priority << ": " << id;
 
       if (observer_ != NULL)
       {
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/JobsEngine/Operations/IJobOperation.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/IJobOperation.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/JobsEngine/Operations/IJobOperationValue.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/IJobOperationValue.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/JobsEngine/Operations/NullOperationValue.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/NullOperationValue.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/JobsEngine/Operations/NullOperationValue.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/NullOperationValue.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -398,7 +398,7 @@
   }
 
 
-  float SequenceOfOperationsJob::GetProgress()
+  float SequenceOfOperationsJob::GetProgress() const
   {
     boost::mutex::scoped_lock lock(mutex_);
       
@@ -406,13 +406,13 @@
             static_cast<float>(operations_.size() + 1));
   }
 
-  void SequenceOfOperationsJob::GetJobType(std::string& target)
+  void SequenceOfOperationsJob::GetJobType(std::string& target) const
   {
     target = "SequenceOfOperations";
   }
 
 
-  void SequenceOfOperationsJob::GetPublicContent(Json::Value& value)
+  void SequenceOfOperationsJob::GetPublicContent(Json::Value& value) const
   {
     boost::mutex::scoped_lock lock(mutex_);
 
@@ -421,7 +421,7 @@
   }
 
 
-  bool SequenceOfOperationsJob::Serialize(Json::Value& value)
+  bool SequenceOfOperationsJob::Serialize(Json::Value& value) const
   {
     boost::mutex::scoped_lock lock(mutex_);
 
--- a/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -54,7 +54,7 @@
 
     std::string                       description_;
     bool                              done_;
-    boost::mutex                      mutex_;
+    mutable boost::mutex              mutex_;
     std::vector<Operation*>           operations_;
     size_t                            current_;
     boost::condition_variable         operationAdded_;
@@ -117,13 +117,13 @@
 
     virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
 
-    virtual float GetProgress() ORTHANC_OVERRIDE;
+    virtual float GetProgress() const ORTHANC_OVERRIDE;
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE;
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE;
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE;
 
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
--- a/OrthancFramework/Sources/JobsEngine/Operations/StringOperationValue.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/StringOperationValue.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/JobsEngine/Operations/StringOperationValue.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/StringOperationValue.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -137,7 +137,7 @@
   }
 
 
-  float SetOfCommandsJob::GetProgress()
+  float SetOfCommandsJob::GetProgress() const
   {
     if (commands_.empty())
     {
@@ -237,13 +237,13 @@
   static const char* KEY_COMMANDS = "Commands";
 
   
-  void SetOfCommandsJob::GetPublicContent(Json::Value& value)
+  void SetOfCommandsJob::GetPublicContent(Json::Value& value) const
   {
     value[KEY_DESCRIPTION] = GetDescription();
   }    
 
 
-  bool SetOfCommandsJob::Serialize(Json::Value& target)
+  bool SetOfCommandsJob::Serialize(Json::Value& target) const
   {
     target = Json::objectValue;
 
--- a/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -92,7 +92,7 @@
     
     virtual void Start() ORTHANC_OVERRIDE;
     
-    virtual float GetProgress() ORTHANC_OVERRIDE;
+    virtual float GetProgress() const ORTHANC_OVERRIDE;
 
     bool IsStarted() const;
 
@@ -100,9 +100,9 @@
       
     virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE;
     
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
     
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
 
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
--- a/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -209,7 +209,7 @@
   static const char* KEY_FAILED_INSTANCES = "FailedInstances";
   static const char* KEY_PARENT_RESOURCES = "ParentResources";
 
-  void SetOfInstancesJob::GetPublicContent(Json::Value& target)
+  void SetOfInstancesJob::GetPublicContent(Json::Value& target) const
   {
     SetOfCommandsJob::GetPublicContent(target);
     target["InstancesCount"] = static_cast<uint32_t>(GetInstancesCount());
@@ -222,7 +222,7 @@
   }
 
 
-  bool SetOfInstancesJob::Serialize(Json::Value& target) 
+  bool SetOfInstancesJob::Serialize(Json::Value& target) const 
   {
     if (SetOfCommandsJob::Serialize(target))
     {
--- a/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -77,8 +77,8 @@
 
     virtual void Reset() ORTHANC_OVERRIDE;
 
-    virtual void GetPublicContent(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& target) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancFramework/Sources/Logging.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Logging.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Logging.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Logging.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Lua/LuaContext.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Lua/LuaContext.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Lua/LuaContext.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Lua/LuaContext.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -124,5 +124,17 @@
                                       lua_State* state,
                                       int top,
                                       bool keyToLowerCase);
+
+#if ORTHANC_ENABLE_CURL == 1
+    void SetHttpsVerifyPeers(bool verify)
+    {
+      httpClient_.SetHttpsVerifyPeers(verify);
+    }
+
+    bool IsHttpsVerifyPeers() const
+    {
+      return httpClient_.IsHttpsVerifyPeers();
+    }
+#endif
   };
 }
--- a/OrthancFramework/Sources/Lua/LuaFunctionCall.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Lua/LuaFunctionCall.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Lua/LuaFunctionCall.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Lua/LuaFunctionCall.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/MallocMemoryBuffer.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/MallocMemoryBuffer.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/MallocMemoryBuffer.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/MallocMemoryBuffer.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/MetricsRegistry.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/MetricsRegistry.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -139,6 +139,8 @@
 
       void Increment(const T& delta)
       {
+        time_ = GetNow();
+
         if (hasValue_)
         {
           value_ += delta;
@@ -146,6 +148,7 @@
         else
         {
           value_ = delta;
+          hasValue_ = true;
         }
       }
 
--- a/OrthancFramework/Sources/MetricsRegistry.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/MetricsRegistry.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/MultiThreading/IRunnableBySteps.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/MultiThreading/IRunnableBySteps.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/MultiThreading/Mutex.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/MultiThreading/Mutex.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/MultiThreading/Semaphore.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/MultiThreading/Semaphore.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/MultiThreading/Semaphore.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/MultiThreading/Semaphore.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/MultiThreading/SharedMessageQueue.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/MultiThreading/SharedMessageQueue.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/MultiThreading/SharedMessageQueue.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/MultiThreading/SharedMessageQueue.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/OrthancException.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/OrthancException.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/OrthancException.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/OrthancException.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/OrthancFramework.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/OrthancFramework.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/OrthancFramework.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/OrthancFramework.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Pkcs11.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Pkcs11.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Pkcs11.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Pkcs11.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/PrecompiledHeaders.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/PrecompiledHeaders.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/PrecompiledHeaders.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/PrecompiledHeaders.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/RestApi/RestApi.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApi.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/RestApi/RestApi.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApi.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/RestApi/RestApiCall.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiCall.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/RestApi/RestApiCall.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiCall.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/RestApi/RestApiDeleteCall.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiDeleteCall.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/RestApi/RestApiGetCall.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiGetCall.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -69,4 +69,31 @@
                              name + "\", found: " + found->second);
     }
   }
+
+  uint32_t RestApiGetCall::GetUnsignedInteger32Argument(const std::string& name,
+                                                        uint32_t defaultValue) const
+  {
+    HttpToolbox::Arguments::const_iterator found = getArguments_.find(name);
+
+    uint32_t value;
+    
+    if (found == getArguments_.end())
+    {
+      return defaultValue;
+    }
+    else if (found->second.empty())
+    {
+      return true;
+    }
+    else if (SerializationToolbox::ParseUnsignedInteger32(value, found->second))
+    {
+      return value;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange, "Expected a Unsigned Int for GET argument \"" +
+                             name + "\", found: " + found->second);
+    }
+  }
+
 }
--- a/OrthancFramework/Sources/RestApi/RestApiGetCall.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiGetCall.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -65,7 +65,10 @@
 
     bool GetBooleanArgument(const std::string& name,
                             bool defaultValue) const;
-    
+
+    uint32_t GetUnsignedInteger32Argument(const std::string& name,      
+                                          uint32_t defaultValue) const;
+  
     virtual bool ParseJsonRequest(Json::Value& result) const ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancFramework/Sources/RestApi/RestApiHierarchy.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiHierarchy.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/RestApi/RestApiHierarchy.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiHierarchy.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/RestApi/RestApiOutput.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiOutput.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/RestApi/RestApiOutput.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiOutput.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/RestApi/RestApiPath.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiPath.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/RestApi/RestApiPath.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiPath.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/RestApi/RestApiPostCall.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiPostCall.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/RestApi/RestApiPutCall.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiPutCall.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/SQLite/Connection.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/Connection.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -2,10 +2,10 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
+ * Medical Physics Department, University Hospital of Liege, Belgium
  * Copyright (C) 2017-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
@@ -19,7 +19,7 @@
  * copyright notice, this list of conditions and the following disclaimer
  * in the documentation and/or other materials provided with the
  * distribution.
- *    * Neither the name of Google Inc., the name of the CHU of Liege,
+ *    * Neither the name of Google Inc., the name of the University Hospital of Liege,
  * nor the names of its contributors may be used to endorse or promote
  * products derived from this software without specific prior written
  * permission.
--- a/OrthancFramework/Sources/SQLite/Connection.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/Connection.h	Tue Mar 18 13:37:18 2025 +0100
@@ -2,10 +2,10 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
+ * Medical Physics Department, University Hospital of Liege, Belgium
  * Copyright (C) 2017-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
@@ -19,7 +19,7 @@
  * copyright notice, this list of conditions and the following disclaimer
  * in the documentation and/or other materials provided with the
  * distribution.
- *    * Neither the name of Google Inc., the name of the CHU of Liege,
+ *    * Neither the name of Google Inc., the name of the University Hospital of Liege,
  * nor the names of its contributors may be used to endorse or promote
  * products derived from this software without specific prior written
  * permission.
@@ -57,6 +57,7 @@
 #endif
 
 #define SQLITE_FROM_HERE ::Orthanc::SQLite::StatementId(__ORTHANC_FILE__, __LINE__)
+#define SQLITE_FROM_HERE_DYNAMIC(sql) ::Orthanc::SQLite::StatementId(__ORTHANC_FILE__, __LINE__, sql)
 
 namespace Orthanc
 {
--- a/OrthancFramework/Sources/SQLite/FunctionContext.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/FunctionContext.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -2,10 +2,10 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
+ * Medical Physics Department, University Hospital of Liege, Belgium
  * Copyright (C) 2017-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions are
@@ -17,7 +17,7 @@
  * copyright notice, this list of conditions and the following disclaimer
  * in the documentation and/or other materials provided with the
  * distribution.
- *    * Neither the name of the CHU of Liege, nor the names of its
+ *    * Neither the name of the University Hospital of Liege, nor the names of its
  * contributors may be used to endorse or promote products derived
  * from this software without specific prior written permission.
  *
--- a/OrthancFramework/Sources/SQLite/FunctionContext.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/FunctionContext.h	Tue Mar 18 13:37:18 2025 +0100
@@ -2,10 +2,10 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
+ * Medical Physics Department, University Hospital of Liege, Belgium
  * Copyright (C) 2017-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions are
@@ -17,7 +17,7 @@
  * copyright notice, this list of conditions and the following disclaimer
  * in the documentation and/or other materials provided with the
  * distribution.
- *    * Neither the name of the CHU of Liege, nor the names of its
+ *    * Neither the name of the University Hospital of Liege, nor the names of its
  * contributors may be used to endorse or promote products derived
  * from this software without specific prior written permission.
  *
--- a/OrthancFramework/Sources/SQLite/IScalarFunction.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/IScalarFunction.h	Tue Mar 18 13:37:18 2025 +0100
@@ -2,10 +2,10 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
+ * Medical Physics Department, University Hospital of Liege, Belgium
  * Copyright (C) 2017-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions are
@@ -17,7 +17,7 @@
  * copyright notice, this list of conditions and the following disclaimer
  * in the documentation and/or other materials provided with the
  * distribution.
- *    * Neither the name of the CHU of Liege, nor the names of its
+ *    * Neither the name of the University Hospital of Liege, nor the names of its
  * contributors may be used to endorse or promote products derived
  * from this software without specific prior written permission.
  *
--- a/OrthancFramework/Sources/SQLite/ITransaction.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/ITransaction.h	Tue Mar 18 13:37:18 2025 +0100
@@ -2,10 +2,10 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
+ * Medical Physics Department, University Hospital of Liege, Belgium
  * Copyright (C) 2017-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
@@ -19,7 +19,7 @@
  * copyright notice, this list of conditions and the following disclaimer
  * in the documentation and/or other materials provided with the
  * distribution.
- *    * Neither the name of Google Inc., the name of the CHU of Liege,
+ *    * Neither the name of Google Inc., the name of the University Hospital of Liege,
  * nor the names of its contributors may be used to endorse or promote
  * products derived from this software without specific prior written
  * permission.
--- a/OrthancFramework/Sources/SQLite/NonCopyable.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/NonCopyable.h	Tue Mar 18 13:37:18 2025 +0100
@@ -2,10 +2,10 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
+ * Medical Physics Department, University Hospital of Liege, Belgium
  * Copyright (C) 2017-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions are
@@ -17,7 +17,7 @@
  * copyright notice, this list of conditions and the following disclaimer
  * in the documentation and/or other materials provided with the
  * distribution.
- *    * Neither the name of Google Inc., the name of the CHU of Liege,
+ *    * Neither the name of Google Inc., the name of the University Hospital of Liege,
  * nor the names of its contributors may be used to endorse or promote
  * products derived from this software without specific prior written
  * permission.
--- a/OrthancFramework/Sources/SQLite/OrthancSQLiteException.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/OrthancSQLiteException.h	Tue Mar 18 13:37:18 2025 +0100
@@ -2,10 +2,10 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
+ * Medical Physics Department, University Hospital of Liege, Belgium
  * Copyright (C) 2017-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
@@ -19,7 +19,7 @@
  * copyright notice, this list of conditions and the following disclaimer
  * in the documentation and/or other materials provided with the
  * distribution.
- *    * Neither the name of Google Inc., the name of the CHU of Liege,
+ *    * Neither the name of Google Inc., the name of the University Hospital of Liege,
  * nor the names of its contributors may be used to endorse or promote
  * products derived from this software without specific prior written
  * permission.
@@ -129,7 +129,7 @@
             return "SQLite: Cannot step over a cached statement";
 
           case ErrorCode_SQLiteBindOutOfRange:
-            return "SQLite: Bing a value while out of range (serious error)";
+            return "SQLite: Bind a value while out of range (serious error)";
 
           case ErrorCode_SQLitePrepareStatement:
             return "SQLite: Cannot prepare a cached statement";
--- a/OrthancFramework/Sources/SQLite/SQLiteTypes.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/SQLiteTypes.h	Tue Mar 18 13:37:18 2025 +0100
@@ -2,10 +2,10 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
+ * Medical Physics Department, University Hospital of Liege, Belgium
  * Copyright (C) 2017-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions are
@@ -17,7 +17,7 @@
  * copyright notice, this list of conditions and the following disclaimer
  * in the documentation and/or other materials provided with the
  * distribution.
- *    * Neither the name of the CHU of Liege, nor the names of its
+ *    * Neither the name of the University Hospital of Liege, nor the names of its
  * contributors may be used to endorse or promote products derived
  * from this software without specific prior written permission.
  *
--- a/OrthancFramework/Sources/SQLite/Statement.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/Statement.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -2,10 +2,10 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
+ * Medical Physics Department, University Hospital of Liege, Belgium
  * Copyright (C) 2017-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
@@ -19,7 +19,7 @@
  * copyright notice, this list of conditions and the following disclaimer
  * in the documentation and/or other materials provided with the
  * distribution.
- *    * Neither the name of Google Inc., the name of the CHU of Liege,
+ *    * Neither the name of Google Inc., the name of the University Hospital of Liege,
  * nor the names of its contributors may be used to endorse or promote
  * products derived from this software without specific prior written
  * permission.
--- a/OrthancFramework/Sources/SQLite/Statement.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/Statement.h	Tue Mar 18 13:37:18 2025 +0100
@@ -2,10 +2,10 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
+ * Medical Physics Department, University Hospital of Liege, Belgium
  * Copyright (C) 2017-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
@@ -19,7 +19,7 @@
  * copyright notice, this list of conditions and the following disclaimer
  * in the documentation and/or other materials provided with the
  * distribution.
- *    * Neither the name of Google Inc., the name of the CHU of Liege,
+ *    * Neither the name of Google Inc., the name of the University Hospital of Liege,
  * nor the names of its contributors may be used to endorse or promote
  * products derived from this software without specific prior written
  * permission.
--- a/OrthancFramework/Sources/SQLite/StatementId.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/StatementId.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -2,10 +2,10 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
+ * Medical Physics Department, University Hospital of Liege, Belgium
  * Copyright (C) 2017-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
@@ -19,7 +19,7 @@
  * copyright notice, this list of conditions and the following disclaimer
  * in the documentation and/or other materials provided with the
  * distribution.
- *    * Neither the name of Google Inc., the name of the CHU of Liege,
+ *    * Neither the name of Google Inc., the name of the University Hospital of Liege,
  * nor the names of its contributors may be used to endorse or promote
  * products derived from this software without specific prior written
  * permission.
@@ -57,12 +57,24 @@
     {
     }
 
+    Orthanc::SQLite::StatementId::StatementId(const char *file,
+                                              int line,
+                                              const std::string& statement) :
+      file_(file),
+      line_(line),
+      statement_(statement)
+    {
+    }
+
     bool StatementId::operator< (const StatementId& other) const
     {
       if (line_ != other.line_)
         return line_ < other.line_;
 
-      return strcmp(file_, other.file_) < 0;
+      if (strcmp(file_, other.file_) < 0)
+        return true;
+
+      return statement_ < other.statement_;
     }
   }
 }
--- a/OrthancFramework/Sources/SQLite/StatementId.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/StatementId.h	Tue Mar 18 13:37:18 2025 +0100
@@ -2,10 +2,10 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
+ * Medical Physics Department, University Hospital of Liege, Belgium
  * Copyright (C) 2017-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
@@ -19,7 +19,7 @@
  * copyright notice, this list of conditions and the following disclaimer
  * in the documentation and/or other materials provided with the
  * distribution.
- *    * Neither the name of Google Inc., the name of the CHU of Liege,
+ *    * Neither the name of Google Inc., the name of the University Hospital of Liege,
  * nor the names of its contributors may be used to endorse or promote
  * products derived from this software without specific prior written
  * permission.
@@ -55,6 +55,7 @@
     private:
       const char* file_;
       int line_;
+      std::string statement_;
 
       StatementId(); // Forbidden
 
@@ -62,6 +63,10 @@
       StatementId(const char* file,
                   int line);
 
+      StatementId(const char* file,
+                  int line,
+                  const std::string& statement);
+
       bool operator< (const StatementId& other) const;
     };
   }
--- a/OrthancFramework/Sources/SQLite/StatementReference.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/StatementReference.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -2,10 +2,10 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
+ * Medical Physics Department, University Hospital of Liege, Belgium
  * Copyright (C) 2017-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
@@ -19,7 +19,7 @@
  * copyright notice, this list of conditions and the following disclaimer
  * in the documentation and/or other materials provided with the
  * distribution.
- *    * Neither the name of Google Inc., the name of the CHU of Liege,
+ *    * Neither the name of Google Inc., the name of the University Hospital of Liege,
  * nor the names of its contributors may be used to endorse or promote
  * products derived from this software without specific prior written
  * permission.
--- a/OrthancFramework/Sources/SQLite/StatementReference.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/StatementReference.h	Tue Mar 18 13:37:18 2025 +0100
@@ -2,10 +2,10 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
+ * Medical Physics Department, University Hospital of Liege, Belgium
  * Copyright (C) 2017-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
@@ -19,7 +19,7 @@
  * copyright notice, this list of conditions and the following disclaimer
  * in the documentation and/or other materials provided with the
  * distribution.
- *    * Neither the name of Google Inc., the name of the CHU of Liege,
+ *    * Neither the name of Google Inc., the name of the University Hospital of Liege,
  * nor the names of its contributors may be used to endorse or promote
  * products derived from this software without specific prior written
  * permission.
--- a/OrthancFramework/Sources/SQLite/Transaction.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/Transaction.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -2,10 +2,10 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
+ * Medical Physics Department, University Hospital of Liege, Belgium
  * Copyright (C) 2017-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
@@ -19,7 +19,7 @@
  * copyright notice, this list of conditions and the following disclaimer
  * in the documentation and/or other materials provided with the
  * distribution.
- *    * Neither the name of Google Inc., the name of the CHU of Liege,
+ *    * Neither the name of Google Inc., the name of the University Hospital of Liege,
  * nor the names of its contributors may be used to endorse or promote
  * products derived from this software without specific prior written
  * permission.
--- a/OrthancFramework/Sources/SQLite/Transaction.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/Transaction.h	Tue Mar 18 13:37:18 2025 +0100
@@ -2,10 +2,10 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
+ * Medical Physics Department, University Hospital of Liege, Belgium
  * Copyright (C) 2017-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
@@ -19,7 +19,7 @@
  * copyright notice, this list of conditions and the following disclaimer
  * in the documentation and/or other materials provided with the
  * distribution.
- *    * Neither the name of Google Inc., the name of the CHU of Liege,
+ *    * Neither the name of Google Inc., the name of the University Hospital of Liege,
  * nor the names of its contributors may be used to endorse or promote
  * products derived from this software without specific prior written
  * permission.
--- a/OrthancFramework/Sources/SerializationToolbox.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/SerializationToolbox.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/SerializationToolbox.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/SerializationToolbox.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/SharedLibrary.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/SharedLibrary.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/SharedLibrary.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/SharedLibrary.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/StringMemoryBuffer.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/StringMemoryBuffer.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/StringMemoryBuffer.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/StringMemoryBuffer.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/SystemToolbox.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/SystemToolbox.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/SystemToolbox.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/SystemToolbox.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/TemporaryFile.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/TemporaryFile.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/TemporaryFile.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/TemporaryFile.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/Toolbox.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Toolbox.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -801,7 +801,6 @@
     return result;
   }
 
-
   void Toolbox::ComputeSHA1(std::string& result,
                             const void* data,
                             size_t size)
@@ -813,11 +812,31 @@
       sha1.process_bytes(data, size);
     }
 
-    unsigned int digest[5];
+#if BOOST_VERSION >= 108600
+    unsigned char digest[20];
 
     // Sanity check for the memory layout: A SHA-1 digest is 160 bits wide
-    assert(sizeof(unsigned int) == 4 && sizeof(digest) == (160 / 8)); 
-    
+    assert(sizeof(digest) == (160 / 8));
+    assert(sizeof(boost::uuids::detail::sha1::digest_type) == 20);
+
+    // From Boost 1.86, digest_type is "unsigned char[20]" while it was "unsigned int[5]"" in previous versions.
+    // Always perform the cast even if it is useless for Boost < 1.86
+    sha1.get_digest(digest);
+
+    result.resize(8 * 5 + 4);
+    sprintf(&result[0], "%02x%02x%02x%02x-%02x%02x%02x%02x-%02x%02x%02x%02x-%02x%02x%02x%02x-%02x%02x%02x%02x",
+            digest[0], digest[1], digest[2], digest[3],
+            digest[4], digest[5], digest[6], digest[7],
+            digest[8], digest[9], digest[10], digest[11],
+            digest[12], digest[13], digest[14], digest[15],
+            digest[16], digest[17], digest[18], digest[19]);
+
+#else
+    unsigned int digest[5];
+    // Sanity check for the memory layout: A SHA-1 digest is 160 bits wide
+    assert(sizeof(unsigned int) == 4 && sizeof(digest) == (160 / 8));
+    assert(sizeof(boost::uuids::detail::sha1::digest_type) == 20);
+
     sha1.get_digest(digest);
 
     result.resize(8 * 5 + 4);
@@ -827,6 +846,9 @@
             digest[2],
             digest[3],
             digest[4]);
+
+#endif
+
   }
 
   void Toolbox::ComputeSHA1(std::string& result,
@@ -1885,7 +1907,7 @@
 
   std::string Toolbox::GenerateUuid()
   {
-#ifdef WIN32
+#ifdef _WIN32
     UUID uuid;
     UuidCreate ( &uuid );
 
--- a/OrthancFramework/Sources/Toolbox.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/Toolbox.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -262,7 +262,7 @@
       }
     }
 
-    // returns true if all element of 'needles' are found in 'haystack'
+    // returns the elements that are both in a and b
     template <typename T> static void GetIntersection(std::set<T>& target, const std::set<T>& a, const std::set<T>& b)
     {
       target.clear();
--- a/OrthancFramework/Sources/WebServiceParameters.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/WebServiceParameters.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/Sources/WebServiceParameters.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/Sources/WebServiceParameters.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/UnitTestsSources/CMakeLists.txt	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/UnitTestsSources/CMakeLists.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancFramework/UnitTestsSources/DicomMapTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/UnitTestsSources/DicomMapTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -148,10 +148,10 @@
 
   TEST_F(DicomMapMainTagsTests, Signatures)
   {
-    std::string defaultPatientSignature = DicomMap::GetDefaultMainDicomTagsSignature(ResourceType_Patient);
-    std::string defaultStudySignature = DicomMap::GetDefaultMainDicomTagsSignature(ResourceType_Study);
-    std::string defaultSeriesSignature = DicomMap::GetDefaultMainDicomTagsSignature(ResourceType_Series);
-    std::string defaultInstanceSignature = DicomMap::GetDefaultMainDicomTagsSignature(ResourceType_Instance);
+    std::string defaultPatientSignature = DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType_Patient);
+    std::string defaultStudySignature = DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType_Study);
+    std::string defaultSeriesSignature = DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType_Series);
+    std::string defaultInstanceSignature = DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType_Instance);
 
     ASSERT_NE(defaultInstanceSignature, defaultPatientSignature);
     ASSERT_NE(defaultSeriesSignature, defaultStudySignature);
@@ -162,11 +162,11 @@
     std::string seriesSignature = DicomMap::GetMainDicomTagsSignature(ResourceType_Series);
     std::string instanceSignature = DicomMap::GetMainDicomTagsSignature(ResourceType_Instance);
 
-    // at start, default and current signature should be equal
-    ASSERT_EQ(defaultPatientSignature, patientSignature);
-    ASSERT_EQ(defaultStudySignature, studySignature);
-    ASSERT_EQ(defaultSeriesSignature, seriesSignature);
-    ASSERT_EQ(defaultInstanceSignature, instanceSignature);
+    // // at start, default and current signature should be equal  !! This is not true anymore since we have added new MainDicomTags in 1.12.5
+    // ASSERT_EQ(defaultPatientSignature, patientSignature);
+    // ASSERT_EQ(defaultStudySignature, studySignature);
+    // ASSERT_EQ(defaultSeriesSignature, seriesSignature);
+    // ASSERT_EQ(defaultInstanceSignature, instanceSignature);
 
     DicomMap::AddMainDicomTag(DICOM_TAG_BITS_ALLOCATED, ResourceType_Instance);
     instanceSignature = DicomMap::GetMainDicomTagsSignature(ResourceType_Instance);
@@ -266,6 +266,7 @@
     if (level == ResourceType_Study &&
         (*it == DicomTag(0x0008, 0x0080) ||  /* InstitutionName, from Visit identification module, related to Visit */
          *it == DicomTag(0x0032, 0x1032) ||  /* RequestingPhysician, from Imaging Service Request module, related to Study */
+         *it == DicomTag(0x0008, 0x0201) ||  /* TimezoneOffsetFromUTC */
          *it == DicomTag(0x0032, 0x1060)))   /* RequestedProcedureDescription, from Requested Procedure module, related to Study */
     {
       ok = true;
@@ -284,6 +285,7 @@
          *it == DicomTag(0x0054, 0x0101) ||  /* NumberOfTimeSlices, from PET Series module */
          *it == DicomTag(0x0054, 0x1000) ||  /* SeriesType, from PET Series module */
          *it == DicomTag(0x0018, 0x1400) ||  /* AcquisitionDeviceProcessingDescription, from CR/X-Ray/DX/WholeSlideMicro Image (SIMPLIFICATION => Series) */
+         *it == DicomTag(0x0008, 0x0201) ||  /* TimezoneOffsetFromUTC */
          *it == DicomTag(0x0018, 0x0010)))   /* ContrastBolusAgent, from Contrast/Bolus module (SIMPLIFICATION => Series) */
     {
       ok = true;
@@ -804,6 +806,7 @@
   dicom.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "SB1^SB2^SB3^SB4^SB5");
   dicom.ReplacePlainString(DICOM_TAG_IMAGE_ORIENTATION_PATIENT, "1\\2.3\\4");
   dicom.ReplacePlainString(DICOM_TAG_IMAGE_POSITION_PATIENT, "");
+  dicom.ReplacePlainString(DICOM_TAG_PIXEL_SPACING, "0,143\\0,143");  // seen in https://discourse.orthanc-server.org/t/dicomwebplugin-does-not-return-series-metadata-properly/5195
 
   DicomWebJsonVisitor visitor;
   dicom.Apply(visitor);
@@ -815,10 +818,10 @@
     ASSERT_EQ(EnumerationToString(ValueRepresentation_DecimalString), tag["vr"].asString());
     ASSERT_EQ(2u, tag.getMemberNames().size());
     ASSERT_EQ(3u, value.size());
-    ASSERT_EQ(Json::realValue, value[1].type());
-    ASSERT_FLOAT_EQ(1.0f, value[0].asFloat());
-    ASSERT_FLOAT_EQ(2.3f, value[1].asFloat());
-    ASSERT_FLOAT_EQ(4.0f, value[2].asFloat());
+    ASSERT_EQ(Json::stringValue, value[1].type());  // since Orthanc 1.12.5, this is now stored as a string
+    ASSERT_EQ("1", value[0].asString());
+    ASSERT_EQ("2.3", value[1].asString());
+    ASSERT_EQ("4", value[2].asString());
   }
 
   {
@@ -827,13 +830,23 @@
     ASSERT_EQ(1u, tag.getMemberNames().size());
   }
 
+  {
+    const Json::Value& tag = visitor.GetResult() ["00280030"];  // PixelSpacing
+    const Json::Value& value = tag["Value"];
+
+    ASSERT_EQ(EnumerationToString(ValueRepresentation_DecimalString), tag["vr"].asString());
+    ASSERT_EQ(2u, value.size());
+    ASSERT_EQ("0.143", value[0].asString());
+    ASSERT_EQ("0.143", value[1].asString());
+  }
+
   std::string xml;
   visitor.FormatXml(xml);
 
   {
     DicomMap m;
     m.FromDicomWeb(visitor.GetResult());
-    ASSERT_EQ(3u, m.GetSize());
+    ASSERT_EQ(4u, m.GetSize());
 
     std::string s;
     ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, false));
@@ -869,12 +882,12 @@
     ASSERT_EQ(EnumerationToString(ValueRepresentation_DecimalString), tag["vr"].asString());
     ASSERT_EQ(2u, tag.getMemberNames().size());
     ASSERT_EQ(4u, value.size());
-    ASSERT_EQ(Json::realValue, value[0].type());
+    ASSERT_EQ(Json::stringValue, value[0].type());
     ASSERT_EQ(Json::nullValue, value[1].type());
     ASSERT_EQ(Json::nullValue, value[2].type());
-    ASSERT_EQ(Json::realValue, value[3].type());
-    ASSERT_FLOAT_EQ(1.5f, value[0].asFloat());
-    ASSERT_FLOAT_EQ(2.5f, value[3].asFloat());
+    ASSERT_EQ(Json::stringValue, value[3].type());
+    ASSERT_EQ("1.5", value[0].asString());
+    ASSERT_EQ("2.5", value[3].asString());
   }
 
   std::string xml;
@@ -912,8 +925,8 @@
   target.FromDicomWeb(visitor.GetResult());
 
   ASSERT_EQ("DS", visitor.GetResult() ["00280030"]["vr"].asString());
-  ASSERT_FLOAT_EQ(1.5f, visitor.GetResult() ["00280030"]["Value"][0].asFloat());
-  ASSERT_FLOAT_EQ(1.3f, visitor.GetResult() ["00280030"]["Value"][1].asFloat());
+  ASSERT_EQ("1.5", visitor.GetResult() ["00280030"]["Value"][0].asString());
+  ASSERT_EQ("1.3", visitor.GetResult() ["00280030"]["Value"][1].asString());
 
   std::string s;
   ASSERT_TRUE(target.LookupStringValue(s, DICOM_TAG_PIXEL_SPACING, false));
@@ -1085,7 +1098,7 @@
     std::set<DicomTag> tags;
     m.GetTags(tags);
 
-    // This corresponds to the values of DEFAULT_PATIENT_MAIN_DICOM_TAGS
+    // This corresponds to the values of DEFAULT_1_11_PATIENT_MAIN_DICOM_TAGS
     ASSERT_EQ(5u, tags.size());
     ASSERT_EQ("", m.GetStringValue(DICOM_TAG_PATIENT_ID, "nope", false));
 
--- a/OrthancFramework/UnitTestsSources/FileStorageTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -37,6 +37,7 @@
 #include "../Sources/Logging.h"
 #include "../Sources/OrthancException.h"
 #include "../Sources/Toolbox.h"
+#include "../Sources/SystemToolbox.h"
 
 #include <ctype.h>
 
@@ -88,6 +89,48 @@
   ASSERT_EQ(s.GetSize(uid), data.size());
 }
 
+TEST(FilesystemStorage, FileWithSameNameAsTopDirectory)
+{
+  FilesystemStorage s("UnitTestsStorageTop");
+  s.Clear();
+
+  std::vector<uint8_t> data;
+  StringToVector(data, Toolbox::GenerateUuid());
+
+  SystemToolbox::WriteFile("toto", "UnitTestsStorageTop/12");
+  ASSERT_THROW(s.Create("12345678-1234-1234-1234-1234567890ab", &data[0], data.size(), FileContentType_Unknown), OrthancException);
+  s.Clear();
+}
+
+TEST(FilesystemStorage, FileWithSameNameAsChildDirectory)
+{
+  FilesystemStorage s("UnitTestsStorageChild");
+  s.Clear();
+
+  std::vector<uint8_t> data;
+  StringToVector(data, Toolbox::GenerateUuid());
+
+  SystemToolbox::MakeDirectory("UnitTestsStorageChild/12");
+  SystemToolbox::WriteFile("toto", "UnitTestsStorageChild/12/34");
+  ASSERT_THROW(s.Create("12345678-1234-1234-1234-1234567890ab", &data[0], data.size(), FileContentType_Unknown), OrthancException);
+  s.Clear();
+}
+
+TEST(FilesystemStorage, FileAlreadyExists)
+{
+  FilesystemStorage s("UnitTestsStorageFileAlreadyExists");
+  s.Clear();
+
+  std::vector<uint8_t> data;
+  StringToVector(data, Toolbox::GenerateUuid());
+
+  SystemToolbox::MakeDirectory("UnitTestsStorageFileAlreadyExists/12/34");
+  SystemToolbox::WriteFile("toto", "UnitTestsStorageFileAlreadyExists/12/34/12345678-1234-1234-1234-1234567890ab");
+  ASSERT_THROW(s.Create("12345678-1234-1234-1234-1234567890ab", &data[0], data.size(), FileContentType_Unknown), OrthancException);
+  s.Clear();
+}
+
+
 TEST(FilesystemStorage, EndToEnd)
 {
   FilesystemStorage s("UnitTestsStorage");
@@ -193,3 +236,92 @@
   ASSERT_THROW(accessor.Read(r, uncompressedInfo.GetUuid(), FileContentType_Unknown), OrthancException);
   */
 }
+
+
+TEST(StorageAccessor, Range)
+{
+  {
+    StorageAccessor::Range range;
+    ASSERT_FALSE(range.HasStart());
+    ASSERT_FALSE(range.HasEnd());
+    ASSERT_THROW(range.GetStartInclusive(), OrthancException);
+    ASSERT_THROW(range.GetEndInclusive(), OrthancException);
+    ASSERT_EQ("bytes 0-99/100", range.FormatHttpContentRange(100));
+    ASSERT_EQ("bytes 0-0/1", range.FormatHttpContentRange(1));
+    ASSERT_THROW(range.FormatHttpContentRange(0), OrthancException);
+    ASSERT_EQ(100u, range.GetContentLength(100));
+    ASSERT_EQ(1u, range.GetContentLength(1));
+    ASSERT_THROW(range.GetContentLength(0), OrthancException);
+
+    range.SetStartInclusive(10);
+    ASSERT_TRUE(range.HasStart());
+    ASSERT_FALSE(range.HasEnd());
+    ASSERT_EQ(10u, range.GetStartInclusive());
+    ASSERT_THROW(range.GetEndInclusive(), OrthancException);
+    ASSERT_EQ("bytes 10-99/100", range.FormatHttpContentRange(100));
+    ASSERT_EQ("bytes 10-10/11", range.FormatHttpContentRange(11));
+    ASSERT_THROW(range.FormatHttpContentRange(10), OrthancException);
+    ASSERT_EQ(90u, range.GetContentLength(100));
+    ASSERT_EQ(1u, range.GetContentLength(11));
+    ASSERT_THROW(range.GetContentLength(10), OrthancException);
+
+    range.SetEndInclusive(30);
+    ASSERT_TRUE(range.HasStart());
+    ASSERT_TRUE(range.HasEnd());
+    ASSERT_EQ(10u, range.GetStartInclusive());
+    ASSERT_EQ(30u, range.GetEndInclusive());
+    ASSERT_EQ("bytes 10-30/100", range.FormatHttpContentRange(100));
+    ASSERT_EQ("bytes 10-30/31", range.FormatHttpContentRange(31));
+    ASSERT_THROW(range.FormatHttpContentRange(30), OrthancException);
+    ASSERT_EQ(21u, range.GetContentLength(100));
+    ASSERT_EQ(21u, range.GetContentLength(31));
+    ASSERT_THROW(range.GetContentLength(30), OrthancException);
+  }
+
+  {
+    StorageAccessor::Range range;
+    range.SetEndInclusive(20);
+    ASSERT_FALSE(range.HasStart());
+    ASSERT_TRUE(range.HasEnd());
+    ASSERT_THROW(range.GetStartInclusive(), OrthancException);
+    ASSERT_EQ(20u, range.GetEndInclusive());
+    ASSERT_EQ("bytes 0-20/100", range.FormatHttpContentRange(100));
+    ASSERT_EQ("bytes 0-20/21", range.FormatHttpContentRange(21));
+    ASSERT_THROW(range.FormatHttpContentRange(20), OrthancException);
+    ASSERT_EQ(21u, range.GetContentLength(100));
+    ASSERT_EQ(21u, range.GetContentLength(21));
+    ASSERT_THROW(range.GetContentLength(20), OrthancException);
+  }
+
+  {
+    StorageAccessor::Range range = StorageAccessor::Range::ParseHttpRange("bytes=1-30");
+    ASSERT_TRUE(range.HasStart());
+    ASSERT_TRUE(range.HasEnd());
+    ASSERT_EQ(1u, range.GetStartInclusive());
+    ASSERT_EQ(30u, range.GetEndInclusive());
+    ASSERT_EQ("bytes 1-30/100", range.FormatHttpContentRange(100));
+  }
+
+  ASSERT_THROW(StorageAccessor::Range::ParseHttpRange("bytes="), OrthancException);
+  ASSERT_THROW(StorageAccessor::Range::ParseHttpRange("bytes=-1-30"), OrthancException);
+  ASSERT_THROW(StorageAccessor::Range::ParseHttpRange("bytes=100-30"), OrthancException);
+
+  ASSERT_EQ("bytes 0-99/100", StorageAccessor::Range::ParseHttpRange("bytes=-").FormatHttpContentRange(100));
+  ASSERT_EQ("bytes 0-10/100", StorageAccessor::Range::ParseHttpRange("bytes=-10").FormatHttpContentRange(100));
+  ASSERT_EQ("bytes 10-99/100", StorageAccessor::Range::ParseHttpRange("bytes=10-").FormatHttpContentRange(100));
+
+  {
+    std::string s;
+    StorageAccessor::Range::ParseHttpRange("bytes=1-2").Extract(s, "Hello");
+    ASSERT_EQ("el", s);
+    StorageAccessor::Range::ParseHttpRange("bytes=-2").Extract(s, "Hello");
+    ASSERT_EQ("Hel", s);
+    StorageAccessor::Range::ParseHttpRange("bytes=3-").Extract(s, "Hello");
+    ASSERT_EQ("lo", s);
+    StorageAccessor::Range::ParseHttpRange("bytes=-").Extract(s, "Hello");
+    ASSERT_EQ("Hello", s);
+    StorageAccessor::Range::ParseHttpRange("bytes=4-").Extract(s, "Hello");
+    ASSERT_EQ("o", s);
+    ASSERT_THROW(StorageAccessor::Range::ParseHttpRange("bytes=5-").Extract(s, "Hello"), OrthancException);
+  }
+}
\ No newline at end of file
--- a/OrthancFramework/UnitTestsSources/FrameworkTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/UnitTestsSources/FrameworkTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -2043,7 +2043,7 @@
   ASSERT_EQ("DA", visitor.GetResult() ["00080012"]["vr"].asString());
   ASSERT_EQ("DA", visitor.GetResult() ["00080012"]["Value"][0].asString());
   ASSERT_EQ("DS", visitor.GetResult() ["00101020"]["vr"].asString());
-  ASSERT_FLOAT_EQ(42.0f, visitor.GetResult() ["00101020"]["Value"][0].asFloat());
+  ASSERT_EQ("42", visitor.GetResult() ["00101020"]["Value"][0].asString());
   ASSERT_EQ("DT", visitor.GetResult() ["0008002A"]["vr"].asString());
   ASSERT_EQ("DT", visitor.GetResult() ["0008002A"]["Value"][0].asString());
   ASSERT_EQ("FL", visitor.GetResult() ["00109431"]["vr"].asString());
@@ -3092,6 +3092,21 @@
 }
 
 
+TEST(ParsedDicomFile, MultipleFloatValue)
+{
+  // from https://discourse.orthanc-server.org/t/qido-includefield-with-sequences/4746/6
+  Json::Value v = Json::objectValue;
+  v["4010,1001"][0]["4010,1004"] = "30\\20\\10";
+  std::unique_ptr<ParsedDicomFile> dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, ""));
+  ASSERT_TRUE(dicom->HasTag(Orthanc::DicomTag(0x4010, 0x1001)));
+
+  DicomMap m;
+  ASSERT_TRUE(dicom->LookupSequenceItem(m, DicomPath(DicomTag(0x4010, 0x1001)), 0));
+  ASSERT_EQ(1u, m.GetSize());
+  std::string value = m.GetStringValue(DicomTag(0x4010, 0x1004), "", false);
+  ASSERT_EQ("30\\20\\10", value);
+}
+
 
 TEST(ParsedDicomFile, ImageInformation)
 {
@@ -3546,7 +3561,7 @@
   scu.SetCommonClassesProposed(false);
   scu.SetRetiredBigEndianProposed(true);
 
-  DcmtkTranscoder transcoder;
+  DcmtkTranscoder transcoder(1);
 
   for (int j = 0; j < 2; j++)
   {
@@ -3603,7 +3618,7 @@
   DicomTransferSyntax sourceSyntax;
   ASSERT_TRUE(FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, *toto));
 
-  DcmtkTranscoder transcoder;
+  DcmtkTranscoder transcoder(1);
 
   for (int i = 0; i <= DicomTransferSyntax_XML; i++)
   {
--- a/OrthancFramework/UnitTestsSources/GithubCACertificates.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/UnitTestsSources/GithubCACertificates.h	Tue Mar 18 13:37:18 2025 +0100
@@ -1,30 +1,37 @@
 #define GITHUB_CERTIFICATES  \
 "-----BEGIN CERTIFICATE-----\n"  \
-"MIIEyDCCA7CgAwIBAgIQDPW9BitWAvR6uFAsI8zwZjANBgkqhkiG9w0BAQsFADBh\n"  \
-"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n"  \
-"d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\n"  \
-"MjAeFw0yMTAzMzAwMDAwMDBaFw0zMTAzMjkyMzU5NTlaMFkxCzAJBgNVBAYTAlVT\n"  \
-"MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxMzAxBgNVBAMTKkRpZ2lDZXJ0IEdsb2Jh\n"  \
-"bCBHMiBUTFMgUlNBIFNIQTI1NiAyMDIwIENBMTCCASIwDQYJKoZIhvcNAQEBBQAD\n"  \
-"ggEPADCCAQoCggEBAMz3EGJPprtjb+2QUlbFbSd7ehJWivH0+dbn4Y+9lavyYEEV\n"  \
-"cNsSAPonCrVXOFt9slGTcZUOakGUWzUb+nv6u8W+JDD+Vu/E832X4xT1FE3LpxDy\n"  \
-"FuqrIvAxIhFhaZAmunjZlx/jfWardUSVc8is/+9dCopZQ+GssjoP80j812s3wWPc\n"  \
-"3kbW20X+fSP9kOhRBx5Ro1/tSUZUfyyIxfQTnJcVPAPooTncaQwywa8WV0yUR0J8\n"  \
-"osicfebUTVSvQpmowQTCd5zWSOTOEeAqgJnwQ3DPP3Zr0UxJqyRewg2C/Uaoq2yT\n"  \
-"zGJSQnWS+Jr6Xl6ysGHlHx+5fwmY6D36g39HaaECAwEAAaOCAYIwggF+MBIGA1Ud\n"  \
-"EwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFHSFgMBmx9833s+9KTeqAx2+7c0XMB8G\n"  \
-"A1UdIwQYMBaAFE4iVCAYlebjbuYP+vq5Eu0GF485MA4GA1UdDwEB/wQEAwIBhjAd\n"  \
-"BgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwdgYIKwYBBQUHAQEEajBoMCQG\n"  \
-"CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQAYIKwYBBQUHMAKG\n"  \
-"NGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RH\n"  \
-"Mi5jcnQwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2NybDMuZGlnaWNlcnQuY29t\n"  \
-"L0RpZ2lDZXJ0R2xvYmFsUm9vdEcyLmNybDA9BgNVHSAENjA0MAsGCWCGSAGG/WwC\n"  \
-"ATAHBgVngQwBATAIBgZngQwBAgEwCAYGZ4EMAQICMAgGBmeBDAECAzANBgkqhkiG\n"  \
-"9w0BAQsFAAOCAQEAkPFwyyiXaZd8dP3A+iZ7U6utzWX9upwGnIrXWkOH7U1MVl+t\n"  \
-"wcW1BSAuWdH/SvWgKtiwla3JLko716f2b4gp/DA/JIS7w7d7kwcsr4drdjPtAFVS\n"  \
-"slme5LnQ89/nD/7d+MS5EHKBCQRfz5eeLjJ1js+aWNJXMX43AYGyZm0pGrFmCW3R\n"  \
-"bpD0ufovARTFXFZkAdl9h6g4U5+LXUZtXMYnhIHUfoyMo5tS58aI7Dd8KvvwVVo4\n"  \
-"chDYABPPTHPbqjc1qCmBaZx2vN4Ye5DUys/vZwP9BFohFrH/6j/f3IL16/RZkiMN\n"  \
-"JCqVJUzKoZHm1Lesh3Sz8W2jmdv51b2EQJ8HmA==\n"  \
+"MIIGEzCCA/ugAwIBAgIQfVtRJrR2uhHbdBYLvFMNpzANBgkqhkiG9w0BAQwFADCB\n"  \
+"iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl\n"  \
+"cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV\n"  \
+"BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTgx\n"  \
+"MTAyMDAwMDAwWhcNMzAxMjMxMjM1OTU5WjCBjzELMAkGA1UEBhMCR0IxGzAZBgNV\n"  \
+"BAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEYMBYGA1UE\n"  \
+"ChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQDEy5TZWN0aWdvIFJTQSBEb21haW4g\n"  \
+"VmFsaWRhdGlvbiBTZWN1cmUgU2VydmVyIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC\n"  \
+"AQ8AMIIBCgKCAQEA1nMz1tc8INAA0hdFuNY+B6I/x0HuMjDJsGz99J/LEpgPLT+N\n"  \
+"TQEMgg8Xf2Iu6bhIefsWg06t1zIlk7cHv7lQP6lMw0Aq6Tn/2YHKHxYyQdqAJrkj\n"  \
+"eocgHuP/IJo8lURvh3UGkEC0MpMWCRAIIz7S3YcPb11RFGoKacVPAXJpz9OTTG0E\n"  \
+"oKMbgn6xmrntxZ7FN3ifmgg0+1YuWMQJDgZkW7w33PGfKGioVrCSo1yfu4iYCBsk\n"  \
+"Haswha6vsC6eep3BwEIc4gLw6uBK0u+QDrTBQBbwb4VCSmT3pDCg/r8uoydajotY\n"  \
+"uK3DGReEY+1vVv2Dy2A0xHS+5p3b4eTlygxfFQIDAQABo4IBbjCCAWowHwYDVR0j\n"  \
+"BBgwFoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFI2MXsRUrYrhd+mb\n"  \
+"+ZsF4bgBjWHhMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0G\n"  \
+"A1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNVHSAEFDASMAYGBFUdIAAw\n"  \
+"CAYGZ4EMAQIBMFAGA1UdHwRJMEcwRaBDoEGGP2h0dHA6Ly9jcmwudXNlcnRydXN0\n"  \
+"LmNvbS9VU0VSVHJ1c3RSU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDB2Bggr\n"  \
+"BgEFBQcBAQRqMGgwPwYIKwYBBQUHMAKGM2h0dHA6Ly9jcnQudXNlcnRydXN0LmNv\n"  \
+"bS9VU0VSVHJ1c3RSU0FBZGRUcnVzdENBLmNydDAlBggrBgEFBQcwAYYZaHR0cDov\n"  \
+"L29jc3AudXNlcnRydXN0LmNvbTANBgkqhkiG9w0BAQwFAAOCAgEAMr9hvQ5Iw0/H\n"  \
+"ukdN+Jx4GQHcEx2Ab/zDcLRSmjEzmldS+zGea6TvVKqJjUAXaPgREHzSyrHxVYbH\n"  \
+"7rM2kYb2OVG/Rr8PoLq0935JxCo2F57kaDl6r5ROVm+yezu/Coa9zcV3HAO4OLGi\n"  \
+"H19+24rcRki2aArPsrW04jTkZ6k4Zgle0rj8nSg6F0AnwnJOKf0hPHzPE/uWLMUx\n"  \
+"RP0T7dWbqWlod3zu4f+k+TY4CFM5ooQ0nBnzvg6s1SQ36yOoeNDT5++SR2RiOSLv\n"  \
+"xvcRviKFxmZEJCaOEDKNyJOuB56DPi/Z+fVGjmO+wea03KbNIaiGCpXZLoUmGv38\n"  \
+"sbZXQm2V0TP2ORQGgkE49Y9Y3IBbpNV9lXj9p5v//cWoaasm56ekBYdbqbe4oyAL\n"  \
+"l6lFhd2zi+WJN44pDfwGF/Y4QA5C5BIG+3vzxhFoYt/jmPQT2BVPi7Fp2RBgvGQq\n"  \
+"6jG35LWjOhSbJuMLe/0CjraZwTiXWTb2qHSihrZe68Zk6s+go/lunrotEbaGmAhY\n"  \
+"LcmsJWTyXnW0OMGuf1pGg+pRyrbxmRE1a6Vqe8YAsOf4vmSyrcjC8azjUeqkk+B5\n"  \
+"yOGBQMkKW+ESPMFgKuOXwIlCypTPRpgSabuY0MLTDXJLR27lk8QyKGOHQ+SwMj4K\n"  \
+"00u/I5sUKUErmgQfky3xxzlIPK1aEn8=\n"  \
 "-----END CERTIFICATE-----\n"  \
 "\n" 
--- a/OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/UnitTestsSources/ImageTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/UnitTestsSources/ImageTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/UnitTestsSources/JobsTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/UnitTestsSources/JobsTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -103,24 +103,24 @@
     {
     }
 
-    virtual float GetProgress() ORTHANC_OVERRIDE
+    virtual float GetProgress() const ORTHANC_OVERRIDE
     {
       return static_cast<float>(count_) / static_cast<float>(steps_ - 1);
     }
 
-    virtual void GetJobType(std::string& type) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& type) const ORTHANC_OVERRIDE
     {
       type = "DummyJob";
     }
 
-    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE
+    virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE
     {
       value = Json::objectValue;
       value["Type"] = "DummyJob";
       return true;
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE
     {
       value["hello"] = "world";
     }
@@ -199,7 +199,7 @@
     {
     }
 
-    virtual void GetJobType(std::string& s) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& s) const ORTHANC_OVERRIDE
     {
       s = "DummyInstancesJob";
     }
@@ -783,7 +783,7 @@
 
 
 static bool CheckIdempotentSerialization(IJobUnserializer& unserializer,
-                                         IJob& job)
+                                         const IJob& job)
 {
   Json::Value a = 42;
   
@@ -809,7 +809,7 @@
 
 
 static bool CheckIdempotentSetOfInstances(IJobUnserializer& unserializer,
-                                          SetOfInstancesJob& job)
+                                          const SetOfInstancesJob& job)
 {
   Json::Value a = 42;
   
--- a/OrthancFramework/UnitTestsSources/JpegLosslessTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/UnitTestsSources/JpegLosslessTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/UnitTestsSources/LoggingTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/UnitTestsSources/LoggingTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/UnitTestsSources/LuaTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/UnitTestsSources/LuaTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/UnitTestsSources/MemoryCacheTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/UnitTestsSources/MemoryCacheTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/UnitTestsSources/RestApiTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/UnitTestsSources/RestApiTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -73,19 +73,24 @@
   ASSERT_TRUE(c.IsVerbose());
   c.SetVerbose(false);
   ASSERT_FALSE(c.IsVerbose());
+  ASSERT_TRUE(c.IsRedirectionFollowed());
+  c.SetRedirectionFollowed(false);
+  ASSERT_FALSE(c.IsRedirectionFollowed());
 
 #if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1
-  // The "http://www.orthanc-server.com/downloads/third-party/" does
-  // not automatically redirect to HTTPS, so we cas use it even if the
-  // OpenSSL/HTTPS support is disabled in curl
-  const std::string BASE = "http://www.orthanc-server.com/downloads/third-party/";
+  // The "http://httpbin.org/get" URL does not automatically redirect
+  // to HTTPS, so we can use it even if the OpenSSL/HTTPS support is
+  // disabled in curl
 
+  const std::string URL = "http://httpbin.org/get";
+  
   Json::Value v;
-  c.SetUrl(BASE + "Product.json");
+  c.SetUrl(URL);
 
   c.Apply(v);
   ASSERT_TRUE(v.type() == Json::objectValue);
-  ASSERT_TRUE(v.isMember("Description"));
+  ASSERT_TRUE(v.isMember("url"));
+  ASSERT_EQ(URL, v["url"].asString());
 #endif
 }
 #endif
@@ -94,9 +99,9 @@
 #if (UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1) && (ORTHANC_ENABLE_SSL == 1) && (ORTHANC_SANDBOXED != 1)
 
 /**
-   The HTTPS CA certificates for BitBucket were extracted as follows:
+   The HTTPS CA certificates for Github were extracted as follows:
    
-   (1) We retrieve the URI of the root CA of BitBucket:
+   (1) We retrieve the URI of the root CA of Github:
 
    # echo | openssl s_client -servername raw.githubusercontent.com -connect raw.githubusercontent.com:443 2>/dev/null | openssl x509 -text | grep "CA Issuers"
 
@@ -104,7 +109,7 @@
    macro that can be used by libcurl:
 
    # cd UnitTestsSources
-   # python2 ../Resources/RetrieveCACertificates.py GITHUB_CERTIFICATES http://cacerts.digicert.com/DigiCertGlobalG2TLSRSASHA2562020CA1-1.crt > GithubCACertificates.h
+   # python2 ../Resources/RetrieveCACertificates.py GITHUB_CERTIFICATES http://crt.sectigo.com/SectigoRSADomainValidationSecureServerCA.crt > GithubCACertificates.h
 **/
 
 #include "GithubCACertificates.h"
--- a/OrthancFramework/UnitTestsSources/SQLiteChromiumTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/UnitTestsSources/SQLiteChromiumTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/UnitTestsSources/SQLiteTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/UnitTestsSources/SQLiteTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/UnitTestsSources/SharedLibraryUnitTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/UnitTestsSources/SharedLibraryUnitTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/UnitTestsSources/StreamTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/UnitTestsSources/StreamTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/UnitTestsSources/ToolboxTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/UnitTestsSources/ToolboxTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancFramework/UnitTestsSources/ZipTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancFramework/UnitTestsSources/ZipTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/CMakeLists.txt	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/CMakeLists.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
@@ -88,13 +88,18 @@
 #####################################################################
 
 set(ORTHANC_SERVER_SOURCES
-  ${CMAKE_SOURCE_DIR}/Sources/Database/BaseDatabaseWrapper.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/Database/BaseCompatibilityTransaction.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/DatabaseLookup.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/GenericFind.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/ICreateInstance.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/IGetChildrenMetadata.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/ILookupResourceAndParent.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/ILookupResources.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/SetOfResources.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/Database/FindRequest.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/Database/FindResponse.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/Database/MainDicomTagsRegistry.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/Database/OrthancIdentifiers.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/ResourcesContent.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/SQLiteDatabaseWrapper.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/StatelessDatabaseOperations.cpp
@@ -119,7 +124,10 @@
   ${CMAKE_SOURCE_DIR}/Sources/OrthancRestApi/OrthancRestSystem.cpp
   ${CMAKE_SOURCE_DIR}/Sources/OrthancWebDav.cpp
   ${CMAKE_SOURCE_DIR}/Sources/QueryRetrieveHandler.cpp
-  ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseConstraint.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/ResourceFinder.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseDicomTagConstraint.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseDicomTagConstraints.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseMetadataConstraint.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseLookup.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Search/DicomTagConstraint.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Search/HierarchicalMatcher.cpp
@@ -130,6 +138,8 @@
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/ArchiveJob.cpp
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/CleaningInstancesJob.cpp
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/DicomModalityStoreJob.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/DicomRetrieveScuBaseJob.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/DicomGetScuJob.cpp
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/DicomMoveScuJob.cpp
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/LuaJobManager.cpp
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/MergeStudyJob.cpp
@@ -146,6 +156,7 @@
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/StorageCommitmentScpJob.cpp
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp  
   ${CMAKE_SOURCE_DIR}/Sources/ServerToolbox.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/SimpleInstanceOrdering.cpp
   ${CMAKE_SOURCE_DIR}/Sources/SliceOrdering.cpp
   ${CMAKE_SOURCE_DIR}/Sources/StorageCommitmentReports.cpp
   )
@@ -175,6 +186,7 @@
   ${CMAKE_SOURCE_DIR}/UnitTestsSources/DatabaseLookupTests.cpp
   ${CMAKE_SOURCE_DIR}/UnitTestsSources/LuaServerTests.cpp
   ${CMAKE_SOURCE_DIR}/UnitTestsSources/PluginsTests.cpp
+  ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerConfigTests.cpp
   ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerIndexTests.cpp
   ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerJobsTests.cpp
   ${CMAKE_SOURCE_DIR}/UnitTestsSources/SizeOfTests.cpp
@@ -326,7 +338,6 @@
 
 add_definitions(
   -DORTHANC_BUILD_UNIT_TESTS=1
-  -DORTHANC_BUILDING_SERVER_LIBRARY=1
 
   # Macros for the plugins
   -DHAS_ORTHANC_EXCEPTION=0
--- a/OrthancServer/OrthancExplorer/explorer.css	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/OrthancExplorer/explorer.css	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/OrthancExplorer/explorer.js	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/OrthancExplorer/explorer.js	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -71,6 +71,11 @@
   return s.match(/^[0-9a-zA-Z]+$/);
 }
 
+function IsValidLabelName(s)
+{
+  return s.match(/^[0-9a-zA-Z\-_]+$/);
+}
+
 
 function DeepCopy(obj)
 {
@@ -715,7 +720,7 @@
     $('.' + liClass).remove();
     for (var key in attachments) {
       if (attachments[key] >= 1024) {
-        target.append('<li data-icon="gear" class="' + liClass + '"><a href="#" id="' + attachments[key] + '">Download ' + key + '</a></li>')
+        target.append('<li data-icon="gear" class="' + liClass + '"><a href="#" id="' + attachments[key] + '">Download attachment "' + key + '"</a></li>')
       }
     }
     target.listview('refresh');
@@ -780,7 +785,7 @@
             click: function () {
               var label = $.mobile.sdLastInput;
               if (label.length > 0) {
-                if (IsAlphanumeric(label)) {
+                if (IsValidLabelName(label)) {
                   $.ajax({
                     url: '../' + resourceLevel + '/' + resourceId + '/labels/' + label,
                     dataType: 'json',
--- a/OrthancServer/OrthancExplorer/file-upload.js	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/OrthancExplorer/file-upload.js	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/OrthancExplorer/query-retrieve.js	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/OrthancExplorer/query-retrieve.js	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -341,9 +341,9 @@
       success: function(system) {
         $('#retrieve-target').val(system['DicomAet']);
 
+        $('#retrieve-form').unbind('submit');
         $('#retrieve-form').submit(function(event) {
           var aet;
-
           event.preventDefault();
 
           aet = $('#retrieve-target').val();
--- a/OrthancServer/Plugins/Engine/IPluginServiceProvider.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Engine/IPluginServiceProvider.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -44,7 +44,7 @@
 namespace Orthanc
 {
   class OrthancPluginDatabase::Transaction :
-    public BaseDatabaseWrapper::BaseTransaction,
+    public BaseCompatibilityTransaction,
     public Compatibility::ICreateInstance,
     public Compatibility::IGetChildrenMetadata,
     public Compatibility::ILookupResources,
@@ -564,7 +564,7 @@
     
     virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
                                       std::list<std::string>* instancesId,
-                                      const std::vector<DatabaseConstraint>& lookup,
+                                      const DatabaseDicomTagConstraints& lookup,
                                       ResourceType queryLevel,
                                       const std::set<std::string>& labels,
                                       LabelsConstraint labelsConstraint,
@@ -586,20 +586,20 @@
         std::vector<OrthancPluginDatabaseConstraint> constraints;
         std::vector< std::vector<const char*> > constraintsValues;
 
-        constraints.resize(lookup.size());
-        constraintsValues.resize(lookup.size());
+        constraints.resize(lookup.GetSize());
+        constraintsValues.resize(lookup.GetSize());
 
-        for (size_t i = 0; i < lookup.size(); i++)
+        for (size_t i = 0; i < lookup.GetSize(); i++)
         {
-          lookup[i].EncodeForPlugins(constraints[i], constraintsValues[i]);
+          lookup.GetConstraint(i).EncodeForPlugins(constraints[i], constraintsValues[i]);
         }
 
         ResetAnswers();
         answerMatchingResources_ = &resourcesId;
         answerMatchingInstances_ = instancesId;
       
-        CheckSuccess(that_.extensions_.lookupResources(that_.GetContext(), that_.payload_, lookup.size(),
-                                                       (lookup.empty() ? NULL : &constraints[0]),
+        CheckSuccess(that_.extensions_.lookupResources(that_.GetContext(), that_.payload_, lookup.GetSize(),
+                                                       (lookup.IsEmpty() ? NULL : &constraints[0]),
                                                        Plugins::Convert(queryLevel),
                                                        limit, (instancesId == NULL ? 0 : 1)));
       }
@@ -806,10 +806,10 @@
     }
 
 
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType,
-                                 int64_t since,
-                                 uint32_t limit) ORTHANC_OVERRIDE
+    virtual void GetAllPublicIdsCompatibility(std::list<std::string>& target,
+                                              ResourceType resourceType,
+                                              int64_t since,
+                                              uint32_t limit) ORTHANC_OVERRIDE
     {
       if (that_.extensions_.getAllPublicIdsWithLimit != NULL)
       {
@@ -1660,4 +1660,9 @@
       LOG(WARNING) << "Received an answer from the database index plugin, but not transaction is active";
     }
   }
+
+  uint64_t OrthancPluginDatabase::MeasureLatency()
+  {
+    throw OrthancException(ErrorCode_NotImplemented);
+  }
 }
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -26,7 +26,7 @@
 #if ORTHANC_ENABLE_PLUGINS == 1
 
 #include "../../../OrthancFramework/Sources/SharedLibrary.h"
-#include "../../Sources/Database/BaseDatabaseWrapper.h"
+#include "../../Sources/Database/BaseCompatibilityTransaction.h"
 #include "../Include/orthanc/OrthancCDatabasePlugin.h"
 #include "PluginsErrorDictionary.h"
 
@@ -46,7 +46,7 @@
    * able to rollback the modifications. Read-only accesses didn't
    * start a transaction, as they were protected by the global mutex.
    **/
-  class OrthancPluginDatabase : public BaseDatabaseWrapper
+  class OrthancPluginDatabase : public IDatabaseWrapper
   {
   private:
     class Transaction;
@@ -111,6 +111,13 @@
     }
 
     void AnswerReceived(const _OrthancPluginDatabaseAnswer& answer);
+
+    uint64_t MeasureLatency() ORTHANC_OVERRIDE;
+
+    bool HasIntegratedFind() const ORTHANC_OVERRIDE
+    {
+      return false;
+    }
   };
 }
 
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -46,7 +46,7 @@
 
 namespace Orthanc
 {
-  class OrthancPluginDatabaseV3::Transaction : public BaseDatabaseWrapper::BaseTransaction
+  class OrthancPluginDatabaseV3::Transaction : public BaseCompatibilityTransaction
   {
   private:
     OrthancPluginDatabaseV3&           that_;
@@ -386,10 +386,10 @@
     }
 
     
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType,
-                                 int64_t since,
-                                 uint32_t limit) ORTHANC_OVERRIDE
+    virtual void GetAllPublicIdsCompatibility(std::list<std::string>& target,
+                                              ResourceType resourceType,
+                                              int64_t since,
+                                              uint32_t limit) ORTHANC_OVERRIDE
     {
       CheckSuccess(that_.backend_.getAllPublicIdsWithLimit(
                      transaction_, Plugins::Convert(resourceType),
@@ -798,7 +798,7 @@
     
     virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
                                       std::list<std::string>* instancesId, // Can be NULL if not needed
-                                      const std::vector<DatabaseConstraint>& lookup,
+                                      const DatabaseDicomTagConstraints& lookup,
                                       ResourceType queryLevel,
                                       const std::set<std::string>& labels,
                                       LabelsConstraint labelsConstraint,
@@ -812,16 +812,16 @@
       std::vector<OrthancPluginDatabaseConstraint> constraints;
       std::vector< std::vector<const char*> > constraintsValues;
 
-      constraints.resize(lookup.size());
-      constraintsValues.resize(lookup.size());
+      constraints.resize(lookup.GetSize());
+      constraintsValues.resize(lookup.GetSize());
 
-      for (size_t i = 0; i < lookup.size(); i++)
+      for (size_t i = 0; i < lookup.GetSize(); i++)
       {
-        lookup[i].EncodeForPlugins(constraints[i], constraintsValues[i]);
+        lookup.GetConstraint(i).EncodeForPlugins(constraints[i], constraintsValues[i]);
       }
 
-      CheckSuccess(that_.backend_.lookupResources(transaction_, lookup.size(),
-                                                  (lookup.empty() ? NULL : &constraints[0]),
+      CheckSuccess(that_.backend_.lookupResources(transaction_, lookup.GetSize(),
+                                                  (lookup.IsEmpty() ? NULL : &constraints[0]),
                                                   Plugins::Convert(queryLevel),
                                                   limit, (instancesId == NULL ? 0 : 1)));
       CheckNoEvent();
@@ -1256,4 +1256,9 @@
     }
   }
 
+  uint64_t OrthancPluginDatabaseV3::MeasureLatency()
+  {
+    throw OrthancException(ErrorCode_NotImplemented);
+  }
+
 }
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -26,13 +26,13 @@
 #if ORTHANC_ENABLE_PLUGINS == 1
 
 #include "../../../OrthancFramework/Sources/SharedLibrary.h"
-#include "../../Sources/Database/BaseDatabaseWrapper.h"
+#include "../../Sources/Database/BaseCompatibilityTransaction.h"
 #include "../Include/orthanc/OrthancCDatabasePlugin.h"
 #include "PluginsErrorDictionary.h"
 
 namespace Orthanc
 {
-  class OrthancPluginDatabaseV3 : public BaseDatabaseWrapper
+  class OrthancPluginDatabaseV3 : public IDatabaseWrapper
   {
   private:
     class Transaction;
@@ -82,6 +82,13 @@
     {
       return dbCapabilities_;
     }
+
+    uint64_t MeasureLatency() ORTHANC_OVERRIDE;
+
+    bool HasIntegratedFind() const ORTHANC_OVERRIDE
+    {
+      return false;
+    }
   };
 }
 
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -31,10 +31,12 @@
 #include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
 #include "../../../OrthancFramework/Sources/Logging.h"
 #include "../../../OrthancFramework/Sources/OrthancException.h"
+#include "../../Sources/Database/Compatibility/GenericFind.h"
 #include "../../Sources/Database/ResourcesContent.h"
 #include "../../Sources/Database/VoidDatabaseListener.h"
 #include "../../Sources/ServerToolbox.h"
 #include "PluginsEnumerations.h"
+#include "../../Sources/Database/MainDicomTagsRegistry.h"
 
 #include "OrthancDatabasePlugin.pb.h"  // Auto-generated file
 
@@ -134,6 +136,238 @@
   }
 
 
+  static void Convert(DatabasePluginMessages::DatabaseConstraint& target,
+                      const DatabaseDicomTagConstraint& source)
+  {
+    target.set_level(Convert(source.GetLevel()));
+    target.set_tag_group(source.GetTag().GetGroup());
+    target.set_tag_element(source.GetTag().GetElement());
+    target.set_is_identifier_tag(source.IsIdentifier());
+    target.set_is_case_sensitive(source.IsCaseSensitive());
+    target.set_is_mandatory(source.IsMandatory());
+
+    target.mutable_values()->Reserve(source.GetValuesCount());
+    for (size_t j = 0; j < source.GetValuesCount(); j++)
+    {
+      target.add_values(source.GetValue(j));
+    }
+
+    switch (source.GetConstraintType())
+    {
+      case ConstraintType_Equal:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_EQUAL);
+        break;
+
+      case ConstraintType_SmallerOrEqual:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_SMALLER_OR_EQUAL);
+        break;
+
+      case ConstraintType_GreaterOrEqual:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_GREATER_OR_EQUAL);
+        break;
+
+      case ConstraintType_Wildcard:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_WILDCARD);
+        break;
+
+      case ConstraintType_List:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_LIST);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  static void Convert(DatabasePluginMessages::DatabaseMetadataConstraint& target,
+                      const DatabaseMetadataConstraint& source)
+  {
+    target.set_metadata(source.GetMetadata());
+    target.set_is_case_sensitive(source.IsCaseSensitive());
+    target.set_is_mandatory(source.IsMandatory());
+
+    target.mutable_values()->Reserve(source.GetValuesCount());
+    for (size_t j = 0; j < source.GetValuesCount(); j++)
+    {
+      target.add_values(source.GetValue(j));
+    }
+
+    switch (source.GetConstraintType())
+    {
+      case ConstraintType_Equal:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_EQUAL);
+        break;
+
+      case ConstraintType_SmallerOrEqual:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_SMALLER_OR_EQUAL);
+        break;
+
+      case ConstraintType_GreaterOrEqual:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_GREATER_OR_EQUAL);
+        break;
+
+      case ConstraintType_Wildcard:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_WILDCARD);
+        break;
+
+      case ConstraintType_List:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_LIST);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  static void Convert(DatabasePluginMessages::Find_Request_Ordering& target,
+                      const FindRequest::Ordering& source)
+  {
+    switch (source.GetKeyType())
+    {
+      case FindRequest::KeyType_DicomTag:
+      {
+        ResourceType tagLevel;
+        DicomTagType tagType;
+        MainDicomTagsRegistry registry;
+
+        registry.LookupTag(tagLevel, tagType, source.GetDicomTag());
+
+        target.set_key_type(DatabasePluginMessages::ORDERING_KEY_TYPE_DICOM_TAG);
+        target.set_tag_group(source.GetDicomTag().GetGroup());
+        target.set_tag_element(source.GetDicomTag().GetElement());
+        target.set_is_identifier_tag(tagType == DicomTagType_Identifier);
+        target.set_tag_level(Convert(tagLevel));
+
+      }; break;
+
+      case FindRequest::KeyType_Metadata:
+        target.set_key_type(DatabasePluginMessages::ORDERING_KEY_TYPE_METADATA);
+        target.set_metadata(source.GetMetadataType());
+
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    switch (source.GetDirection())
+    {
+      case FindRequest::OrderingDirection_Ascending:
+        target.set_direction(DatabasePluginMessages::ORDERING_DIRECTION_ASC);
+        break;
+
+      case FindRequest::OrderingDirection_Descending:
+        target.set_direction(DatabasePluginMessages::ORDERING_DIRECTION_DESC);
+
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    switch (source.GetCast())
+    {
+      case FindRequest::OrderingCast_Int:
+        target.set_cast(DatabasePluginMessages::ORDERING_CAST_INT);
+        break;
+
+      case FindRequest::OrderingCast_Float:
+        target.set_cast(DatabasePluginMessages::ORDERING_CAST_FLOAT);
+        break;
+
+      case FindRequest::OrderingCast_String:
+        target.set_cast(DatabasePluginMessages::ORDERING_CAST_STRING);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  static DatabasePluginMessages::LabelsConstraintType Convert(LabelsConstraint constraint)
+  {
+    switch (constraint)
+    {
+      case LabelsConstraint_All:
+        return DatabasePluginMessages::LABELS_CONSTRAINT_ALL;
+
+      case LabelsConstraint_Any:
+        return DatabasePluginMessages::LABELS_CONSTRAINT_ANY;
+
+      case LabelsConstraint_None:
+        return DatabasePluginMessages::LABELS_CONSTRAINT_NONE;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  static void Convert(DatabasePluginMessages::Find_Request_ChildrenSpecification& target,
+                      const FindRequest::ChildrenSpecification& source)
+  {
+    target.set_retrieve_identifiers(source.IsRetrieveIdentifiers());
+    target.set_retrieve_count(source.IsRetrieveCount());
+
+    for (std::set<MetadataType>::const_iterator it = source.GetMetadata().begin(); it != source.GetMetadata().end(); ++it)
+    {
+      target.add_retrieve_metadata(*it);
+    }
+
+    for (std::set<DicomTag>::const_iterator it = source.GetMainDicomTags().begin(); it != source.GetMainDicomTags().end(); ++it)
+    {
+      DatabasePluginMessages::Find_Request_Tag* tag = target.add_retrieve_main_dicom_tags();
+      tag->set_group(it->GetGroup());
+      tag->set_element(it->GetElement());
+    }
+  }
+
+
+  static void Convert(FindResponse::Resource& target,
+                      ResourceType level,
+                      const DatabasePluginMessages::Find_Response_ResourceContent& source)
+  {
+    for (int i = 0; i < source.main_dicom_tags().size(); i++)
+    {
+      target.AddStringDicomTag(level, source.main_dicom_tags(i).group(),
+                               source.main_dicom_tags(i).element(), source.main_dicom_tags(i).value());
+    }
+
+    for (int i = 0; i < source.metadata().size(); i++)
+    {
+      target.AddMetadata(level, static_cast<MetadataType>(source.metadata(i).key()),
+                         source.metadata(i).value(), source.metadata(i).revision());
+    }
+  }
+
+
+  static void Convert(FindResponse::Resource& target,
+                      ResourceType level,
+                      const DatabasePluginMessages::Find_Response_ChildrenContent& source)
+  {
+    for (int i = 0; i < source.identifiers().size(); i++)
+    {
+      target.AddChildIdentifier(level, source.identifiers(i));
+    }
+
+    target.SetChildrenCount(level, source.count());
+
+    for (int i = 0; i < source.main_dicom_tags().size(); i++)
+    {
+      const DicomTag tag(source.main_dicom_tags(i).group(), source.main_dicom_tags(i).element());
+      target.AddChildrenMainDicomTagValue(level, tag, source.main_dicom_tags(i).value());
+    }
+
+    for (int i = 0; i < source.metadata().size(); i++)
+    {
+      MetadataType key = static_cast<MetadataType>(source.metadata(i).key());
+      target.AddChildrenMetadataValue(level, key, source.metadata(i).value());
+    }
+  }
+
+
   static void Execute(DatabasePluginMessages::Response& response,
                       const OrthancPluginDatabaseV4& database,
                       const DatabasePluginMessages::Request& request)
@@ -178,7 +412,9 @@
   }
 
   
-  class OrthancPluginDatabaseV4::Transaction : public IDatabaseWrapper::ITransaction
+  class OrthancPluginDatabaseV4::Transaction :
+    public IDatabaseWrapper::ITransaction,
+    public IDatabaseWrapper::ICompatibilityTransaction
   {
   private:
     OrthancPluginDatabaseV4&  database_;
@@ -453,10 +689,10 @@
     }
 
     
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType,
-                                 int64_t since,
-                                 uint32_t limit) ORTHANC_OVERRIDE
+    virtual void GetAllPublicIdsCompatibility(std::list<std::string>& target,
+                                              ResourceType resourceType,
+                                              int64_t since,
+                                              uint32_t limit) ORTHANC_OVERRIDE
     {
       DatabasePluginMessages::TransactionRequest request;
       request.mutable_get_all_public_ids_with_limits()->set_resource_type(Convert(resourceType));
@@ -495,6 +731,37 @@
       }
     }
 
+    virtual void GetChangesExtended(std::list<ServerIndexChange>& target /*out*/,
+                                    bool& done /*out*/,
+                                    int64_t since,
+                                    int64_t to,
+                                    uint32_t limit,
+                                    const std::set<ChangeType>& changeTypes) ORTHANC_OVERRIDE
+    {
+      assert(database_.GetDatabaseCapabilities().HasExtendedChanges());
+
+      DatabasePluginMessages::TransactionRequest request;
+      DatabasePluginMessages::TransactionResponse response;
+
+      request.mutable_get_changes_extended()->set_since(since);
+      request.mutable_get_changes_extended()->set_limit(limit);
+      request.mutable_get_changes_extended()->set_to(to);
+      for (std::set<ChangeType>::const_iterator it = changeTypes.begin(); it != changeTypes.end(); ++it)
+      {
+        request.mutable_get_changes_extended()->add_change_type(*it);
+      }
+      
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_CHANGES_EXTENDED, request);
+
+      done = response.get_changes_extended().done();
+
+      target.clear();
+      for (int i = 0; i < response.get_changes_extended().changes().size(); i++)
+      {
+        target.push_back(Convert(response.get_changes_extended().changes(i)));
+      }
+    }
+
     
     virtual void GetChildrenInternalId(std::list<int64_t>& target,
                                        int64_t id) ORTHANC_OVERRIDE
@@ -972,10 +1239,10 @@
       return response.is_disk_size_above().result();
     }
 
-    
+
     virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
                                       std::list<std::string>* instancesId, // Can be NULL if not needed
-                                      const std::vector<DatabaseConstraint>& lookup,
+                                      const DatabaseDicomTagConstraints& lookup,
                                       ResourceType queryLevel,
                                       const std::set<std::string>& labels,
                                       LabelsConstraint labelsConstraint,
@@ -992,49 +1259,11 @@
       request.mutable_lookup_resources()->set_limit(limit);
       request.mutable_lookup_resources()->set_retrieve_instances_ids(instancesId != NULL);
 
-      request.mutable_lookup_resources()->mutable_lookup()->Reserve(lookup.size());
+      request.mutable_lookup_resources()->mutable_lookup()->Reserve(lookup.GetSize());
       
-      for (size_t i = 0; i < lookup.size(); i++)
+      for (size_t i = 0; i < lookup.GetSize(); i++)
       {
-        DatabasePluginMessages::DatabaseConstraint* constraint = request.mutable_lookup_resources()->add_lookup();
-        constraint->set_level(Convert(lookup[i].GetLevel()));
-        constraint->set_tag_group(lookup[i].GetTag().GetGroup());
-        constraint->set_tag_element(lookup[i].GetTag().GetElement());
-        constraint->set_is_identifier_tag(lookup[i].IsIdentifier());
-        constraint->set_is_case_sensitive(lookup[i].IsCaseSensitive());
-        constraint->set_is_mandatory(lookup[i].IsMandatory());
-
-        constraint->mutable_values()->Reserve(lookup[i].GetValuesCount());
-        for (size_t j = 0; j < lookup[i].GetValuesCount(); j++)
-        {
-          constraint->add_values(lookup[i].GetValue(j));
-        }
-
-        switch (lookup[i].GetConstraintType())
-        {
-          case ConstraintType_Equal:
-            constraint->set_type(DatabasePluginMessages::CONSTRAINT_EQUAL);
-            break;
-            
-          case ConstraintType_SmallerOrEqual:
-            constraint->set_type(DatabasePluginMessages::CONSTRAINT_SMALLER_OR_EQUAL);
-            break;
-            
-          case ConstraintType_GreaterOrEqual:
-            constraint->set_type(DatabasePluginMessages::CONSTRAINT_GREATER_OR_EQUAL);
-            break;
-            
-          case ConstraintType_Wildcard:
-            constraint->set_type(DatabasePluginMessages::CONSTRAINT_WILDCARD);
-            break;
-            
-          case ConstraintType_List:
-            constraint->set_type(DatabasePluginMessages::CONSTRAINT_LIST);
-            break;
-
-          default:
-            throw OrthancException(ErrorCode_ParameterOutOfRange);
-        }
+        Convert(*request.mutable_lookup_resources()->add_lookup(), lookup.GetConstraint(i));
       }
 
       for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
@@ -1042,23 +1271,7 @@
         request.mutable_lookup_resources()->add_labels(*it);
       }
 
-      switch (labelsConstraint)
-      {
-        case LabelsConstraint_All:
-          request.mutable_lookup_resources()->set_labels_constraint(DatabasePluginMessages::LABELS_CONSTRAINT_ALL);
-          break;
-            
-        case LabelsConstraint_Any:
-          request.mutable_lookup_resources()->set_labels_constraint(DatabasePluginMessages::LABELS_CONSTRAINT_ANY);
-          break;
-            
-        case LabelsConstraint_None:
-          request.mutable_lookup_resources()->set_labels_constraint(DatabasePluginMessages::LABELS_CONSTRAINT_NONE);
-          break;
-            
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
+      request.mutable_lookup_resources()->set_labels_constraint(Convert(labelsConstraint));
       
       DatabasePluginMessages::TransactionResponse response;
       ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LOOKUP_RESOURCES, request);
@@ -1276,6 +1489,322 @@
     {
       ListLabelsInternal(target, false, -1);
     }
+
+
+    virtual void ExecuteCount(uint64_t& count,
+                              const FindRequest& request,
+                              const Capabilities& capabilities) ORTHANC_OVERRIDE
+    {
+      if (capabilities.HasFindSupport())
+      {
+        DatabasePluginMessages::TransactionRequest dbRequest;
+        dbRequest.mutable_find()->set_level(Convert(request.GetLevel()));
+
+        if (request.GetOrthancIdentifiers().HasPatientId())
+        {
+          dbRequest.mutable_find()->set_orthanc_id_patient(request.GetOrthancIdentifiers().GetPatientId());
+        }
+
+        if (request.GetOrthancIdentifiers().HasStudyId())
+        {
+          dbRequest.mutable_find()->set_orthanc_id_study(request.GetOrthancIdentifiers().GetStudyId());
+        }
+
+        if (request.GetOrthancIdentifiers().HasSeriesId())
+        {
+          dbRequest.mutable_find()->set_orthanc_id_series(request.GetOrthancIdentifiers().GetSeriesId());
+        }
+
+        if (request.GetOrthancIdentifiers().HasInstanceId())
+        {
+          dbRequest.mutable_find()->set_orthanc_id_instance(request.GetOrthancIdentifiers().GetInstanceId());
+        }
+
+        for (size_t i = 0; i < request.GetDicomTagConstraints().GetSize(); i++)
+        {
+          Convert(*dbRequest.mutable_find()->add_dicom_tag_constraints(), request.GetDicomTagConstraints().GetConstraint(i));
+        }
+
+        for (std::deque<DatabaseMetadataConstraint*>::const_iterator it = request.GetMetadataConstraint().begin(); it != request.GetMetadataConstraint().end(); ++it)
+        {
+          Convert(*dbRequest.mutable_find()->add_metadata_constraints(), *(*it)); 
+        }
+
+        for (std::set<std::string>::const_iterator it = request.GetLabels().begin(); it != request.GetLabels().end(); ++it)
+        {
+          dbRequest.mutable_find()->add_labels(*it);
+        }
+
+        dbRequest.mutable_find()->set_labels_constraint(Convert(request.GetLabelsConstraint()));
+
+        DatabasePluginMessages::TransactionResponse dbResponse;
+        ExecuteTransaction(dbResponse, DatabasePluginMessages::OPERATION_COUNT_RESOURCES, dbRequest);
+
+        count = dbResponse.count_resources().count();
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_NotImplemented);
+      }
+    }
+
+    virtual void ExecuteFind(FindResponse& response,
+                             const FindRequest& request,
+                             const Capabilities& capabilities) ORTHANC_OVERRIDE
+    {
+      if (capabilities.HasFindSupport())
+      {
+        DatabasePluginMessages::TransactionRequest dbRequest;
+        dbRequest.mutable_find()->set_level(Convert(request.GetLevel()));
+
+        if (request.GetOrthancIdentifiers().HasPatientId())
+        {
+          dbRequest.mutable_find()->set_orthanc_id_patient(request.GetOrthancIdentifiers().GetPatientId());
+        }
+
+        if (request.GetOrthancIdentifiers().HasStudyId())
+        {
+          dbRequest.mutable_find()->set_orthanc_id_study(request.GetOrthancIdentifiers().GetStudyId());
+        }
+
+        if (request.GetOrthancIdentifiers().HasSeriesId())
+        {
+          dbRequest.mutable_find()->set_orthanc_id_series(request.GetOrthancIdentifiers().GetSeriesId());
+        }
+
+        if (request.GetOrthancIdentifiers().HasInstanceId())
+        {
+          dbRequest.mutable_find()->set_orthanc_id_instance(request.GetOrthancIdentifiers().GetInstanceId());
+        }
+
+        for (size_t i = 0; i < request.GetDicomTagConstraints().GetSize(); i++)
+        {
+          Convert(*dbRequest.mutable_find()->add_dicom_tag_constraints(), request.GetDicomTagConstraints().GetConstraint(i));
+        }
+
+        for (std::deque<DatabaseMetadataConstraint*>::const_iterator it = request.GetMetadataConstraint().begin(); it != request.GetMetadataConstraint().end(); ++it)
+        {
+          Convert(*dbRequest.mutable_find()->add_metadata_constraints(), *(*it)); 
+        }
+
+        for (std::deque<FindRequest::Ordering*>::const_iterator it = request.GetOrdering().begin(); it != request.GetOrdering().end(); ++it)
+        {
+          Convert(*dbRequest.mutable_find()->add_ordering(), *(*it)); 
+        }
+
+        if (request.HasLimits())
+        {
+          dbRequest.mutable_find()->mutable_limits()->set_since(request.GetLimitsSince());
+          dbRequest.mutable_find()->mutable_limits()->set_count(request.GetLimitsCount());
+        }
+
+        for (std::set<std::string>::const_iterator it = request.GetLabels().begin(); it != request.GetLabels().end(); ++it)
+        {
+          dbRequest.mutable_find()->add_labels(*it);
+        }
+
+        dbRequest.mutable_find()->set_labels_constraint(Convert(request.GetLabelsConstraint()));
+
+        dbRequest.mutable_find()->set_retrieve_main_dicom_tags(request.IsRetrieveMainDicomTags());
+        dbRequest.mutable_find()->set_retrieve_metadata(request.IsRetrieveMetadata());
+        dbRequest.mutable_find()->set_retrieve_labels(request.IsRetrieveLabels());
+        dbRequest.mutable_find()->set_retrieve_attachments(request.IsRetrieveAttachments());
+        dbRequest.mutable_find()->set_retrieve_parent_identifier(request.IsRetrieveParentIdentifier());
+
+        if (request.GetLevel() == ResourceType_Instance)
+        {
+          dbRequest.mutable_find()->set_retrieve_one_instance_metadata_and_attachments(false);
+        }
+        else
+        {
+          dbRequest.mutable_find()->set_retrieve_one_instance_metadata_and_attachments(request.IsRetrieveOneInstanceMetadataAndAttachments());
+        }
+
+        if (request.GetLevel() == ResourceType_Study ||
+            request.GetLevel() == ResourceType_Series ||
+            request.GetLevel() == ResourceType_Instance)
+        {
+          dbRequest.mutable_find()->mutable_parent_patient()->set_retrieve_main_dicom_tags(request.GetParentSpecification(ResourceType_Patient).IsRetrieveMainDicomTags());
+          dbRequest.mutable_find()->mutable_parent_patient()->set_retrieve_metadata(request.GetParentSpecification(ResourceType_Patient).IsRetrieveMetadata());
+        }
+
+        if (request.GetLevel() == ResourceType_Series ||
+            request.GetLevel() == ResourceType_Instance)
+        {
+          dbRequest.mutable_find()->mutable_parent_study()->set_retrieve_main_dicom_tags(request.GetParentSpecification(ResourceType_Study).IsRetrieveMainDicomTags());
+          dbRequest.mutable_find()->mutable_parent_study()->set_retrieve_metadata(request.GetParentSpecification(ResourceType_Study).IsRetrieveMetadata());
+        }
+
+        if (request.GetLevel() == ResourceType_Instance)
+        {
+          dbRequest.mutable_find()->mutable_parent_series()->set_retrieve_main_dicom_tags(request.GetParentSpecification(ResourceType_Series).IsRetrieveMainDicomTags());
+          dbRequest.mutable_find()->mutable_parent_series()->set_retrieve_metadata(request.GetParentSpecification(ResourceType_Series).IsRetrieveMetadata());
+        }
+
+        if (request.GetLevel() == ResourceType_Patient)
+        {
+          Convert(*dbRequest.mutable_find()->mutable_children_studies(), request.GetChildrenSpecification(ResourceType_Study));
+        }
+
+        if (request.GetLevel() == ResourceType_Patient ||
+            request.GetLevel() == ResourceType_Study)
+        {
+          Convert(*dbRequest.mutable_find()->mutable_children_series(), request.GetChildrenSpecification(ResourceType_Series));
+        }
+
+        if (request.GetLevel() == ResourceType_Patient ||
+            request.GetLevel() == ResourceType_Study ||
+            request.GetLevel() == ResourceType_Series)
+        {
+          Convert(*dbRequest.mutable_find()->mutable_children_instances(), request.GetChildrenSpecification(ResourceType_Instance));
+        }
+
+        DatabasePluginMessages::TransactionResponse dbResponse;
+        ExecuteTransaction(dbResponse, DatabasePluginMessages::OPERATION_FIND, dbRequest);
+
+        for (int i = 0; i < dbResponse.find().size(); i++)
+        {
+          const DatabasePluginMessages::Find_Response& source = dbResponse.find(i);
+
+          std::unique_ptr<FindResponse::Resource> target(
+            new FindResponse::Resource(request.GetLevel(), source.internal_id(), source.public_id()));
+
+          if (request.IsRetrieveParentIdentifier())
+          {
+            target->SetParentIdentifier(source.parent_public_id());
+          }
+
+          for (int j = 0; j < source.labels().size(); j++)
+          {
+            target->AddLabel(source.labels(j));
+          }
+
+          if (source.attachments().size() != source.attachments_revisions().size())
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+
+          for (int j = 0; j < source.attachments().size(); j++)
+          {
+            target->AddAttachment(Convert(source.attachments(j)), source.attachments_revisions(j));
+          }
+
+          Convert(*target, ResourceType_Patient, source.patient_content());
+
+          if (request.GetLevel() == ResourceType_Study ||
+              request.GetLevel() == ResourceType_Series ||
+              request.GetLevel() == ResourceType_Instance)
+          {
+            Convert(*target, ResourceType_Study, source.study_content());
+          }
+
+          if (request.GetLevel() == ResourceType_Series ||
+              request.GetLevel() == ResourceType_Instance)
+          {
+            Convert(*target, ResourceType_Series, source.series_content());
+          }
+
+          if (request.GetLevel() == ResourceType_Instance)
+          {
+            Convert(*target, ResourceType_Instance, source.instance_content());
+          }
+
+          if (request.GetLevel() == ResourceType_Patient)
+          {
+            Convert(*target, ResourceType_Study, source.children_studies_content());
+          }
+
+          if (request.GetLevel() == ResourceType_Patient ||
+              request.GetLevel() == ResourceType_Study)
+          {
+            Convert(*target, ResourceType_Series, source.children_series_content());
+          }
+
+          if (request.GetLevel() == ResourceType_Patient ||
+              request.GetLevel() == ResourceType_Study ||
+              request.GetLevel() == ResourceType_Series)
+          {
+            Convert(*target, ResourceType_Instance, source.children_instances_content());
+          }
+
+          if (request.GetLevel() != ResourceType_Instance &&
+              request.IsRetrieveOneInstanceMetadataAndAttachments())
+          {
+            std::map<MetadataType, std::string> metadata;
+            for (int j = 0; j < source.one_instance_metadata().size(); j++)
+            {
+              MetadataType key = static_cast<MetadataType>(source.one_instance_metadata(j).key());
+              if (metadata.find(key) == metadata.end())
+              {
+                metadata[key] = source.one_instance_metadata(j).value();
+              }
+              else
+              {
+                throw OrthancException(ErrorCode_DatabasePlugin);
+              }
+            }
+
+            std::map<FileContentType, FileInfo> attachments;
+
+            for (int j = 0; j < source.one_instance_attachments().size(); j++)
+            {
+              FileInfo info(Convert(source.one_instance_attachments(j)));
+              if (attachments.find(info.GetContentType()) == attachments.end())
+              {
+                attachments[info.GetContentType()] = info;
+              }
+              else
+              {
+                throw OrthancException(ErrorCode_DatabasePlugin);
+              }
+            }
+
+            target->SetOneInstanceMetadataAndAttachments(source.one_instance_public_id(), metadata, attachments);
+          }
+
+          response.Add(target.release());
+        }
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_NotImplemented);
+      }
+    }
+
+
+    virtual void ExecuteFind(std::list<std::string>& identifiers,
+                             const Capabilities& capabilities,
+                             const FindRequest& request) ORTHANC_OVERRIDE
+    {
+      if (capabilities.HasFindSupport())
+      {
+        // The integrated version of "ExecuteFind()" should have been called
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        Compatibility::GenericFind find(*this, *this);
+        find.ExecuteFind(identifiers, capabilities, request);
+      }
+    }
+
+
+    virtual void ExecuteExpand(FindResponse& response,
+                               const Capabilities& capabilities,
+                               const FindRequest& request,
+                               const std::string& identifier) ORTHANC_OVERRIDE
+    {
+      if (capabilities.HasFindSupport())
+      {
+        // The integrated version of "ExecuteFind()" should have been called
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        Compatibility::GenericFind find(*this, *this);
+        find.ExecuteExpand(response, capabilities, request, identifier);
+      }
+    }
   };
 
 
@@ -1362,8 +1891,10 @@
       dbCapabilities_.SetRevisionsSupport(systemInfo.supports_revisions());
       dbCapabilities_.SetLabelsSupport(systemInfo.supports_labels());
       dbCapabilities_.SetAtomicIncrementGlobalProperty(systemInfo.supports_increment_global_property());
-      dbCapabilities_.SetUpdateAndGetStatistics(systemInfo.has_update_and_get_statistics());
+      dbCapabilities_.SetHasUpdateAndGetStatistics(systemInfo.has_update_and_get_statistics());
       dbCapabilities_.SetMeasureLatency(systemInfo.has_measure_latency());
+      dbCapabilities_.SetHasExtendedChanges(systemInfo.has_extended_changes());
+      dbCapabilities_.SetHasFindSupport(systemInfo.supports_find());
     }
 
     open_ = true;
@@ -1490,4 +2021,10 @@
       return dbCapabilities_;
     }
   }
+
+
+  bool OrthancPluginDatabaseV4::HasIntegratedFind() const
+  {
+    return dbCapabilities_.HasFindSupport();
+  }
 }
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -93,6 +93,8 @@
     virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE;
 
     virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE;
+
+    virtual bool HasIntegratedFind() const ORTHANC_OVERRIDE;
   };
 }
 
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -1101,40 +1101,41 @@
     class PluginHttpOutput : public boost::noncopyable
     {
     private:
-      enum MultipartState
-      {
-        MultipartState_None,
-        MultipartState_FirstPart,
-        MultipartState_SecondPart,
-        MultipartState_NextParts
+      enum State
+      {
+        State_None,
+        State_MultipartFirstPart,
+        State_MultipartSecondPart,
+        State_MultipartNextParts,
+        State_WritingStream
       };
 
       HttpOutput&                 output_;
       std::unique_ptr<std::string>  errorDetails_;
       bool                        logDetails_;
-      MultipartState              multipartState_;
+      State                       state_;
       std::string                 multipartSubType_;
       std::string                 multipartContentType_;
       std::string                 multipartFirstPart_;
       std::map<std::string, std::string>  multipartFirstHeaders_;
-      
+
     public:
       explicit PluginHttpOutput(HttpOutput& output) :
         output_(output),
         logDetails_(false),
-        multipartState_(MultipartState_None)
+        state_(State_None)
       {
       }
 
       HttpOutput& GetOutput()
       {
-        if (multipartState_ == MultipartState_None)
+        if (state_ == State_None)
         {
           return output_;
         }
         else
         {
-          // Must use "SendMultipartItem()" on multipart streams
+          // Must use "SendMultipartItem()" on multipart streams or "SendStreamChunk()"
           throw OrthancException(ErrorCode_BadSequenceOfCalls);
         }
       }
@@ -1171,18 +1172,45 @@
       void StartMultipart(const char* subType,
                           const char* contentType)
       {
-        if (multipartState_ != MultipartState_None)
+        if (state_ != State_None)
         {
           throw OrthancException(ErrorCode_BadSequenceOfCalls);
         }
         else
         {
-          multipartState_ = MultipartState_FirstPart;
+          state_ = State_MultipartFirstPart;
           multipartSubType_ = subType;
           multipartContentType_ = contentType;
         }
       }
 
+      void StartStream(const char* contentType)
+      {
+        if (state_ != State_None)
+        {
+          throw OrthancException(ErrorCode_BadSequenceOfCalls);
+        }
+        else
+        {
+          output_.StartStream(contentType);
+          state_ = State_WritingStream;
+        }
+      }
+
+      void SendStreamItem(const void* data,
+                          size_t size)
+      {
+        if (state_ != State_WritingStream)
+        {
+          throw OrthancException(ErrorCode_BadSequenceOfCalls);
+        }
+        else
+        {
+          output_.SendStreamItem(data, size);
+        }
+      }
+
+
       void SendMultipartItem(const void* data,
                              size_t size,
                              const std::map<std::string, std::string>& headers)
@@ -1192,19 +1220,19 @@
           throw OrthancException(ErrorCode_NullPointer);
         }
 
-        switch (multipartState_)
+        switch (state_)
         {
-          case MultipartState_None:
+          case State_None:
             // Must call "StartMultipart()" before
             throw OrthancException(ErrorCode_BadSequenceOfCalls);
 
-          case MultipartState_FirstPart:
+          case State_MultipartFirstPart:
             multipartFirstPart_.assign(reinterpret_cast<const char*>(data), size);
             multipartFirstHeaders_ = headers;
-            multipartState_ = MultipartState_SecondPart;
+            state_ = State_MultipartSecondPart;
             break;
 
-          case MultipartState_SecondPart:
+          case State_MultipartSecondPart:
             // Start an actual stream for chunked transfer as soon as
             // there are more than 2 elements in the multipart stream
             output_.StartMultipart(multipartSubType_, multipartContentType_);
@@ -1213,10 +1241,10 @@
             multipartFirstPart_.clear();  // Release memory
 
             output_.SendMultipartItem(data, size, headers);
-            multipartState_ = MultipartState_NextParts;
+            state_ = State_MultipartNextParts;
             break;
 
-          case MultipartState_NextParts:
+          case State_MultipartNextParts:
             output_.SendMultipartItem(data, size, headers);
             break;
 
@@ -1230,21 +1258,21 @@
       {
         if (error == OrthancPluginErrorCode_Success)
         {
-          switch (multipartState_)
+          switch (state_)
           {
-            case MultipartState_None:
+            case State_None:
               assert(!output_.IsWritingMultipart());
               break;
 
-            case MultipartState_FirstPart:   // Multipart started, but no part was sent
-            case MultipartState_SecondPart:  // Multipart started, first part is pending
+            case State_MultipartFirstPart:   // Multipart started, but no part was sent
+            case State_MultipartSecondPart:  // Multipart started, first part is pending
             {
               assert(!output_.IsWritingMultipart());
               std::vector<const void*> parts;
               std::vector<size_t> sizes;
               std::vector<const std::map<std::string, std::string>*> headers;
 
-              if (multipartState_ == MultipartState_SecondPart)
+              if (state_ == State_MultipartSecondPart)
               {
                 parts.push_back(multipartFirstPart_.c_str());
                 sizes.push_back(multipartFirstPart_.size());
@@ -1256,9 +1284,15 @@
               break;
             }
 
-            case MultipartState_NextParts:
+            case State_MultipartNextParts:
               assert(output_.IsWritingMultipart());
               output_.CloseMultipart();
+              break;
+
+            case State_WritingStream:
+              assert(output_.IsWritingStream());
+              output_.CloseStream();
+              break;
 
             default:
               throw OrthancException(ErrorCode_InternalError);
@@ -4515,9 +4549,8 @@
             PImpl::ServerContextReference lock(*pimpl_);
 
             std::string s;
-            int64_t revision;  // unused
             if (lock.GetContext().GetIndex().LookupMetadata(
-                  s, revision, params.instanceId,
+                  s, params.instanceId,
                   ResourceType_Instance, MetadataType_Instance_PixelDataVR))
             {
               hasPixelData = true;
@@ -4536,7 +4569,7 @@
               }
             }
             else if (lock.GetContext().GetIndex().LookupMetadata(
-                       s, revision, params.instanceId,
+                       s, params.instanceId,
                        ResourceType_Instance, MetadataType_Instance_PixelDataOffset))
             {
               // This file was stored by an older version of Orthanc,
@@ -4651,7 +4684,7 @@
 
     if (entry == NULL)
     {
-      throw OrthancException(ErrorCode_UnknownDicomTag);
+      throw OrthancException(ErrorCode_UnknownDicomTag, p.name);
     }
     else
     {
@@ -4893,6 +4926,21 @@
         ApplySendMultipartItem2(parameters);
         return true;
 
+      case _OrthancPluginService_StartStreamAnswer:
+      {
+        const _OrthancPluginStartStreamAnswer& p =
+          *reinterpret_cast<const _OrthancPluginStartStreamAnswer*>(parameters);
+        reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->StartStream(p.contentType);
+        return true;
+      }
+
+      case _OrthancPluginService_SendStreamChunk:
+      {
+        const _OrthancPluginAnswerBuffer& p =
+          *reinterpret_cast<const _OrthancPluginAnswerBuffer*>(parameters);
+        reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->SendStreamItem(p.answer, p.answerSize);
+        return true;
+      }
       case _OrthancPluginService_ReadFile:
       {
         const _OrthancPluginReadFile& p =
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -595,5 +595,74 @@
           throw OrthancException(ErrorCode_ParameterOutOfRange);
       }
     }
+
+
+    OrthancPluginResourceType Convert(ResourceType type)
+    {
+      switch (type)
+      {
+        case ResourceType_Patient:
+          return OrthancPluginResourceType_Patient;
+
+        case ResourceType_Study:
+          return OrthancPluginResourceType_Study;
+
+        case ResourceType_Series:
+          return OrthancPluginResourceType_Series;
+
+        case ResourceType_Instance:
+          return OrthancPluginResourceType_Instance;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+
+    ResourceType Convert(OrthancPluginResourceType type)
+    {
+      switch (type)
+      {
+        case OrthancPluginResourceType_Patient:
+          return ResourceType_Patient;
+
+        case OrthancPluginResourceType_Study:
+          return ResourceType_Study;
+
+        case OrthancPluginResourceType_Series:
+          return ResourceType_Series;
+
+        case OrthancPluginResourceType_Instance:
+          return ResourceType_Instance;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+
+    OrthancPluginConstraintType Convert(ConstraintType constraint)
+    {
+      switch (constraint)
+      {
+        case ConstraintType_Equal:
+          return OrthancPluginConstraintType_Equal;
+
+        case ConstraintType_GreaterOrEqual:
+          return OrthancPluginConstraintType_GreaterOrEqual;
+
+        case ConstraintType_SmallerOrEqual:
+          return OrthancPluginConstraintType_SmallerOrEqual;
+
+        case ConstraintType_Wildcard:
+          return OrthancPluginConstraintType_Wildcard;
+
+        case ConstraintType_List:
+          return OrthancPluginConstraintType_List;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
   }
 }
--- a/OrthancServer/Plugins/Engine/PluginsEnumerations.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -25,15 +25,7 @@
 
 #if ORTHANC_ENABLE_PLUGINS == 1
 
-/**
- * NB: Conversions to/from "OrthancPluginConstraintType" and
- * "OrthancPluginResourceType" are located in file
- * "../../Sources/Search/DatabaseConstraint.h" to be shared with the
- * "orthanc-databases" project.
- **/
-
 #include "../../../OrthancFramework/Sources/MetricsRegistry.h"
-#include "../../Sources/Search/DatabaseConstraint.h"
 #include "../../Sources/ServerEnumerations.h"
 #include "../Include/orthanc/OrthancCPlugin.h"
 
@@ -75,6 +67,12 @@
     StorageCommitmentFailureReason Convert(OrthancPluginStorageCommitmentFailureReason reason);
 
     MetricsUpdatePolicy Convert(OrthancPluginMetricsType type);
+
+    OrthancPluginResourceType Convert(ResourceType type);
+
+    ResourceType Convert(OrthancPluginResourceType type);
+
+    OrthancPluginConstraintType Convert(ConstraintType constraint);
   }
 }
 
--- a/OrthancServer/Plugins/Engine/PluginsErrorDictionary.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsErrorDictionary.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Engine/PluginsErrorDictionary.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsErrorDictionary.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Engine/PluginsJob.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsJob.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -147,7 +147,7 @@
     }
   }
 
-  float PluginsJob::GetProgress()
+  float PluginsJob::GetProgress() const
   {
     return parameters_.getProgress(parameters_.job);
   }
@@ -194,7 +194,7 @@
     };
   }
   
-  void PluginsJob::GetPublicContent(Json::Value& value)
+  void PluginsJob::GetPublicContent(Json::Value& value) const
   {
     if (parameters_.getContent != NULL)
     {
@@ -232,7 +232,7 @@
     }
   }
 
-  bool PluginsJob::Serialize(Json::Value& value)
+  bool PluginsJob::Serialize(Json::Value& value) const
   {
     if (parameters_.getSerialized != NULL)
     {
--- a/OrthancServer/Plugins/Engine/PluginsJob.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsJob.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -58,16 +58,16 @@
 
     virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
 
-    virtual float GetProgress() ORTHANC_OVERRIDE;
+    virtual float GetProgress() const ORTHANC_OVERRIDE;
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
     {
       target = type_;
     }
     
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE;
 
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
--- a/OrthancServer/Plugins/Engine/PluginsManager.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsManager.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -37,7 +37,7 @@
 #include <memory>
 #include <boost/filesystem.hpp>
 
-#ifdef WIN32
+#ifdef _WIN32
 #define PLUGIN_EXTENSION ".dll"
 #elif defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) || defined(__OpenBSD__)
 #define PLUGIN_EXTENSION ".so"
@@ -244,8 +244,21 @@
   {
     if (!boost::filesystem::exists(path))
     {
-      LOG(ERROR) << "Inexistent path to plugins: " << path;
-      return;
+      boost::filesystem::path p(path);
+      std::string extension = p.extension().string();
+      Toolbox::ToLowerCase(extension);
+
+      if (extension == PLUGIN_EXTENSION)
+      { 
+        // if this is a plugin path, fail to start
+        throw OrthancException(ErrorCode_SharedLibrary, "Inexistent path to plugin: " + path);
+      }
+      else
+      { 
+        // it might be a directory -> just log a warning
+        LOG(WARNING) << "Inexistent path to plugins: " << path;
+        return;
+      }
     }
 
     if (boost::filesystem::is_directory(path))
--- a/OrthancServer/Plugins/Engine/PluginsManager.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsManager.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Tue Mar 18 13:37:18 2025 +0100
@@ -7,8 +7,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Tue Mar 18 13:37:18 2025 +0100
@@ -86,8 +86,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -111,7 +111,7 @@
 #include <stdio.h>
 #include <string.h>
 
-#ifdef WIN32
+#ifdef _WIN32
 #  define ORTHANC_PLUGINS_API __declspec(dllexport)
 #elif __GNUC__ >= 4
 #  define ORTHANC_PLUGINS_API __attribute__ ((visibility ("default")))
@@ -121,7 +121,7 @@
 
 #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     1
 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     12
-#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  4
+#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  6
 
 
 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
@@ -262,6 +262,7 @@
     OrthancPluginErrorCode_MainDicomTagsMultiplyDefined = 44    /*!< A main DICOM Tag has been defined multiple times for the same resource level */,
     OrthancPluginErrorCode_ForbiddenAccess = 45    /*!< Access to a resource is forbidden */,
     OrthancPluginErrorCode_DuplicateResource = 46    /*!< Duplicate resource */,
+    OrthancPluginErrorCode_IncompatibleConfigurations = 47    /*!< Your configuration file contains configuration that are mutually incompatible */,
     OrthancPluginErrorCode_SQLiteNotOpened = 1000    /*!< SQLite: The database is not opened */,
     OrthancPluginErrorCode_SQLiteAlreadyOpened = 1001    /*!< SQLite: Connection is already open */,
     OrthancPluginErrorCode_SQLiteCannotOpen = 1002    /*!< SQLite: Unable to open the database */,
@@ -273,7 +274,7 @@
     OrthancPluginErrorCode_SQLiteFlush = 1008    /*!< SQLite: Unable to flush the database */,
     OrthancPluginErrorCode_SQLiteCannotRun = 1009    /*!< SQLite: Cannot run a cached statement */,
     OrthancPluginErrorCode_SQLiteCannotStep = 1010    /*!< SQLite: Cannot step over a cached statement */,
-    OrthancPluginErrorCode_SQLiteBindOutOfRange = 1011    /*!< SQLite: Bing a value while out of range (serious error) */,
+    OrthancPluginErrorCode_SQLiteBindOutOfRange = 1011    /*!< SQLite: Bind a value while out of range (serious error) */,
     OrthancPluginErrorCode_SQLitePrepareStatement = 1012    /*!< SQLite: Cannot prepare a cached statement */,
     OrthancPluginErrorCode_SQLiteTransactionAlreadyStarted = 1013    /*!< SQLite: Beginning the same transaction twice */,
     OrthancPluginErrorCode_SQLiteTransactionCommit = 1014    /*!< SQLite: Failure when committing the transaction */,
@@ -323,6 +324,7 @@
     OrthancPluginErrorCode_AlreadyExistingTag = 2042    /*!< Cannot override the value of a tag that already exists */,
     OrthancPluginErrorCode_NoStorageCommitmentHandler = 2043    /*!< No request handler factory for DICOM N-ACTION SCP (storage commitment) */,
     OrthancPluginErrorCode_NoCGetHandler = 2044    /*!< No request handler factory for DICOM C-GET SCP */,
+    OrthancPluginErrorCode_DicomGetUnavailable = 2045    /*!< DicomUserConnection: The C-GET command is not supported by the remote SCP */,
     OrthancPluginErrorCode_UnsupportedMediaType = 3000    /*!< Unsupported media type */,
 
     _OrthancPluginErrorCode_INTERNAL = 0x7fffffff
@@ -506,6 +508,8 @@
     _OrthancPluginService_CompressAndAnswerImage = 2011,
     _OrthancPluginService_SendMultipartItem2 = 2012,
     _OrthancPluginService_SetHttpErrorDetails = 2013,
+    _OrthancPluginService_StartStreamAnswer = 2014,
+    _OrthancPluginService_SendStreamChunk = 2015,
 
     /* Access to the Orthanc database and API */
     _OrthancPluginService_GetDicomForInstance = 3000,
@@ -748,6 +752,7 @@
 
   /**
    * The supported types of changes that can be signaled to the change callback.
+   * Note: this enum is not used to store changes in the DB !
    * @ingroup Callbacks
    **/
   typedef enum
@@ -9566,6 +9571,66 @@
   }
 
 
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    const char*              contentType;
+  } _OrthancPluginStartStreamAnswer;
+
+  /**
+   * @brief Start an HTTP stream answer.
+   *
+   * Initiates an HTTP stream answer, as the result of a REST request.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param contentType The MIME type of the items in the stream answer.
+   * @return 0 if success, or the error code if failure.
+   * @see OrthancPluginSendStreamChunk()
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStartStreamAnswer(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              contentType)
+  {
+    _OrthancPluginStartStreamAnswer params;
+    params.output = output;
+    params.contentType = contentType;
+    return context->InvokeService(context, _OrthancPluginService_StartStreamAnswer, &params);
+  }
+
+
+  /**
+   * @brief Send a chunk as a part of an HTTP stream answer.
+   *
+   * This function sends a chunk as part of an HTTP stream
+   * answer that was initiated by OrthancPluginStartStreamAnswer().
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param answer Pointer to the memory buffer containing the item.
+   * @param answerSize Number of bytes of the item.
+   * @return 0 if success, or the error code if failure (this notably happens
+   * if the connection is closed by the client).
+   * @see OrthancPluginStartStreamAnswer()
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSendStreamChunk(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const void*              answer,
+    uint32_t                 answerSize)
+  {
+    _OrthancPluginAnswerBuffer params;
+    params.output = output;
+    params.answer = answer;
+    params.answerSize = answerSize;
+    params.mimeType = NULL;
+    return context->InvokeService(context, _OrthancPluginService_SendStreamChunk, &params);
+  }
+
+
 #ifdef  __cplusplus
 }
 #endif
--- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -78,6 +78,22 @@
   LABELS_CONSTRAINT_NONE = 2;
 }
 
+enum OrderingKeyType {
+  ORDERING_KEY_TYPE_DICOM_TAG = 0;
+  ORDERING_KEY_TYPE_METADATA = 1;
+}
+
+enum OrderingDirection {
+  ORDERING_DIRECTION_ASC = 0;
+  ORDERING_DIRECTION_DESC = 1;
+}
+
+enum OrderingCast {
+  ORDERING_CAST_STRING = 0;
+  ORDERING_CAST_INT = 1;
+  ORDERING_CAST_FLOAT = 2;
+}
+
 message ServerIndexChange {
   int64         seq = 1;
   int32         change_type = 2;   // opaque "ChangeType" in Orthanc
@@ -109,6 +125,13 @@
   repeated string  values = 8;
 }
 
+message DatabaseMetadataConstraint {
+  int32            metadata = 1;
+  bool             is_case_sensitive = 2;
+  bool             is_mandatory = 3;
+  ConstraintType   type = 4;
+  repeated string  values = 5;
+}
 
 /**
  * Database-level operations.
@@ -141,6 +164,8 @@
     bool supports_increment_global_property = 5;
     bool has_update_and_get_statistics = 6;
     bool has_measure_latency = 7;
+    bool supports_find = 8;         // New in Orthanc 1.12.5
+    bool has_extended_changes = 9;  // New in Orthanc 1.12.5
   }
 }
 
@@ -288,11 +313,14 @@
   OPERATION_GET_CHILDREN_METADATA = 42;
   OPERATION_GET_LAST_CHANGE_INDEX = 43;
   OPERATION_LOOKUP_RESOURCE_AND_PARENT = 44;
-  OPERATION_ADD_LABEL = 45;        // New in Orthanc 1.12.0
-  OPERATION_REMOVE_LABEL = 46;     // New in Orthanc 1.12.0
-  OPERATION_LIST_LABELS = 47;      // New in Orthanc 1.12.0
-  OPERATION_INCREMENT_GLOBAL_PROPERTY = 48;      // New in Orthanc 1.12.3
-  OPERATION_UPDATE_AND_GET_STATISTICS = 49;      // New in Orthanc 1.12.3
+  OPERATION_ADD_LABEL = 45;                   // New in Orthanc 1.12.0
+  OPERATION_REMOVE_LABEL = 46;                // New in Orthanc 1.12.0
+  OPERATION_LIST_LABELS = 47;                 // New in Orthanc 1.12.0
+  OPERATION_INCREMENT_GLOBAL_PROPERTY = 48;   // New in Orthanc 1.12.3
+  OPERATION_UPDATE_AND_GET_STATISTICS = 49;   // New in Orthanc 1.12.3
+  OPERATION_FIND = 50;                        // New in Orthanc 1.12.5
+  OPERATION_GET_CHANGES_EXTENDED = 51;        // New in Orthanc 1.12.5
+  OPERATION_COUNT_RESOURCES = 52;             // New in Orthanc 1.12.5
 }
 
 message Rollback {
@@ -413,6 +441,19 @@
   }
 }
 
+message GetChangesExtended {
+  message Request {
+    int64 since = 1;
+    int64 to = 2;
+    repeated int32 change_type = 3;
+    uint32 limit = 4;
+  }
+  message Response {
+    repeated ServerIndexChange changes = 1;
+    bool done = 2;
+  }
+}
+
 message GetChildrenInternalId {
   message Request {
     int64 id = 1;
@@ -824,6 +865,115 @@
   }
 }
 
+message Find {        // New in Orthanc 1.12.5
+  message Request {   // This corresponds to "FindRequest" in C++
+    message Tag {
+      uint32 group = 1;
+      uint32 element = 2;
+    }
+    message Limits {
+      uint64 since = 1;
+      uint64 count = 2;
+    }
+    message ParentSpecification {
+      bool retrieve_main_dicom_tags = 1;
+      bool retrieve_metadata = 2;
+    }
+    message ChildrenSpecification {
+      bool retrieve_identifiers = 1;
+      repeated int32 retrieve_metadata = 2;
+      repeated Tag retrieve_main_dicom_tags = 3;
+      bool retrieve_count = 4;
+    }
+    message Ordering {
+      OrderingKeyType key_type = 1;
+      OrderingDirection direction = 2;
+      OrderingCast cast = 3;
+      uint32 tag_group = 4;
+      uint32 tag_element = 5;
+      bool is_identifier_tag = 6;
+      ResourceType tag_level = 7;
+      int32 metadata = 8;
+    }
+
+    // Part 1 of the request: Constraints
+    ResourceType level = 1;
+    string orthanc_id_patient = 2;   // optional - GetOrthancIdentifiers().GetPatientId();
+    string orthanc_id_study = 3;     // optional - GetOrthancIdentifiers().GetStudyId();
+    string orthanc_id_series = 4;    // optional - GetOrthancIdentifiers().GetSeriesId();
+    string orthanc_id_instance = 5;  // optional - GetOrthancIdentifiers().GetInstanceId();
+    repeated DatabaseConstraint dicom_tag_constraints = 6;
+    Limits limits = 7;               // optional
+    repeated string labels = 8;
+    LabelsConstraintType labels_constraint = 9;
+    repeated Ordering ordering = 10;
+    repeated DatabaseMetadataConstraint metadata_constraints = 11;
+
+
+    // Part 2 of the request: What is to be retrieved
+    bool retrieve_main_dicom_tags = 100;
+    bool retrieve_metadata = 101;
+    bool retrieve_labels = 102;
+    bool retrieve_attachments = 103;
+    bool retrieve_parent_identifier = 104;
+    bool retrieve_one_instance_metadata_and_attachments = 105;
+    ParentSpecification parent_patient = 106;
+    ParentSpecification parent_study = 107;
+    ParentSpecification parent_series = 108;
+    ChildrenSpecification children_studies = 109;
+    ChildrenSpecification children_series = 110;
+    ChildrenSpecification children_instances = 111;
+  }
+
+  message Response {  // This corresponds to "FindResponse" in C++
+    message Tag {
+      uint32 group = 1;
+      uint32 element = 2;
+      string value = 3;
+    }
+    message Metadata {
+      int32 key = 1;
+      string value = 2;
+      int64 revision = 3;
+    }
+    message ResourceContent {
+      repeated Tag main_dicom_tags = 1;
+      repeated Metadata metadata = 2;
+    }
+    message ChildrenContent {
+      repeated string identifiers = 1;
+      repeated Tag main_dicom_tags = 2;
+      repeated Metadata metadata = 3;  // As of Orthanc 1.12.5, the "revision" field is unused in this case
+      uint64 count = 4;
+    }
+
+    int64 internal_id = 1;
+    string public_id = 2;
+    string parent_public_id = 3;   // optional
+    repeated string labels = 4;
+    repeated FileInfo attachments = 5;
+    ResourceContent patient_content = 6;
+    ResourceContent study_content = 7;
+    ResourceContent series_content = 8;
+    ResourceContent instance_content = 9;
+    ChildrenContent children_studies_content = 10;
+    ChildrenContent children_series_content = 11;
+    ChildrenContent children_instances_content = 12;
+    string one_instance_public_id = 13;
+    repeated Metadata one_instance_metadata = 14;
+    repeated FileInfo one_instance_attachments = 15;
+    repeated int64 attachments_revisions = 16;
+  }
+}
+
+message CountResources
+{
+  message Response
+  {
+    uint64 count = 1;
+  }
+}
+
 message TransactionRequest {
   sfixed64              transaction = 1;
   TransactionOperation  operation = 2;
@@ -878,6 +1028,9 @@
   ListLabels.Request                      list_labels = 147;
   IncrementGlobalProperty.Request         increment_global_property = 148;
   UpdateAndGetStatistics.Request          update_and_get_statistics = 149;
+  Find.Request                            find = 150;
+  GetChangesExtended.Request              get_changes_extended = 151;
+  Find.Request                            count_resources = 152;
 }
 
 message TransactionResponse {
@@ -931,6 +1084,9 @@
   ListLabels.Response                      list_labels = 147;
   IncrementGlobalProperty.Response         increment_global_property = 148;
   UpdateAndGetStatistics.Response          update_and_get_statistics = 149;
+  repeated Find.Response                   find = 150;   // One message per found resource
+  GetChangesExtended.Response              get_changes_extended = 151;
+  CountResources.Response                  count_resources = 152;
 }
 
 enum RequestType {
--- a/OrthancServer/Plugins/Samples/AutomatedJpeg2kCompression/CMakeLists.txt	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/AutomatedJpeg2kCompression/CMakeLists.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/AutomatedJpeg2kCompression/Plugin.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/AutomatedJpeg2kCompression/Plugin.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/Basic/CMakeLists.txt	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/Basic/CMakeLists.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/Basic/Plugin.c	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/Basic/Plugin.c	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -26,6 +26,7 @@
 #include <boost/algorithm/string/predicate.hpp>
 #include <boost/move/unique_ptr.hpp>
 #include <boost/thread.hpp>
+#include <boost/algorithm/string/join.hpp>
 
 
 #include <json/reader.h>
@@ -334,9 +335,9 @@
     std::vector<const char*> headersValues_;
 
   public:
-    explicit PluginHttpHeaders(const std::map<std::string, std::string>& httpHeaders)
-    {
-      for (std::map<std::string, std::string>::const_iterator
+    explicit PluginHttpHeaders(const HttpHeaders& httpHeaders)
+    {
+      for (HttpHeaders::const_iterator
              it = httpHeaders.begin(); it != httpHeaders.end(); ++it)
       {
         headersKeys_.push_back(it->first.c_str());
@@ -361,7 +362,7 @@
   };
 
   bool MemoryBuffer::RestApiGet(const std::string& uri,
-                                const std::map<std::string, std::string>& httpHeaders,
+                                const HttpHeaders& httpHeaders,
                                 bool applyPlugins)
   {
     Clear();
@@ -400,7 +401,7 @@
   bool MemoryBuffer::RestApiPost(const std::string& uri,
                                  const void* body,
                                  size_t bodySize,
-                                 const std::map<std::string, std::string>& httpHeaders,
+                                 const HttpHeaders& httpHeaders,
                                  bool applyPlugins)
   {
     MemoryBuffer answerHeaders;
@@ -422,7 +423,7 @@
 
   bool MemoryBuffer::RestApiPost(const std::string& uri,
                                  const Json::Value& body,
-                                 const std::map<std::string, std::string>& httpHeaders,
+                                 const HttpHeaders& httpHeaders,
                                  bool applyPlugins)
   {
     std::string s;
@@ -1490,7 +1491,7 @@
 
   bool RestApiGetString(std::string& result,
                         const std::string& uri,
-                        const std::map<std::string, std::string>& httpHeaders,
+                        const HttpHeaders& httpHeaders,
                         bool applyPlugins)
   {
     MemoryBuffer answer;
@@ -1508,7 +1509,7 @@
 
   bool RestApiGet(Json::Value& result,
                   const std::string& uri,
-                  const std::map<std::string, std::string>& httpHeaders,
+                  const HttpHeaders& httpHeaders,
                   bool applyPlugins)
   {
     MemoryBuffer answer;
@@ -1598,7 +1599,7 @@
   bool RestApiPost(Json::Value& result,
                    const std::string& uri,
                    const Json::Value& body,
-                   const std::map<std::string, std::string>& httpHeaders,
+                   const HttpHeaders& httpHeaders,
                    bool applyPlugins)
   {
     MemoryBuffer answer;
@@ -1963,7 +1964,7 @@
   bool OrthancPeers::DoGet(MemoryBuffer& target,
                            size_t index,
                            const std::string& uri,
-                           const std::map<std::string, std::string>& headers) const
+                           const HttpHeaders& headers) const
   {
     if (index >= index_.size())
     {
@@ -1994,7 +1995,7 @@
   bool OrthancPeers::DoGet(MemoryBuffer& target,
                            const std::string& name,
                            const std::string& uri,
-                           const std::map<std::string, std::string>& headers) const
+                           const HttpHeaders& headers) const
   {
     size_t index;
     return (LookupName(index, name) &&
@@ -2005,7 +2006,7 @@
   bool OrthancPeers::DoGet(Json::Value& target,
                            size_t index,
                            const std::string& uri,
-                           const std::map<std::string, std::string>& headers) const
+                           const HttpHeaders& headers) const
   {
     MemoryBuffer buffer;
 
@@ -2024,7 +2025,7 @@
   bool OrthancPeers::DoGet(Json::Value& target,
                            const std::string& name,
                            const std::string& uri,
-                           const std::map<std::string, std::string>& headers) const
+                           const HttpHeaders& headers) const
   {
     MemoryBuffer buffer;
 
@@ -2044,7 +2045,7 @@
                             const std::string& name,
                             const std::string& uri,
                             const std::string& body,
-                            const std::map<std::string, std::string>& headers) const
+                            const HttpHeaders& headers) const
   {
     size_t index;
     return (LookupName(index, name) &&
@@ -2056,7 +2057,7 @@
                             size_t index,
                             const std::string& uri,
                             const std::string& body,
-                            const std::map<std::string, std::string>& headers) const
+                            const HttpHeaders& headers) const
   {
     MemoryBuffer buffer;
 
@@ -2076,7 +2077,7 @@
                             const std::string& name,
                             const std::string& uri,
                             const std::string& body,
-                            const std::map<std::string, std::string>& headers) const
+                            const HttpHeaders& headers) const
   {
     MemoryBuffer buffer;
 
@@ -2096,7 +2097,7 @@
                             size_t index,
                             const std::string& uri,
                             const std::string& body,
-                            const std::map<std::string, std::string>& headers) const
+                            const HttpHeaders& headers) const
   {
     if (index >= index_.size())
     {
@@ -2133,7 +2134,7 @@
   bool OrthancPeers::DoPut(size_t index,
                            const std::string& uri,
                            const std::string& body,
-                           const std::map<std::string, std::string>& headers) const
+                           const HttpHeaders& headers) const
   {
     if (index >= index_.size())
     {
@@ -2169,7 +2170,7 @@
   bool OrthancPeers::DoPut(const std::string& name,
                            const std::string& uri,
                            const std::string& body,
-                           const std::map<std::string, std::string>& headers) const
+                           const HttpHeaders& headers) const
   {
     size_t index;
     return (LookupName(index, name) &&
@@ -2179,7 +2180,7 @@
 
   bool OrthancPeers::DoDelete(size_t index,
                               const std::string& uri,
-                              const std::map<std::string, std::string>& headers) const
+                              const HttpHeaders& headers) const
   {
     if (index >= index_.size())
     {
@@ -2208,7 +2209,7 @@
 
   bool OrthancPeers::DoDelete(const std::string& name,
                               const std::string& uri,
-                              const std::map<std::string, std::string>& headers) const
+                              const HttpHeaders& headers) const
   {
     size_t index;
     return (LookupName(index, name) &&
@@ -2923,12 +2924,12 @@
       std::vector<const char*>  headersValues_;
 
     public:
-      HeadersWrapper(const HttpClient::HttpHeaders& headers)
+      HeadersWrapper(const HttpHeaders& headers)
       {
         headersKeys_.reserve(headers.size());
         headersValues_.reserve(headers.size());
 
-        for (HttpClient::HttpHeaders::const_iterator it = headers.begin(); it != headers.end(); ++it)
+        for (HttpHeaders::const_iterator it = headers.begin(); it != headers.end(); ++it)
         {
           headersKeys_.push_back(it->first.c_str());
           headersValues_.push_back(it->second.c_str());
@@ -3076,11 +3077,11 @@
     class MemoryAnswer : public HttpClient::IAnswer
     {
     private:
-      HttpClient::HttpHeaders  headers_;
-      ChunkedBuffer            body_;
+      HttpHeaders    headers_;
+      ChunkedBuffer  body_;
 
     public:
-      const HttpClient::HttpHeaders& GetHeaders() const
+      const HttpHeaders& GetHeaders() const
       {
         return headers_;
       }
@@ -3168,6 +3169,35 @@
 #endif    
 
 
+  static void DecodeHttpHeaders(HttpHeaders& target,
+                                const MemoryBuffer& source)
+  {
+    Json::Value v;
+    source.ToJson(v);
+
+    if (v.type() != Json::objectValue)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+
+    Json::Value::Members members = v.getMemberNames();
+    target.clear();
+
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      const Json::Value& h = v[members[i]];
+      if (h.type() != Json::stringValue)
+      {
+        ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+      }
+      else
+      {
+        target[members[i]] = h.asString();
+      }
+    }
+  }
+
+
   void HttpClient::ExecuteWithoutStream(uint16_t& httpStatus,
                                         HttpHeaders& answerHeaders,
                                         std::string& answerBody,
@@ -3208,30 +3238,7 @@
       ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(error);
     }
 
-    Json::Value v;
-    answerHeadersBuffer.ToJson(v);
-
-    if (v.type() != Json::objectValue)
-    {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
-    }
-
-    Json::Value::Members members = v.getMemberNames();
-    answerHeaders.clear();
-
-    for (size_t i = 0; i < members.size(); i++)
-    {
-      const Json::Value& h = v[members[i]];
-      if (h.type() != Json::stringValue)
-      {
-        ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
-      }
-      else
-      {
-        answerHeaders[members[i]] = h.asString();
-      }
-    }
-
+    DecodeHttpHeaders(answerHeaders, answerHeadersBuffer);
     answerBodyBuffer.ToString(answerBody);
   }
 
@@ -4061,7 +4068,7 @@
   }
 #endif
 
-  void GetHttpHeaders(std::map<std::string, std::string>& result, const OrthancPluginHttpRequest* request)
+  void GetHttpHeaders(HttpHeaders& result, const OrthancPluginHttpRequest* request)
   {
     result.clear();
 
@@ -4071,6 +4078,26 @@
     }    
   }
 
+  void SerializeGetArguments(std::string& output, const OrthancPluginHttpRequest* request)
+  {
+    output.clear();
+    std::vector<std::string> arguments;
+    for (uint32_t i = 0; i < request->getCount; ++i)
+    {
+      if (request->getValues[i] && strlen(request->getValues[i]) > 0)
+      {
+        arguments.push_back(std::string(request->getKeys[i]) + "=" + std::string(request->getValues[i]));
+      }
+      else
+      {
+        arguments.push_back(std::string(request->getKeys[i]));
+      }
+    }
+
+    output = boost::algorithm::join(arguments, "&");
+  }
+
+
 #if !ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 4)
   static void SetPluginProperty(const std::string& pluginIdentifier,
                                 _OrthancPluginProperty property,
@@ -4114,4 +4141,179 @@
     SetPluginProperty(pluginIdentifier, _OrthancPluginProperty_OrthancExplorer, javascript);
 #endif
   }
+
+
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+  RestApiClient::RestApiClient() :
+    method_(OrthancPluginHttpMethod_Get),
+    path_("/"),
+    afterPlugins_(false),
+    httpStatus_(0)
+  {
+  }
+
+  RestApiClient::RestApiClient(const char* url,
+                               const OrthancPluginHttpRequest* request) :
+    method_(request->method),
+    path_(url),
+    afterPlugins_(false),
+    httpStatus_(0)
+  {
+    OrthancPlugins::GetHttpHeaders(requestHeaders_, request);
+
+    std::string getArguments;
+    OrthancPlugins::SerializeGetArguments(getArguments, request);
+
+    if (!getArguments.empty())
+    {
+      path_ += "?" + getArguments;
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+  void RestApiClient::AddRequestHeader(const std::string& key,
+                                       const std::string& value)
+  {
+    if (requestHeaders_.find(key) == requestHeaders_.end())
+    {
+      requestHeaders_[key] = value;
+    }
+    else
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls);
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+  bool RestApiClient::Execute()
+  {
+    if (requestBody_.size() > 0xffffffffu)
+    {
+      ORTHANC_PLUGINS_LOG_ERROR("Cannot handle body size > 4GB");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+
+    PluginHttpHeaders converted(requestHeaders_);
+
+    MemoryBuffer body;
+    MemoryBuffer headers;
+
+    OrthancPluginErrorCode code = OrthancPluginCallRestApi(GetGlobalContext(), *body, *headers, &httpStatus_, method_, path_.c_str(),
+                                                           requestHeaders_.size(), converted.GetKeys(), converted.GetValues(),
+                                                           requestBody_.c_str(), requestBody_.size(), afterPlugins_ ? 1 : 0);
+
+    answerHeaders_.clear();
+    answerBody_.clear();
+
+    if (code == OrthancPluginErrorCode_Success)
+    {
+      if (httpStatus_ == 0)
+      {
+        ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+      }
+
+      DecodeHttpHeaders(answerHeaders_, headers);
+      body.ToString(answerBody_);
+      return true;
+    }
+    else
+    {
+      if (code == OrthancPluginErrorCode_UnknownResource ||
+          code == OrthancPluginErrorCode_InexistentItem)
+      {
+        httpStatus_ = 404;
+        return false;
+      }
+      else
+      {
+        ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+      }
+    }
+  }
+
+  void RestApiClient::Forward(OrthancPluginContext* context, OrthancPluginRestOutput* output)
+  {
+    if (Execute() && httpStatus_ == 200)
+    {
+      const char* mimeType = NULL;
+      for (HttpHeaders::const_iterator h = answerHeaders_.begin(); h != answerHeaders_.end(); ++h)
+      {
+        if (h->first == "content-type")
+        {
+          mimeType = h->second.c_str();
+        }
+      }
+      
+      AnswerString(answerBody_, mimeType, output);
+    }
+    else
+    {
+      AnswerHttpError(httpStatus_, output);
+    }
+  }
+
+  bool RestApiClient::GetAnswerJson(Json::Value& output) const
+  {
+    return ReadJson(output, answerBody_);
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+  uint16_t RestApiClient::GetHttpStatus() const
+  {
+    if (httpStatus_ == 0)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls);
+    }
+    else
+    {
+      return httpStatus_;
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+  bool RestApiClient::LookupAnswerHeader(std::string& value,
+                                         const std::string& key) const
+  {
+    if (httpStatus_ == 0)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls);
+    }
+    else
+    {
+      HttpHeaders::const_iterator found = answerHeaders_.find(key);
+      if (found == answerHeaders_.end())
+      {
+        return false;
+      }
+      else
+      {
+        value = found->second;
+        return true;
+      }
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+  const std::string& RestApiClient::GetAnswerBody() const
+  {
+    if (httpStatus_ == 0)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls);
+    }
+    else
+    {
+      return answerBody_;
+    }
+  }
+#endif
 }
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -170,6 +170,8 @@
 
 namespace OrthancPlugins
 {
+  typedef std::map<std::string, std::string>  HttpHeaders;
+
   typedef void (*RestCallback) (OrthancPluginRestOutput* output,
                                 const char* url,
                                 const OrthancPluginHttpRequest* request);
@@ -257,7 +259,7 @@
                     bool applyPlugins);
 
     bool RestApiGet(const std::string& uri,
-                    const std::map<std::string, std::string>& httpHeaders,
+                    const HttpHeaders& httpHeaders,
                     bool applyPlugins);
 
     bool RestApiPost(const std::string& uri,
@@ -277,13 +279,13 @@
 #if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
     bool RestApiPost(const std::string& uri,
                      const Json::Value& body,
-                     const std::map<std::string, std::string>& httpHeaders,
+                     const HttpHeaders& httpHeaders,
                      bool applyPlugins);
 
     bool RestApiPost(const std::string& uri,
                      const void* body,
                      size_t bodySize,
-                     const std::map<std::string, std::string>& httpHeaders,
+                     const HttpHeaders& httpHeaders,
                      bool applyPlugins);
 #endif
 
@@ -581,7 +583,7 @@
 
   bool RestApiGet(Json::Value& result,
                   const std::string& uri,
-                  const std::map<std::string, std::string>& httpHeaders,
+                  const HttpHeaders& httpHeaders,
                   bool applyPlugins);
 
   bool RestApiGetString(std::string& result,
@@ -590,7 +592,7 @@
 
   bool RestApiGetString(std::string& result,
                         const std::string& uri,
-                        const std::map<std::string, std::string>& httpHeaders,
+                        const HttpHeaders& httpHeaders,
                         bool applyPlugins);
 
   bool RestApiPost(std::string& result,
@@ -609,7 +611,7 @@
   bool RestApiPost(Json::Value& result,
                    const std::string& uri,
                    const Json::Value& body,
-                   const std::map<std::string, std::string>& httpHeaders,
+                   const HttpHeaders& httpHeaders,
                    bool applyPlugins);
 #endif
 
@@ -829,64 +831,64 @@
     bool DoGet(MemoryBuffer& target,
                size_t index,
                const std::string& uri,
-               const std::map<std::string, std::string>& headers) const;
+               const HttpHeaders& headers) const;
 
     bool DoGet(MemoryBuffer& target,
                const std::string& name,
                const std::string& uri,
-               const std::map<std::string, std::string>& headers) const;
+               const HttpHeaders& headers) const;
 
     bool DoGet(Json::Value& target,
                size_t index,
                const std::string& uri,
-               const std::map<std::string, std::string>& headers) const;
+               const HttpHeaders& headers) const;
 
     bool DoGet(Json::Value& target,
                const std::string& name,
                const std::string& uri,
-               const std::map<std::string, std::string>& headers) const;
+               const HttpHeaders& headers) const;
 
     bool DoPost(MemoryBuffer& target,
                 size_t index,
                 const std::string& uri,
                 const std::string& body,
-                const std::map<std::string, std::string>& headers) const;
+                const HttpHeaders& headers) const;
 
     bool DoPost(MemoryBuffer& target,
                 const std::string& name,
                 const std::string& uri,
                 const std::string& body,
-                const std::map<std::string, std::string>& headers) const;
+                const HttpHeaders& headers) const;
 
     bool DoPost(Json::Value& target,
                 size_t index,
                 const std::string& uri,
                 const std::string& body,
-                const std::map<std::string, std::string>& headers) const;
+                const HttpHeaders& headers) const;
 
     bool DoPost(Json::Value& target,
                 const std::string& name,
                 const std::string& uri,
                 const std::string& body,
-                const std::map<std::string, std::string>& headers) const;
+                const HttpHeaders& headers) const;
 
     bool DoPut(size_t index,
                const std::string& uri,
                const std::string& body,
-               const std::map<std::string, std::string>& headers) const;
+               const HttpHeaders& headers) const;
 
     bool DoPut(const std::string& name,
                const std::string& uri,
                const std::string& body,
-               const std::map<std::string, std::string>& headers) const;
+               const HttpHeaders& headers) const;
 
     bool DoDelete(size_t index,
                   const std::string& uri,
-                  const std::map<std::string, std::string>& headers) const;
+                  const HttpHeaders& headers) const;
 
     bool DoDelete(const std::string& name,
                   const std::string& uri,
-                  const std::map<std::string, std::string>& headers) const;
+                  const HttpHeaders& headers) const;
   };
 #endif
 
@@ -996,8 +998,6 @@
   class HttpClient : public boost::noncopyable
   {
   public:
-    typedef std::map<std::string, std::string>  HttpHeaders;
-
     class IRequestBody : public boost::noncopyable
     {
     public:
@@ -1397,7 +1397,10 @@
   };
 
 // helper method to convert Http headers from the plugin SDK to a std::map
-void GetHttpHeaders(std::map<std::string, std::string>& result, const OrthancPluginHttpRequest* request);
+void GetHttpHeaders(HttpHeaders& result, const OrthancPluginHttpRequest* request);
+
+// helper method to re-serialize the get arguments from the SDK into a string
+void SerializeGetArguments(std::string& output, const OrthancPluginHttpRequest* request);
 
 #if HAS_ORTHANC_PLUGIN_WEBDAV == 1
   class IWebDavCollection : public boost::noncopyable
@@ -1508,4 +1511,97 @@
 
   void ExtendOrthancExplorer(const std::string& pluginIdentifier,
                              const std::string& javascript);
+
+
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+  class RestApiClient : public boost::noncopyable
+  {
+  private:
+    // Request
+    OrthancPluginHttpMethod  method_;
+    std::string              path_;
+    HttpHeaders              requestHeaders_;
+    std::string              requestBody_;
+    bool                     afterPlugins_;
+
+    // Answer
+    uint16_t                 httpStatus_;
+    HttpHeaders              answerHeaders_;
+    std::string              answerBody_;
+
+  public:
+    RestApiClient();
+    
+    // used to forward a call from the plugin to the core
+    RestApiClient(const char* url,
+                  const OrthancPluginHttpRequest* request);
+
+    void SetMethod(OrthancPluginHttpMethod method)
+    {
+      method_ = method;
+    }
+
+    OrthancPluginHttpMethod GetMethod() const
+    {
+      return method_;
+    }
+
+    void SetPath(const std::string& path)
+    {
+      path_ = path;
+    }
+
+    const std::string& GetPath() const
+    {
+      return path_;
+    }
+
+    void AddRequestHeader(const std::string& key,
+                          const std::string& value);
+
+    const HttpHeaders& GetRequestHeaders() const
+    {
+      return requestHeaders_;
+    }
+
+    void SetRequestBody(const std::string& body)
+    {
+      requestBody_ = body;
+    }
+
+    void SwapRequestBody(std::string& body)
+    {
+      requestBody_.swap(body);
+    }
+
+    void SetAfterPlugins(bool afterPlugins)
+    {
+      afterPlugins_ = afterPlugins;
+    }
+
+    bool IsAfterPlugins() const
+    {
+      return afterPlugins_;
+    }
+
+    const std::string& GetRequestBody() const
+    {
+      return requestBody_;
+    }
+
+    bool Execute();
+
+    // Execute and forward the response as is
+    void Forward(OrthancPluginContext* context, OrthancPluginRestOutput* output);
+
+    uint16_t GetHttpStatus() const;
+
+    bool LookupAnswerHeader(std::string& value,
+                            const std::string& key) const;
+
+    const std::string& GetAnswerBody() const;
+
+    bool GetAnswerJson(Json::Value& output) const;
+  };
+#endif
 }
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginException.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginException.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/Common/OrthancPlugins.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPlugins.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginsExports.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginsExports.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/ConnectivityChecks/CMakeLists.txt	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/ConnectivityChecks/CMakeLists.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/ConnectivityChecks/JavaScriptLibraries.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/ConnectivityChecks/JavaScriptLibraries.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/ConnectivityChecks/OrthancFrameworkDependencies.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/ConnectivityChecks/OrthancFrameworkDependencies.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/ConnectivityChecks/Plugin.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/ConnectivityChecks/Plugin.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/ConnectivityChecks/WebResources/app.js	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/ConnectivityChecks/WebResources/app.js	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/CustomImageDecoder/CMakeLists.txt	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/CustomImageDecoder/CMakeLists.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/CustomImageDecoder/Plugin.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/CustomImageDecoder/Plugin.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/CMakeLists.txt	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/DelayedDeletion/CMakeLists.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
@@ -60,7 +60,6 @@
   -DORTHANC_PLUGIN_VERSION="${PLUGIN_VERSION}"
   -DORTHANC_ENABLE_LOGGING=1
   -DORTHANC_ENABLE_PLUGINS=1
-  -DORTHANC_BUILDING_SERVER_LIBRARY=0
   )
 
 include_directories(
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/LargeDeleteJob.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/DelayedDeletion/LargeDeleteJob.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/LargeDeleteJob.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/DelayedDeletion/LargeDeleteJob.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/OrthancFrameworkDependencies.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/DelayedDeletion/OrthancFrameworkDependencies.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/PendingDeletionsDatabase.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/DelayedDeletion/PendingDeletionsDatabase.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/PendingDeletionsDatabase.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/DelayedDeletion/PendingDeletionsDatabase.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/Plugin.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/DelayedDeletion/Plugin.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -549,39 +549,46 @@
       const Json::Value& change = changes["Changes"][i];
       int64_t seq = change["Seq"].asInt64();
 
-      if (!limitToChange_.empty()) // if updating only maindicomtags for a single level 
+      try
       {
-        if (change["ChangeType"] == limitToChange_)
+        if (!limitToChange_.empty()) // if updating only maindicomtags for a single level 
+        {
+          if (change["ChangeType"] == limitToChange_)
+          {
+            Json::Value result;
+            Json::Value request;
+            request["ReconstructFiles"] = false;
+            request["LimitToThisLevelMainDicomTags"] = true;
+            OrthancPlugins::RestApiPost(result, "/" + limitToUrl_ + "/" + change["ID"].asString() + "/reconstruct", request, false);
+          }
+        }
+        else
         {
-          Json::Value result;
-          Json::Value request;
-          request["ReconstructFiles"] = false;
-          request["LimitToThisLevelMainDicomTags"] = true;
-          OrthancPlugins::RestApiPost(result, "/" + limitToUrl_ + "/" + change["ID"].asString() + "/reconstruct", request, false);
+          if (change["ChangeType"] == "NewStudy") // some StableStudy might be missing if orthanc was shutdown during a StableAge -> consider only the NewStudy events that can not be missed
+          {
+            Json::Value result;
+
+            if (needsReconstruct || needsReingest ||force_)
+            {
+              Json::Value request;
+              if (needsReingest)
+              {
+                request["ReconstructFiles"] = true;
+              }
+              OrthancPlugins::RestApiPost(result, "/studies/" + change["ID"].asString() + "/reconstruct", request, false);
+            }
+
+            if (needsDicomWebCaching)
+            {
+              Json::Value request;
+              OrthancPlugins::RestApiPost(result, "/studies/" + change["ID"].asString() + "/update-dicomweb-cache", request, true);
+            }
+          }
         }
       }
-      else
+      catch (...)
       {
-        if (change["ChangeType"] == "NewStudy") // some StableStudy might be missing if orthanc was shutdown during a StableAge -> consider only the NewStudy events that can not be missed
-        {
-          Json::Value result;
-
-          if (needsReconstruct || needsReingest)
-          {
-            Json::Value request;
-            if (needsReingest)
-            {
-              request["ReconstructFiles"] = true;
-            }
-            OrthancPlugins::RestApiPost(result, "/studies/" + change["ID"].asString() + "/reconstruct", request, false);
-          }
-
-          if (needsDicomWebCaching)
-          {
-            Json::Value request;
-            OrthancPlugins::RestApiPost(result, "/studies/" + change["ID"].asString() + "/update-dicomweb-cache", request, true);
-          }
-        }
+        ORTHANC_PLUGINS_LOG_ERROR("Housekeeper: unhandled error while processing change " + boost::lexical_cast<std::string>(seq) + ", skipping resource.");
       }
 
       {
@@ -703,9 +710,17 @@
   {
     if (runningPeriods_.isInPeriod())
     {
-      completed = ProcessChanges(needsReconstruct, needsReingest, needsDicomWebCaching, currentDbConfiguration);
-      SaveStatusInDb();
-      
+      try
+      {
+        completed = ProcessChanges(needsReconstruct, needsReingest, needsDicomWebCaching, currentDbConfiguration);
+        SaveStatusInDb();
+      }
+      catch (...)
+      {
+        ORTHANC_PLUGINS_LOG_ERROR("Housekeeper: unhandled error while processing change " + boost::lexical_cast<std::string>(pluginStatus_.lastProcessedChange) +
+                                 " / " + boost::lexical_cast<std::string>(pluginStatus_.lastChangeToProcess));
+      }
+
       if (!completed)
       {
         boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);
@@ -859,12 +874,15 @@
               "StorageCompressionChange": true,
               "MainDicomTagsChange": true,
               "UnnecessaryDicomAsJsonFiles": true,
+              "IngestTranscodingChange": true,
               "DicomWebCacheChange": true   // new in 1.12.2
             },
 
-            // When rebuilding MainDicomTags, limit to a single level of resource.
-            // Allowed values: "Patient", "Study", "Series", "Instance"
-            "LimitMainDicomTagsReconstructLevel": "Study"
+            // When rebuilding MainDicomTags, limit to a single level of resource
+            // which can greatly improve performances e.g. if you have only updated 
+            // the Study level ExtraMainDicomTags.
+            // Allowed values: "Patient", "Study", "Series", "Instance", "All"
+            "LimitMainDicomTagsReconstructLevel": "All"
 
           }
         }
@@ -887,11 +905,12 @@
         triggerOnDicomWebCacheChange_ = triggers.GetBooleanValue("DicomWebCacheChange", true);
       }
 
-      limitMainDicomTagsReconstructLevel_ = housekeeper.GetStringValue("LimitMainDicomTagsReconstructLevel", "");
+      limitMainDicomTagsReconstructLevel_ = housekeeper.GetStringValue("LimitMainDicomTagsReconstructLevel", "All");
       if (limitMainDicomTagsReconstructLevel_ != "Patient" && limitMainDicomTagsReconstructLevel_ != "Study"
-        && limitMainDicomTagsReconstructLevel_ != "Series" && limitMainDicomTagsReconstructLevel_ != "Instance")
+        && limitMainDicomTagsReconstructLevel_ != "Series" && limitMainDicomTagsReconstructLevel_ != "Instance" && limitMainDicomTagsReconstructLevel_ != "All")
       {
         ORTHANC_PLUGINS_LOG_ERROR("Housekeeper invalid value for 'LimitMainDicomTagsReconstructLevel': '" + limitMainDicomTagsReconstructLevel_ + "'");
+        return -1;
       }
       else if (limitMainDicomTagsReconstructLevel_ == "Patient")
       {
--- a/OrthancServer/Plugins/Samples/ModalityWorklists/CMakeLists.txt	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/ModalityWorklists/CMakeLists.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/ModalityWorklists/Plugin.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/ModalityWorklists/Plugin.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/CMakeLists.txt	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/CMakeLists.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -29,10 +29,11 @@
 #include "../../../../OrthancFramework/Sources/OrthancException.h"
 
 #include "../Common/OrthancPluginCppWrapper.h"
-
+#include <dcmtk/dcmdata/dcuid.h>        /* for variable dcmAllStorageSOPClassUIDs */
 
 DicomFilter::DicomFilter() :
-  hasAcceptedTransferSyntaxes_(false)
+  hasAcceptedTransferSyntaxes_(false),
+  hasAcceptedStorageClasses_(false)
 {
   {
     OrthancPlugins::OrthancConfiguration config;
@@ -200,6 +201,18 @@
 }
 
 
+void DicomFilter::GetProposedStorageTransferSyntaxes(std::list<Orthanc::DicomTransferSyntax>& target,
+                                                     const std::string& remoteIp,
+                                                     const std::string& remoteAet,
+                                                     const std::string& calledAet)
+{
+  // default TS
+  target.push_back(Orthanc::DicomTransferSyntax_LittleEndianExplicit);
+  target.push_back(Orthanc::DicomTransferSyntax_LittleEndianImplicit);
+  target.push_back(Orthanc::DicomTransferSyntax_BigEndianExplicit);
+}
+
+
 bool DicomFilter::IsUnknownSopClassAccepted(const std::string& remoteIp,
                                             const std::string& remoteAet,
                                             const std::string& calledAet)
@@ -207,3 +220,46 @@
   boost::shared_lock<boost::shared_mutex>  lock(mutex_);
   return unknownSopClassAccepted_;
 }
+
+
+void DicomFilter::GetAcceptedSopClasses(std::set<std::string>& sopClasses, size_t maxCount)
+{
+  boost::unique_lock<boost::shared_mutex>  lock(mutex_);
+
+  if (!hasAcceptedStorageClasses_)
+  {
+    Json::Value jsonSopClasses;
+
+    if (!OrthancPlugins::RestApiGet(jsonSopClasses, "/tools/accepted-sop-classes", false) ||
+        jsonSopClasses.type() != Json::arrayValue)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+    else
+    {
+      for (Json::Value::ArrayIndex i = 0; i < jsonSopClasses.size(); i++)
+      {
+        if (jsonSopClasses[i].type() != Json::stringValue)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+        else
+        {
+          acceptedStorageClasses_.insert(jsonSopClasses[i].asString());
+        }
+      }
+    }
+
+    hasAcceptedStorageClasses_ = true;
+  }
+
+  std::set<std::string>::const_iterator it = acceptedStorageClasses_.begin();
+    size_t count = 0;
+
+  while (it != acceptedStorageClasses_.end() && (maxCount == 0 || count < maxCount))
+  {
+    sopClasses.insert(*it);
+    count++;
+    ++it;
+  }
+}
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -44,6 +44,8 @@
 
   bool hasAcceptedTransferSyntaxes_;
   std::set<Orthanc::DicomTransferSyntax>  acceptedTransferSyntaxes_;
+  bool hasAcceptedStorageClasses_;
+  std::set<std::string>                   acceptedStorageClasses_;
 
 public:
   DicomFilter();
@@ -62,7 +64,15 @@
                                            const std::string& remoteAet,
                                            const std::string& calledAet) ORTHANC_OVERRIDE;
 
+  virtual void GetProposedStorageTransferSyntaxes(std::list<Orthanc::DicomTransferSyntax>& target,
+                                                  const std::string& remoteIp,
+                                                  const std::string& remoteAet,
+                                                  const std::string& calledAet) ORTHANC_OVERRIDE;
+
+
   virtual bool IsUnknownSopClassAccepted(const std::string& remoteIp,
                                          const std::string& remoteAet,
                                          const std::string& calledAet) ORTHANC_OVERRIDE;
+  
+  virtual void GetAcceptedSopClasses(std::set<std::string>& sopClasses, size_t maxCount) ORTHANC_OVERRIDE;
 };
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/FindRequestHandler.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/FindRequestHandler.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/FindRequestHandler.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/FindRequestHandler.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/MoveRequestHandler.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/MoveRequestHandler.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/MoveRequestHandler.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/MoveRequestHandler.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/OrthancFrameworkDependencies.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/OrthancFrameworkDependencies.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/Plugin.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/Plugin.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/PluginEnumerations.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/PluginEnumerations.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/PluginToolbox.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/PluginToolbox.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/PluginToolbox.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/PluginToolbox.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/StoreRequestHandler.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/StoreRequestHandler.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/StoreRequestHandler.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/StoreRequestHandler.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/Sanitizer/CMakeLists.txt	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/Sanitizer/CMakeLists.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/Sanitizer/Plugin.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/Sanitizer/Plugin.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/ServeFolders/CMakeLists.txt	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/ServeFolders/CMakeLists.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/ServeFolders/Plugin.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/ServeFolders/Plugin.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/StorageArea/CMakeLists.txt	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/StorageArea/CMakeLists.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/StorageArea/Plugin.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/StorageArea/Plugin.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/StorageCommitmentScp/CMakeLists.txt	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/StorageCommitmentScp/CMakeLists.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/StorageCommitmentScp/Plugin.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/StorageCommitmentScp/Plugin.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/WebDavFilesystem/CMakeLists.txt	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/WebDavFilesystem/CMakeLists.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/WebDavFilesystem/Plugin.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/WebDavFilesystem/Plugin.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/WebSkeleton/CMakeLists.txt	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/WebSkeleton/CMakeLists.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/WebSkeleton/Configuration.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/WebSkeleton/Configuration.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/WebSkeleton/Framework/EmbedResources.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/WebSkeleton/Framework/EmbedResources.py	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/WebSkeleton/Framework/Framework.cmake	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/WebSkeleton/Framework/Framework.cmake	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Plugins/Samples/WebSkeleton/Framework/Plugin.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Plugins/Samples/WebSkeleton/Framework/Plugin.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Configuration.json	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Configuration.json	Tue Mar 18 13:37:18 2025 +0100
@@ -202,9 +202,47 @@
   // SOP classes (aka. "promiscuous mode")
   "UnknownSopClassAccepted" : false,
 
+  // The list of accepted Storage SOP classes.
+  // If empty or not defined, this list defaults
+  // to all storage classes defined in DCMTK in case of 
+  // C-STORE SCP and to a reduced list of 120 common storage
+  // classes in case of C-GET SCU.
+  // List of DCMTK default Storage SOP Classes:
+  // https://github.com/DCMTK/dcmtk/blob/410ffe2019b9db6a8f4036daac742a6f5e4d36c2/dcmdata/libsrc/dcuid.cc#L664
+  // Each entry can contain wildcards ("?" or "*") to add
+  // subsets of SOP classes that are defined in DCMTK defaults.
+  // "?" accepts any single character at that position.
+  // "*" accepts any string value at that position.
+  // If you want to add a a SOP class that is not defined in
+  // DCMTK defaults, you must add it explicitly.
+  // (new in Orthanc 1.12.6)
+  // Example to add a non standard class
+  // and keep the default ones:
+  // "AcceptedSopClasses" : [
+  //   "1.3.12.2.1107.5.9.1",
+  //   "1.2.840.*"
+  // ]
+  // Example to limit to 2 SOP Classes:
+  // "AcceptedSopClasses" : [
+  //   "1.2.840.10008.5.1.4.1.1.2",
+  //   "1.2.840.10008.5.1.4.1.1.4"
+  // ]
+
+  // The list of rejected Storage SOP classes.
+  // This configuration is only meaningful if
+  // "AcceptedSopClasses" is using regular expressions
+  // or if it is not defined.
+  // Each entry can contain wildcards ("?" or "*").
+  // (new in Orthanc 1.12.6)
+  // "RejectedSopClasses" : [
+  //   "1.2.840.10008.5.1.4.1.1.2",
+  //   "1.2.840.10008.5.1.4.1.1.4"
+  // ]
+
   // Set the timeout (in seconds) after which the DICOM associations
   // are closed by the Orthanc SCP (server) if no further DIMSE
   // command is received from the SCU (client).
+  // A value of 0 means "no timeout".
   "DicomScpTimeout" : 30,
 
 
@@ -311,6 +349,8 @@
   // to "true" (resp. "false") corresponds to "--require-peer-cert"
   // (resp. "--ignore-peer-cert") in the DCMTK command-line
   // tools. (new in Orthanc 1.9.3)
+  // Once you set this configuration to true, you must provide a list of
+  // trusted certificates in DicomTlsTrustedCertificates.
   "DicomTlsRemoteCertificateRequired" : true,
 
   // Sets the minimum accepted TLS protocol version for the DICOM server
@@ -456,6 +496,10 @@
      * for Orthanc when initiating an SCU to this very specific
      * modality. Similarly, "Timeout" allows one to overwrite the
      * global value "DicomScuTimeout" on a per-modality basis.
+     *
+     * The "RetrieveMethod" option allows one to overwrite the global
+     * "DicomDefaultRetrieveMethod" configuration option for this
+     * specific modality. (Allowed values: "C-MOVE" or "C-GET").
      **/
     //"untrusted" : {
     //  "AET" : "ORTHANC",
@@ -472,7 +516,8 @@
     //  "AllowTranscoding" : true,         // new in 1.7.0
     //  "UseDicomTls" : false,             // new in 1.9.0
     //  "LocalAet" : "HELLO",              // new in 1.9.0
-    //  "Timeout" : 60                     // new in 1.9.1
+    //  "Timeout" : 60,                    // new in 1.9.1
+    //  "RetrieveMethod": "C-MOVE"         // new in 1.12.6
     //}
   },
 
@@ -486,9 +531,17 @@
   // accept C-FIND requests from Orthanc (new in Orthanc 1.8.1).
   "DicomEchoChecksFind" : false,
 
+  // Whether Orthanc uses C-MOVE or C-GET to retrieve a resource after
+  // a C-Find (when calling /queries/.../retrieve).
+  // This configuration can be overridden for each modality by providing
+  // "RetrieveMethod" in the "DicomModalities" entry.
+  // (new in Orthanc 1.12.6)
+  "DicomDefaultRetrieveMethod" : "C-MOVE",
+
   // The timeout (in seconds) after which the DICOM associations are
   // considered as closed by the Orthanc SCU (client) if the remote
   // DICOM SCP (server) does not answer.
+  // A value of 0 means "no timeout".
   "DicomScuTimeout" : 10,
 
   // During a C-STORE SCU request initiated by Orthanc, if the remote
@@ -568,10 +621,10 @@
   // Set the timeout for HTTP requests issued by Orthanc (in seconds).
   "HttpTimeout" : 60,
 
-  // Enable the verification of the peers during HTTPS requests. This
-  // option must be set to "false" if using self-signed certificates.
-  // Pay attention that setting this option to "false" results in
-  // security risks!
+  // Enable the verification of the peers certificates during HTTPS 
+  // requests. Setting this option to false is equivalent to the 
+  // "--insecure" curl option. Pay attention that setting this option 
+  // to "false" results in security risks!
   // Reference: http://curl.haxx.se/docs/sslcerts.html
   "HttpsVerifyPeers" : true,
 
@@ -581,7 +634,11 @@
   // verify the peers. The file may contain multiple CA
   // certificates. The certificate(s) must be in PEM format." On
   // Debian-based systems, this option can be set to
-  // "/etc/ssl/certs/ca-certificates.crt"
+  // "/etc/ssl/certs/ca-certificates.crt".
+  // Starting with Orthanc 1.12.6 and provided that Orthanc has been
+  // built with libcurl > 8.2.0, when this option is empty,
+  // Orthanc uses the operating system native CA store ("--ca-native"
+  // option)
   "HttpsCACertificates" : "",
 
 
@@ -863,8 +920,10 @@
   // that have a compressed transfer syntax (new in Orthanc 1.8.2).
   "IngestTranscodingOfCompressed" : true,
   
-  // The compression level that is used when transcoding to one of the
-  // lossy/JPEG transfer syntaxes (integer between 1 and 100).
+  // The default compression level that is used when transcoding to one
+  // of the lossy/JPEG transfer syntaxes (integer between 1 and 100).
+  // This value is currently only used by the default built-in DCMTK
+  // transcoder and is not provided to transcoding plugins.
   "DicomLossyTranscodingQuality" : 90,
 
   // Whether "fsync()" is called after each write to the storage area
@@ -888,7 +947,8 @@
 
   // Deidentify/anonymize the contents of the logs (notably C-FIND,
   // C-GET, and C-MOVE queries submitted to Orthanc) according to
-  // Table E.1-1 of the DICOM standard (new in Orthanc 1.8.2)
+  // Table E.1-1 of the DICOM standard (new in Orthanc 1.8.2).
+  // Note that, the DICOM logs at TRACE level are not deidentified !
   "DeidentifyLogs" : true,
 
   // If "DeidentifyLogs" is true, this sets the DICOM standard to
@@ -981,8 +1041,48 @@
     // saved with another "ExtraMainDicomTags" configuration which means that
     // your response might be incomplete/inconsistent.
     // You should call patients|studies|series|instances/../reconstruct to rebuild
-    // the DB.  You may also check for the "Housekeeper" plugin
-    "W002_InconsistentDicomTagsInDb": true
-  }
+    // the DB.  You may also check for the "Housekeeper" plugin.
+    "W002_InconsistentDicomTagsInDb": true,
+
+    // Display a warning message when Orthanc and its plugins are unable
+    // to decode a frame (new in Orthanc 1.12.5).
+    "W003_DecoderFailure": true,
+
+    // Display a warning when the MainDicomTagsSignature metadata has not been
+    // found which means that the resource has been saved with a version prior
+    // to 1.11.0.
+    // You should call patients|studies|series|instances/../reconstruct to rebuild
+    // the DB.  You may also check for the "Housekeeper" plugin.
+    // (new in Orthanc 1.12.5)
+    "W004_NoMainDicomTagsSignature": true,
+
+    // Display a warning when a user performs a find request and requests a tag
+    // from a lower resource level; e.g. when requesting "StudyDescription" at
+    // Patient level.
+    // (new in Orthanc 1.12.5)
+    "W005_RequestingTagFromLowerResourceLevel": true,
+
+    // Display a warning when a user performs a find request and requests a tag
+    // from the DICOM Meta Header.
+    // (new in Orthanc 1.12.5)
+    "W006_RequestingTagFromMetaHeader": true,
+
+    // Display a warning when a user requests a tag that can not be read from disk
+    // because "StorageAccessOnFind" is set to "Never".
+    // (new in Orthanc 1.12.5)
+    "W007_MissingRequestedTagsNotReadFromDisk": true
+  },
+
+  // Configure Orthanc in read only mode.
+  // In this mode, many Orthanc features that requires a write access to the 
+  // Index DB or the disk storage won't be available at all.
+  // (new in Orthanc 1.12.5)
+  "ReadOnly" : false,
+
+  // Maximum number of DCMTK transcoders that are simultaneously running
+  // at any given time. A value of "0" indicates to use all the
+  // available CPU logical cores. Prior to Orthanc 1.12.6, there were not limit.
+  // (new in Orthanc 1.12.6)
+  "MaximumConcurrentDcmtkTranscoders" : 0
 
 }
--- a/OrthancServer/Resources/DicomConformanceStatement.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/DicomConformanceStatement.py	Tue Mar 18 13:37:18 2025 +0100
@@ -4,8 +4,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/DicomConformanceStatement.txt	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/DicomConformanceStatement.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -209,6 +209,16 @@
   MOVEStudyRootQueryRetrieveInformationModel    | 1.2.840.10008.5.1.4.1.2.2.2
 
 
+-------------------
+Get SCU Conformance
+-------------------
+
+Orthanc supports the following SOP Classes as an SCU for C-Get:
+
+  GETPatientRootQueryRetrieveInformationModel    | 1.2.840.10008.5.1.4.1.2.1.3
+  GETStudyRootQueryRetrieveInformationModel      | 1.2.840.10008.5.1.4.1.2.2.3
+
+
 -----------------
 Transfer Syntaxes
 -----------------
--- a/OrthancServer/Resources/Fonts/GenerateFont.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Fonts/GenerateFont.py	Tue Mar 18 13:37:18 2025 +0100
@@ -4,8 +4,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/GenerateAnonymizationProfile.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/GenerateAnonymizationProfile.py	Tue Mar 18 13:37:18 2025 +0100
@@ -4,8 +4,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Graveyard/DatabaseOptimizations/LookupIdentifierQuery.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,205 +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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "LookupIdentifierQuery.h"
-
-#include "../../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../../Core/OrthancException.h"
-#include "../ServerToolbox.h"
-#include "SetOfResources.h"
-
-#include <cassert>
-
-
-
-namespace Orthanc
-{
-  LookupIdentifierQuery::SingleConstraint::
-  SingleConstraint(const DicomTag& tag,
-                   IdentifierConstraintType type,
-                   const std::string& value) : 
-    tag_(tag),
-    type_(type),
-    value_(ServerToolbox::NormalizeIdentifier(value))
-  {
-  }
-
-
-  LookupIdentifierQuery::RangeConstraint::
-  RangeConstraint(const DicomTag& tag,
-                  const std::string& start,
-                  const std::string& end) : 
-    tag_(tag),
-    start_(ServerToolbox::NormalizeIdentifier(start)),
-    end_(ServerToolbox::NormalizeIdentifier(end))
-  {
-  }
-
-
-  LookupIdentifierQuery::Disjunction::~Disjunction()
-  {
-    for (size_t i = 0; i < singleConstraints_.size(); i++)
-    {
-      delete singleConstraints_[i];
-    }
-
-    for (size_t i = 0; i < rangeConstraints_.size(); i++)
-    {
-      delete rangeConstraints_[i];
-    }
-  }
-
-
-  void LookupIdentifierQuery::Disjunction::Add(const DicomTag& tag,
-                                               IdentifierConstraintType type,
-                                               const std::string& value)
-  {
-    singleConstraints_.push_back(new SingleConstraint(tag, type, value));
-  }
-
-
-  void LookupIdentifierQuery::Disjunction::AddRange(const DicomTag& tag,
-                                                    const std::string& start,
-                                                    const std::string& end)
-  {
-    rangeConstraints_.push_back(new RangeConstraint(tag, start, end));
-  }
-
-
-  LookupIdentifierQuery::~LookupIdentifierQuery()
-  {
-    for (Disjunctions::iterator it = disjunctions_.begin();
-         it != disjunctions_.end(); ++it)
-    {
-      delete *it;
-    }
-  }
-
-
-  bool LookupIdentifierQuery::IsIdentifier(const DicomTag& tag)
-  {
-    return ServerToolbox::IsIdentifier(tag, level_);
-  }
-
-
-  void LookupIdentifierQuery::AddConstraint(DicomTag tag,
-                                            IdentifierConstraintType type,
-                                            const std::string& value)
-  {
-    assert(IsIdentifier(tag));
-    disjunctions_.push_back(new Disjunction);
-    disjunctions_.back()->Add(tag, type, value);
-  }
-
-
-  void LookupIdentifierQuery::AddRange(DicomTag tag,
-                                       const std::string& start,
-                                       const std::string& end)
-  {
-    assert(IsIdentifier(tag));
-    disjunctions_.push_back(new Disjunction);
-    disjunctions_.back()->AddRange(tag, start, end);
-  }
-
-
-  LookupIdentifierQuery::Disjunction& LookupIdentifierQuery::AddDisjunction()
-  {
-    disjunctions_.push_back(new Disjunction);
-    return *disjunctions_.back();
-  }
-
-
-  void LookupIdentifierQuery::Apply(std::list<std::string>& result,
-                                    IDatabaseWrapper& database)
-  {
-    SetOfResources resources(database, level_);
-    Apply(resources, database);
-
-    resources.Flatten(result);
-  }
-
-
-  void LookupIdentifierQuery::Apply(SetOfResources& result,
-                                    IDatabaseWrapper& database)
-  {
-    for (size_t i = 0; i < disjunctions_.size(); i++)
-    {
-      std::list<int64_t> a;
-
-      for (size_t j = 0; j < disjunctions_[i]->GetSingleConstraintsCount(); j++)
-      {
-        const SingleConstraint& constraint = disjunctions_[i]->GetSingleConstraint(j);
-        std::list<int64_t> b;
-        database.LookupIdentifier(b, level_, constraint.GetTag(), 
-                                  constraint.GetType(), constraint.GetValue());
-
-        a.splice(a.end(), b);
-      }
-
-      for (size_t j = 0; j < disjunctions_[i]->GetRangeConstraintsCount(); j++)
-      {
-        const RangeConstraint& constraint = disjunctions_[i]->GetRangeConstraint(j);
-        std::list<int64_t> b;
-        database.LookupIdentifierRange(b, level_, constraint.GetTag(), 
-                                       constraint.GetStart(), constraint.GetEnd());
-
-        a.splice(a.end(), b);
-      }
-
-      result.Intersect(a);
-    }
-  }
-
-
-  void LookupIdentifierQuery::Print(std::ostream& s) const
-  {
-    s << "Constraint: " << std::endl;
-    for (Disjunctions::const_iterator
-           it = disjunctions_.begin(); it != disjunctions_.end(); ++it)
-    {
-      if (it == disjunctions_.begin())
-        s << "   ";
-      else
-        s << "OR ";
-
-      for (size_t j = 0; j < (*it)->GetSingleConstraintsCount(); j++)
-      {
-        const SingleConstraint& c = (*it)->GetSingleConstraint(j);
-        s << FromDcmtkBridge::GetTagName(c.GetTag(), "");
-
-        switch (c.GetType())
-        {
-          case IdentifierConstraintType_Equal: s << " == "; break;
-          case IdentifierConstraintType_SmallerOrEqual: s << " <= "; break;
-          case IdentifierConstraintType_GreaterOrEqual: s << " >= "; break;
-          case IdentifierConstraintType_Wildcard: s << " ~= "; break;
-          default:
-            s << " ? ";
-        }
-
-        s << c.GetValue() << std::endl;
-      }
-    }
-  }
-}
--- a/OrthancServer/Resources/Graveyard/DatabaseOptimizations/LookupIdentifierQuery.h	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,197 +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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../IDatabaseWrapper.h"
-
-#include "SetOfResources.h"
-
-#include <vector>
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  /**
-   * Primitive for wildcard matching, as defined in DICOM:
-   * http://dicom.nema.org/dicom/2013/output/chtml/part04/sect_C.2.html#sect_C.2.2.2.4
-   * 
-   * "Any occurrence of an "*" or a "?", then "*" shall match any
-   * sequence of characters (including a zero length value) and "?"
-   * shall match any single character. This matching is case
-   * sensitive, except for Attributes with an PN Value
-   * Representation (e.g., Patient Name (0010,0010))."
-   * 
-   * Pay attention to the fact that "*" (resp. "?") generally
-   * corresponds to "%" (resp. "_") in primitive LIKE of SQL. The
-   * values "%", "_", "\" should in the user request should
-   * respectively be escaped as "\%", "\_" and "\\".
-   *
-   * This matching must be case sensitive: The special case of PN VR
-   * is taken into consideration by normalizing the query string in
-   * method "NormalizeIdentifier()".
-   **/
-
-  class LookupIdentifierQuery : public boost::noncopyable
-  {
-    // This class encodes a conjunction ("AND") of disjunctions. Each
-    // disjunction represents an "OR" of several constraints.
-
-  public:
-    class SingleConstraint
-    {
-    private:
-      DicomTag                  tag_;
-      IdentifierConstraintType  type_;
-      std::string               value_;
-
-    public:
-      SingleConstraint(const DicomTag& tag,
-                       IdentifierConstraintType type,
-                       const std::string& value);
-
-      const DicomTag& GetTag() const
-      {
-        return tag_;
-      }
-
-      IdentifierConstraintType GetType() const
-      {
-        return type_;
-      }
-      
-      const std::string& GetValue() const
-      {
-        return value_;
-      }
-    };
-
-
-    class RangeConstraint
-    {
-    private:
-      DicomTag     tag_;
-      std::string  start_;
-      std::string  end_;
-
-    public:
-      RangeConstraint(const DicomTag& tag,
-                      const std::string& start,
-                      const std::string& end);
-
-      const DicomTag& GetTag() const
-      {
-        return tag_;
-      }
-
-      const std::string& GetStart() const
-      {
-        return start_;
-      }
-
-      const std::string& GetEnd() const
-      {
-        return end_;
-      }
-    };
-
-
-    class Disjunction : public boost::noncopyable
-    {
-    private:
-      std::vector<SingleConstraint*>  singleConstraints_;
-      std::vector<RangeConstraint*>   rangeConstraints_;
-
-    public:
-      ~Disjunction();
-
-      void Add(const DicomTag& tag,
-               IdentifierConstraintType type,
-               const std::string& value);
-
-      void AddRange(const DicomTag& tag,
-                    const std::string& start,
-                    const std::string& end);
-
-      size_t GetSingleConstraintsCount() const
-      {
-        return singleConstraints_.size();
-      }
-
-      const SingleConstraint&  GetSingleConstraint(size_t i) const
-      {
-        return *singleConstraints_[i];
-      }
-
-      size_t GetRangeConstraintsCount() const
-      {
-        return rangeConstraints_.size();
-      }
-
-      const RangeConstraint&  GetRangeConstraint(size_t i) const
-      {
-        return *rangeConstraints_[i];
-      }
-    };
-
-
-  private:
-    typedef std::vector<Disjunction*>  Disjunctions;
-
-    ResourceType  level_;
-    Disjunctions  disjunctions_;
-
-  public:
-    LookupIdentifierQuery(ResourceType level) : level_(level)
-    {
-    }
-
-    ~LookupIdentifierQuery();
-
-    bool IsIdentifier(const DicomTag& tag);
-
-    void AddConstraint(DicomTag tag,
-                       IdentifierConstraintType type,
-                       const std::string& value);
-
-    void AddRange(DicomTag tag,
-                  const std::string& start,
-                  const std::string& end);
-
-    Disjunction& AddDisjunction();
-
-    ResourceType GetLevel() const
-    {
-      return level_;
-    }
-
-    // The database must be locked
-    void Apply(std::list<std::string>& result,
-               IDatabaseWrapper& database);
-
-    void Apply(SetOfResources& result,
-               IDatabaseWrapper& database);
-
-    void Print(std::ostream& s) const;
-  };
-}
--- a/OrthancServer/Resources/Graveyard/DatabaseOptimizations/LookupResource.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,469 +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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "LookupResource.h"
-
-#include "../../Core/OrthancException.h"
-#include "../../Core/FileStorage/StorageAccessor.h"
-#include "../ServerToolbox.h"
-#include "../../Core/DicomParsing/FromDcmtkBridge.h"
-
-
-namespace Orthanc
-{
-  static bool DoesDicomMapMatch(const DicomMap& dicom,
-                                const DicomTag& tag,
-                                const IFindConstraint& constraint)
-  {
-    const DicomValue* value = dicom.TestAndGetValue(tag);
-
-    return (value != NULL &&
-            !value->IsNull() &&
-            !value->IsBinary() &&
-            constraint.Match(value->GetContent()));
-  }
-
-  
-  LookupResource::Level::Level(ResourceType level) : level_(level)
-  {
-    const DicomTag* tags = NULL;
-    size_t size;
-    
-    ServerToolbox::LoadIdentifiers(tags, size, level);
-    
-    for (size_t i = 0; i < size; i++)
-    {
-      identifiers_.insert(tags[i]);
-    }
-    
-    DicomMap::LoadMainDicomTags(tags, size, level);
-    
-    for (size_t i = 0; i < size; i++)
-    {
-      if (identifiers_.find(tags[i]) == identifiers_.end())
-      {
-        mainTags_.insert(tags[i]);
-      }
-    }    
-  }
-
-  LookupResource::Level::~Level()
-  {
-    for (Constraints::iterator it = mainTagsConstraints_.begin();
-         it != mainTagsConstraints_.end(); ++it)
-    {
-      delete it->second;
-    }
-
-    for (Constraints::iterator it = identifiersConstraints_.begin();
-         it != identifiersConstraints_.end(); ++it)
-    {
-      delete it->second;
-    }
-  }
-
-  bool LookupResource::Level::Add(const DicomTag& tag,
-                                  std::auto_ptr<IFindConstraint>& constraint)
-  {
-    if (identifiers_.find(tag) != identifiers_.end())
-    {
-      if (level_ == ResourceType_Patient)
-      {
-        // The filters on the patient level must be cloned to the study level
-        identifiersConstraints_[tag] = constraint->Clone();
-      }
-      else
-      {
-        identifiersConstraints_[tag] = constraint.release();
-      }
-
-      return true;
-    }
-    else if (mainTags_.find(tag) != mainTags_.end())
-    {
-      if (level_ == ResourceType_Patient)
-      {
-        // The filters on the patient level must be cloned to the study level
-        mainTagsConstraints_[tag] = constraint->Clone();
-      }
-      else
-      {
-        mainTagsConstraints_[tag] = constraint.release();
-      }
-
-      return true;
-    }
-    else
-    {
-      // This is not a main DICOM tag
-      return false;
-    }
-  }
-
-
-  bool LookupResource::Level::IsMatch(const DicomMap& dicom) const
-  {
-    for (Constraints::const_iterator it = identifiersConstraints_.begin();
-         it != identifiersConstraints_.end(); ++it)
-    {
-      assert(it->second != NULL);
-
-      if (!DoesDicomMapMatch(dicom, it->first, *it->second))
-      {
-        return false;
-      }
-    }
-
-    for (Constraints::const_iterator it = mainTagsConstraints_.begin();
-         it != mainTagsConstraints_.end(); ++it)
-    {
-      assert(it->second != NULL);
-
-      if (!DoesDicomMapMatch(dicom, it->first, *it->second))
-      {
-        return false;
-      }
-    }
-
-    return true;
-  }
-  
-
-  LookupResource::LookupResource(ResourceType level) : level_(level)
-  {
-    switch (level)
-    {
-      case ResourceType_Patient:
-        levels_[ResourceType_Patient] = new Level(ResourceType_Patient);
-        break;
-
-      case ResourceType_Instance:
-        levels_[ResourceType_Instance] = new Level(ResourceType_Instance);
-        // Do not add "break" here
-
-      case ResourceType_Series:
-        levels_[ResourceType_Series] = new Level(ResourceType_Series);
-        // Do not add "break" here
-
-      case ResourceType_Study:
-        levels_[ResourceType_Study] = new Level(ResourceType_Study);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-
-
-  LookupResource::~LookupResource()
-  {
-    for (Levels::iterator it = levels_.begin();
-         it != levels_.end(); ++it)
-    {
-      delete it->second;
-    }
-
-    for (Constraints::iterator it = unoptimizedConstraints_.begin();
-         it != unoptimizedConstraints_.end(); ++it)
-    {
-      delete it->second;
-    }    
-  }
-
-
-
-  bool LookupResource::AddInternal(ResourceType level,
-                                   const DicomTag& tag,
-                                   std::auto_ptr<IFindConstraint>& constraint)
-  {
-    Levels::iterator it = levels_.find(level);
-    if (it != levels_.end())
-    {
-      if (it->second->Add(tag, constraint))
-      {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-
-  void LookupResource::Add(const DicomTag& tag,
-                           IFindConstraint* constraint)
-  {
-    std::auto_ptr<IFindConstraint> c(constraint);
-
-    if (!AddInternal(ResourceType_Patient, tag, c) &&
-        !AddInternal(ResourceType_Study, tag, c) &&
-        !AddInternal(ResourceType_Series, tag, c) &&
-        !AddInternal(ResourceType_Instance, tag, c))
-    {
-      unoptimizedConstraints_[tag] = c.release();
-    }
-  }
-
-
-  static bool Match(const DicomMap& tags,
-                    const DicomTag& tag,
-                    const IFindConstraint& constraint)
-  {
-    const DicomValue* value = tags.TestAndGetValue(tag);
-
-    if (value == NULL ||
-        value->IsNull() ||
-        value->IsBinary())
-    {
-      return false;
-    }
-    else
-    {
-      return constraint.Match(value->GetContent());
-    }
-  }
-
-
-  void LookupResource::Level::Apply(SetOfResources& candidates,
-                                    IDatabaseWrapper& database) const
-  {
-    // First, use the indexed identifiers
-    LookupIdentifierQuery query(level_);
-
-    for (Constraints::const_iterator it = identifiersConstraints_.begin(); 
-         it != identifiersConstraints_.end(); ++it)
-    {
-      it->second->Setup(query, it->first);
-    }
-
-    query.Apply(candidates, database);
-
-    /*{
-      query.Print(std::cout);
-      std::list<int64_t>  source;
-      candidates.Flatten(source);
-      printf("=> %d\n", source.size());
-      }*/
-
-    // Secondly, filter using the main DICOM tags
-    if (!identifiersConstraints_.empty() ||
-        !mainTagsConstraints_.empty())
-    {
-      std::list<int64_t>  source;
-      candidates.Flatten(source);
-      candidates.Clear();
-
-      std::list<int64_t>  filtered;
-      for (std::list<int64_t>::const_iterator candidate = source.begin(); 
-           candidate != source.end(); ++candidate)
-      {
-        DicomMap tags;
-        database.GetMainDicomTags(tags, *candidate);
-
-        bool match = true;
-
-        // Re-apply the identifier constraints, as their "Setup"
-        // method is less restrictive than their "Match" method
-        for (Constraints::const_iterator it = identifiersConstraints_.begin(); 
-             match && it != identifiersConstraints_.end(); ++it)
-        {
-          if (!Match(tags, it->first, *it->second))
-          {
-            match = false;
-          }
-        }
-
-        for (Constraints::const_iterator it = mainTagsConstraints_.begin(); 
-             match && it != mainTagsConstraints_.end(); ++it)
-        {
-          if (!Match(tags, it->first, *it->second))
-          {
-            match = false;
-          }
-        }
-
-        if (match)
-        {
-          filtered.push_back(*candidate);
-        }
-      }
-      
-      candidates.Intersect(filtered);
-    }
-  }
-
-
-
-  bool LookupResource::IsMatch(const DicomMap& dicom) const
-  {
-    for (Levels::const_iterator it = levels_.begin(); it != levels_.end(); ++it)
-    {
-      if (!it->second->IsMatch(dicom))
-      {
-        return false;
-      }
-    }
-
-    for (Constraints::const_iterator it = unoptimizedConstraints_.begin(); 
-         it != unoptimizedConstraints_.end(); ++it)
-    {
-      assert(it->second != NULL);
-
-      if (!DoesDicomMapMatch(dicom, it->first, *it->second))
-      {
-        return false;
-      }
-    }
-
-    return true;
-  }
-
-
-  void LookupResource::ApplyLevel(SetOfResources& candidates,
-                                  ResourceType level,
-                                  IDatabaseWrapper& database) const
-  {
-    Levels::const_iterator it = levels_.find(level);
-    if (it != levels_.end())
-    {
-      it->second->Apply(candidates, database);
-    }
-
-    if (level == ResourceType_Study &&
-        modalitiesInStudy_.get() != NULL)
-    {
-      // There is a constraint on the "ModalitiesInStudy" DICOM
-      // extension. Check out whether one child series has one of the
-      // allowed modalities
-      std::list<int64_t> allStudies, matchingStudies;
-      candidates.Flatten(allStudies);
- 
-      for (std::list<int64_t>::const_iterator
-             study = allStudies.begin(); study != allStudies.end(); ++study)
-      {
-        std::list<int64_t> childrenSeries;
-        database.GetChildrenInternalId(childrenSeries, *study);
-
-        for (std::list<int64_t>::const_iterator
-               series = childrenSeries.begin(); series != childrenSeries.end(); ++series)
-        {
-          DicomMap tags;
-          database.GetMainDicomTags(tags, *series);
-
-          const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY);
-          if (value != NULL &&
-              !value->IsNull() &&
-              !value->IsBinary())
-          {
-            if (modalitiesInStudy_->Match(value->GetContent()))
-            {
-              matchingStudies.push_back(*study);
-              break;
-            }
-          }
-        }
-      }
-
-      candidates.Intersect(matchingStudies);
-    }
-  }
-
-
-  void LookupResource::FindCandidates(std::list<int64_t>& result,
-                                      IDatabaseWrapper& database) const
-  {
-    ResourceType startingLevel;
-    if (level_ == ResourceType_Patient)
-    {
-      startingLevel = ResourceType_Patient;
-    }
-    else
-    {
-      startingLevel = ResourceType_Study;
-    }
-
-    SetOfResources candidates(database, startingLevel);
-
-    switch (level_)
-    {
-      case ResourceType_Patient:
-        ApplyLevel(candidates, ResourceType_Patient, database);
-        break;
-
-      case ResourceType_Study:
-        ApplyLevel(candidates, ResourceType_Study, database);
-        break;
-
-      case ResourceType_Series:
-        ApplyLevel(candidates, ResourceType_Study, database);
-        candidates.GoDown();
-        ApplyLevel(candidates, ResourceType_Series, database);
-        break;
-
-      case ResourceType_Instance:
-        ApplyLevel(candidates, ResourceType_Study, database);
-        candidates.GoDown();
-        ApplyLevel(candidates, ResourceType_Series, database);
-        candidates.GoDown();
-        ApplyLevel(candidates, ResourceType_Instance, database);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    candidates.Flatten(result);
-  }
-
-
-  void LookupResource::SetModalitiesInStudy(const std::string& modalities)
-  {
-    modalitiesInStudy_.reset(new ListConstraint(true /* case sensitive */));
-    
-    std::vector<std::string> items;
-    Toolbox::TokenizeString(items, modalities, '\\');
-    
-    for (size_t i = 0; i < items.size(); i++)
-    {
-      modalitiesInStudy_->AddAllowedValue(items[i]);
-    }
-  }
-
-
-  void LookupResource::AddDicomConstraint(const DicomTag& tag,
-                                          const std::string& dicomQuery,
-                                          bool caseSensitive)
-  {
-    // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained
-    // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html  
-    if (tag == DICOM_TAG_MODALITIES_IN_STUDY)
-    {
-      SetModalitiesInStudy(dicomQuery);
-    }
-    else 
-    {
-      Add(tag, IFindConstraint::ParseDicomConstraint(tag, dicomQuery, caseSensitive));
-    }
-  }
-
-}
--- a/OrthancServer/Resources/Graveyard/DatabaseOptimizations/LookupResource.h	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,105 +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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "ListConstraint.h"
-#include "SetOfResources.h"
-
-#include <memory>
-
-namespace Orthanc
-{
-  class LookupResource : public boost::noncopyable
-  {
-  private:
-    typedef std::map<DicomTag, IFindConstraint*>  Constraints;
-    
-    class Level
-    {
-    private:
-      ResourceType        level_;
-      std::set<DicomTag>  identifiers_;
-      std::set<DicomTag>  mainTags_;
-      Constraints         identifiersConstraints_;
-      Constraints         mainTagsConstraints_;
-
-    public:
-      Level(ResourceType level);
-
-      ~Level();
-
-      bool Add(const DicomTag& tag,
-               std::auto_ptr<IFindConstraint>& constraint);
-
-      void Apply(SetOfResources& candidates,
-                 IDatabaseWrapper& database) const;
-
-      bool IsMatch(const DicomMap& dicom) const;
-    };
-
-    typedef std::map<ResourceType, Level*>  Levels;
-
-    ResourceType                    level_;
-    Levels                          levels_;
-    Constraints                     unoptimizedConstraints_;   // Constraints on non-main DICOM tags
-    std::auto_ptr<ListConstraint>   modalitiesInStudy_;
-
-    bool AddInternal(ResourceType level,
-                     const DicomTag& tag,
-                     std::auto_ptr<IFindConstraint>& constraint);
-
-    void ApplyLevel(SetOfResources& candidates,
-                    ResourceType level,
-                    IDatabaseWrapper& database) const;
-
-  public:
-    LookupResource(ResourceType level);
-
-    ~LookupResource();
-
-    ResourceType GetLevel() const
-    {
-      return level_;
-    }
-
-    void SetModalitiesInStudy(const std::string& modalities); 
-
-    void Add(const DicomTag& tag,
-             IFindConstraint* constraint);   // Takes ownership
-
-    void AddDicomConstraint(const DicomTag& tag,
-                            const std::string& dicomQuery,
-                            bool caseSensitive);
-
-    void FindCandidates(std::list<int64_t>& result,
-                        IDatabaseWrapper& database) const;
-
-    bool HasOnlyMainDicomTags() const
-    {
-      return unoptimizedConstraints_.empty();
-    }
-
-    bool IsMatch(const DicomMap& dicom) const;
-  };
-}
--- a/OrthancServer/Resources/Graveyard/DatabasePluginSample/CMakeLists.txt	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,69 +0,0 @@
-cmake_minimum_required(VERSION 2.8)
-
-project(SampleDatabasePlugin)
-
-# Parameters of the build
-SET(SAMPLE_DATABASE_VERSION "0.0" CACHE STRING "Version of the plugin")
-SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)")
-SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages")
-SET(STANDALONE_BUILD ON)
-
-# Advanced parameters to fine-tune linking against system libraries
-SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of Boost")
-SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp")
-SET(USE_SYSTEM_SQLITE ON CACHE BOOL "Use the system version of SQLite")
-
-include(${CMAKE_SOURCE_DIR}/../../../Plugins/Samples/Common/OrthancPlugins.cmake)
-
-include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake)
-set(ENABLE_SQLITE ON)
-include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake)
-
-EmbedResources(
-  --system-exception  # Use "std::runtime_error" instead of "OrthancException" for embedded resources
-  PREPARE_DATABASE  ${CMAKE_SOURCE_DIR}/../../../Sources/Database/PrepareDatabase.sql
-  )
-
-message("Setting the version of the plugin to ${SAMPLE_DATABASE_VERSION}")
-
-add_definitions(
-  -DORTHANC_SQLITE_STANDALONE=1
-  -DORTHANC_ENABLE_PLUGINS=1
-  -DORTHANC_SANDBOXED=0
-  -DSAMPLE_DATABASE_VERSION="${SAMPLE_DATABASE_VERSION}"
-  )
-
-add_library(SampleDatabase SHARED 
-  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/DicomFormat/DicomArray.cpp
-  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp
-  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/DicomFormat/DicomTag.cpp
-  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/DicomFormat/DicomValue.cpp
-  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/Enumerations.cpp
-  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/SQLite/Connection.cpp
-  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/SQLite/FunctionContext.cpp
-  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/SQLite/Statement.cpp
-  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/SQLite/StatementId.cpp
-  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/SQLite/StatementReference.cpp
-  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/SQLite/Transaction.cpp
-  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/Toolbox.cpp
-  ${CMAKE_SOURCE_DIR}/../../../Plugins/Engine/PluginsEnumerations.cpp
-
-  Database.cpp
-  Plugin.cpp
-  DatabaseWrapperBase.cpp
-  
-  ${BOOST_SOURCES}
-  ${JSONCPP_SOURCES}
-  ${SQLITE_SOURCES}
-  ${AUTOGENERATED_SOURCES}
-  )
-
-set_target_properties(SampleDatabase PROPERTIES 
-  VERSION ${SAMPLE_DATABASE_VERSION} 
-  SOVERSION ${SAMPLE_DATABASE_VERSION})
-
-install(
-  TARGETS SampleDatabase
-  RUNTIME DESTINATION lib    # Destination for Windows
-  LIBRARY DESTINATION share/orthanc/plugins    # Destination for Linux
-  )
--- a/OrthancServer/Resources/Graveyard/DatabasePluginSample/Database.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,555 +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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "Database.h"
-
-#include "../../../../OrthancFramework/Sources/DicomFormat/DicomArray.h"
-
-#include <EmbeddedResources.h>
-#include <boost/lexical_cast.hpp>
-
-
-namespace Internals
-{
-  class SignalFileDeleted : public Orthanc::SQLite::IScalarFunction
-  {
-  private:
-    OrthancPlugins::DatabaseBackendOutput&  output_;
-
-  public:
-    SignalFileDeleted(OrthancPlugins::DatabaseBackendOutput&  output) : output_(output)
-    {
-    }
-
-    virtual const char* GetName() const
-    {
-      return "SignalFileDeleted";
-    }
-
-    virtual unsigned int GetCardinality() const
-    {
-      return 7;
-    }
-
-    virtual void Compute(Orthanc::SQLite::FunctionContext& context)
-    {
-      std::string uncompressedMD5, compressedMD5;
-
-      if (!context.IsNullValue(5))
-      {
-        uncompressedMD5 = context.GetStringValue(5);
-      }
-
-      if (!context.IsNullValue(6))
-      {
-        compressedMD5 = context.GetStringValue(6);
-      }
-      
-      output_.SignalDeletedAttachment(context.GetStringValue(0),
-                                      context.GetIntValue(1),
-                                      context.GetInt64Value(2),
-                                      uncompressedMD5,
-                                      context.GetIntValue(3),
-                                      context.GetInt64Value(4),
-                                      compressedMD5);
-    }
-  };
-
-
-  class SignalResourceDeleted : public Orthanc::SQLite::IScalarFunction
-  {
-  private:
-    OrthancPlugins::DatabaseBackendOutput&  output_;
-
-  public:
-    SignalResourceDeleted(OrthancPlugins::DatabaseBackendOutput&  output) : output_(output)
-    {
-    }
-
-    virtual const char* GetName() const
-    {
-      return "SignalResourceDeleted";
-    }
-
-    virtual unsigned int GetCardinality() const
-    {
-      return 2;
-    }
-
-    virtual void Compute(Orthanc::SQLite::FunctionContext& context)
-    {
-      output_.SignalDeletedResource(context.GetStringValue(0),
-                                    Orthanc::Plugins::Convert(static_cast<Orthanc::ResourceType>(context.GetIntValue(1))));
-    }
-  };
-}
-
-
-class Database::SignalRemainingAncestor : public Orthanc::SQLite::IScalarFunction
-{
-private:
-  bool hasRemainingAncestor_;
-  std::string remainingPublicId_;
-  OrthancPluginResourceType remainingType_;
-
-public:
-  SignalRemainingAncestor() : 
-    hasRemainingAncestor_(false),
-    remainingType_(OrthancPluginResourceType_Instance)  // Some dummy value
-  {
-  }
-
-  void Reset()
-  {
-    hasRemainingAncestor_ = false;
-  }
-
-  virtual const char* GetName() const
-  {
-    return "SignalRemainingAncestor";
-  }
-
-  virtual unsigned int GetCardinality() const
-  {
-    return 2;
-  }
-
-  virtual void Compute(Orthanc::SQLite::FunctionContext& context)
-  {
-    if (!hasRemainingAncestor_ ||
-        remainingType_ >= context.GetIntValue(1))
-    {
-      hasRemainingAncestor_ = true;
-      remainingPublicId_ = context.GetStringValue(0);
-      remainingType_ = Orthanc::Plugins::Convert(static_cast<Orthanc::ResourceType>(context.GetIntValue(1)));
-    }
-  }
-
-  bool HasRemainingAncestor() const
-  {
-    return hasRemainingAncestor_;
-  }
-
-  const std::string& GetRemainingAncestorId() const
-  {
-    assert(hasRemainingAncestor_);
-    return remainingPublicId_;
-  }
-
-  OrthancPluginResourceType GetRemainingAncestorType() const
-  {
-    assert(hasRemainingAncestor_);
-    return remainingType_;
-  }
-};
-
-
-
-Database::Database(const std::string& path) : 
-  path_(path),
-  base_(db_),
-  signalRemainingAncestor_(NULL)
-{
-}
-
-
-void Database::Open()
-{
-  db_.Open(path_);
-
-  db_.Execute("PRAGMA ENCODING=\"UTF-8\";");
-
-  // http://www.sqlite.org/pragma.html
-  db_.Execute("PRAGMA SYNCHRONOUS=NORMAL;");
-  db_.Execute("PRAGMA JOURNAL_MODE=WAL;");
-  db_.Execute("PRAGMA LOCKING_MODE=EXCLUSIVE;");
-  db_.Execute("PRAGMA WAL_AUTOCHECKPOINT=1000;");
-  //db_.Execute("PRAGMA TEMP_STORE=memory");
-
-  if (!db_.DoesTableExist("GlobalProperties"))
-  {
-    std::string query;
-    Orthanc::EmbeddedResources::GetFileResource(query, Orthanc::EmbeddedResources::PREPARE_DATABASE);
-    db_.Execute(query);
-  }
-
-  signalRemainingAncestor_ = new SignalRemainingAncestor;
-  db_.Register(signalRemainingAncestor_);
-  db_.Register(new Internals::SignalFileDeleted(GetOutput()));
-  db_.Register(new Internals::SignalResourceDeleted(GetOutput()));
-}
-
-
-void Database::Close()
-{
-  db_.Close();
-}
-
-
-void Database::AddAttachment(int64_t id,
-                             const OrthancPluginAttachment& attachment)
-{
-  Orthanc::FileInfo info(attachment.uuid,
-                         static_cast<Orthanc::FileContentType>(attachment.contentType),
-                         attachment.uncompressedSize,
-                         attachment.uncompressedHash,
-                         static_cast<Orthanc::CompressionType>(attachment.compressionType),
-                         attachment.compressedSize,
-                         attachment.compressedHash);
-  base_.AddAttachment(id, info);
-}
-
-
-void Database::DeleteResource(int64_t id)
-{
-  signalRemainingAncestor_->Reset();
-
-  Orthanc::SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Resources WHERE internalId=?");
-  s.BindInt64(0, id);
-  s.Run();
-
-  if (signalRemainingAncestor_->HasRemainingAncestor())
-  {
-    GetOutput().SignalRemainingAncestor(signalRemainingAncestor_->GetRemainingAncestorId(),
-                                        signalRemainingAncestor_->GetRemainingAncestorType());
-  }
-}
-
-
-static void Answer(OrthancPlugins::DatabaseBackendOutput& output,
-                   const Orthanc::ServerIndexChange& change)
-{
-  output.AnswerChange(change.GetSeq(), 
-                      change.GetChangeType(),
-                      Orthanc::Plugins::Convert(change.GetResourceType()),
-                      change.GetPublicId(),
-                      change.GetDate());
-}
-
-
-static void Answer(OrthancPlugins::DatabaseBackendOutput& output,
-                   const Orthanc::ExportedResource& resource)
-{
-  output.AnswerExportedResource(resource.GetSeq(),
-                                Orthanc::Plugins::Convert(resource.GetResourceType()),
-                                resource.GetPublicId(),
-                                resource.GetModality(),
-                                resource.GetDate(),
-                                resource.GetPatientId(),
-                                resource.GetStudyInstanceUid(),
-                                resource.GetSeriesInstanceUid(),
-                                resource.GetSopInstanceUid());
-}
-
-
-void Database::GetChanges(bool& done /*out*/,
-                          int64_t since,
-                          uint32_t maxResults)
-{
-  typedef std::list<Orthanc::ServerIndexChange> Changes;
-
-  Changes changes;
-  base_.GetChanges(changes, done, since, maxResults);
-
-  for (Changes::const_iterator it = changes.begin(); it != changes.end(); ++it)
-  {
-    Answer(GetOutput(), *it);
-  }
-}
-
-
-void Database::GetExportedResources(bool& done /*out*/,
-                                    int64_t since,
-                                    uint32_t maxResults)
-{
-  typedef std::list<Orthanc::ExportedResource> Resources;
-
-  Resources resources;
-  base_.GetExportedResources(resources, done, since, maxResults);
-
-  for (Resources::const_iterator it = resources.begin(); it != resources.end(); ++it)
-  {
-    Answer(GetOutput(), *it);
-  }
-}
-
-
-void Database::GetLastChange()
-{
-  std::list<Orthanc::ServerIndexChange> change;
-  Orthanc::ErrorCode code = base_.GetLastChange(change);
-  
-  if (code != Orthanc::ErrorCode_Success)
-  {
-    throw OrthancPlugins::DatabaseException(static_cast<OrthancPluginErrorCode>(code));
-  }
-
-  if (!change.empty())
-  {
-    Answer(GetOutput(), change.front());
-  }
-}
-
-
-void Database::GetLastExportedResource()
-{
-  std::list<Orthanc::ExportedResource> resource;
-  base_.GetLastExportedResource(resource);
-  
-  if (!resource.empty())
-  {
-    Answer(GetOutput(), resource.front());
-  }
-}
-
-
-void Database::GetMainDicomTags(int64_t id)
-{
-  Orthanc::DicomMap tags;
-  base_.GetMainDicomTags(tags, id);
-
-  Orthanc::DicomArray arr(tags);
-  for (size_t i = 0; i < arr.GetSize(); i++)
-  {
-    GetOutput().AnswerDicomTag(arr.GetElement(i).GetTag().GetGroup(),
-                               arr.GetElement(i).GetTag().GetElement(),
-                               arr.GetElement(i).GetValue().GetContent());
-  }
-}
-
-
-std::string Database::GetPublicId(int64_t resourceId)
-{
-  std::string id;
-  if (base_.GetPublicId(id, resourceId))
-  {
-    return id;
-  }
-  else
-  {
-    throw OrthancPlugins::DatabaseException(OrthancPluginErrorCode_UnknownResource);
-  }
-}
-
-
-OrthancPluginResourceType Database::GetResourceType(int64_t resourceId)
-{
-  Orthanc::ResourceType  result;
-  Orthanc::ErrorCode  code = base_.GetResourceType(result, resourceId);
-
-  if (code == Orthanc::ErrorCode_Success)
-  {
-    return Orthanc::Plugins::Convert(result);
-  }
-  else
-  {
-    throw OrthancPlugins::DatabaseException(static_cast<OrthancPluginErrorCode>(code));
-  }
-}
-
-
-
-template <typename I>
-static void ConvertList(std::list<int32_t>& target,
-                        const std::list<I>& source)
-{
-  for (typename std::list<I>::const_iterator 
-         it = source.begin(); it != source.end(); ++it)
-  {
-    target.push_back(*it);
-  }
-}
-
-
-void Database::ListAvailableMetadata(std::list<int32_t>& target /*out*/,
-                                     int64_t id)
-{
-  std::list<Orthanc::MetadataType> tmp;
-  base_.ListAvailableMetadata(tmp, id);
-  ConvertList(target, tmp);
-}
-
-
-void Database::ListAvailableAttachments(std::list<int32_t>& target /*out*/,
-                                        int64_t id)
-{
-  std::list<Orthanc::FileContentType> tmp;
-  base_.ListAvailableAttachments(tmp, id);
-  ConvertList(target, tmp);
-}
-
-
-void Database::LogChange(const OrthancPluginChange& change)
-{
-  int64_t id;
-  OrthancPluginResourceType type;
-  if (!LookupResource(id, type, change.publicId) ||
-      type != change.resourceType)
-  {
-    throw OrthancPlugins::DatabaseException(OrthancPluginErrorCode_DatabasePlugin);
-  }
-
-  Orthanc::ServerIndexChange tmp(change.seq,
-                                 static_cast<Orthanc::ChangeType>(change.changeType),
-                                 Orthanc::Plugins::Convert(change.resourceType),
-                                 change.publicId,
-                                 change.date);
-
-  base_.LogChange(id, tmp);
-}
-
-
-void Database::LogExportedResource(const OrthancPluginExportedResource& resource) 
-{
-  Orthanc::ExportedResource tmp(resource.seq,
-                                Orthanc::Plugins::Convert(resource.resourceType),
-                                resource.publicId,
-                                resource.modality,
-                                resource.date,
-                                resource.patientId,
-                                resource.studyInstanceUid,
-                                resource.seriesInstanceUid,
-                                resource.sopInstanceUid);
-
-  base_.LogExportedResource(tmp);
-}
-
-    
-bool Database::LookupAttachment(int64_t id,
-                                int32_t contentType)
-{
-  Orthanc::FileInfo attachment;
-  if (base_.LookupAttachment(attachment, id, static_cast<Orthanc::FileContentType>(contentType)))
-  {
-    GetOutput().AnswerAttachment(attachment.GetUuid(),
-                                 attachment.GetContentType(),
-                                 attachment.GetUncompressedSize(),
-                                 attachment.GetUncompressedMD5(),
-                                 attachment.GetCompressionType(),
-                                 attachment.GetCompressedSize(),
-                                 attachment.GetCompressedMD5());
-    return true;
-  }
-  else
-  {
-    return false;
-  }
-}
-
-
-bool Database::LookupParent(int64_t& parentId /*out*/,
-                            int64_t resourceId)
-{
-  bool found;
-  Orthanc::ErrorCode code = base_.LookupParent(found, parentId, resourceId);
-
-  if (code == Orthanc::ErrorCode_Success)
-  {
-    return found;
-  }
-  else
-  {
-    throw OrthancPlugins::DatabaseException(static_cast<OrthancPluginErrorCode>(code));
-  }
-}
-
-
-bool Database::LookupResource(int64_t& id /*out*/,
-                              OrthancPluginResourceType& type /*out*/,
-                              const char* publicId)
-{
-  Orthanc::ResourceType tmp;
-  if (base_.LookupResource(id, tmp, publicId))
-  {
-    type = Orthanc::Plugins::Convert(tmp);
-    return true;
-  }
-  else
-  {
-    return false;
-  }
-}
-
-
-void Database::StartTransaction()
-{
-  transaction_.reset(new Orthanc::SQLite::Transaction(db_));
-  transaction_->Begin();
-}
-
-
-void Database::RollbackTransaction()
-{
-  transaction_->Rollback();
-  transaction_.reset(NULL);
-}
-
-
-void Database::CommitTransaction()
-{
-  transaction_->Commit();
-  transaction_.reset(NULL);
-}
-
-
-uint32_t Database::GetDatabaseVersion()
-{
-  std::string version;
-
-  if (!LookupGlobalProperty(version, Orthanc::GlobalProperty_DatabaseSchemaVersion))
-  {
-    throw OrthancPlugins::DatabaseException(OrthancPluginErrorCode_InternalError);
-  }
-
-  try
-  {
-    return boost::lexical_cast<uint32_t>(version);
-  }
-  catch (boost::bad_lexical_cast&)
-  {
-    throw OrthancPlugins::DatabaseException(OrthancPluginErrorCode_InternalError);
-  }
-}
-
-
-void Database::UpgradeDatabase(uint32_t  targetVersion,
-                               OrthancPluginStorageArea* storageArea)
-{
-  if (targetVersion == 6)
-  {
-    OrthancPluginErrorCode code = OrthancPluginReconstructMainDicomTags(GetOutput().GetContext(), storageArea, 
-                                                                        OrthancPluginResourceType_Study);
-    if (code == OrthancPluginErrorCode_Success)
-    {
-      code = OrthancPluginReconstructMainDicomTags(GetOutput().GetContext(), storageArea, 
-                                                   OrthancPluginResourceType_Series);
-    }
-
-    if (code != OrthancPluginErrorCode_Success)
-    {
-      throw OrthancPlugins::DatabaseException(code);
-    }
-
-    base_.SetGlobalProperty(Orthanc::GlobalProperty_DatabaseSchemaVersion, "6");
-  }
-}
--- a/OrthancServer/Resources/Graveyard/DatabasePluginSample/Database.h	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,275 +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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "OrthancCppDatabasePlugin.h"
-
-#include "../../../../OrthancFramework/Sources/SQLite/Connection.h"
-#include "../../../../OrthancFramework/Sources/SQLite/Transaction.h"
-#include "../../../Plugins/Engine/PluginsEnumerations.h"
-#include "DatabaseWrapperBase.h"
-
-#include <memory>
-
-class Database : public OrthancPlugins::IDatabaseBackend
-{
-private:
-  class SignalRemainingAncestor;
-
-  std::string                   path_;
-  Orthanc::SQLite::Connection   db_;
-  Orthanc::DatabaseWrapperBase  base_;
-  SignalRemainingAncestor*      signalRemainingAncestor_;
-
-  std::auto_ptr<Orthanc::SQLite::Transaction>  transaction_;
-
-public:
-  Database(const std::string& path);
-
-  virtual void Open();
-
-  virtual void Close();
-
-  virtual void AddAttachment(int64_t id,
-                             const OrthancPluginAttachment& attachment);
-
-  virtual void AttachChild(int64_t parent,
-                           int64_t child)
-  {
-    base_.AttachChild(parent, child);
-  }
-
-  virtual void ClearChanges()
-  {
-    db_.Execute("DELETE FROM Changes");    
-  }
-
-  virtual void ClearExportedResources()
-  {
-    db_.Execute("DELETE FROM ExportedResources");    
-  }
-
-  virtual int64_t CreateResource(const char* publicId,
-                                 OrthancPluginResourceType type)
-  {
-    return base_.CreateResource(publicId, Orthanc::Plugins::Convert(type));
-  }
-
-  virtual void DeleteAttachment(int64_t id,
-                                int32_t attachment)
-  {
-    base_.DeleteAttachment(id, static_cast<Orthanc::FileContentType>(attachment));
-  }
-
-  virtual void DeleteMetadata(int64_t id,
-                              int32_t metadataType)
-  {
-    base_.DeleteMetadata(id, static_cast<Orthanc::MetadataType>(metadataType));
-  }
-
-  virtual void DeleteResource(int64_t id);
-
-  virtual void GetAllInternalIds(std::list<int64_t>& target,
-                                 OrthancPluginResourceType resourceType)
-  {
-    base_.GetAllInternalIds(target, Orthanc::Plugins::Convert(resourceType));
-  }
-
-  virtual void GetAllPublicIds(std::list<std::string>& target,
-                               OrthancPluginResourceType resourceType)
-  {
-    base_.GetAllPublicIds(target, Orthanc::Plugins::Convert(resourceType));
-  }
-
-  virtual void GetAllPublicIds(std::list<std::string>& target,
-                               OrthancPluginResourceType resourceType,
-                               uint64_t since,
-                               uint64_t limit)
-  {
-    base_.GetAllPublicIds(target, Orthanc::Plugins::Convert(resourceType), since, limit);
-  }
-
-  virtual void GetChanges(bool& done /*out*/,
-                          int64_t since,
-                          uint32_t maxResults);
-
-  virtual void GetChildrenInternalId(std::list<int64_t>& target /*out*/,
-                                     int64_t id)
-  {
-    base_.GetChildrenInternalId(target, id);
-  }
-
-  virtual void GetChildrenPublicId(std::list<std::string>& target /*out*/,
-                                   int64_t id)
-  {
-    base_.GetChildrenPublicId(target, id);
-  }
-
-  virtual void GetExportedResources(bool& done /*out*/,
-                                    int64_t since,
-                                    uint32_t maxResults);
-
-  virtual void GetLastChange();
-
-  virtual void GetLastExportedResource();
-
-  virtual void GetMainDicomTags(int64_t id);
-
-  virtual std::string GetPublicId(int64_t resourceId);
-
-  virtual uint64_t GetResourceCount(OrthancPluginResourceType resourceType)
-  {
-    return base_.GetResourceCount(Orthanc::Plugins::Convert(resourceType));
-  }
-
-  virtual OrthancPluginResourceType GetResourceType(int64_t resourceId);
-
-  virtual uint64_t GetTotalCompressedSize()
-  {
-    return base_.GetTotalCompressedSize();
-  }
-    
-  virtual uint64_t GetTotalUncompressedSize()
-  {
-    return base_.GetTotalUncompressedSize();
-  }
-
-  virtual bool IsExistingResource(int64_t internalId)
-  {
-    return base_.IsExistingResource(internalId);
-  }
-
-  virtual bool IsProtectedPatient(int64_t internalId)
-  {
-    return base_.IsProtectedPatient(internalId);
-  }
-
-  virtual void ListAvailableMetadata(std::list<int32_t>& target /*out*/,
-                                     int64_t id);
-
-  virtual void ListAvailableAttachments(std::list<int32_t>& target /*out*/,
-                                        int64_t id);
-
-  virtual void LogChange(const OrthancPluginChange& change);
-
-  virtual void LogExportedResource(const OrthancPluginExportedResource& resource);
-    
-  virtual bool LookupAttachment(int64_t id,
-                                int32_t contentType);
-
-  virtual bool LookupGlobalProperty(std::string& target /*out*/,
-                                    int32_t property)
-  {
-    return base_.LookupGlobalProperty(target, static_cast<Orthanc::GlobalProperty>(property));
-  }
-
-  virtual void LookupIdentifier(std::list<int64_t>& target /*out*/,
-                                OrthancPluginResourceType level,
-                                uint16_t group,
-                                uint16_t element,
-                                OrthancPluginIdentifierConstraint constraint,
-                                const char* value)
-  {
-    base_.LookupIdentifier(target, Orthanc::Plugins::Convert(level),
-                           Orthanc::DicomTag(group, element), 
-                           Orthanc::Plugins::Convert(constraint), value);
-  }
-
-  virtual bool LookupMetadata(std::string& target /*out*/,
-                              int64_t id,
-                              int32_t metadataType)
-  {
-    return base_.LookupMetadata(target, id, static_cast<Orthanc::MetadataType>(metadataType));
-  }
-
-  virtual bool LookupParent(int64_t& parentId /*out*/,
-                            int64_t resourceId);
-
-  virtual bool LookupResource(int64_t& id /*out*/,
-                              OrthancPluginResourceType& type /*out*/,
-                              const char* publicId);
-
-  virtual bool SelectPatientToRecycle(int64_t& internalId /*out*/)
-  {
-    return base_.SelectPatientToRecycle(internalId);
-  }
-
-  virtual bool SelectPatientToRecycle(int64_t& internalId /*out*/,
-                                      int64_t patientIdToAvoid)
-  {
-    return base_.SelectPatientToRecycle(internalId, patientIdToAvoid);
-  }
-
-
-  virtual void SetGlobalProperty(int32_t property,
-                                 const char* value)
-  {
-    base_.SetGlobalProperty(static_cast<Orthanc::GlobalProperty>(property), value);
-  }
-
-  virtual void SetMainDicomTag(int64_t id,
-                               uint16_t group,
-                               uint16_t element,
-                               const char* value)
-  {
-    base_.SetMainDicomTag(id, Orthanc::DicomTag(group, element), value);
-  }
-
-  virtual void SetIdentifierTag(int64_t id,
-                                uint16_t group,
-                                uint16_t element,
-                                const char* value)
-  {
-    base_.SetIdentifierTag(id, Orthanc::DicomTag(group, element), value);
-  }
-
-  virtual void SetMetadata(int64_t id,
-                           int32_t metadataType,
-                           const char* value)
-  {
-    base_.SetMetadata(id, static_cast<Orthanc::MetadataType>(metadataType), value);
-  }
-
-  virtual void SetProtectedPatient(int64_t internalId, 
-                                   bool isProtected)
-  {
-    base_.SetProtectedPatient(internalId, isProtected);
-  }
-
-  virtual void StartTransaction();
-
-  virtual void RollbackTransaction();
-
-  virtual void CommitTransaction();
-
-  virtual uint32_t GetDatabaseVersion();
-
-  virtual void UpgradeDatabase(uint32_t  targetVersion,
-                               OrthancPluginStorageArea* storageArea);
-
-  virtual void ClearMainDicomTags(int64_t internalId)
-  {
-    base_.ClearMainDicomTags(internalId);
-  }
-};
--- a/OrthancServer/Resources/Graveyard/DatabasePluginSample/DatabaseWrapperBase.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,747 +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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../../../Sources/PrecompiledHeadersServer.h"
-#include "DatabaseWrapperBase.h"
-
-#include <stdio.h>
-#include <memory>
-
-namespace Orthanc
-{
-  void DatabaseWrapperBase::SetGlobalProperty(GlobalProperty property,
-                                              const std::string& value)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO GlobalProperties VALUES(?, ?)");
-    s.BindInt(0, property);
-    s.BindString(1, value);
-    s.Run();
-  }
-
-  bool DatabaseWrapperBase::LookupGlobalProperty(std::string& target,
-                                                 GlobalProperty property)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT value FROM GlobalProperties WHERE property=?");
-    s.BindInt(0, property);
-
-    if (!s.Step())
-    {
-      return false;
-    }
-    else
-    {
-      target = s.ColumnString(0);
-      return true;
-    }
-  }
-
-  int64_t DatabaseWrapperBase::CreateResource(const std::string& publicId,
-                                              ResourceType type)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(NULL, ?, ?, NULL)");
-    s.BindInt(0, type);
-    s.BindString(1, publicId);
-    s.Run();
-    return db_.GetLastInsertRowId();
-  }
-
-  bool DatabaseWrapperBase::LookupResource(int64_t& id,
-                                           ResourceType& type,
-                                           const std::string& publicId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT internalId, resourceType FROM Resources WHERE publicId=?");
-    s.BindString(0, publicId);
-
-    if (!s.Step())
-    {
-      return false;
-    }
-    else
-    {
-      id = s.ColumnInt(0);
-      type = static_cast<ResourceType>(s.ColumnInt(1));
-
-      // Check whether there is a single resource with this public id
-      assert(!s.Step());
-
-      return true;
-    }
-  }
-
-  ErrorCode DatabaseWrapperBase::LookupParent(bool& found,
-                                              int64_t& parentId,
-                                              int64_t resourceId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT parentId FROM Resources WHERE internalId=?");
-    s.BindInt64(0, resourceId);
-
-    if (!s.Step())
-    {
-      return ErrorCode_UnknownResource;
-    }
-
-    if (s.ColumnIsNull(0))
-    {
-      found = false;
-    }
-    else
-    {
-      found = true;
-      parentId = s.ColumnInt(0);
-    }
-
-    return ErrorCode_Success;
-  }
-
-  bool DatabaseWrapperBase::GetPublicId(std::string& result,
-                                        int64_t resourceId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT publicId FROM Resources WHERE internalId=?");
-    s.BindInt64(0, resourceId);
-    
-    if (!s.Step())
-    { 
-      return false;
-    }
-    else
-    {
-      result = s.ColumnString(0);
-      return true;
-    }
-  }
-
-
-  ErrorCode DatabaseWrapperBase::GetResourceType(ResourceType& result,
-                                                 int64_t resourceId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT resourceType FROM Resources WHERE internalId=?");
-    s.BindInt64(0, resourceId);
-    
-    if (s.Step())
-    {
-      result = static_cast<ResourceType>(s.ColumnInt(0));
-      return ErrorCode_Success;
-    }
-    else
-    { 
-      return ErrorCode_UnknownResource;
-    }
-  }
-
-
-  void DatabaseWrapperBase::AttachChild(int64_t parent,
-                                        int64_t child)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "UPDATE Resources SET parentId = ? WHERE internalId = ?");
-    s.BindInt64(0, parent);
-    s.BindInt64(1, child);
-    s.Run();
-  }
-
-
-  void DatabaseWrapperBase::SetMetadata(int64_t id,
-                                        MetadataType type,
-                                        const std::string& value)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata VALUES(?, ?, ?)");
-    s.BindInt64(0, id);
-    s.BindInt(1, type);
-    s.BindString(2, value);
-    s.Run();
-  }
-
-  void DatabaseWrapperBase::DeleteMetadata(int64_t id,
-                                           MetadataType type)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Metadata WHERE id=? and type=?");
-    s.BindInt64(0, id);
-    s.BindInt(1, type);
-    s.Run();
-  }
-
-  bool DatabaseWrapperBase::LookupMetadata(std::string& target,
-                                           int64_t id,
-                                           MetadataType type)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT value FROM Metadata WHERE id=? AND type=?");
-    s.BindInt64(0, id);
-    s.BindInt(1, type);
-
-    if (!s.Step())
-    {
-      return false;
-    }
-    else
-    {
-      target = s.ColumnString(0);
-      return true;
-    }
-  }
-
-  void DatabaseWrapperBase::ListAvailableMetadata(std::list<MetadataType>& target,
-                                                  int64_t id)
-  {
-    target.clear();
-
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type FROM Metadata WHERE id=?");
-    s.BindInt64(0, id);
-
-    while (s.Step())
-    {
-      target.push_back(static_cast<MetadataType>(s.ColumnInt(0)));
-    }
-  }
-
-
-  void DatabaseWrapperBase::AddAttachment(int64_t id,
-                                          const FileInfo& attachment)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?, ?, ?, ?)");
-    s.BindInt64(0, id);
-    s.BindInt(1, attachment.GetContentType());
-    s.BindString(2, attachment.GetUuid());
-    s.BindInt64(3, attachment.GetCompressedSize());
-    s.BindInt64(4, attachment.GetUncompressedSize());
-    s.BindInt(5, attachment.GetCompressionType());
-    s.BindString(6, attachment.GetUncompressedMD5());
-    s.BindString(7, attachment.GetCompressedMD5());
-    s.Run();
-  }
-
-
-  void DatabaseWrapperBase::DeleteAttachment(int64_t id,
-                                             FileContentType attachment)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM AttachedFiles WHERE id=? AND fileType=?");
-    s.BindInt64(0, id);
-    s.BindInt(1, attachment);
-    s.Run();
-  }
-
-
-
-  void DatabaseWrapperBase::ListAvailableAttachments(std::list<FileContentType>& target,
-                                                     int64_t id)
-  {
-    target.clear();
-
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT fileType FROM AttachedFiles WHERE id=?");
-    s.BindInt64(0, id);
-
-    while (s.Step())
-    {
-      target.push_back(static_cast<FileContentType>(s.ColumnInt(0)));
-    }
-  }
-
-  bool DatabaseWrapperBase::LookupAttachment(FileInfo& attachment,
-                                             int64_t id,
-                                             FileContentType contentType)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT uuid, uncompressedSize, compressionType, compressedSize, uncompressedMD5, compressedMD5 FROM AttachedFiles WHERE id=? AND fileType=?");
-    s.BindInt64(0, id);
-    s.BindInt(1, contentType);
-
-    if (!s.Step())
-    {
-      return false;
-    }
-    else
-    {
-      attachment = FileInfo(s.ColumnString(0),
-                            contentType,
-                            s.ColumnInt64(1),
-                            s.ColumnString(4),
-                            static_cast<CompressionType>(s.ColumnInt(2)),
-                            s.ColumnInt64(3),
-                            s.ColumnString(5));
-      return true;
-    }
-  }
-
-
-  void DatabaseWrapperBase::ClearMainDicomTags(int64_t id)
-  {
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM DicomIdentifiers WHERE id=?");
-      s.BindInt64(0, id);
-      s.Run();
-    }
-
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM MainDicomTags WHERE id=?");
-      s.BindInt64(0, id);
-      s.Run();
-    }
-  }
-
-
-  void DatabaseWrapperBase::SetMainDicomTag(int64_t id,
-                                            const DicomTag& tag,
-                                            const std::string& value)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)");
-    s.BindInt64(0, id);
-    s.BindInt(1, tag.GetGroup());
-    s.BindInt(2, tag.GetElement());
-    s.BindString(3, value);
-    s.Run();
-  }
-
-
-  void DatabaseWrapperBase::SetIdentifierTag(int64_t id,
-                                             const DicomTag& tag,
-                                             const std::string& value)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO DicomIdentifiers VALUES(?, ?, ?, ?)");
-    s.BindInt64(0, id);
-    s.BindInt(1, tag.GetGroup());
-    s.BindInt(2, tag.GetElement());
-    s.BindString(3, value);
-    s.Run();
-  }
-
-
-  void DatabaseWrapperBase::GetMainDicomTags(DicomMap& map,
-                                             int64_t id)
-  {
-    map.Clear();
-
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM MainDicomTags WHERE id=?");
-    s.BindInt64(0, id);
-    while (s.Step())
-    {
-      map.SetValue(s.ColumnInt(1),
-                   s.ColumnInt(2),
-                   s.ColumnString(3), false);
-    }
-  }
-
-
-
-  void DatabaseWrapperBase::GetChildrenPublicId(std::list<std::string>& target,
-                                                int64_t id)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b  "
-                        "WHERE a.parentId = b.internalId AND b.internalId = ?");     
-    s.BindInt64(0, id);
-
-    target.clear();
-
-    while (s.Step())
-    {
-      target.push_back(s.ColumnString(0));
-    }
-  }
-
-
-  void DatabaseWrapperBase::GetChildrenInternalId(std::list<int64_t>& target,
-                                                  int64_t id)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.internalId FROM Resources AS a, Resources AS b  "
-                        "WHERE a.parentId = b.internalId AND b.internalId = ?");     
-    s.BindInt64(0, id);
-
-    target.clear();
-
-    while (s.Step())
-    {
-      target.push_back(s.ColumnInt64(0));
-    }
-  }
-
-
-  void DatabaseWrapperBase::LogChange(int64_t internalId,
-                                      const ServerIndexChange& change)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes VALUES(NULL, ?, ?, ?, ?)");
-    s.BindInt(0, change.GetChangeType());
-    s.BindInt64(1, internalId);
-    s.BindInt(2, change.GetResourceType());
-    s.BindString(3, change.GetDate());
-    s.Run();
-  }
-
-
-  ErrorCode DatabaseWrapperBase::GetChangesInternal(std::list<ServerIndexChange>& target,
-                                                    bool& done,
-                                                    SQLite::Statement& s,
-                                                    uint32_t maxResults)
-  {
-    target.clear();
-
-    while (target.size() < maxResults && s.Step())
-    {
-      int64_t seq = s.ColumnInt64(0);
-      ChangeType changeType = static_cast<ChangeType>(s.ColumnInt(1));
-      ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(3));
-      const std::string& date = s.ColumnString(4);
-
-      int64_t internalId = s.ColumnInt64(2);
-      std::string publicId;
-      if (!GetPublicId(publicId, internalId))
-      {
-        return ErrorCode_UnknownResource;
-      }
-
-      target.push_back(ServerIndexChange(seq, changeType, resourceType, publicId, date));
-    }
-
-    done = !(target.size() == maxResults && s.Step());
-    return ErrorCode_Success;
-  }
-
-
-  ErrorCode DatabaseWrapperBase::GetChanges(std::list<ServerIndexChange>& target,
-                                            bool& done,
-                                            int64_t since,
-                                            uint32_t maxResults)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?");
-    s.BindInt64(0, since);
-    s.BindInt(1, maxResults + 1);
-    return GetChangesInternal(target, done, s, maxResults);
-  }
-
-  ErrorCode DatabaseWrapperBase::GetLastChange(std::list<ServerIndexChange>& target)
-  {
-    bool done;  // Ignored
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1");
-    return GetChangesInternal(target, done, s, 1);
-  }
-
-
-  void DatabaseWrapperBase::LogExportedResource(const ExportedResource& resource)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "INSERT INTO ExportedResources VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?)");
-
-    s.BindInt(0, resource.GetResourceType());
-    s.BindString(1, resource.GetPublicId());
-    s.BindString(2, resource.GetModality());
-    s.BindString(3, resource.GetPatientId());
-    s.BindString(4, resource.GetStudyInstanceUid());
-    s.BindString(5, resource.GetSeriesInstanceUid());
-    s.BindString(6, resource.GetSopInstanceUid());
-    s.BindString(7, resource.GetDate());
-    s.Run();      
-  }
-
-
-  void DatabaseWrapperBase::GetExportedResourcesInternal(std::list<ExportedResource>& target,
-                                                         bool& done,
-                                                         SQLite::Statement& s,
-                                                         uint32_t maxResults)
-  {
-    target.clear();
-
-    while (target.size() < maxResults && s.Step())
-    {
-      int64_t seq = s.ColumnInt64(0);
-      ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(1));
-      std::string publicId = s.ColumnString(2);
-
-      ExportedResource resource(seq, 
-                                resourceType,
-                                publicId,
-                                s.ColumnString(3),  // modality
-                                s.ColumnString(8),  // date
-                                s.ColumnString(4),  // patient ID
-                                s.ColumnString(5),  // study instance UID
-                                s.ColumnString(6),  // series instance UID
-                                s.ColumnString(7)); // sop instance UID
-
-      target.push_back(resource);
-    }
-
-    done = !(target.size() == maxResults && s.Step());
-  }
-
-
-  void DatabaseWrapperBase::GetExportedResources(std::list<ExportedResource>& target,
-                                                 bool& done,
-                                                 int64_t since,
-                                                 uint32_t maxResults)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT * FROM ExportedResources WHERE seq>? ORDER BY seq LIMIT ?");
-    s.BindInt64(0, since);
-    s.BindInt(1, maxResults + 1);
-    GetExportedResourcesInternal(target, done, s, maxResults);
-  }
-
-    
-  void DatabaseWrapperBase::GetLastExportedResource(std::list<ExportedResource>& target)
-  {
-    bool done;  // Ignored
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT * FROM ExportedResources ORDER BY seq DESC LIMIT 1");
-    GetExportedResourcesInternal(target, done, s, 1);
-  }
-
-
-    
-  uint64_t DatabaseWrapperBase::GetTotalCompressedSize()
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles");
-    s.Run();
-    return static_cast<uint64_t>(s.ColumnInt64(0));
-  }
-
-    
-  uint64_t DatabaseWrapperBase::GetTotalUncompressedSize()
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles");
-    s.Run();
-    return static_cast<uint64_t>(s.ColumnInt64(0));
-  }
-
-  void DatabaseWrapperBase::GetAllInternalIds(std::list<int64_t>& target,
-                                              ResourceType resourceType)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT internalId FROM Resources WHERE resourceType=?");
-    s.BindInt(0, resourceType);
-
-    target.clear();
-    while (s.Step())
-    {
-      target.push_back(s.ColumnInt64(0));
-    }
-  }
-
-
-  void DatabaseWrapperBase::GetAllPublicIds(std::list<std::string>& target,
-                                            ResourceType resourceType)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=?");
-    s.BindInt(0, resourceType);
-
-    target.clear();
-    while (s.Step())
-    {
-      target.push_back(s.ColumnString(0));
-    }
-  }
-
-  void DatabaseWrapperBase::GetAllPublicIds(std::list<std::string>& target,
-                                            ResourceType resourceType,
-                                            size_t since,
-                                            size_t limit)
-  {
-    if (limit == 0)
-    {
-      target.clear();
-      return;
-    }
-
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=? LIMIT ? OFFSET ?");
-    s.BindInt(0, resourceType);
-    s.BindInt64(1, limit);
-    s.BindInt64(2, since);
-
-    target.clear();
-    while (s.Step())
-    {
-      target.push_back(s.ColumnString(0));
-    }
-  }
-
-
-  uint64_t DatabaseWrapperBase::GetResourceCount(ResourceType resourceType)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT COUNT(*) FROM Resources WHERE resourceType=?");
-    s.BindInt(0, resourceType);
-    
-    if (!s.Step())
-    {
-      return 0;
-    }
-    else
-    {
-      int64_t c = s.ColumnInt(0);
-      assert(!s.Step());
-      return c;
-    }
-  }
-
-
-  bool DatabaseWrapperBase::SelectPatientToRecycle(int64_t& internalId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE,
-                        "SELECT patientId FROM PatientRecyclingOrder ORDER BY seq ASC LIMIT 1");
-   
-    if (!s.Step())
-    {
-      // No patient remaining or all the patients are protected
-      return false;
-    }
-    else
-    {
-      internalId = s.ColumnInt(0);
-      return true;
-    }    
-  }
-
-  bool DatabaseWrapperBase::SelectPatientToRecycle(int64_t& internalId,
-                                                   int64_t patientIdToAvoid)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE,
-                        "SELECT patientId FROM PatientRecyclingOrder "
-                        "WHERE patientId != ? ORDER BY seq ASC LIMIT 1");
-    s.BindInt64(0, patientIdToAvoid);
-
-    if (!s.Step())
-    {
-      // No patient remaining or all the patients are protected
-      return false;
-    }
-    else
-    {
-      internalId = s.ColumnInt(0);
-      return true;
-    }   
-  }
-
-  bool DatabaseWrapperBase::IsProtectedPatient(int64_t internalId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE,
-                        "SELECT * FROM PatientRecyclingOrder WHERE patientId = ?");
-    s.BindInt64(0, internalId);
-    return !s.Step();
-  }
-
-  void DatabaseWrapperBase::SetProtectedPatient(int64_t internalId, 
-                                                bool isProtected)
-  {
-    if (isProtected)
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM PatientRecyclingOrder WHERE patientId=?");
-      s.BindInt64(0, internalId);
-      s.Run();
-    }
-    else if (IsProtectedPatient(internalId))
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)");
-      s.BindInt64(0, internalId);
-      s.Run();
-    }
-    else
-    {
-      // Nothing to do: The patient is already unprotected
-    }
-  }
-
-
-
-  bool DatabaseWrapperBase::IsExistingResource(int64_t internalId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT * FROM Resources WHERE internalId=?");
-    s.BindInt64(0, internalId);
-    return s.Step();
-  }
-
-
-
-  void DatabaseWrapperBase::LookupIdentifier(std::list<int64_t>& target,
-                                             ResourceType level,
-                                             const DicomTag& tag,
-                                             IdentifierConstraintType type,
-                                             const std::string& value)
-  {
-    static const char* COMMON = ("SELECT d.id FROM DicomIdentifiers AS d, Resources AS r WHERE "
-                                 "d.id = r.internalId AND r.resourceType=? AND "
-                                 "d.tagGroup=? AND d.tagElement=? AND ");
-
-    std::auto_ptr<SQLite::Statement> s;
-
-    switch (type)
-    {
-      case IdentifierConstraintType_GreaterOrEqual:
-        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value>=?"));
-        break;
-
-      case IdentifierConstraintType_SmallerOrEqual:
-        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value<=?"));
-        break;
-
-      case IdentifierConstraintType_Wildcard:
-        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value GLOB ?"));
-        break;
-
-      case IdentifierConstraintType_Equal:
-      default:
-        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value=?"));
-        break;
-    }
-
-    assert(s.get() != NULL);
-
-    s->BindInt(0, level);
-    s->BindInt(1, tag.GetGroup());
-    s->BindInt(2, tag.GetElement());
-    s->BindString(3, value);
-
-    target.clear();
-
-    while (s->Step())
-    {
-      target.push_back(s->ColumnInt64(0));
-    }    
-  }
-
-
-  void DatabaseWrapperBase::LookupIdentifierRange(std::list<int64_t>& target,
-                                                  ResourceType level,
-                                                  const DicomTag& tag,
-                                                  const std::string& start,
-                                                  const std::string& end)
-  {
-    SQLite::Statement statement(db_, SQLITE_FROM_HERE,
-                                "SELECT d.id FROM DicomIdentifiers AS d, Resources AS r WHERE "
-                                "d.id = r.internalId AND r.resourceType=? AND "
-                                "d.tagGroup=? AND d.tagElement=? AND d.value>=? AND d.value<=?");
-
-    statement.BindInt(0, level);
-    statement.BindInt(1, tag.GetGroup());
-    statement.BindInt(2, tag.GetElement());
-    statement.BindString(3, start);
-    statement.BindString(4, end);
-
-    target.clear();
-
-    while (statement.Step())
-    {
-      target.push_back(statement.ColumnInt64(0));
-    }    
-  }
-}
--- a/OrthancServer/Resources/Graveyard/DatabasePluginSample/DatabaseWrapperBase.h	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,200 +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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../../../../OrthancFramework/Sources/DicomFormat/DicomMap.h"
-#include "../../../../OrthancFramework/Sources/DicomFormat/DicomTag.h"
-#include "../../../../OrthancFramework/Sources/Enumerations.h"
-#include "../../../../OrthancFramework/Sources/FileStorage/FileInfo.h"
-#include "../../../../OrthancFramework/Sources/SQLite/Connection.h"
-#include "../../../Sources/ExportedResource.h"
-#include "../../../Sources/ServerIndexChange.h"
-#include "../../../Sources/ServerEnumerations.h"
-
-#include <list>
-
-
-namespace Orthanc
-{
-  /**
-   * This class is shared between the Orthanc core and the sample
-   * database plugin whose code is in
-   * "../Plugins/Samples/DatabasePlugin".
-   **/
-  class DatabaseWrapperBase
-  {
-  private:
-    SQLite::Connection&  db_;
-
-    ErrorCode GetChangesInternal(std::list<ServerIndexChange>& target,
-                                 bool& done,
-                                 SQLite::Statement& s,
-                                 uint32_t maxResults);
-
-    void GetExportedResourcesInternal(std::list<ExportedResource>& target,
-                                      bool& done,
-                                      SQLite::Statement& s,
-                                      uint32_t maxResults);
-
-  public:
-    DatabaseWrapperBase(SQLite::Connection& db) : db_(db)
-    {
-    }
-
-    void SetGlobalProperty(GlobalProperty property,
-                           const std::string& value);
-
-    bool LookupGlobalProperty(std::string& target,
-                              GlobalProperty property);
-
-    int64_t CreateResource(const std::string& publicId,
-                           ResourceType type);
-
-    bool LookupResource(int64_t& id,
-                        ResourceType& type,
-                        const std::string& publicId);
-
-    ErrorCode LookupParent(bool& found,
-                           int64_t& parentId,
-                           int64_t resourceId);
-
-    bool GetPublicId(std::string& result,
-                     int64_t resourceId);
-
-    ErrorCode GetResourceType(ResourceType& result,
-                              int64_t resourceId);
-
-    void AttachChild(int64_t parent,
-                     int64_t child);
-
-    void SetMetadata(int64_t id,
-                     MetadataType type,
-                     const std::string& value);
-
-    void DeleteMetadata(int64_t id,
-                        MetadataType type);
-
-    bool LookupMetadata(std::string& target,
-                        int64_t id,
-                        MetadataType type);
-
-    void ListAvailableMetadata(std::list<MetadataType>& target,
-                               int64_t id);
-
-    void AddAttachment(int64_t id,
-                       const FileInfo& attachment);
-
-    void DeleteAttachment(int64_t id,
-                          FileContentType attachment);
-
-    void ListAvailableAttachments(std::list<FileContentType>& target,
-                                  int64_t id);
-
-    bool LookupAttachment(FileInfo& attachment,
-                          int64_t id,
-                          FileContentType contentType);
-
-
-    void ClearMainDicomTags(int64_t id);
-
-
-    void SetMainDicomTag(int64_t id,
-                         const DicomTag& tag,
-                         const std::string& value);
-
-    void SetIdentifierTag(int64_t id,
-                          const DicomTag& tag,
-                          const std::string& value);
-
-    void GetMainDicomTags(DicomMap& map,
-                          int64_t id);
-
-    void GetChildrenPublicId(std::list<std::string>& target,
-                             int64_t id);
-
-    void GetChildrenInternalId(std::list<int64_t>& target,
-                               int64_t id);
-
-    void LogChange(int64_t internalId,
-                   const ServerIndexChange& change);
-
-    ErrorCode GetChanges(std::list<ServerIndexChange>& target,
-                         bool& done,
-                         int64_t since,
-                         uint32_t maxResults);
-
-    ErrorCode GetLastChange(std::list<ServerIndexChange>& target);
-
-    void LogExportedResource(const ExportedResource& resource);
-
-    void GetExportedResources(std::list<ExportedResource>& target,
-                              bool& done,
-                              int64_t since,
-                              uint32_t maxResults);
-    
-    void GetLastExportedResource(std::list<ExportedResource>& target);
-    
-    uint64_t GetTotalCompressedSize();
-    
-    uint64_t GetTotalUncompressedSize();
-
-    void GetAllInternalIds(std::list<int64_t>& target,
-                           ResourceType resourceType);
-
-    void GetAllPublicIds(std::list<std::string>& target,
-                         ResourceType resourceType);
-
-    void GetAllPublicIds(std::list<std::string>& target,
-                         ResourceType resourceType,
-                         size_t since,
-                         size_t limit);
-
-    uint64_t GetResourceCount(ResourceType resourceType);
-
-    bool SelectPatientToRecycle(int64_t& internalId);
-
-    bool SelectPatientToRecycle(int64_t& internalId,
-                                int64_t patientIdToAvoid);
-
-    bool IsProtectedPatient(int64_t internalId);
-
-    void SetProtectedPatient(int64_t internalId, 
-                             bool isProtected);
-
-    bool IsExistingResource(int64_t internalId);
-
-    void LookupIdentifier(std::list<int64_t>& result,
-                          ResourceType level,
-                          const DicomTag& tag,
-                          IdentifierConstraintType type,
-                          const std::string& value);
-
-    void LookupIdentifierRange(std::list<int64_t>& result,
-                               ResourceType level,
-                               const DicomTag& tag,
-                               const std::string& start,
-                               const std::string& end);
-  };
-}
-
--- a/OrthancServer/Resources/Graveyard/DatabasePluginSample/Plugin.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,92 +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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "Database.h"
-
-#include <memory>
-#include <iostream>
-#include <boost/algorithm/string/predicate.hpp>
-
-static OrthancPluginContext*  context_ = NULL;
-static std::auto_ptr<OrthancPlugins::IDatabaseBackend>  backend_;
-
-
-extern "C"
-{
-  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
-  {
-    context_ = c;
-    OrthancPluginLogWarning(context_, "Sample plugin is initializing");
-
-    /* Check the version of the Orthanc core */
-    if (OrthancPluginCheckVersion(c) == 0)
-    {
-      char info[256];
-      sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin",
-              c->orthancVersion,
-              ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
-              ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
-              ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
-      OrthancPluginLogError(context_, info);
-      return -1;
-    }
-
-    std::string path = "SampleDatabase.sqlite";
-    uint32_t argCount = OrthancPluginGetCommandLineArgumentsCount(context_);
-    for (uint32_t i = 0; i < argCount; i++)
-    {
-      char* tmp = OrthancPluginGetCommandLineArgument(context_, i);
-      std::string argument(tmp);
-      OrthancPluginFreeString(context_, tmp);
-
-      if (boost::starts_with(argument, "--database="))
-      {
-        path = argument.substr(11);
-      }
-    }
-
-    std::string s = "Using the following SQLite database: " + path;
-    OrthancPluginLogWarning(context_, s.c_str());
-
-    backend_.reset(new Database(path));
-    OrthancPlugins::DatabaseBackendAdapter::Register(context_, *backend_);
-
-    return 0;
-  }
-
-  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
-  {
-    backend_.reset(NULL);
-  }
-
-  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
-  {
-    return "sample-database";
-  }
-
-
-  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
-  {
-    return "1.0";
-  }
-}
--- a/OrthancServer/Resources/Graveyard/SetupAnonymization2011.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,258 +0,0 @@
-  /**
-   * This is a manual implementation by Alain Mazy. Only kept for reference.
-   * https://bitbucket.org/sjodogne/orthanc/commits/c6defdc4c611fca2ab528ba2c6937a742e0329a8?at=issue-46-anonymization
-   **/
-  
-  void DicomModification::SetupAnonymization2011()
-  {
-    // This is Table E.1-1 from PS 3.15-2011 - DICOM Part 15: Security and System Management Profiles
-    // https://raw.githubusercontent.com/jodogne/dicom-specification/master/2011/11_15pu.pdf
-    
-    removals_.insert(DicomTag(0x0000, 0x1000));  // Affected SOP Instance UID
-    removals_.insert(DicomTag(0x0000, 0x1001));  // Requested SOP Instance UID
-    removals_.insert(DicomTag(0x0002, 0x0003));  // Media Storage SOP Instance UID => TODO: replace with a non-zero length UID that is internally consistent within a set of Instances
-    removals_.insert(DicomTag(0x0004, 0x1511));  // Referenced SOP Instance UID in File
-    removals_.insert(DicomTag(0x0008, 0x0010));  // Irradiation Event UID
-    removals_.insert(DicomTag(0x0008, 0x0014));  // Instance Creator UID
-    //removals_.insert(DicomTag(0x0008, 0x0018));  // SOP Instance UID => set in Apply()
-    clearings_.insert(DicomTag(0x0008, 0x0020)); // Study Date
-    clearings_.insert(DicomTag(0x0008, 0x0021)); // Series Date
-    clearings_.insert(DicomTag(0x0008, 0x0030)); // Study Time
-    clearings_.insert(DicomTag(0x0008, 0x0031)); // Series Time
-    removals_.insert(DicomTag(0x0008, 0x0022));  // Acquisition Date
-    removals_.insert(DicomTag(0x0008, 0x0023));  // Content Date
-    removals_.insert(DicomTag(0x0008, 0x0024));  // Overlay Date
-    removals_.insert(DicomTag(0x0008, 0x0025));  // Curve Date
-    removals_.insert(DicomTag(0x0008, 0x002a));  // Acquisition DateTime
-    removals_.insert(DicomTag(0x0008, 0x0032));  // Acquisition Time
-    removals_.insert(DicomTag(0x0008, 0x0033));  // Content Time
-    removals_.insert(DicomTag(0x0008, 0x0034));  // Overlay Time
-    removals_.insert(DicomTag(0x0008, 0x0035));  // Curve Time
-    removals_.insert(DicomTag(0x0008, 0x0050));  // Accession Number
-    removals_.insert(DicomTag(0x0008, 0x0058));  // Failed SOP Instance UID List
-    removals_.insert(DicomTag(0x0008, 0x0080));  // Institution Name
-    removals_.insert(DicomTag(0x0008, 0x0081));  // Institution Address
-    removals_.insert(DicomTag(0x0008, 0x0082));  // Institution Code Sequence
-    removals_.insert(DicomTag(0x0008, 0x0090));  // Referring Physician's Name
-    removals_.insert(DicomTag(0x0008, 0x0092));  // Referring Physician's Address 
-    removals_.insert(DicomTag(0x0008, 0x0094));  // Referring Physician's Telephone Numbers 
-    removals_.insert(DicomTag(0x0008, 0x0096));  // Referring Physician's Identification Sequence
-    removals_.insert(DicomTag(0x0008, 0x010d));  // Context Group Extension Creator UID
-    removals_.insert(DicomTag(0x0008, 0x0201));  // Timezone Offset From UTC
-    removals_.insert(DicomTag(0x0008, 0x0300));  // Current Patient Location
-    removals_.insert(DicomTag(0x0008, 0x1010));  // Station Name
-    removals_.insert(DicomTag(0x0008, 0x1030));  // Study Description 
-    removals_.insert(DicomTag(0x0008, 0x103e));  // Series Description 
-    removals_.insert(DicomTag(0x0008, 0x1040));  // Institutional Department Name 
-    removals_.insert(DicomTag(0x0008, 0x1048));  // Physician(s) of Record 
-    removals_.insert(DicomTag(0x0008, 0x1049));  // Physician(s) of Record Identification Sequence
-    removals_.insert(DicomTag(0x0008, 0x1050));  // Performing Physicians' Name
-    removals_.insert(DicomTag(0x0008, 0x1052));  // Performing Physicians Identification Sequence
-    removals_.insert(DicomTag(0x0008, 0x1060));  // Name of Physician(s) Reading Study
-    removals_.insert(DicomTag(0x0008, 0x1062));  // Physician Reading Study Identification Sequence
-    removals_.insert(DicomTag(0x0008, 0x1070));  // Operators' Name
-    removals_.insert(DicomTag(0x0008, 0x1072));  // Operators' Identification Sequence
-    removals_.insert(DicomTag(0x0008, 0x1080));  // Admitting Diagnoses Description
-    removals_.insert(DicomTag(0x0008, 0x1084));  // Admitting Diagnoses Code Sequence
-    removals_.insert(DicomTag(0x0008, 0x1110));  // Referenced Study Sequence
-    removals_.insert(DicomTag(0x0008, 0x1111));  // Referenced Performed Procedure Step Sequence
-    removals_.insert(DicomTag(0x0008, 0x1120));  // Referenced Patient Sequence
-    removals_.insert(DicomTag(0x0008, 0x1140));  // Referenced Image Sequence
-    removals_.insert(DicomTag(0x0008, 0x1155));  // Referenced SOP Instance UID
-    removals_.insert(DicomTag(0x0008, 0x1195));  // Transaction UID
-    removals_.insert(DicomTag(0x0008, 0x2111));  // Derivation Description
-    removals_.insert(DicomTag(0x0008, 0x2112));  // Source Image Sequence
-    removals_.insert(DicomTag(0x0008, 0x4000));  // Identifying Comments
-    removals_.insert(DicomTag(0x0008, 0x9123));  // Creator Version UID
-    //removals_.insert(DicomTag(0x0010, 0x0010));  // Patient's Name => cf. below (*)
-    //removals_.insert(DicomTag(0x0010, 0x0020));  // Patient ID => cf. below (*)
-    removals_.insert(DicomTag(0x0010, 0x0030));  // Patient's Birth Date 
-    removals_.insert(DicomTag(0x0010, 0x0032));  // Patient's Birth Time 
-    clearings_.insert(DicomTag(0x0010, 0x0040)); // Patient's Sex
-    removals_.insert(DicomTag(0x0010, 0x0050));  // Patient's Insurance Plan Code Sequence
-    removals_.insert(DicomTag(0x0010, 0x0101));  // Patient's Primary Language Code Sequence
-    removals_.insert(DicomTag(0x0010, 0x0102));  // Patient's Primary Language Modifier Code Sequence
-    removals_.insert(DicomTag(0x0010, 0x1000));  // Other Patient Ids
-    removals_.insert(DicomTag(0x0010, 0x1001));  // Other Patient Names 
-    removals_.insert(DicomTag(0x0010, 0x1002));  // Other Patient IDs Sequence
-    removals_.insert(DicomTag(0x0010, 0x1005));  // Patient's Birth Name
-    removals_.insert(DicomTag(0x0010, 0x1010));  // Patient's Age
-    removals_.insert(DicomTag(0x0010, 0x1020));  // Patient's Size 
-    removals_.insert(DicomTag(0x0010, 0x1030));  // Patient's Weight 
-    removals_.insert(DicomTag(0x0010, 0x1040));  // Patient's Address
-    removals_.insert(DicomTag(0x0010, 0x1050));  // Insurance Plan Identification
-    removals_.insert(DicomTag(0x0010, 0x1060));  // Patient's Mother's Birth Name
-    removals_.insert(DicomTag(0x0010, 0x1080));  // Military Rank
-    removals_.insert(DicomTag(0x0010, 0x1081));  // Branch of Service
-    removals_.insert(DicomTag(0x0010, 0x1090));  // Medical Record Locator
-    removals_.insert(DicomTag(0x0010, 0x2000));  // Medical Alerts
-    removals_.insert(DicomTag(0x0010, 0x2110));  // Allergies
-    removals_.insert(DicomTag(0x0010, 0x2150));  // Country of Residence
-    removals_.insert(DicomTag(0x0010, 0x2152));  // Region of Residence
-    removals_.insert(DicomTag(0x0010, 0x2154));  // PatientTelephoneNumbers
-    removals_.insert(DicomTag(0x0010, 0x2160));  // Ethnic Group
-    removals_.insert(DicomTag(0x0010, 0x2180));  // Occupation 
-    removals_.insert(DicomTag(0x0010, 0x21a0));  // Smoking Status
-    removals_.insert(DicomTag(0x0010, 0x21b0));  // Additional Patient's History
-    removals_.insert(DicomTag(0x0010, 0x21c0));  // Pregnancy Status
-    removals_.insert(DicomTag(0x0010, 0x21d0));  // Last Menstrual Date
-    removals_.insert(DicomTag(0x0010, 0x21f0));  // Patient's Religious Preference
-    removals_.insert(DicomTag(0x0010, 0x2203));  // Patient's Sex Neutered
-    removals_.insert(DicomTag(0x0010, 0x2297));  // Responsible Person
-    removals_.insert(DicomTag(0x0010, 0x2299));  // Responsible Organization
-    removals_.insert(DicomTag(0x0010, 0x4000));  // Patient Comments
-    removals_.insert(DicomTag(0x0018, 0x0010));  // Contrast Bolus Agent
-    removals_.insert(DicomTag(0x0018, 0x1000));  // Device Serial Number
-    removals_.insert(DicomTag(0x0018, 0x1002));  // Device UID
-    removals_.insert(DicomTag(0x0018, 0x1004));  // Plate ID
-    removals_.insert(DicomTag(0x0018, 0x1005));  // Generator ID
-    removals_.insert(DicomTag(0x0018, 0x1007));  // Cassette ID
-    removals_.insert(DicomTag(0x0018, 0x1008));  // Gantry ID
-    removals_.insert(DicomTag(0x0018, 0x1030));  // Protocol Name
-    removals_.insert(DicomTag(0x0018, 0x1400));  // Acquisition Device Processing Description
-    removals_.insert(DicomTag(0x0018, 0x4000));  // Acquisition Comments
-    removals_.insert(DicomTag(0x0018, 0x700a));  // Detector ID
-    removals_.insert(DicomTag(0x0018, 0xa003));  // Contribution Description
-    removals_.insert(DicomTag(0x0018, 0x9424));  // Acquisition Protocol Description
-    //removals_.insert(DicomTag(0x0020, 0x000d));  // Study Instance UID => set in Apply()
-    //removals_.insert(DicomTag(0x0020, 0x000e));  // Series Instance UID => set in Apply()
-    removals_.insert(DicomTag(0x0020, 0x0010));  // Study ID
-    removals_.insert(DicomTag(0x0020, 0x0052));  // Frame of Reference UID 
-    removals_.insert(DicomTag(0x0020, 0x0200));  // Synchronization Frame of Reference UID 
-    removals_.insert(DicomTag(0x0020, 0x3401));  // Modifying Device ID
-    removals_.insert(DicomTag(0x0020, 0x3404));  // Modifying Device Manufacturer
-    removals_.insert(DicomTag(0x0020, 0x3406));  // Modified Image Description
-    removals_.insert(DicomTag(0x0020, 0x4000));  // Image Comments
-    removals_.insert(DicomTag(0x0020, 0x9158));  // Frame Comments
-    removals_.insert(DicomTag(0x0020, 0x9161));  // Concatenation UID
-    removals_.insert(DicomTag(0x0020, 0x9164));  // Dimension Organization UID
-    //removals_.insert(DicomTag(0x0028, 0x1199));  // Palette Color Lookup Table UID => TODO: replace with a non-zero length UID that is internally consistent within a set of Instances
-    //removals_.insert(DicomTag(0x0028, 0x1214));  // Large Palette Color Lookup Table UID => TODO: replace with a non-zero length UID that is internally consistent within a set of Instances
-    removals_.insert(DicomTag(0x0028, 0x4000));  // Image Presentation Comments
-    removals_.insert(DicomTag(0x0032, 0x0012));  // Study ID Issuer
-    removals_.insert(DicomTag(0x0032, 0x1020));  // Scheduled Study Location
-    removals_.insert(DicomTag(0x0032, 0x1021));  // Scheduled Study Location AE Title
-    removals_.insert(DicomTag(0x0032, 0x1030));  // Reason for Study
-    removals_.insert(DicomTag(0x0032, 0x1032));  // Requesting Physician
-    removals_.insert(DicomTag(0x0032, 0x1033));  // Requesting Service
-    removals_.insert(DicomTag(0x0032, 0x1060));  // Requesting Procedure Description
-    removals_.insert(DicomTag(0x0032, 0x1070));  // Requested Contrast Agent
-    removals_.insert(DicomTag(0x0032, 0x4000));  // Study Comments
-    removals_.insert(DicomTag(0x0038, 0x0010));  // Admission ID
-    removals_.insert(DicomTag(0x0038, 0x0011));  // Issuer of Admission ID
-    removals_.insert(DicomTag(0x0038, 0x001e));  // Scheduled Patient Institution Residence
-    removals_.insert(DicomTag(0x0038, 0x0020));  // Admitting Date
-    removals_.insert(DicomTag(0x0038, 0x0021));  // Admitting Time
-    removals_.insert(DicomTag(0x0038, 0x0040));  // Discharge Diagnosis Description
-    removals_.insert(DicomTag(0x0038, 0x0050));  // Special Needs
-    removals_.insert(DicomTag(0x0038, 0x0060));  // Service Episode ID
-    removals_.insert(DicomTag(0x0038, 0x0061));  // Issuer of Service Episode ID
-    removals_.insert(DicomTag(0x0038, 0x0062));  // Service Episode Description
-    removals_.insert(DicomTag(0x0038, 0x0400));  // Patient's Institution Residence
-    removals_.insert(DicomTag(0x0038, 0x0500));  // Patient State
-    removals_.insert(DicomTag(0x0038, 0x4000));  // Visit Comments
-    removals_.insert(DicomTag(0x0038, 0x1234));  // Referenced Patient Alias Sequence
-    removals_.insert(DicomTag(0x0040, 0x0001));  // Scheduled Station AE Title
-    removals_.insert(DicomTag(0x0040, 0x0002));  // Scheduled Procedure Step Start Date
-    removals_.insert(DicomTag(0x0040, 0x0003));  // Scheduled Procedure Step Start Time
-    removals_.insert(DicomTag(0x0040, 0x0004));  // Scheduled Procedure Step End Date
-    removals_.insert(DicomTag(0x0040, 0x0005));  // Scheduled Procedure Step End Time
-    removals_.insert(DicomTag(0x0040, 0x0006));  // Scheduled Performing Physician Name
-    removals_.insert(DicomTag(0x0040, 0x0007));  // Scheduled Procedure Step Description
-    removals_.insert(DicomTag(0x0040, 0x000b));  // Scheduled Performing Physician Identification Sequence
-    removals_.insert(DicomTag(0x0040, 0x0010));  // Scheduled Station Name
-    removals_.insert(DicomTag(0x0040, 0x0011));  // Scheduled Procedure Step Location
-    removals_.insert(DicomTag(0x0040, 0x0012));  // Pre-Medication
-    removals_.insert(DicomTag(0x0040, 0x0241));  // Performed Station AE Title
-    removals_.insert(DicomTag(0x0040, 0x0242));  // Performed Station Name
-    removals_.insert(DicomTag(0x0040, 0x0243));  // Performed Location
-    removals_.insert(DicomTag(0x0040, 0x0244));  // Performed Procedure Step Start Date
-    removals_.insert(DicomTag(0x0040, 0x0245));  // Performed Procedure Step Start Time
-    removals_.insert(DicomTag(0x0040, 0x0248));  // Performed Station Name Code Sequence
-    removals_.insert(DicomTag(0x0040, 0x0253));  // Performed Procedure Step ID
-    removals_.insert(DicomTag(0x0040, 0x0254));  // Performed Procedure Step Description
-    removals_.insert(DicomTag(0x0040, 0x0275));  // Request Attributes Sequence
-    removals_.insert(DicomTag(0x0040, 0x0280));  // Comments on Performed Procedure Step
-    removals_.insert(DicomTag(0x0040, 0x0555));  // Acquisition Context Sequence
-    removals_.insert(DicomTag(0x0040, 0x1001));  // Requested Procedure ID
-    removals_.insert(DicomTag(0x0040, 0x1010));  // Names of Intended Recipient of Results
-    removals_.insert(DicomTag(0x0040, 0x1011));  // Intended Recipient of Results Identification Sequence
-    removals_.insert(DicomTag(0x0040, 0x1004));  // Patient Transport Arrangements
-    removals_.insert(DicomTag(0x0040, 0x1005));  // Requested Procedure Location
-    removals_.insert(DicomTag(0x0040, 0x1101));  // Person Identification Code Sequence
-    removals_.insert(DicomTag(0x0040, 0x1102));  // Person Address
-    removals_.insert(DicomTag(0x0040, 0x1103));  // Person Telephone Numbers
-    removals_.insert(DicomTag(0x0040, 0x1400));  // Requested Procedure Comments
-    removals_.insert(DicomTag(0x0040, 0x2001));  // Reason for Imaging Service Request
-    removals_.insert(DicomTag(0x0040, 0x2008));  // Order Entered By
-    removals_.insert(DicomTag(0x0040, 0x2009));  // Order Enterer Location
-    removals_.insert(DicomTag(0x0040, 0x2010));  // Order Callback Phone Number
-    removals_.insert(DicomTag(0x0040, 0x2016));  // Placer Order Number of Imaging Service Request
-    removals_.insert(DicomTag(0x0040, 0x2017));  // Filler Order Number of Imaging Service Request
-    removals_.insert(DicomTag(0x0040, 0x2400));  // Imaging Service Request Comments
-    removals_.insert(DicomTag(0x0040, 0x4023));  // Referenced General Purpose Scheduled Procedure Step Transaction UID
-    removals_.insert(DicomTag(0x0040, 0x4025));  // Scheduled Station Name Code Sequence
-    removals_.insert(DicomTag(0x0040, 0x4027));  // Scheduled Station Geographic Location Code Sequence
-    removals_.insert(DicomTag(0x0040, 0x4030));  // Performed Station Geographic Location Code Sequence
-    removals_.insert(DicomTag(0x0040, 0x4034));  // Scheduled Human Performers Sequence
-    removals_.insert(DicomTag(0x0040, 0x4035));  // Actual Human Performers Sequence
-    removals_.insert(DicomTag(0x0040, 0x4036));  // Human Performers Organization
-    removals_.insert(DicomTag(0x0040, 0x4037));  // Human Performers Name
-    removals_.insert(DicomTag(0x0040, 0xa027));  // Verifying Organization
-    removals_.insert(DicomTag(0x0040, 0xa073));  // Verifying Observer Sequence
-    removals_.insert(DicomTag(0x0040, 0xa075));  // Verifying Observer Name
-    removals_.insert(DicomTag(0x0040, 0xa078));  // Author Observer Sequence
-    removals_.insert(DicomTag(0x0040, 0xa07a));  // Participant Sequence
-    removals_.insert(DicomTag(0x0040, 0xa07c));  // Custodial Organization Sequence
-    removals_.insert(DicomTag(0x0040, 0xa088));  // Verifying Observer Identification Code Sequence
-    removals_.insert(DicomTag(0x0040, 0xa123));  // Person Name
-    removals_.insert(DicomTag(0x0040, 0xa124));  // UID
-    removals_.insert(DicomTag(0x0040, 0xa730));  // Content Sequence 
-    removals_.insert(DicomTag(0x0040, 0x3001));  // Confidentiality Constraint on Patient Data Description
-    removals_.insert(DicomTag(0x0040, 0xdb0c));  // Template Extension Organization UID
-    removals_.insert(DicomTag(0x0040, 0xdb0d));  // Template Extension Creator UID
-    removals_.insert(DicomTag(0x0070, 0x0001));  // Graphic Annotation Sequence
-    removals_.insert(DicomTag(0x0070, 0x0084));  // Content Creator's Name
-    removals_.insert(DicomTag(0x0070, 0x0086));  // Content Creator's Identification Code Sequence
-    removals_.insert(DicomTag(0x0070, 0x031a));  // Fiducial UID
-    removals_.insert(DicomTag(0x0088, 0x0140));  // Storage Media File-set UID
-    removals_.insert(DicomTag(0x0088, 0x0200));  // Icon Image Sequence
-    removals_.insert(DicomTag(0x0088, 0x0904));  // Topic Title
-    removals_.insert(DicomTag(0x0088, 0x0906));  // Topic Subject
-    removals_.insert(DicomTag(0x0088, 0x0910));  // Topic Author
-    removals_.insert(DicomTag(0x0088, 0x0912));  // Topic Key Words
-    removals_.insert(DicomTag(0x0400, 0x0100));  // Digital Signature UID
-    removals_.insert(DicomTag(0x0400, 0x0402));  // Referenced Digital Signature Sequence
-    removals_.insert(DicomTag(0x0400, 0x0403));  // Referenced SOP Instance MAC Sequence
-    removals_.insert(DicomTag(0x0400, 0x0404));  // MAC
-    removals_.insert(DicomTag(0x0400, 0x0550));  // Modified Attributes Sequence
-    removals_.insert(DicomTag(0x0400, 0x0561));  // Original Attributes Sequence
-    removals_.insert(DicomTag(0x2030, 0x0020));  // Text String
-    removals_.insert(DicomTag(0x3006, 0x0024));  // Referenced Frame of Reference UID
-    removals_.insert(DicomTag(0x3006, 0x00c2));  // Related Frame of Reference UID 
-    removals_.insert(DicomTag(0x300a, 0x0013));  // Dose Reference UID
-    removals_.insert(DicomTag(0x300e, 0x0008));  // Reviewer Name
-    removals_.insert(DicomTag(0x4000, 0x0010));  // Arbitrary
-    removals_.insert(DicomTag(0x4000, 0x4000));  // Text Comments
-    removals_.insert(DicomTag(0x4008, 0x0042));  // Results ID Issuer
-    removals_.insert(DicomTag(0x4008, 0x0102));  // Interpretation Recorder
-    removals_.insert(DicomTag(0x4008, 0x010a));  // Interpretation Transcriber
-    removals_.insert(DicomTag(0x4008, 0x010b));  // Interpretation Text
-    removals_.insert(DicomTag(0x4008, 0x010c));  // Interpretation Author
-    removals_.insert(DicomTag(0x4008, 0x0111));  // Interpretation Approver Sequence
-    removals_.insert(DicomTag(0x4008, 0x0114));  // Physician Approving Interpretation
-    removals_.insert(DicomTag(0x4008, 0x0115));  // Interpretation Diagnosis Description
-    removals_.insert(DicomTag(0x4008, 0x0118));  // Results Distribution List Sequence
-    removals_.insert(DicomTag(0x4008, 0x0119));  // Distribution Name
-    removals_.insert(DicomTag(0x4008, 0x011a));  // Distribution Address
-    removals_.insert(DicomTag(0x4008, 0x0202));  // Interpretation ID Issuer
-    removals_.insert(DicomTag(0x4008, 0x0300));  // Impressions
-    removals_.insert(DicomTag(0x4008, 0x4000));  // Results Comments
-    removals_.insert(DicomTag(0xfffa, 0xfffa));  // Digital Signature Sequence
-    removals_.insert(DicomTag(0xfffc, 0xfffc));  // Data Set Trailing Padding
-    //removals_.insert(DicomTag(0x60xx, 0x4000));  // Overlay Comments => TODO
-    //removals_.insert(DicomTag(0x60xx, 0x3000));  // Overlay Data => TODO
-
-    // Set the DeidentificationMethod tag
-    ReplaceInternal(DICOM_TAG_DEIDENTIFICATION_METHOD, ORTHANC_DEIDENTIFICATION_METHOD_2011);
-  }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/ImplementationNotes/DatabasesClassHierarchy.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,30 @@
+The main object to access the DB is the ServerIndex class that is accessible from the ServerContext.
+
+ServerIndex inherits from StatelessDatabaseOperations.
+
+StatelessDatabaseOperations owns an IDatabaseWrapper member (db).
+StatelessDatabaseOperations has 2 internal Transaction classes (ReadOnlyTransactions and ReadWriteTransactions) that implements the DB
+operations by calling the methods from IDatabaseWrapper:ITransaction.
+
+IDatabaseWrapper has 2 direct derived classes:
+- BaseDatabaseWrapper which simply provides a "not implemented" implementation of new methods to its derived classes:
+  - OrthancPluginDatabase    that is a legacy plugin interface
+  - OrthancPluginDatabaseV3  that is a legacy plugin interface
+  - SQLiteDatabaseWrapper    that is used by the default SQLite DB in Orthanc
+- OrthancPluginDatabaseV4 that is the latest plugin interface and uses protobuf
+
+When you add a new method in the DB (e.g: UpdateAndGetStatistics with a new signature), you must:
+- define it as a member of StatelessDatabaseOperations
+- define it as a member of StatelessDatabaseOperations::ReadWriteTransactions or StatelessDatabaseOperations::ReadOnlyTransactions
+- define it as a member of IDatabaseWrapper:ITransaction
+- define it in OrthancDatabasePlugin.proto (new request + new response + new message)
+- define it in OrthancPluginDatabaseV4
+- define a NotImplemented default implementation in BaseDatabaseWrapper
+- optionally define it in SQLiteDatabaseWrapper if it can be implemented in SQLite
+- very likely define it as a DbCapabilities in IDatabaseWrapper::DbCapabilities (e.g: Has/SetHasUpdateAndGetStatistics()) such that the Orthanc
+  core knows if it can use it or not.
+
+Then, in the orthanc-databases repo, you should:
+- define it as a virtual member of IDatabaseBackend
+- define it as a member of IndexBackend
+- add a handler for the new protobuf message in DatabaseBackendAdapterV4
--- a/OrthancServer/Resources/Orthanc.doxygen	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Orthanc.doxygen	Tue Mar 18 13:37:18 2025 +0100
@@ -755,6 +755,7 @@
 # Note: If this tag is empty the current directory is searched.
 
 INPUT                  = @CMAKE_SOURCE_DIR@/../OrthancFramework/Sources \
+                         @CMAKE_SOURCE_DIR@/Plugins/Engine \
                          @CMAKE_SOURCE_DIR@/Sources
 
 # This tag can be used to specify the character encoding of the source files
--- a/OrthancServer/Resources/PreventProtobufDirectoryLeaks.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/PreventProtobufDirectoryLeaks.py	Tue Mar 18 13:37:18 2025 +0100
@@ -4,8 +4,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
@@ -29,13 +29,21 @@
 with open(sys.argv[1], 'r') as f:
     s = f.read()
 
-s = s.replace('__FILE__', '__ORTHANC_FILE__')
+if False:
+    # This was the version in Orthanc 1.12.4, doesn't seem to work anymore
+    s = s.replace('__FILE__', '__ORTHANC_FILE__')
 
-s = """
+    s = """
 #if !defined(__ORTHANC_FILE__)
 #  define __ORTHANC_FILE__ __FILE__
 #endif
 """ + s
+else:
+    # New version in Orthanc 1.12.5
+    s = """
+#undef __FILE__
+#define __FILE__ __ORTHANC_FILE__
+""" + s
 
 with open(sys.argv[1], 'w') as f:
     f.write(s)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/RunCppCheck-2.17.0.sh	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,110 @@
+#!/bin/bash
+
+set -ex
+
+CPPCHECK=cppcheck
+
+if [ $# -ge 1 ]; then
+    CPPCHECK=$1
+fi
+
+cat <<EOF > /tmp/cppcheck-suppressions.txt
+nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:321
+stlFindInsert:../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1525
+stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:166
+stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:74
+stlFindInsert:../../OrthancServer/Sources/Database/MainDicomTagsRegistry.cpp:65
+stlFindInsert:../../OrthancServer/Sources/OrthancWebDav.cpp:328
+stlFindInsert:../../OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp:41
+stlFindInsert:../../OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp:191
+stlFindInsert:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:361
+syntaxError:../../OrthancFramework/Sources/SQLite/FunctionContext.h:53
+syntaxError:../../OrthancFramework/UnitTestsSources/DicomMapTests.cpp:74
+syntaxError:../../OrthancFramework/UnitTestsSources/ZipTests.cpp:133
+syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:322
+uninitMemberVar:../../OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp:417
+unreadVariable:../../OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp:1173
+useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:91
+useInitializationList:../../OrthancFramework/Sources/Images/PngWriter.cpp:99
+useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:275
+assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:277
+assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:1026
+assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:292
+assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:391
+assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3058
+assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:286
+assertWithSideEffect:../../OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp:454
+EOF
+
+${CPPCHECK} --enable=all --std=gnu++11 --library=boost \
+            --suppressions-list=/tmp/cppcheck-suppressions.txt \
+            -I/usr/include/ \
+            -I/usr/include/jsoncpp/ \
+            -I/usr/include/linux/ \
+            -I/usr/include/c++/11/ \
+            -I/usr/include/c++/11/tr1/ \
+            -I/usr/include/x86_64-linux-gnu/c++/11/ \
+            -DBOOST_HAS_DATE_TIME=1 \
+            -DBOOST_HAS_FILESYSTEM_V3=1 \
+            -DBOOST_HAS_REGEX=1 \
+            -DCIVETWEB_HAS_DISABLE_KEEP_ALIVE=1 \
+            -DCIVETWEB_HAS_WEBDAV_WRITING=1 \
+            -DDCMTK_VERSION_NUMBER=369 \
+            -DJCONFIG_INCLUDED \
+            -DHAVE_MALLOPT=1 \
+            -DMONGOOSE_USE_CALLBACKS=1 \
+            -DJSONCPP_VERSION_MAJOR=1 \
+            -DJSONCPP_VERSION_MINOR=0 \
+            -DORTHANC_BUILDING_FRAMEWORK_LIBRARY=0 \
+            -DORTHANC_BUILD_UNIT_TESTS=1 \
+            -DORTHANC_ENABLE_BASE64=1 \
+            -DORTHANC_ENABLE_CIVETWEB=1 \
+            -DORTHANC_ENABLE_CURL=1 \
+            -DORTHANC_ENABLE_DCMTK=1 \
+            -DORTHANC_ENABLE_DCMTK_JPEG=1 \
+            -DORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS=1 \
+            -DORTHANC_ENABLE_DCMTK_NETWORKING=1 \
+            -DORTHANC_ENABLE_DCMTK_TRANSCODING=1 \
+            -DORTHANC_ENABLE_JPEG=1 \
+            -DORTHANC_ENABLE_LOCALE=1 \
+            -DORTHANC_ENABLE_LOGGING=1 \
+            -DORTHANC_ENABLE_LOGGING_STDIO=1 \
+            -DORTHANC_ENABLE_LUA=1 \
+            -DORTHANC_ENABLE_MD5=1 \
+            -DORTHANC_ENABLE_MONGOOSE=0 \
+            -DORTHANC_ENABLE_PKCS11=1 \
+            -DORTHANC_ENABLE_PLUGINS=1 \
+            -DORTHANC_ENABLE_PNG=1 \
+            -DORTHANC_ENABLE_PUGIXML=1 \
+            -DORTHANC_ENABLE_SQLITE=1 \
+            -DORTHANC_ENABLE_SSL=1 \
+            -DORTHANC_ENABLE_ZLIB=1 \
+            -DORTHANC_SANDBOXED=0 \
+            -DORTHANC_SQLITE_VERSION=3027001 \
+            -DORTHANC_UNIT_TESTS_LINK_FRAMEWORK=0 \
+            -DPUGIXML_VERSION=150 \
+            -DUNIT_TESTS_WITH_HTTP_CONNEXIONS=1 \
+            -D__BYTE_ORDER=__LITTLE_ENDIAN \
+            -D__GNUC__=11 \
+            -D__GNUC_MINOR__=4 \
+            -D__GNUC_PATCHLEVEL_=0 \
+            -D__STDC__ \
+            -D__cplusplus=201103 \
+            -D__linux__ \
+            -UNDEBUG \
+            -DHAS_ORTHANC_EXCEPTION=1 \
+            \
+            ../../OrthancFramework/Sources \
+            ../../OrthancFramework/UnitTestsSources \
+            ../../OrthancServer/Plugins/Engine \
+            ../../OrthancServer/Plugins/Include \
+            ../../OrthancServer/Sources \
+            ../../OrthancServer/UnitTestsSources \
+            ../../OrthancServer/Plugins/Samples/Common \
+            ../../OrthancServer/Plugins/Samples/ConnectivityChecks \
+            ../../OrthancServer/Plugins/Samples/DelayedDeletion \
+            ../../OrthancServer/Plugins/Samples/Housekeeper \
+            ../../OrthancServer/Plugins/Samples/ModalityWorklists \
+            ../../OrthancServer/Plugins/Samples/MultitenantDicom \
+            \
+            2>&1
--- a/OrthancServer/Resources/RunCppCheck.sh	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/RunCppCheck.sh	Tue Mar 18 13:37:18 2025 +0100
@@ -12,12 +12,12 @@
 constParameter:../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp
 knownArgument:../../OrthancFramework/UnitTestsSources/ImageTests.cpp
 knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp
-nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:316
-stlFindInsert:../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1477
+nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:321
+stlFindInsert:../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1525
 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:166
 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:74
-stlFindInsert:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:374
-stlFindInsert:../../OrthancServer/Sources/OrthancWebDav.cpp:378
+stlFindInsert:../../OrthancServer/Sources/Database/MainDicomTagsRegistry.cpp:65
+stlFindInsert:../../OrthancServer/Sources/OrthancWebDav.cpp:328
 stlFindInsert:../../OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp:41
 stlFindInsert:../../OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp:191
 stlFindInsert:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:361
@@ -27,16 +27,16 @@
 syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:322
 uninitMemberVar:../../OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp:417
 unreadVariable:../../OrthancFramework/Sources/FileStorage/StorageAccessor.cpp
-unreadVariable:../../OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp:1123
+unreadVariable:../../OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp:1173
 unusedFunction
 useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:91
 useInitializationList:../../OrthancFramework/Sources/Images/PngWriter.cpp:99
 useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:275
 assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:277
 assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:1026
-assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:290
-assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:389
-assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3663
+assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:292
+assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:391
+assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3058
 assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:286
 assertWithSideEffect:../../OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp:454
 EOF
@@ -55,7 +55,6 @@
             -DJSONCPP_VERSION_MAJOR=1 \
             -DJSONCPP_VERSION_MINOR=0 \
             -DORTHANC_BUILDING_FRAMEWORK_LIBRARY=0 \
-            -DORTHANC_BUILDING_SERVER_LIBRARY=1 \
             -DORTHANC_BUILD_UNIT_TESTS=1 \
             -DORTHANC_ENABLE_BASE64=1 \
             -DORTHANC_ENABLE_CIVETWEB=1 \
--- a/OrthancServer/Resources/Samples/CppHelpers/Logging/ILogger.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/CppHelpers/Logging/ILogger.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/CppHelpers/Logging/NullLogger.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/CppHelpers/Logging/NullLogger.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/CppHelpers/Logging/OrthancLogger.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/CppHelpers/Logging/OrthancLogger.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/CppHelpers/Logging/OrthancLogger.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/CppHelpers/Logging/OrthancLogger.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/CppHelpers/Logging/OrthancPluginLogger.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/CppHelpers/Logging/OrthancPluginLogger.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/CppHelpers/Logging/OrthancPluginLogger.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/CppHelpers/Logging/OrthancPluginLogger.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/ImportDicomFiles/ImportDicomFiles.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/ImportDicomFiles/ImportDicomFiles.py	Tue Mar 18 13:37:18 2025 +0100
@@ -4,8 +4,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/ImportDicomFiles/OrthancImport.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/ImportDicomFiles/OrthancImport.py	Tue Mar 18 13:37:18 2025 +0100
@@ -4,8 +4,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/Lua/CallWebService.js	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/Lua/CallWebService.js	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/Python/AnonymizeAllPatients.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/Python/AnonymizeAllPatients.py	Tue Mar 18 13:37:18 2025 +0100
@@ -5,8 +5,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/Python/ArchiveAllPatients.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/Python/ArchiveAllPatients.py	Tue Mar 18 13:37:18 2025 +0100
@@ -5,8 +5,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/Python/ArchiveStudiesInTimeRange.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/Python/ArchiveStudiesInTimeRange.py	Tue Mar 18 13:37:18 2025 +0100
@@ -5,8 +5,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/Python/AutoClassify.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/Python/AutoClassify.py	Tue Mar 18 13:37:18 2025 +0100
@@ -5,8 +5,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/Python/ChangesLoop.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/Python/ChangesLoop.py	Tue Mar 18 13:37:18 2025 +0100
@@ -4,8 +4,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/Python/ContinuousPatientAnonymization.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/Python/ContinuousPatientAnonymization.py	Tue Mar 18 13:37:18 2025 +0100
@@ -4,8 +4,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/Python/DeleteAllStudies.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/Python/DeleteAllStudies.py	Tue Mar 18 13:37:18 2025 +0100
@@ -5,8 +5,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/Python/DicomizeImage.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/Python/DicomizeImage.py	Tue Mar 18 13:37:18 2025 +0100
@@ -5,8 +5,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/Python/DownloadAnonymized.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/Python/DownloadAnonymized.py	Tue Mar 18 13:37:18 2025 +0100
@@ -5,8 +5,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/Python/HighPerformanceAutoRouting.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/Python/HighPerformanceAutoRouting.py	Tue Mar 18 13:37:18 2025 +0100
@@ -5,8 +5,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/Python/ManualModification.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/Python/ManualModification.py	Tue Mar 18 13:37:18 2025 +0100
@@ -4,8 +4,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/Python/MicroCTDicomization.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/Python/MicroCTDicomization.py	Tue Mar 18 13:37:18 2025 +0100
@@ -5,8 +5,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/Python/Replicate.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/Python/Replicate.py	Tue Mar 18 13:37:18 2025 +0100
@@ -4,8 +4,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/Python/RestToolbox.py	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/Python/RestToolbox.py	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/Tools/CMakeLists.txt	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/Tools/CMakeLists.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/Tools/RecoverCompressedFile.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/Tools/RecoverCompressedFile.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/WebApplications/DrawingDicomizer.js	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/WebApplications/DrawingDicomizer.js	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/WebApplications/DrawingDicomizer/orthanc.js	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/WebApplications/DrawingDicomizer/orthanc.js	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Samples/WebApplications/NodeToolbox.js	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Samples/WebApplications/NodeToolbox.js	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Resources/Testing/Issue32/Cpp/CMakeLists.txt	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Resources/Testing/Issue32/Cpp/CMakeLists.txt	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 # 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-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/BaseCompatibilityTransaction.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,88 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "BaseCompatibilityTransaction.h"
+
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+#include "Compatibility/GenericFind.h"
+
+namespace Orthanc
+{
+  int64_t BaseCompatibilityTransaction::IncrementGlobalProperty(GlobalProperty property,
+                                                                int64_t increment,
+                                                                bool shared)
+  {
+    throw OrthancException(ErrorCode_NotImplemented);  // Not supported
+  }
+
+  void BaseCompatibilityTransaction::UpdateAndGetStatistics(int64_t& patientsCount,
+                                                            int64_t& studiesCount,
+                                                            int64_t& seriesCount,
+                                                            int64_t& instancesCount,
+                                                            int64_t& compressedSize,
+                                                            int64_t& uncompressedSize)
+  {
+    throw OrthancException(ErrorCode_NotImplemented);  // Not supported
+  }
+
+  void BaseCompatibilityTransaction::GetChangesExtended(std::list<ServerIndexChange>& target /*out*/,
+                                                        bool& done /*out*/,
+                                                        int64_t since,
+                                                        int64_t to,
+                                                        uint32_t limit,
+                                                        const std::set<ChangeType>& filterType)
+  {
+    throw OrthancException(ErrorCode_NotImplemented);  // Not supported
+  }
+
+  void BaseCompatibilityTransaction::ExecuteCount(uint64_t& count,
+                                                  const FindRequest& request,
+                                                  const IDatabaseWrapper::Capabilities& capabilities)
+  {
+    throw OrthancException(ErrorCode_NotImplemented);  // Not supported
+  }
+
+  void BaseCompatibilityTransaction::ExecuteFind(FindResponse& response,
+                                                 const FindRequest& request,
+                                                 const IDatabaseWrapper::Capabilities& capabilities)
+  {
+    throw OrthancException(ErrorCode_NotImplemented);  // Not supported
+  }
+
+  void BaseCompatibilityTransaction::ExecuteFind(std::list<std::string>& identifiers,
+                                                 const IDatabaseWrapper::Capabilities& capabilities,
+                                                 const FindRequest& request)
+  {
+    Compatibility::GenericFind find(*this, *this);
+    find.ExecuteFind(identifiers, capabilities, request);
+  }
+
+  void BaseCompatibilityTransaction::ExecuteExpand(FindResponse& response,
+                                                   const IDatabaseWrapper::Capabilities& capabilities,
+                                                   const FindRequest& request,
+                                                   const std::string& identifier)
+  {
+    Compatibility::GenericFind find(*this, *this);
+    find.ExecuteExpand(response, capabilities, request, identifier);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/BaseCompatibilityTransaction.h	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,76 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IDatabaseWrapper.h"
+
+namespace Orthanc
+{
+  /**
+   * This class provides a default "not implemented" implementation
+   * for all recent methods (1.12.X)
+   **/
+
+  class BaseCompatibilityTransaction :
+    public IDatabaseWrapper::ITransaction,
+    public IDatabaseWrapper::ICompatibilityTransaction
+  {
+  public:
+    virtual int64_t IncrementGlobalProperty(GlobalProperty property,
+                                            int64_t increment,
+                                            bool shared) ORTHANC_OVERRIDE;
+
+    virtual void UpdateAndGetStatistics(int64_t& patientsCount,
+                                        int64_t& studiesCount,
+                                        int64_t& seriesCount,
+                                        int64_t& instancesCount,
+                                        int64_t& compressedSize,
+                                        int64_t& uncompressedSize) ORTHANC_OVERRIDE;
+
+    virtual void ExecuteCount(uint64_t& count,
+                              const FindRequest& request,
+                              const IDatabaseWrapper::Capabilities& capabilities) ORTHANC_OVERRIDE;
+
+    virtual void ExecuteFind(FindResponse& response,
+                             const FindRequest& request,
+                             const IDatabaseWrapper::Capabilities& capabilities) ORTHANC_OVERRIDE;
+
+    virtual void ExecuteFind(std::list<std::string>& identifiers,
+                             const IDatabaseWrapper::Capabilities& capabilities,
+                             const FindRequest& request) ORTHANC_OVERRIDE;
+
+    virtual void ExecuteExpand(FindResponse& response,
+                               const IDatabaseWrapper::Capabilities& capabilities,
+                               const FindRequest& request,
+                               const std::string& identifier) ORTHANC_OVERRIDE;
+
+    virtual void GetChangesExtended(std::list<ServerIndexChange>& target /*out*/,
+                                    bool& done /*out*/,
+                                    int64_t since,
+                                    int64_t to,
+                                    uint32_t limit,
+                                    const std::set<ChangeType>& filterType) ORTHANC_OVERRIDE;
+  };
+
+}
--- a/OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "BaseDatabaseWrapper.h"
-
-#include "../../../OrthancFramework/Sources/OrthancException.h"
-
-namespace Orthanc
-{
-  int64_t BaseDatabaseWrapper::BaseTransaction::IncrementGlobalProperty(GlobalProperty property,
-                                                                        int64_t increment,
-                                                                        bool shared)
-  {
-    throw OrthancException(ErrorCode_NotImplemented);  // Not supported
-  }
-
-
-  void BaseDatabaseWrapper::BaseTransaction::UpdateAndGetStatistics(int64_t& patientsCount,
-                                                                    int64_t& studiesCount,
-                                                                    int64_t& seriesCount,
-                                                                    int64_t& instancesCount,
-                                                                    int64_t& compressedSize,
-                                                                    int64_t& uncompressedSize)
-  {
-    throw OrthancException(ErrorCode_NotImplemented);  // Not supported
-  }
-
-
-  uint64_t BaseDatabaseWrapper::MeasureLatency()
-  {
-    throw OrthancException(ErrorCode_NotImplemented);  // only implemented in V4
-  }
-}
--- a/OrthancServer/Sources/Database/BaseDatabaseWrapper.h	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,54 +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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "IDatabaseWrapper.h"
-
-namespace Orthanc
-{
-  /**
-   * This class provides a default "not implemented" implementation
-   * for all recent methods (1.12.X)
-   **/
-  class BaseDatabaseWrapper : public IDatabaseWrapper
-  {
-  public:
-    class BaseTransaction : public IDatabaseWrapper::ITransaction
-    {
-    public:
-      virtual int64_t IncrementGlobalProperty(GlobalProperty property,
-                                              int64_t increment,
-                                              bool shared) ORTHANC_OVERRIDE;
-
-      virtual void UpdateAndGetStatistics(int64_t& patientsCount,
-                                          int64_t& studiesCount,
-                                          int64_t& seriesCount,
-                                          int64_t& instancesCount,
-                                          int64_t& compressedSize,
-                                          int64_t& uncompressedSize) ORTHANC_OVERRIDE;
-    };
-
-    virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE;
-  };
-}
--- a/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -74,7 +74,7 @@
           }
         }
         
-        void Add(const DatabaseConstraint& constraint)
+        void Add(const DatabaseDicomTagConstraint& constraint)
         {
           constraints_.push_back(new DicomTagConstraint(constraint));
         }          
@@ -84,7 +84,7 @@
     
     static void ApplyIdentifierConstraint(SetOfResources& candidates,
                                           ILookupResources& compatibility,
-                                          const DatabaseConstraint& constraint,
+                                          const DatabaseDicomTagConstraint& constraint,
                                           ResourceType level)
     {
       std::list<int64_t> matches;
@@ -134,8 +134,8 @@
     
     static void ApplyIdentifierRange(SetOfResources& candidates,
                                      ILookupResources& compatibility,
-                                     const DatabaseConstraint& smaller,
-                                     const DatabaseConstraint& greater,
+                                     const DatabaseDicomTagConstraint& smaller,
+                                     const DatabaseDicomTagConstraint& greater,
                                      ResourceType level)
     {
       assert(smaller.GetConstraintType() == ConstraintType_SmallerOrEqual &&
@@ -153,10 +153,10 @@
     static void ApplyLevel(SetOfResources& candidates,
                            IDatabaseWrapper::ITransaction& transaction,
                            ILookupResources& compatibility,
-                           const std::vector<DatabaseConstraint>& lookup,
+                           const DatabaseDicomTagConstraints& lookup,
                            ResourceType level)
     {
-      typedef std::set<const DatabaseConstraint*>  SetOfConstraints;
+      typedef std::set<const DatabaseDicomTagConstraint*>  SetOfConstraints;
       typedef std::map<DicomTag, SetOfConstraints> Identifiers;
 
       // (1) Select which constraints apply to this level, and split
@@ -166,17 +166,19 @@
       Identifiers       identifiers;
       SetOfConstraints  mainTags;
       
-      for (size_t i = 0; i < lookup.size(); i++)
+      for (size_t i = 0; i < lookup.GetSize(); i++)
       {
-        if (lookup[i].GetLevel() == level)
+        const DatabaseDicomTagConstraint& constraint = lookup.GetConstraint(i);
+
+        if (constraint.GetLevel() == level)
         {
-          if (lookup[i].IsIdentifier())
+          if (constraint.IsIdentifier())
           {
-            identifiers[lookup[i].GetTag()].insert(&lookup[i]);
+            identifiers[constraint.GetTag()].insert(&constraint);
           }
           else
           {
-            mainTags.insert(&lookup[i]);
+            mainTags.insert(&constraint);
           }
         }
       }
@@ -189,8 +191,8 @@
       {
         // Check whether some range constraint over identifiers is
         // present at this level
-        const DatabaseConstraint* smaller = NULL;
-        const DatabaseConstraint* greater = NULL;
+        const DatabaseDicomTagConstraint* smaller = NULL;
+        const DatabaseDicomTagConstraint* greater = NULL;
         
         for (SetOfConstraints::const_iterator it2 = it->second.begin();
              it2 != it->second.end(); ++it2)
@@ -306,7 +308,7 @@
 
     void DatabaseLookup::ApplyLookupResources(std::list<std::string>& resourcesId,
                                               std::list<std::string>* instancesId,
-                                              const std::vector<DatabaseConstraint>& lookup,
+                                              const DatabaseDicomTagConstraints& lookup,
                                               ResourceType queryLevel,
                                               size_t limit)
     {
@@ -320,9 +322,9 @@
       ResourceType upperLevel = queryLevel;
       ResourceType lowerLevel = queryLevel;
 
-      for (size_t i = 0; i < lookup.size(); i++)
+      for (size_t i = 0; i < lookup.GetSize(); i++)
       {
-        ResourceType level = lookup[i].GetLevel();
+        ResourceType level = lookup.GetConstraint(i).GetLevel();
 
         if (level < upperLevel)
         {
--- a/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -46,7 +46,7 @@
 
       void ApplyLookupResources(std::list<std::string>& resourcesId,
                                 std::list<std::string>* instancesId,
-                                const std::vector<DatabaseConstraint>& lookup,
+                                const DatabaseDicomTagConstraints& lookup,
                                 ResourceType queryLevel,
                                 size_t limit);
     };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,629 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "GenericFind.h"
+
+#include "../../../../OrthancFramework/Sources/DicomFormat/DicomArray.h"
+#include "../../../../OrthancFramework/Sources/OrthancException.h"
+
+#include <stack>
+
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    static void GetChildren(std::list<int64_t>& target,
+                            IDatabaseWrapper::ITransaction& transaction,
+                            const std::list<int64_t>& resources)
+    {
+      target.clear();
+
+      for (std::list<int64_t>::const_iterator it = resources.begin(); it != resources.end(); ++it)
+      {
+        std::list<int64_t> tmp;
+        transaction.GetChildrenInternalId(tmp, *it);
+        target.splice(target.begin(), tmp);
+      }
+    }
+
+    static void GetChildren(std::list<std::string>& target,
+                            IDatabaseWrapper::ICompatibilityTransaction& transaction,
+                            const std::list<int64_t>& resources)
+    {
+      target.clear();
+
+      for (std::list<int64_t>::const_iterator it = resources.begin(); it != resources.end(); ++it)
+      {
+        std::list<std::string> tmp;
+        transaction.GetChildrenPublicId(tmp, *it);
+        target.splice(target.begin(), tmp);
+      }
+    }
+
+    static void GetChildrenIdentifiers(std::list<std::string>& children,
+                                       IDatabaseWrapper::ITransaction& transaction,
+                                       IDatabaseWrapper::ICompatibilityTransaction& compatibilityTransaction,
+                                       const OrthancIdentifiers& identifiers,
+                                       ResourceType topLevel,
+                                       ResourceType bottomLevel)
+    {
+      if (!IsResourceLevelAboveOrEqual(topLevel, bottomLevel) ||
+          topLevel == bottomLevel)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      std::list<int64_t> currentResources;
+      ResourceType currentLevel;
+
+      {
+        int64_t id;
+        if (!transaction.LookupResource(id, currentLevel, identifiers.GetLevel(topLevel)) ||
+            currentLevel != topLevel)
+        {
+          throw OrthancException(ErrorCode_InexistentItem);
+        }
+
+        currentResources.push_back(id);
+      }
+
+      while (currentLevel != bottomLevel)
+      {
+        ResourceType nextLevel = GetChildResourceType(currentLevel);
+        if (nextLevel == bottomLevel)
+        {
+          GetChildren(children, compatibilityTransaction, currentResources);
+        }
+        else
+        {
+          std::list<int64_t> nextResources;
+          GetChildren(nextResources, transaction, currentResources);
+          currentResources.swap(nextResources);
+        }
+
+        currentLevel = nextLevel;
+      }
+    }
+
+    void GenericFind::ExecuteFind(std::list<std::string>& identifiers,
+                                  const IDatabaseWrapper::Capabilities& capabilities,
+                                  const FindRequest& request)
+    {
+      if (!request.GetLabels().empty() &&
+          !capabilities.HasLabelsSupport())
+      {
+        throw OrthancException(ErrorCode_NotImplemented, "The database backend doesn't support labels");
+      }
+
+      if (!request.GetOrdering().empty())
+      {
+        throw OrthancException(ErrorCode_NotImplemented, "The database backend doesn't support ordering");
+      }
+
+      if (!request.HasConstraints() &&
+          !request.GetOrthancIdentifiers().HasPatientId() &&
+          !request.GetOrthancIdentifiers().HasStudyId() &&
+          !request.GetOrthancIdentifiers().HasSeriesId() &&
+          !request.GetOrthancIdentifiers().HasInstanceId())
+      {
+        if (!request.HasLimits())
+        {
+          transaction_.GetAllPublicIds(identifiers, request.GetLevel());
+        }
+        else if (request.GetLimitsCount() != 0)
+        {
+          compatibilityTransaction_.GetAllPublicIdsCompatibility(identifiers, request.GetLevel(), request.GetLimitsSince(), request.GetLimitsCount());
+        }
+        else
+        {
+          // Starting with Orthanc 1.12.5, "limit=0" means "no limit"
+          std::list<std::string> tmp;
+          transaction_.GetAllPublicIds(tmp, request.GetLevel());
+
+          size_t count = 0;
+          for (std::list<std::string>::const_iterator it = tmp.begin(); it != tmp.end(); ++it)
+          {
+            if (count >= request.GetLimitsSince())
+            {
+              identifiers.push_back(*it);
+            }
+
+            count++;
+          }
+        }
+      }
+      else if (!request.HasConstraints() &&
+               (request.GetLevel() == ResourceType_Study ||
+                request.GetLevel() == ResourceType_Series ||
+                request.GetLevel() == ResourceType_Instance) &&
+               request.GetOrthancIdentifiers().HasPatientId() &&
+               !request.GetOrthancIdentifiers().HasStudyId() &&
+               !request.GetOrthancIdentifiers().HasSeriesId() &&
+               !request.GetOrthancIdentifiers().HasInstanceId())
+      {
+        GetChildrenIdentifiers(identifiers, transaction_, compatibilityTransaction_,
+                               request.GetOrthancIdentifiers(), ResourceType_Patient, request.GetLevel());
+      }
+      else if (!request.HasConstraints() &&
+               (request.GetLevel() == ResourceType_Series ||
+                request.GetLevel() == ResourceType_Instance) &&
+               !request.GetOrthancIdentifiers().HasPatientId() &&
+               request.GetOrthancIdentifiers().HasStudyId() &&
+               !request.GetOrthancIdentifiers().HasSeriesId() &&
+               !request.GetOrthancIdentifiers().HasInstanceId())
+      {
+        GetChildrenIdentifiers(identifiers, transaction_, compatibilityTransaction_,
+                               request.GetOrthancIdentifiers(), ResourceType_Study, request.GetLevel());
+      }
+      else if (!request.HasConstraints() &&
+               request.GetLevel() == ResourceType_Instance &&
+               !request.GetOrthancIdentifiers().HasPatientId() &&
+               !request.GetOrthancIdentifiers().HasStudyId() &&
+               request.GetOrthancIdentifiers().HasSeriesId() &&
+               !request.GetOrthancIdentifiers().HasInstanceId())
+      {
+        GetChildrenIdentifiers(identifiers, transaction_, compatibilityTransaction_,
+                               request.GetOrthancIdentifiers(), ResourceType_Series, request.GetLevel());
+      }
+      else if (request.GetMetadataConstraintsCount() == 0 &&
+               request.GetOrdering().empty() &&
+               !request.GetOrthancIdentifiers().HasPatientId() &&
+               !request.GetOrthancIdentifiers().HasStudyId() &&
+               !request.GetOrthancIdentifiers().HasSeriesId() &&
+               !request.GetOrthancIdentifiers().HasInstanceId())
+      {
+        compatibilityTransaction_.ApplyLookupResources(identifiers, NULL /* TODO-FIND: Could the "instancesId" information be exploited? */,
+                                                       request.GetDicomTagConstraints(), request.GetLevel(), request.GetLabels(),
+                                                       request.GetLabelsConstraint(), request.HasLimits() ? request.GetLimitsCount() : 0);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_NotImplemented);
+      }
+    }
+
+
+    void GenericFind::RetrieveMainDicomTags(FindResponse::Resource& target,
+                                            ResourceType level,
+                                            int64_t internalId)
+    {
+      DicomMap m;
+      transaction_.GetMainDicomTags(m, internalId);
+
+      DicomArray a(m);
+      for (size_t i = 0; i < a.GetSize(); i++)
+      {
+        const DicomElement& element = a.GetElement(i);
+        if (element.GetValue().IsString())
+        {
+          target.AddStringDicomTag(level, element.GetTag().GetGroup(),
+                                   element.GetTag().GetElement(), element.GetValue().GetContent());
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_BadParameterType);
+        }
+      }
+    }
+
+
+    static ResourceType GetTopLevelOfInterest(const FindRequest& request)
+    {
+      switch (request.GetLevel())
+      {
+        case ResourceType_Patient:
+          return ResourceType_Patient;
+
+        case ResourceType_Study:
+          if (request.GetParentSpecification(ResourceType_Patient).IsOfInterest())
+          {
+            return ResourceType_Patient;
+          }
+          else
+          {
+            return ResourceType_Study;
+          }
+
+        case ResourceType_Series:
+          if (request.GetParentSpecification(ResourceType_Patient).IsOfInterest())
+          {
+            return ResourceType_Patient;
+          }
+          else if (request.GetParentSpecification(ResourceType_Study).IsOfInterest())
+          {
+            return ResourceType_Study;
+          }
+          else
+          {
+            return ResourceType_Series;
+          }
+
+        case ResourceType_Instance:
+          if (request.GetParentSpecification(ResourceType_Patient).IsOfInterest())
+          {
+            return ResourceType_Patient;
+          }
+          else if (request.GetParentSpecification(ResourceType_Study).IsOfInterest())
+          {
+            return ResourceType_Study;
+          }
+          else if (request.GetParentSpecification(ResourceType_Series).IsOfInterest())
+          {
+            return ResourceType_Series;
+          }
+          else
+          {
+            return ResourceType_Instance;
+          }
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+
+    static ResourceType GetBottomLevelOfInterest(const FindRequest& request)
+    {
+      switch (request.GetLevel())
+      {
+        case ResourceType_Patient:
+          if (request.GetChildrenSpecification(ResourceType_Instance).IsOfInterest())
+          {
+            return ResourceType_Instance;
+          }
+          else if (request.GetChildrenSpecification(ResourceType_Series).IsOfInterest())
+          {
+            return ResourceType_Series;
+          }
+          else if (request.GetChildrenSpecification(ResourceType_Study).IsOfInterest())
+          {
+            return ResourceType_Study;
+          }
+          else
+          {
+            return ResourceType_Patient;
+          }
+
+        case ResourceType_Study:
+          if (request.GetChildrenSpecification(ResourceType_Instance).IsOfInterest())
+          {
+            return ResourceType_Instance;
+          }
+          else if (request.GetChildrenSpecification(ResourceType_Series).IsOfInterest())
+          {
+            return ResourceType_Series;
+          }
+          else
+          {
+            return ResourceType_Study;
+          }
+
+        case ResourceType_Series:
+          if (request.GetChildrenSpecification(ResourceType_Instance).IsOfInterest())
+          {
+            return ResourceType_Instance;
+          }
+          else
+          {
+            return ResourceType_Series;
+          }
+
+        case ResourceType_Instance:
+          return ResourceType_Instance;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+
+    void GenericFind::ExecuteExpand(FindResponse& response,
+                                    const IDatabaseWrapper::Capabilities& capabilities,
+                                    const FindRequest& request,
+                                    const std::string& identifier)
+    {
+      int64_t internalId;
+      ResourceType level;
+      std::string parent;
+
+      if (request.IsRetrieveParentIdentifier())
+      {
+        if (!compatibilityTransaction_.LookupResourceAndParent(internalId, level, parent, identifier))
+        {
+          return;  // The resource is not available anymore
+        }
+
+        if (level == ResourceType_Patient)
+        {
+          if (!parent.empty())
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+        }
+        else
+        {
+          if (parent.empty())
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+        }
+      }
+      else
+      {
+        if (!transaction_.LookupResource(internalId, level, identifier))
+        {
+          return;  // The resource is not available anymore
+        }
+      }
+
+      if (level != request.GetLevel())
+      {
+        throw OrthancException(ErrorCode_UnknownResource, "Wrong resource level for this ID");  // this might happen e.g if you call /instances/... with a series instance id
+      }
+
+      std::unique_ptr<FindResponse::Resource> resource(new FindResponse::Resource(request.GetLevel(), internalId, identifier));
+
+      if (request.IsRetrieveParentIdentifier())
+      {
+        assert(!parent.empty());
+        resource->SetParentIdentifier(parent);
+      }
+
+      if (request.IsRetrieveMainDicomTags())
+      {
+        RetrieveMainDicomTags(*resource, level, internalId);
+      }
+
+      if (request.IsRetrieveMetadata())
+      {
+        std::map<MetadataType, std::string> metadata;
+        transaction_.GetAllMetadata(metadata, internalId);
+
+        for (std::map<MetadataType, std::string>::const_iterator
+               it = metadata.begin(); it != metadata.end(); ++it)
+        {
+          if (request.IsRetrieveMetadataRevisions() &&
+              capabilities.HasRevisionsSupport())
+          {
+            std::string value;
+            int64_t revision;
+            if (transaction_.LookupMetadata(value, revision, internalId, it->first) &&
+                value == it->second)
+            {
+              resource->AddMetadata(level, it->first, it->second, revision);
+            }
+            else
+            {
+              throw OrthancException(ErrorCode_DatabasePlugin);
+            }
+          }
+          else
+          {
+            resource->AddMetadata(level, it->first, it->second, 0 /* revision not requested */);
+          }
+        }
+      }
+
+      {
+        const ResourceType topLevel = GetTopLevelOfInterest(request);
+
+        int64_t currentId = internalId;
+        ResourceType currentLevel = level;
+
+        while (currentLevel != topLevel)
+        {
+          int64_t parentId;
+          if (transaction_.LookupParent(parentId, currentId))
+          {
+            currentId = parentId;
+            currentLevel = GetParentResourceType(currentLevel);
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+
+          if (request.GetParentSpecification(currentLevel).IsRetrieveMainDicomTags())
+          {
+            RetrieveMainDicomTags(*resource, currentLevel, currentId);
+          }
+
+          if (request.GetParentSpecification(currentLevel).IsRetrieveMetadata())
+          {
+            std::map<MetadataType, std::string> metadata;
+            transaction_.GetAllMetadata(metadata, currentId);
+
+            for (std::map<MetadataType, std::string>::const_iterator
+                   it = metadata.begin(); it != metadata.end(); ++it)
+            {
+              resource->AddMetadata(currentLevel, it->first, it->second, 0 /* revision not request */);
+            }
+          }
+        }
+      }
+
+      if (capabilities.HasLabelsSupport() &&
+          request.IsRetrieveLabels())
+      {
+        compatibilityTransaction_.ListLabels(resource->GetLabels(), internalId);
+      }
+
+      if (request.IsRetrieveAttachments())
+      {
+        std::set<FileContentType> attachments;
+        transaction_.ListAvailableAttachments(attachments, internalId);
+
+        for (std::set<FileContentType>::const_iterator it = attachments.begin(); it != attachments.end(); ++it)
+        {
+          FileInfo info;
+          int64_t revision;
+          if (transaction_.LookupAttachment(info, revision, internalId, *it) &&
+              info.GetContentType() == *it)
+          {
+            resource->AddAttachment(info, revision);
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+        }
+      }
+
+      {
+        const ResourceType bottomLevel = GetBottomLevelOfInterest(request);
+
+        std::list<int64_t> currentIds;
+        currentIds.push_back(internalId);
+
+        ResourceType currentLevel = level;
+
+        while (currentLevel != bottomLevel)
+        {
+          ResourceType childrenLevel = GetChildResourceType(currentLevel);
+
+          if (request.GetChildrenSpecification(childrenLevel).IsRetrieveIdentifiers() || 
+              request.GetChildrenSpecification(childrenLevel).IsRetrieveCount())
+          {
+            for (std::list<int64_t>::const_iterator it = currentIds.begin(); it != currentIds.end(); ++it)
+            {
+              std::list<std::string> ids;
+              compatibilityTransaction_.GetChildrenPublicId(ids, *it);
+
+              for (std::list<std::string>::const_iterator it2 = ids.begin(); it2 != ids.end(); ++it2)
+              {
+                resource->AddChildIdentifier(childrenLevel, *it2);
+              }
+              resource->IncrementChildrenCount(childrenLevel, ids.size());
+            }
+          }
+
+          const std::set<MetadataType>& metadata = request.GetChildrenSpecification(childrenLevel).GetMetadata();
+
+          for (std::set<MetadataType>::const_iterator it = metadata.begin(); it != metadata.end(); ++it)
+          {
+            for (std::list<int64_t>::const_iterator it2 = currentIds.begin(); it2 != currentIds.end(); ++it2)
+            {
+              std::list<std::string> values;
+              transaction_.GetChildrenMetadata(values, *it2, *it);
+
+              for (std::list<std::string>::const_iterator it3 = values.begin(); it3 != values.end(); ++it3)
+              {
+                resource->AddChildrenMetadataValue(childrenLevel, *it, *it3);
+              }
+            }
+          }
+
+          const std::set<DicomTag>& mainDicomTags = request.GetChildrenSpecification(childrenLevel).GetMainDicomTags();
+
+          if (childrenLevel != bottomLevel ||
+              !mainDicomTags.empty())
+          {
+            std::list<int64_t> childrenIds;
+
+            for (std::list<int64_t>::const_iterator it = currentIds.begin(); it != currentIds.end(); ++it)
+            {
+              std::list<int64_t> tmp;
+              transaction_.GetChildrenInternalId(tmp, *it);
+
+              childrenIds.splice(childrenIds.end(), tmp);
+            }
+
+            if (!mainDicomTags.empty())
+            {
+              for (std::list<int64_t>::const_iterator it = childrenIds.begin(); it != childrenIds.end(); ++it)
+              {
+                DicomMap m;
+                transaction_.GetMainDicomTags(m, *it);
+
+                for (std::set<DicomTag>::const_iterator it2 = mainDicomTags.begin(); it2 != mainDicomTags.end(); ++it2)
+                {
+                  std::string value;
+                  if (m.LookupStringValue(value, *it2, false /* no binary allowed */))
+                  {
+                    resource->AddChildrenMainDicomTagValue(childrenLevel, *it2, value);
+                  }
+                }
+              }
+            }
+
+            currentIds = childrenIds;
+          }
+          else
+          {
+            currentIds.clear();
+          }
+
+          currentLevel = childrenLevel;
+        }
+      }
+
+      if (request.GetLevel() != ResourceType_Instance &&
+          request.IsRetrieveOneInstanceMetadataAndAttachments())
+      {
+        int64_t currentId = internalId;
+        ResourceType currentLevel = level;
+
+        while (currentLevel != ResourceType_Instance)
+        {
+          std::list<int64_t> children;
+          transaction_.GetChildrenInternalId(children, currentId);
+          if (children.empty())
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+          else
+          {
+            currentId = children.front();
+            currentLevel = GetChildResourceType(currentLevel);
+          }
+        }
+
+        std::map<MetadataType, std::string> metadata;
+        transaction_.GetAllMetadata(metadata, currentId);
+
+        std::set<FileContentType> attachmentsType;
+        transaction_.ListAvailableAttachments(attachmentsType, currentId);
+
+        std::map<FileContentType, FileInfo> attachments;
+        for (std::set<FileContentType>::const_iterator it = attachmentsType.begin(); it != attachmentsType.end(); ++it)
+        {
+          FileInfo info;
+          int64_t revision;  // Unused in this case
+          if (transaction_.LookupAttachment(info, revision, currentId, *it))
+          {
+            attachments[*it] = info;
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+        }
+
+        resource->SetOneInstanceMetadataAndAttachments(transaction_.GetPublicId(currentId), metadata, attachments);
+      }
+
+      response.Add(resource.release());
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.h	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,60 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../IDatabaseWrapper.h"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    class GenericFind : public boost::noncopyable
+    {
+    private:
+      IDatabaseWrapper::ITransaction&  transaction_;
+      IDatabaseWrapper::ICompatibilityTransaction&  compatibilityTransaction_;
+
+      void RetrieveMainDicomTags(FindResponse::Resource& target,
+                                 ResourceType level,
+                                 int64_t internalId);
+
+    public:
+      explicit GenericFind(IDatabaseWrapper::ITransaction& transaction,
+                           IDatabaseWrapper::ICompatibilityTransaction& compatibilityTransaction) :
+        transaction_(transaction),
+        compatibilityTransaction_(compatibilityTransaction)
+      {
+      }
+
+      void ExecuteFind(std::list<std::string>& identifiers,
+                       const IDatabaseWrapper::Capabilities& capabilities,
+                       const FindRequest& request);
+
+      void ExecuteExpand(FindResponse& response,
+                         const IDatabaseWrapper::Capabilities& capabilities,
+                         const FindRequest& request,
+                         const std::string& identifier);
+    };
+  }
+}
--- a/OrthancServer/Sources/Database/Compatibility/ICreateInstance.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/ICreateInstance.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/Database/Compatibility/ICreateInstance.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/ICreateInstance.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/Database/Compatibility/IGetChildrenMetadata.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/IGetChildrenMetadata.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/Database/Compatibility/IGetChildrenMetadata.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/IGetChildrenMetadata.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/Database/Compatibility/ILookupResourceAndParent.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/ILookupResourceAndParent.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/Database/Compatibility/ILookupResourceAndParent.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/ILookupResourceAndParent.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/Database/Compatibility/ILookupResources.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/ILookupResources.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -35,7 +35,7 @@
       ILookupResources& compatibility,
       std::list<std::string>& resourcesId,
       std::list<std::string>* instancesId,
-      const std::vector<DatabaseConstraint>& lookup,
+      const DatabaseDicomTagConstraints& lookup,
       ResourceType queryLevel,
       size_t limit)
     {
--- a/OrthancServer/Sources/Database/Compatibility/ILookupResources.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/ILookupResources.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -60,7 +60,7 @@
                         ILookupResources& compatibility,
                         std::list<std::string>& resourcesId,
                         std::list<std::string>* instancesId,
-                        const std::vector<DatabaseConstraint>& lookup,
+                        const DatabaseDicomTagConstraints& lookup,
                         ResourceType queryLevel,
                         size_t limit);
     };
--- a/OrthancServer/Sources/Database/Compatibility/ISetResourcesContent.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/ISetResourcesContent.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/Database/Compatibility/SetOfResources.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/SetOfResources.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/Database/Compatibility/SetOfResources.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/SetOfResources.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/FindRequest.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "FindRequest.h"
+
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+
+#include "MainDicomTagsRegistry.h"
+
+#include <cassert>
+
+
+namespace Orthanc
+{
+  FindRequest::ParentSpecification& FindRequest::GetParentSpecification(ResourceType level)
+  {
+    if (!IsResourceLevelAboveOrEqual(level, level_) ||
+        level == level_)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        return retrieveParentPatient_;
+
+      case ResourceType_Study:
+        return retrieveParentStudy_;
+
+      case ResourceType_Series:
+        return retrieveParentSeries_;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  FindRequest::ChildrenSpecification& FindRequest::GetChildrenSpecification(ResourceType level)
+  {
+    if (!IsResourceLevelAboveOrEqual(level_, level) ||
+        level == level_)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    switch (level)
+    {
+      case ResourceType_Study:
+        return retrieveChildrenStudies_;
+
+      case ResourceType_Series:
+        return retrieveChildrenSeries_;
+
+      case ResourceType_Instance:
+        return retrieveChildrenInstances_;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  FindRequest::FindRequest(ResourceType level) :
+    level_(level),
+    hasLimits_(false),
+    limitsSince_(0),
+    limitsCount_(0),
+    labelsConstraint_(LabelsConstraint_All),
+    retrieveMainDicomTags_(false),
+    retrieveMetadata_(false),
+    retrieveMetadataRevisions_(false),
+    retrieveLabels_(false),
+    retrieveAttachments_(false),
+    retrieveParentIdentifier_(false),
+    retrieveOneInstanceMetadataAndAttachments_(false)
+  {
+  }
+
+
+  FindRequest::~FindRequest()
+  {
+    for (std::deque<Ordering*>::iterator it = ordering_.begin(); it != ordering_.end(); ++it)
+    {
+      assert(*it != NULL);
+      delete *it;
+    }
+
+    for (std::deque<DatabaseMetadataConstraint*>::iterator it = metadataConstraints_.begin(); it != metadataConstraints_.end(); ++it)
+    {
+      assert(*it != NULL);
+      delete *it;
+    }
+  }
+
+
+  void FindRequest::SetOrthancId(ResourceType level,
+                                 const std::string& id)
+  {
+    switch (level)
+    {
+      case ResourceType_Patient:
+        SetOrthancPatientId(id);
+        break;
+
+      case ResourceType_Study:
+        SetOrthancStudyId(id);
+        break;
+
+      case ResourceType_Series:
+        SetOrthancSeriesId(id);
+        break;
+
+      case ResourceType_Instance:
+        SetOrthancInstanceId(id);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  void FindRequest::SetOrthancPatientId(const std::string& id)
+  {
+    orthancIdentifiers_.SetPatientId(id);
+  }
+
+
+  void FindRequest::SetOrthancStudyId(const std::string& id)
+  {
+    if (level_ == ResourceType_Patient)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      orthancIdentifiers_.SetStudyId(id);
+    }
+  }
+
+
+  void FindRequest::SetOrthancSeriesId(const std::string& id)
+  {
+    if (level_ == ResourceType_Patient ||
+        level_ == ResourceType_Study)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      orthancIdentifiers_.SetSeriesId(id);
+    }
+  }
+
+
+  void FindRequest::SetOrthancInstanceId(const std::string& id)
+  {
+    if (level_ == ResourceType_Patient ||
+        level_ == ResourceType_Study ||
+        level_ == ResourceType_Series)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      orthancIdentifiers_.SetInstanceId(id);
+    }
+  }
+
+
+  void FindRequest::SetLimits(uint64_t since,
+                              uint64_t count)
+  {
+    hasLimits_ = true;
+    limitsSince_ = since;
+    limitsCount_ = count;
+  }
+
+
+  uint64_t FindRequest::GetLimitsSince() const
+  {
+    if (hasLimits_)
+    {
+      return limitsSince_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  uint64_t FindRequest::GetLimitsCount() const
+  {
+    if (hasLimits_)
+    {
+      return limitsCount_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void FindRequest::AddOrdering(const DicomTag& tag,
+                                OrderingCast cast,
+                                OrderingDirection direction)
+  {
+    ordering_.push_back(new Ordering(Key(tag), cast, direction));
+  }
+
+
+  void FindRequest::AddOrdering(MetadataType metadataType, 
+                                OrderingCast cast,
+                                OrderingDirection direction)
+  {
+    ordering_.push_back(new Ordering(Key(metadataType), cast, direction));
+  }
+
+
+  void FindRequest::AddMetadataConstraint(DatabaseMetadataConstraint* constraint)
+  {
+    metadataConstraints_.push_back(constraint);
+  }
+
+
+  void FindRequest::SetRetrieveParentIdentifier(bool retrieve)
+  {
+    if (level_ == ResourceType_Patient)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+    else
+    {
+      retrieveParentIdentifier_ = retrieve;
+    }
+  }
+
+
+  void FindRequest::SetRetrieveOneInstanceMetadataAndAttachments(bool retrieve)
+  {
+    if (level_ == ResourceType_Instance)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      retrieveOneInstanceMetadataAndAttachments_ = retrieve;
+    }
+  }
+
+
+  bool FindRequest::IsRetrieveOneInstanceMetadataAndAttachments() const
+  {
+    if (level_ == ResourceType_Instance)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return retrieveOneInstanceMetadataAndAttachments_;
+    }
+  }
+
+  bool FindRequest::HasConstraints() const
+  {
+    return (!GetDicomTagConstraints().IsEmpty() ||
+            GetMetadataConstraintsCount() != 0 ||
+            !GetLabels().empty() ||
+            !GetOrdering().empty());
+  }
+
+
+  bool FindRequest::IsTrivialFind(std::string& publicId /* out */) const
+  {
+    if (HasConstraints())
+    {
+      return false;
+    }
+    else if (GetLevel() == ResourceType_Patient &&
+             GetOrthancIdentifiers().HasPatientId() &&
+             !GetOrthancIdentifiers().HasStudyId() &&
+             !GetOrthancIdentifiers().HasSeriesId() &&
+             !GetOrthancIdentifiers().HasInstanceId())
+    {
+      publicId = GetOrthancIdentifiers().GetPatientId();
+      return true;
+    }
+    else if (GetLevel() == ResourceType_Study &&
+             !GetOrthancIdentifiers().HasPatientId() &&
+             GetOrthancIdentifiers().HasStudyId() &&
+             !GetOrthancIdentifiers().HasSeriesId() &&
+             !GetOrthancIdentifiers().HasInstanceId())
+    {
+      publicId = GetOrthancIdentifiers().GetStudyId();
+      return true;
+    }
+    else if (GetLevel() == ResourceType_Series &&
+             !GetOrthancIdentifiers().HasPatientId() &&
+             !GetOrthancIdentifiers().HasStudyId() &&
+             GetOrthancIdentifiers().HasSeriesId() &&
+             !GetOrthancIdentifiers().HasInstanceId())
+    {
+      publicId = GetOrthancIdentifiers().GetSeriesId();
+      return true;
+    }
+    else if (GetLevel() == ResourceType_Instance &&
+             !GetOrthancIdentifiers().HasPatientId() &&
+             !GetOrthancIdentifiers().HasStudyId() &&
+             !GetOrthancIdentifiers().HasSeriesId() &&
+             GetOrthancIdentifiers().HasInstanceId())
+    {
+      publicId = GetOrthancIdentifiers().GetInstanceId();
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/FindRequest.h	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,470 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../../OrthancFramework/Sources/DicomFormat/DicomTag.h"
+#include "../Search/DatabaseDicomTagConstraints.h"
+#include "../Search/DatabaseMetadataConstraint.h"
+#include "../Search/DicomTagConstraint.h"
+#include "../Search/ISqlLookupFormatter.h"
+#include "../ServerEnumerations.h"
+#include "OrthancIdentifiers.h"
+
+#include <deque>
+#include <map>
+#include <set>
+#include <cassert>
+#include <boost/shared_ptr.hpp>
+
+namespace Orthanc
+{
+  class MainDicomTagsRegistry;
+
+  class FindRequest : public boost::noncopyable
+  {
+  public:
+    enum KeyType  // used for ordering and filters
+    {
+      KeyType_DicomTag,
+      KeyType_Metadata
+    };
+
+
+    enum OrderingDirection
+    {
+      OrderingDirection_Ascending,
+      OrderingDirection_Descending
+    };
+
+    enum OrderingCast
+    {
+      OrderingCast_String,
+      OrderingCast_Int,
+      OrderingCast_Float
+    };
+
+    class Key
+    {
+    private:
+      KeyType       type_;
+      DicomTag      dicomTag_;
+      MetadataType  metadata_;
+      
+      // TODO-FIND: to execute the query, we actually need:
+      // ResourceType level_;
+      // DicomTagType dicomTagType_;
+      // these are however only populated in StatelessDatabaseOperations -> we had to add the normalized lookup arg to ExecuteFind
+
+    public:
+      explicit Key(const DicomTag& dicomTag) :
+        type_(KeyType_DicomTag),
+        dicomTag_(dicomTag),
+        metadata_(MetadataType_EndUser)
+      {
+      }
+
+      explicit Key(MetadataType metadata) :
+        type_(KeyType_Metadata),
+        dicomTag_(0, 0),
+        metadata_(metadata)
+      {
+      }
+
+      KeyType GetType() const
+      {
+        return type_;
+      }
+
+      const DicomTag& GetDicomTag() const
+      {
+        assert(GetType() == KeyType_DicomTag);
+        return dicomTag_;
+      }
+
+      MetadataType GetMetadataType() const
+      {
+        assert(GetType() == KeyType_Metadata);
+        return metadata_;
+      }
+    };
+
+    class Ordering : public boost::noncopyable
+    {
+    private:
+      OrderingDirection   direction_;
+      OrderingCast        cast_;
+      Key                 key_;
+
+    public:
+      Ordering(const Key& key,
+               OrderingCast cast,
+               OrderingDirection direction) :
+        direction_(direction),
+        cast_(cast),
+        key_(key)
+      {
+      }
+
+      KeyType GetKeyType() const
+      {
+        return key_.GetType();
+      }
+
+      OrderingDirection GetDirection() const
+      {
+        return direction_;
+      }
+
+      OrderingCast GetCast() const
+      {
+        return cast_;
+      }
+
+      MetadataType GetMetadataType() const
+      {
+        return key_.GetMetadataType();
+      }
+
+      DicomTag GetDicomTag() const
+      {
+        return key_.GetDicomTag();
+      }
+    };
+
+
+    class ParentSpecification : public boost::noncopyable
+    {
+    private:
+      bool  mainDicomTags_;
+      bool  metadata_;
+
+    public:
+      ParentSpecification() :
+        mainDicomTags_(false),
+        metadata_(false)
+      {
+      }
+
+      void SetRetrieveMainDicomTags(bool retrieve)
+      {
+        mainDicomTags_ = retrieve;
+      }
+
+      bool IsRetrieveMainDicomTags() const
+      {
+        return mainDicomTags_;
+      }
+
+      void SetRetrieveMetadata(bool retrieve)
+      {
+        metadata_ = retrieve;
+      }
+
+      bool IsRetrieveMetadata() const
+      {
+        return metadata_;
+      }
+
+      bool IsOfInterest() const
+      {
+        return (mainDicomTags_ || metadata_);
+      }
+    };
+
+
+    class ChildrenSpecification : public boost::noncopyable
+    {
+    private:
+      bool                    identifiers_;
+      std::set<MetadataType>  metadata_;
+      std::set<DicomTag>      mainDicomTags_;
+      bool                    count_;
+
+    public:
+      ChildrenSpecification() :
+        identifiers_(false),
+        count_(false)
+      {
+      }
+
+      void SetRetrieveIdentifiers(bool retrieve)
+      {
+        identifiers_ = retrieve;
+      }
+
+      bool IsRetrieveIdentifiers() const
+      {
+        return identifiers_;
+      }
+
+      void SetRetrieveCount(bool retrieve)
+      {
+        count_ = retrieve;
+      }
+
+      bool IsRetrieveCount() const
+      {
+        return count_;
+      }
+
+      void AddMetadata(MetadataType metadata)
+      {
+        metadata_.insert(metadata);
+      }
+
+      const std::set<MetadataType>& GetMetadata() const
+      {
+        return metadata_;
+      }
+
+      void AddMainDicomTag(const DicomTag& tag)
+      {
+        mainDicomTags_.insert(tag);
+      }
+
+      const std::set<DicomTag>& GetMainDicomTags() const
+      {
+        return mainDicomTags_;
+      }
+
+      bool IsOfInterest() const
+      {
+        return (identifiers_ || !metadata_.empty() || !mainDicomTags_.empty() || count_);
+      }
+    };
+
+
+  private:
+    // filter & ordering fields
+    ResourceType                         level_;                // The level of the response (the filtering on tags, labels and metadata also happens at this level)
+    OrthancIdentifiers                   orthancIdentifiers_;   // The response must belong to this Orthanc resources hierarchy
+    DatabaseDicomTagConstraints          dicomTagConstraints_;  // All tags filters (note: the order is not important)
+    bool                                 hasLimits_;
+    uint64_t                             limitsSince_;
+    uint64_t                             limitsCount_;
+    std::set<std::string>                labels_;
+    LabelsConstraint                     labelsConstraint_;
+
+    std::deque<Ordering*>                ordering_;             // The ordering criteria (note: the order is important !)
+    std::deque<DatabaseMetadataConstraint*>  metadataConstraints_;  // All metadata filters (note: the order is not important)
+
+    bool                                 retrieveMainDicomTags_;
+    bool                                 retrieveMetadata_;
+    bool                                 retrieveMetadataRevisions_;
+    bool                                 retrieveLabels_;
+    bool                                 retrieveAttachments_;
+    bool                                 retrieveParentIdentifier_;
+    ParentSpecification                  retrieveParentPatient_;
+    ParentSpecification                  retrieveParentStudy_;
+    ParentSpecification                  retrieveParentSeries_;
+    ChildrenSpecification                retrieveChildrenStudies_;
+    ChildrenSpecification                retrieveChildrenSeries_;
+    ChildrenSpecification                retrieveChildrenInstances_;
+    bool                                 retrieveOneInstanceMetadataAndAttachments_;
+
+    std::unique_ptr<MainDicomTagsRegistry>  mainDicomTagsRegistry_;
+
+  public:
+    explicit FindRequest(ResourceType level);
+
+    ~FindRequest();
+
+    ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    void SetOrthancId(ResourceType level,
+                      const std::string& id);
+
+    void SetOrthancPatientId(const std::string& id);
+
+    void SetOrthancStudyId(const std::string& id);
+
+    void SetOrthancSeriesId(const std::string& id);
+
+    void SetOrthancInstanceId(const std::string& id);
+
+    const OrthancIdentifiers& GetOrthancIdentifiers() const
+    {
+      return orthancIdentifiers_;
+    }
+
+    DatabaseDicomTagConstraints& GetDicomTagConstraints()
+    {
+      return dicomTagConstraints_;
+    }
+
+    const DatabaseDicomTagConstraints& GetDicomTagConstraints() const
+    {
+      return dicomTagConstraints_;
+    }
+
+    size_t GetMetadataConstraintsCount() const
+    {
+      return metadataConstraints_.size();
+    }
+
+    void ClearLimits()
+    {
+      hasLimits_ = false;
+    }
+
+    void SetLimits(uint64_t since,
+                   uint64_t count);
+
+    bool HasLimits() const
+    {
+      return hasLimits_;
+    }
+
+    uint64_t GetLimitsSince() const;
+
+    uint64_t GetLimitsCount() const;
+
+    void AddOrdering(const DicomTag& tag,
+                     OrderingCast cast,
+                     OrderingDirection direction);
+
+    void AddOrdering(MetadataType metadataType,
+                     OrderingCast cast,
+                     OrderingDirection direction);
+
+    const std::deque<Ordering*>& GetOrdering() const
+    {
+      return ordering_;
+    }
+
+    void AddMetadataConstraint(DatabaseMetadataConstraint* constraint);
+
+    const std::deque<DatabaseMetadataConstraint*>& GetMetadataConstraint() const
+    {
+      return metadataConstraints_;
+    }
+
+    void SetLabels(const std::set<std::string>& labels)
+    {
+      labels_ = labels;
+    }
+
+    void AddLabel(const std::string& label)
+    {
+      labels_.insert(label);
+    }
+
+    const std::set<std::string>& GetLabels() const
+    {
+      return labels_;
+    }
+
+    LabelsConstraint GetLabelsConstraint() const
+    {
+      return labelsConstraint_;
+    }
+
+    void SetLabelsConstraint(LabelsConstraint constraint)
+    {
+      labelsConstraint_ = constraint;
+    }
+
+    void SetRetrieveMainDicomTags(bool retrieve)
+    {
+      retrieveMainDicomTags_ = retrieve;
+    }
+
+    bool IsRetrieveMainDicomTags() const
+    {
+      return retrieveMainDicomTags_;
+    }
+
+    void SetRetrieveMetadata(bool retrieve)
+    {
+      retrieveMetadata_ = retrieve;
+    }
+
+    bool IsRetrieveMetadata() const
+    {
+      return retrieveMetadata_;
+    }
+
+    void SetRetrieveMetadataRevisions(bool retrieve)
+    {
+      retrieveMetadataRevisions_ = retrieve;
+    }
+
+    bool IsRetrieveMetadataRevisions() const
+    {
+      return retrieveMetadataRevisions_;
+    }
+
+    void SetRetrieveLabels(bool retrieve)
+    {
+      retrieveLabels_ = retrieve;
+    }
+
+    bool IsRetrieveLabels() const
+    {
+      return retrieveLabels_;
+    }
+
+    void SetRetrieveAttachments(bool retrieve)
+    {
+      retrieveAttachments_ = retrieve;
+    }
+
+    bool IsRetrieveAttachments() const
+    {
+      return retrieveAttachments_;
+    }
+
+    void SetRetrieveParentIdentifier(bool retrieve);
+
+    bool IsRetrieveParentIdentifier() const
+    {
+      return retrieveParentIdentifier_;
+    }
+
+    ParentSpecification& GetParentSpecification(ResourceType level);
+
+    const ParentSpecification& GetParentSpecification(ResourceType level) const
+    {
+      return const_cast<FindRequest&>(*this).GetParentSpecification(level);
+    }
+
+    ChildrenSpecification& GetChildrenSpecification(ResourceType level);
+
+    const ChildrenSpecification& GetChildrenSpecification(ResourceType level) const
+    {
+      return const_cast<FindRequest&>(*this).GetChildrenSpecification(level);
+    }
+
+    void SetRetrieveOneInstanceMetadataAndAttachments(bool retrieve);
+
+    bool IsRetrieveOneInstanceMetadataAndAttachments() const;
+
+    bool HasConstraints() const;
+
+    bool IsTrivialFind(std::string& publicId /* out */) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/FindResponse.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,916 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "FindResponse.h"
+
+#include "../../../OrthancFramework/Sources/DicomFormat/DicomArray.h"
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+#include "../../../OrthancFramework/Sources/SerializationToolbox.h"
+
+#include <boost/lexical_cast.hpp>
+#include <cassert>
+
+
+namespace Orthanc
+{
+  class FindResponse::MainDicomTagsAtLevel::DicomValue : public boost::noncopyable
+  {
+  public:
+    enum ValueType
+    {
+      ValueType_String,
+      ValueType_Null
+    };
+
+  private:
+    ValueType     type_;
+    std::string   value_;
+
+  public:
+    DicomValue(ValueType type,
+               const std::string& value) :
+      type_(type),
+      value_(value)
+    {
+    }
+
+    ValueType GetType() const
+    {
+      return type_;
+    }
+
+    const std::string& GetValue() const
+    {
+      switch (type_)
+      {
+        case ValueType_Null:
+          throw OrthancException(ErrorCode_BadSequenceOfCalls);
+
+        case ValueType_String:
+          return value_;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+  };
+
+
+  FindResponse::MainDicomTagsAtLevel::~MainDicomTagsAtLevel()
+  {
+    for (MainDicomTags::iterator it = mainDicomTags_.begin(); it != mainDicomTags_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+  }
+
+
+  void FindResponse::MainDicomTagsAtLevel::AddNullDicomTag(uint16_t group,
+                                                           uint16_t element)
+  {
+    const DicomTag tag(group, element);
+
+    if (mainDicomTags_.find(tag) == mainDicomTags_.end())
+    {
+      mainDicomTags_[tag] = new DicomValue(DicomValue::ValueType_Null, "");
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void FindResponse::MainDicomTagsAtLevel::AddStringDicomTag(uint16_t group,
+                                                             uint16_t element,
+                                                             const std::string& value)
+  {
+    const DicomTag tag(group, element);
+
+    if (mainDicomTags_.find(tag) == mainDicomTags_.end())
+    {
+      mainDicomTags_[tag] = new DicomValue(DicomValue::ValueType_String, value);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void FindResponse::MainDicomTagsAtLevel::Export(DicomMap& target) const
+  {
+    for (MainDicomTags::const_iterator it = mainDicomTags_.begin(); it != mainDicomTags_.end(); ++it)
+    {
+      assert(it->second != NULL);
+
+      switch (it->second->GetType())
+      {
+        case DicomValue::ValueType_String:
+          target.SetValue(it->first, it->second->GetValue(), false /* not binary */);
+          break;
+
+        case DicomValue::ValueType_Null:
+          target.SetNullValue(it->first);
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+  }
+
+
+  FindResponse::ChildrenInformation::~ChildrenInformation()
+  {
+    for (MetadataValues::iterator it = metadataValues_.begin(); it != metadataValues_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+
+    for (MainDicomTagValues::iterator it = mainDicomTagValues_.begin(); it != mainDicomTagValues_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+  }
+
+
+  void FindResponse::ChildrenInformation::AddIdentifier(const std::string& identifier)
+  {
+    // The same identifier can be added through AddChildIdentifier and through AddOneInstanceIdentifier
+    identifiers_.insert(identifier);
+  }
+
+
+  void FindResponse::ChildrenInformation::AddMetadataValue(MetadataType metadata,
+                                                           const std::string& value)
+  {
+    MetadataValues::iterator found = metadataValues_.find(metadata);
+
+    if (found == metadataValues_.end())
+    {
+      std::set<std::string> s;
+      s.insert(value);
+      metadataValues_[metadata] = new std::set<std::string>(s);
+    }
+    else
+    {
+      assert(found->second != NULL);
+      found->second->insert(value);
+    }
+  }
+
+
+  void FindResponse::ChildrenInformation::GetMetadataValues(std::set<std::string>& values,
+                                                            MetadataType metadata) const
+  {
+    MetadataValues::const_iterator found = metadataValues_.find(metadata);
+
+    if (found == metadataValues_.end())
+    {
+      values.clear();
+    }
+    else
+    {
+      assert(found->second != NULL);
+      values = *found->second;
+    }
+  }
+
+
+  void FindResponse::ChildrenInformation::AddMainDicomTagValue(const DicomTag& tag,
+                                                               const std::string& value)
+  {
+    MainDicomTagValues::iterator found = mainDicomTagValues_.find(tag);
+
+    if (found == mainDicomTagValues_.end())
+    {
+      std::set<std::string> s;
+      s.insert(value);
+      mainDicomTagValues_[tag] = new std::set<std::string>(s);
+    }
+    else
+    {
+      assert(found->second != NULL);
+      found->second->insert(value);
+    }
+  }
+
+
+  void FindResponse::ChildrenInformation::GetMainDicomTagValues(std::set<std::string>& values,
+                                                                const DicomTag& tag) const
+  {
+    MainDicomTagValues::const_iterator found = mainDicomTagValues_.find(tag);
+
+    if (found == mainDicomTagValues_.end())
+    {
+      values.clear();
+    }
+    else
+    {
+      assert(found->second != NULL);
+      values = *found->second;
+    }
+  }
+
+
+  FindResponse::ChildrenInformation& FindResponse::Resource::GetChildrenInformation(ResourceType level)
+  {
+    switch (level)
+    {
+      case ResourceType_Study:
+        if (level_ == ResourceType_Patient)
+        {
+          return childrenStudiesInformation_;
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+
+      case ResourceType_Series:
+        if (level_ == ResourceType_Patient ||
+            level_ == ResourceType_Study)
+        {
+          return childrenSeriesInformation_;
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+
+      case ResourceType_Instance:
+        if (level_ == ResourceType_Patient ||
+            level_ == ResourceType_Study ||
+            level_ == ResourceType_Series)
+        {
+          return childrenInstancesInformation_;
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  FindResponse::MainDicomTagsAtLevel& FindResponse::Resource::GetMainDicomTagsAtLevel(ResourceType level)
+  {
+    if (!IsResourceLevelAboveOrEqual(level, level_))
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        return mainDicomTagsPatient_;
+
+      case ResourceType_Study:
+        return mainDicomTagsStudy_;
+
+      case ResourceType_Series:
+        return mainDicomTagsSeries_;
+
+      case ResourceType_Instance:
+        return mainDicomTagsInstance_;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  void FindResponse::Resource::GetAllMainDicomTags(DicomMap& target) const
+  {
+    switch (level_)
+    {
+      // Don't reorder or add "break" below
+      case ResourceType_Instance:
+        mainDicomTagsInstance_.Export(target);
+
+      case ResourceType_Series:
+        mainDicomTagsSeries_.Export(target);
+
+      case ResourceType_Study:
+        mainDicomTagsStudy_.Export(target);
+
+      case ResourceType_Patient:
+        mainDicomTagsPatient_.Export(target);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  void FindResponse::Resource::AddMetadata(ResourceType level,
+                                           MetadataType metadata,
+                                           const std::string& value,
+                                           int64_t revision)
+  {
+    std::map<MetadataType, MetadataContent>& m = GetMetadata(level);
+
+    if (m.find(metadata) != m.end())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);  // Metadata already present
+    }
+    else
+    {
+      m[metadata] = MetadataContent(value, revision);
+    }
+  }
+
+
+  std::map<MetadataType, FindResponse::MetadataContent>& FindResponse::Resource::GetMetadata(ResourceType level)
+  {
+    if (!IsResourceLevelAboveOrEqual(level, level_))
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        return metadataPatient_;
+
+      case ResourceType_Study:
+        return metadataStudy_;
+
+      case ResourceType_Series:
+        return metadataSeries_;
+
+      case ResourceType_Instance:
+        return metadataInstance_;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool FindResponse::Resource::LookupMetadata(std::string& value,
+                                              ResourceType level,
+                                              MetadataType metadata) const
+  {
+    const std::map<MetadataType, MetadataContent>& m = GetMetadata(level);
+
+    std::map<MetadataType, MetadataContent>::const_iterator found = m.find(metadata);
+
+    if (found == m.end())
+    {
+      return false;
+    }
+    else
+    {
+      value = found->second.GetValue();
+      return true;
+    }
+  }
+
+
+  bool FindResponse::Resource::LookupMetadata(std::string& value,
+                                              int64_t& revision,
+                                              ResourceType level,
+                                              MetadataType metadata) const
+  {
+    const std::map<MetadataType, MetadataContent>& m = GetMetadata(level);
+
+    std::map<MetadataType, MetadataContent>::const_iterator found = m.find(metadata);
+
+    if (found == m.end())
+    {
+      return false;
+    }
+    else
+    {
+      value = found->second.GetValue();
+      revision = found->second.GetRevision();
+      return true;
+    }
+  }
+
+
+  void FindResponse::Resource::SetParentIdentifier(const std::string& id)
+  {
+    if (level_ == ResourceType_Patient)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+    else if (HasParentIdentifier())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      parentIdentifier_.reset(new std::string(id));
+    }
+  }
+
+
+  const std::string& FindResponse::Resource::GetParentIdentifier() const
+  {
+    if (level_ == ResourceType_Patient)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+    else if (HasParentIdentifier())
+    {
+      return *parentIdentifier_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  bool FindResponse::Resource::HasParentIdentifier() const
+  {
+    if (level_ == ResourceType_Patient)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+    else
+    {
+      return parentIdentifier_.get() != NULL;
+    }
+  }
+
+
+  void FindResponse::Resource::AddLabel(const std::string& label)
+  {
+    if (labels_.find(label) == labels_.end())
+    {
+      labels_.insert(label);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void FindResponse::Resource::AddAttachment(const FileInfo& attachment,
+                                             int64_t revision)
+  {
+    if (attachments_.find(attachment.GetContentType()) == attachments_.end() &&
+        revisions_.find(attachment.GetContentType()) == revisions_.end())
+    {
+      attachments_[attachment.GetContentType()] = attachment;
+      revisions_[attachment.GetContentType()] = revision;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  bool FindResponse::Resource::LookupAttachment(FileInfo& target,
+                                                int64_t& revision,
+                                                FileContentType type) const
+  {
+    std::map<FileContentType, FileInfo>::const_iterator it = attachments_.find(type);
+    std::map<FileContentType, int64_t>::const_iterator it2 = revisions_.find(type);
+
+    if (it != attachments_.end())
+    {
+      if (it2 != revisions_.end())
+      {
+        target = it->second;
+        revision = it2->second;
+        return true;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+    else
+    {
+      if (it2 == revisions_.end())
+      {
+        return false;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+  }
+
+
+  void FindResponse::Resource::ListAttachments(std::set<FileContentType>& target) const
+  {
+    target.clear();
+
+    for (std::map<FileContentType, FileInfo>::const_iterator
+           it = attachments_.begin(); it != attachments_.end(); ++it)
+    {
+      target.insert(it->first);
+    }
+  }
+
+
+  void FindResponse::Resource::SetOneInstanceMetadataAndAttachments(const std::string& instancePublicId,
+                                                                    const std::map<MetadataType, std::string>& metadata,
+                                                                    const std::map<FileContentType, FileInfo>& attachments)
+  {
+    if (hasOneInstanceMetadataAndAttachments_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (instancePublicId.empty())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      hasOneInstanceMetadataAndAttachments_ = true;
+      oneInstancePublicId_ = instancePublicId;
+      oneInstanceMetadata_ = metadata;
+      oneInstanceAttachments_ = attachments;
+    }
+  }
+
+
+  void FindResponse::Resource::SetOneInstancePublicId(const std::string& instancePublicId)
+  {
+    SetOneInstanceMetadataAndAttachments(instancePublicId, std::map<MetadataType, std::string>(),
+                                         std::map<FileContentType, FileInfo>());
+  }
+
+
+  void FindResponse::Resource::AddOneInstanceMetadata(MetadataType metadata,
+                                                      const std::string& value)
+  {
+    if (hasOneInstanceMetadataAndAttachments_)
+    {
+      if (oneInstanceMetadata_.find(metadata) == oneInstanceMetadata_.end())
+      {
+        oneInstanceMetadata_[metadata] = value;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls, "Metadata already exists");
+      }
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void FindResponse::Resource::AddOneInstanceAttachment(const FileInfo& attachment)
+  {
+    if (hasOneInstanceMetadataAndAttachments_)
+    {
+      if (oneInstanceAttachments_.find(attachment.GetContentType()) == oneInstanceAttachments_.end())
+      {
+        oneInstanceAttachments_[attachment.GetContentType()] = attachment;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls, "Attachment already exists");
+      }
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  const std::string& FindResponse::Resource::GetOneInstancePublicId() const
+  {
+    if (hasOneInstanceMetadataAndAttachments_)
+    {
+      return oneInstancePublicId_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  const std::map<MetadataType, std::string>& FindResponse::Resource::GetOneInstanceMetadata() const
+  {
+    if (hasOneInstanceMetadataAndAttachments_)
+    {
+      return oneInstanceMetadata_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  const std::map<FileContentType, FileInfo>& FindResponse::Resource::GetOneInstanceAttachments() const
+  {
+    if (hasOneInstanceMetadataAndAttachments_)
+    {
+      return oneInstanceAttachments_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  static void DebugDicomMap(Json::Value& target,
+                            const DicomMap& m)
+  {
+    DicomArray a(m);
+    for (size_t i = 0; i < a.GetSize(); i++)
+    {
+      if (a.GetElement(i).GetValue().IsNull())
+      {
+        target[a.GetElement(i).GetTag().Format()] = Json::nullValue;
+      }
+      else if (a.GetElement(i).GetValue().IsString())
+      {
+        target[a.GetElement(i).GetTag().Format()] = a.GetElement(i).GetValue().GetContent();
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+  }
+
+
+  static void DebugMetadata(Json::Value& target,
+                            const std::map<MetadataType, std::string>& m)
+  {
+    target = Json::objectValue;
+
+    for (std::map<MetadataType, std::string>::const_iterator it = m.begin(); it != m.end(); ++it)
+    {
+      target[EnumerationToString(it->first)] = it->second;
+    }
+  }
+
+
+  static void DebugMetadata(Json::Value& target,
+                            const std::map<MetadataType, FindResponse::MetadataContent>& m)
+  {
+    target = Json::objectValue;
+
+    for (std::map<MetadataType, FindResponse::MetadataContent>::const_iterator it = m.begin(); it != m.end(); ++it)
+    {
+      target[EnumerationToString(it->first)] = it->second.GetValue();
+    }
+  }
+
+
+  static void DebugAddAttachment(Json::Value& target,
+                                 const FileInfo& info)
+  {
+    Json::Value u = Json::arrayValue;
+    u.append(info.GetUuid());
+    u.append(static_cast<Json::UInt64>(info.GetUncompressedSize()));
+    target[EnumerationToString(info.GetContentType())] = u;
+  }
+
+
+  static void DebugAttachments(Json::Value& target,
+                               const std::map<FileContentType, FileInfo>& attachments)
+  {
+    target = Json::objectValue;
+    for (std::map<FileContentType, FileInfo>::const_iterator it = attachments.begin();
+         it != attachments.end(); ++it)
+    {
+      if (it->first != it->second.GetContentType())
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+      else
+      {
+        DebugAddAttachment(target, it->second);
+      }
+    }
+  }
+
+
+  static void DebugSetOfStrings(Json::Value& target,
+                                const std::set<std::string>& values)
+  {
+    target = Json::arrayValue;
+    for (std::set<std::string>::const_iterator it = values.begin(); it != values.end(); ++it)
+    {
+      target.append(*it);
+    }
+  }
+
+
+  void FindResponse::Resource::DebugExport(Json::Value& target,
+                                           const FindRequest& request) const
+  {
+    target = Json::objectValue;
+
+    target["Level"] = EnumerationToString(GetLevel());
+    target["ID"] = GetIdentifier();
+
+    if (request.IsRetrieveParentIdentifier())
+    {
+      target["ParentID"] = GetParentIdentifier();
+    }
+
+    if (request.IsRetrieveMainDicomTags())
+    {
+      DicomMap m;
+      GetMainDicomTags(m, request.GetLevel());
+      DebugDicomMap(target[EnumerationToString(GetLevel())]["MainDicomTags"], m);
+    }
+
+    if (request.IsRetrieveMetadata())
+    {
+      DebugMetadata(target[EnumerationToString(GetLevel())]["Metadata"], GetMetadata(request.GetLevel()));
+    }
+
+    static const ResourceType levels[4] = { ResourceType_Patient, ResourceType_Study, ResourceType_Series, ResourceType_Instance };
+
+    for (size_t i = 0; i < 4; i++)
+    {
+      const char* level = EnumerationToString(levels[i]);
+
+      if (levels[i] != request.GetLevel() &&
+          IsResourceLevelAboveOrEqual(levels[i], request.GetLevel()))
+      {
+        if (request.GetParentSpecification(levels[i]).IsRetrieveMainDicomTags())
+        {
+          DicomMap m;
+          GetMainDicomTags(m, levels[i]);
+          DebugDicomMap(target[level]["MainDicomTags"], m);
+        }
+
+        if (request.GetParentSpecification(levels[i]).IsRetrieveMetadata())
+        {
+          DebugMetadata(target[level]["Metadata"], GetMetadata(levels[i]));
+        }
+      }
+
+      if (levels[i] != request.GetLevel() &&
+          IsResourceLevelAboveOrEqual(request.GetLevel(), levels[i]))
+      {
+        if (request.GetChildrenSpecification(levels[i]).IsRetrieveIdentifiers())
+        {
+          DebugSetOfStrings(target[level]["Identifiers"], GetChildrenInformation(levels[i]).GetIdentifiers());
+        }
+
+        const std::set<MetadataType>& metadata = request.GetChildrenSpecification(levels[i]).GetMetadata();
+        for (std::set<MetadataType>::const_iterator it = metadata.begin(); it != metadata.end(); ++it)
+        {
+          std::set<std::string> values;
+          GetChildrenInformation(levels[i]).GetMetadataValues(values, *it);
+          DebugSetOfStrings(target[level]["Metadata"][EnumerationToString(*it)], values);
+        }
+
+        const std::set<DicomTag>& tags = request.GetChildrenSpecification(levels[i]).GetMainDicomTags();
+        for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
+        {
+          std::set<std::string> values;
+          GetChildrenInformation(levels[i]).GetMainDicomTagValues(values, *it);
+          DebugSetOfStrings(target[level]["MainDicomTags"][it->Format()], values);
+        }
+      }
+    }
+
+    if (request.IsRetrieveLabels())
+    {
+      DebugSetOfStrings(target["Labels"], labels_);
+    }
+
+    if (request.IsRetrieveAttachments())
+    {
+      DebugAttachments(target["Attachments"], attachments_);
+    }
+
+    if (request.GetLevel() != ResourceType_Instance &&
+        request.IsRetrieveOneInstanceMetadataAndAttachments())
+    {
+      DebugMetadata(target["OneInstance"]["Metadata"], GetOneInstanceMetadata());
+      DebugAttachments(target["OneInstance"]["Attachments"], GetOneInstanceAttachments());
+    }
+  }
+
+
+  FindResponse::~FindResponse()
+  {
+    for (size_t i = 0; i < items_.size(); i++)
+    {
+      assert(items_[i] != NULL);
+      delete items_[i];
+    }
+  }
+
+
+  void FindResponse::Add(Resource* item /* takes ownership */)
+  {
+    std::unique_ptr<Resource> protection(item);
+
+    if (item == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else if (!items_.empty() &&
+             items_[0]->GetLevel() != item->GetLevel())
+    {
+      throw OrthancException(ErrorCode_BadParameterType, "A find response must only contain resources of the same type");
+    }
+    else
+    {
+      const std::string& id = item->GetIdentifier();
+      int64_t internalId = item->GetInternalId();
+
+      if (identifierIndex_.find(id) == identifierIndex_.end() && internalIdIndex_.find(internalId) == internalIdIndex_.end())
+      {
+        items_.push_back(protection.release());
+        identifierIndex_[id] = item;
+        internalIdIndex_[internalId] = item;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls, "This resource has already been added: " + id + "/" + boost::lexical_cast<std::string>(internalId));
+      }
+    }
+  }
+
+
+  const FindResponse::Resource& FindResponse::GetResourceByIndex(size_t index) const
+  {
+    if (index >= items_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(items_[index] != NULL);
+      return *items_[index];
+    }
+  }
+
+
+  FindResponse::Resource& FindResponse::GetResourceByIdentifier(const std::string& id)
+  {
+    IdentifierIndex::const_iterator found = identifierIndex_.find(id);
+
+    if (found == identifierIndex_.end())
+    {
+      throw OrthancException(ErrorCode_InexistentItem);
+    }
+    else
+    {
+      assert(found->second != NULL);
+      return *found->second;
+    }
+  }
+
+
+  FindResponse::Resource& FindResponse::GetResourceByInternalId(int64_t internalId)
+  {
+    InternalIdIndex::const_iterator found = internalIdIndex_.find(internalId);
+
+    if (found == internalIdIndex_.end())
+    {
+      throw OrthancException(ErrorCode_InexistentItem);
+    }
+    else
+    {
+      assert(found->second != NULL);
+      return *found->second;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/FindResponse.h	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,437 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h"
+#include "../../../OrthancFramework/Sources/Enumerations.h"
+#include "../../../OrthancFramework/Sources/FileStorage/FileInfo.h"
+#include "../ServerEnumerations.h"
+#include "OrthancIdentifiers.h"
+#include "FindRequest.h"
+
+#include <boost/noncopyable.hpp>
+#include <deque>
+#include <map>
+#include <set>
+#include <list>
+
+
+namespace Orthanc
+{
+  class FindResponse : public boost::noncopyable
+  {
+  private:
+    class MainDicomTagsAtLevel : public boost::noncopyable
+    {
+    private:
+      class DicomValue;
+
+      typedef std::map<DicomTag, DicomValue*>  MainDicomTags;
+
+      MainDicomTags  mainDicomTags_;
+
+    public:
+      ~MainDicomTagsAtLevel();
+
+      void AddStringDicomTag(uint16_t group,
+                             uint16_t element,
+                             const std::string& value);
+
+      // The "Null" value could be used in the future to indicate a
+      // value that is not available, typically a new "ExtraMainDicomTag"
+      void AddNullDicomTag(uint16_t group,
+                           uint16_t element);
+
+      void Export(DicomMap& target) const;
+    };
+
+    class ChildrenInformation : public boost::noncopyable
+    {
+    private:
+      typedef std::map<MetadataType, std::set<std::string>* >  MetadataValues;
+      typedef std::map<DicomTag, std::set<std::string>* >      MainDicomTagValues;
+
+      std::set<std::string>  identifiers_;
+      uint64_t               count_;
+      MetadataValues         metadataValues_;
+      MainDicomTagValues     mainDicomTagValues_;
+
+    public:
+      ChildrenInformation()
+      : count_(0)
+      {
+      }
+
+      ~ChildrenInformation();
+
+      void AddIdentifier(const std::string& identifier);
+
+      const std::set<std::string>& GetIdentifiers() const
+      {
+        return identifiers_;
+      }
+
+      void SetCount(uint64_t count)
+      {
+        count_ = count;
+      }
+
+      void IncrementCount(uint64_t count)
+      {
+        count_ += count;
+      }
+
+      uint64_t GetCount() const
+      {
+        return count_;
+      }
+
+      void AddMetadataValue(MetadataType metadata,
+                            const std::string& value);
+
+      void GetMetadataValues(std::set<std::string>& values,
+                             MetadataType metadata) const;
+
+      void AddMainDicomTagValue(const DicomTag& tag,
+                                const std::string& value);
+
+      void GetMainDicomTagValues(std::set<std::string>& values,
+                                 const DicomTag& tag) const;
+    };
+
+
+  public:
+    class MetadataContent
+    {
+    private:
+      std::string  value_;
+      int64_t      revision_;
+
+    public:
+      MetadataContent() :
+        revision_(0)
+      {
+      }
+
+      MetadataContent(const std::string& value,
+                      int64_t revision) :
+        value_(value),
+        revision_(revision)
+      {
+      }
+
+      explicit MetadataContent(const std::string& value) :
+        value_(value),
+        revision_(0)
+      {
+      }
+
+      const std::string& GetValue() const
+      {
+        return value_;
+      }
+
+      int64_t GetRevision() const
+      {
+        return revision_;
+      }
+
+      void SetRevision(int64_t revision)
+      {
+        revision_ = revision;
+      }
+    };
+
+
+    class Resource : public boost::noncopyable
+    {
+    private:
+      typedef std::map<MetadataType, std::list<std::string>*>  ChildrenMetadata;
+
+      ResourceType                          level_;
+      int64_t                               internalId_;   // Internal ID of the resource in the database
+      std::string                           identifier_;
+      std::unique_ptr<std::string>          parentIdentifier_;
+      MainDicomTagsAtLevel                  mainDicomTagsPatient_;
+      MainDicomTagsAtLevel                  mainDicomTagsStudy_;
+      MainDicomTagsAtLevel                  mainDicomTagsSeries_;
+      MainDicomTagsAtLevel                  mainDicomTagsInstance_;
+      std::map<MetadataType, MetadataContent>   metadataPatient_;
+      std::map<MetadataType, MetadataContent>   metadataStudy_;
+      std::map<MetadataType, MetadataContent>   metadataSeries_;
+      std::map<MetadataType, MetadataContent>   metadataInstance_;
+      ChildrenInformation                   childrenStudiesInformation_;
+      ChildrenInformation                   childrenSeriesInformation_;
+      ChildrenInformation                   childrenInstancesInformation_;
+      std::set<std::string>                 labels_;
+      std::map<FileContentType, FileInfo>   attachments_;
+      std::map<FileContentType, int64_t>    revisions_;
+      bool                                  hasOneInstanceMetadataAndAttachments_;
+      std::string                           oneInstancePublicId_;
+      std::map<MetadataType, std::string>   oneInstanceMetadata_;
+      std::map<FileContentType, FileInfo>   oneInstanceAttachments_;
+
+      MainDicomTagsAtLevel& GetMainDicomTagsAtLevel(ResourceType level);
+
+      const MainDicomTagsAtLevel& GetMainDicomTagsAtLevel(ResourceType level) const
+      {
+        return const_cast<Resource&>(*this).GetMainDicomTagsAtLevel(level);
+      }
+
+      ChildrenInformation& GetChildrenInformation(ResourceType level);
+
+      const ChildrenInformation& GetChildrenInformation(ResourceType level) const
+      {
+        return const_cast<Resource&>(*this).GetChildrenInformation(level);
+      }
+
+    public:
+      Resource(ResourceType level,
+               int64_t internalId,
+               const std::string& identifier) :
+        level_(level),
+        internalId_(internalId),
+        identifier_(identifier),
+        hasOneInstanceMetadataAndAttachments_(false)
+      {
+      }
+
+      ResourceType GetLevel() const
+      {
+        return level_;
+      }
+
+      int64_t GetInternalId() const
+      {
+        return internalId_;
+      }
+
+      const std::string& GetIdentifier() const
+      {
+        return identifier_;
+      }
+
+      void SetParentIdentifier(const std::string& id);
+
+      const std::string& GetParentIdentifier() const;
+
+      bool HasParentIdentifier() const;
+
+      void AddStringDicomTag(ResourceType level,
+                             uint16_t group,
+                             uint16_t element,
+                             const std::string& value)
+      {
+        GetMainDicomTagsAtLevel(level).AddStringDicomTag(group, element, value);
+      }
+
+      void AddNullDicomTag(ResourceType level,
+                           uint16_t group,
+                           uint16_t element)
+      {
+        GetMainDicomTagsAtLevel(level).AddNullDicomTag(group, element);
+      }
+
+      void GetMainDicomTags(DicomMap& target,
+                            ResourceType level) const
+      {
+        GetMainDicomTagsAtLevel(level).Export(target);
+      }
+
+      void GetAllMainDicomTags(DicomMap& target) const;
+
+      void AddMetadata(ResourceType level,
+                       MetadataType metadata,
+                       const std::string& value,
+                       int64_t revision);
+
+      std::map<MetadataType, MetadataContent>& GetMetadata(ResourceType level);
+
+      const std::map<MetadataType, MetadataContent>& GetMetadata(ResourceType level) const
+      {
+        return const_cast<Resource&>(*this).GetMetadata(level);
+      }
+
+      bool LookupMetadata(std::string& value,
+                          ResourceType level,
+                          MetadataType metadata) const;
+
+      bool LookupMetadata(std::string& value,
+                          int64_t& revision,
+                          ResourceType level,
+                          MetadataType metadata) const;
+
+      void AddChildIdentifier(ResourceType level,
+                              const std::string& childId)
+      {
+        GetChildrenInformation(level).AddIdentifier(childId);
+      }
+
+      const std::set<std::string>& GetChildrenIdentifiers(ResourceType level) const
+      {
+        return GetChildrenInformation(level).GetIdentifiers();
+      }
+
+      void SetChildrenCount(ResourceType level,
+                            uint64_t count)
+      {
+        GetChildrenInformation(level).SetCount(count);
+      }
+
+      void IncrementChildrenCount(ResourceType level,
+                                 uint64_t count)
+      {
+        GetChildrenInformation(level).IncrementCount(count);
+      }
+
+      uint64_t GetChildrenCount(ResourceType level) const
+      {
+        return GetChildrenInformation(level).GetCount();
+      }
+
+      void AddChildrenMetadataValue(ResourceType level,
+                                    MetadataType metadata,
+                                    const std::string& value)
+      {
+        GetChildrenInformation(level).AddMetadataValue(metadata, value);
+      }
+
+      void GetChildrenMetadataValues(std::set<std::string>& values,
+                                     ResourceType level,
+                                     MetadataType metadata) const
+      {
+        GetChildrenInformation(level).GetMetadataValues(values, metadata);
+      }
+
+      void AddChildrenMainDicomTagValue(ResourceType level,
+                                        const DicomTag& tag,
+                                        const std::string& value)
+      {
+        GetChildrenInformation(level).AddMainDicomTagValue(tag, value);
+      }
+
+      void GetChildrenMainDicomTagValues(std::set<std::string>& values,
+                                         ResourceType level,
+                                         const DicomTag& tag) const
+      {
+        GetChildrenInformation(level).GetMainDicomTagValues(values, tag);
+      }
+
+      void AddLabel(const std::string& label);
+
+      std::set<std::string>& GetLabels()
+      {
+        return labels_;
+      }
+
+      const std::set<std::string>& GetLabels() const
+      {
+        return labels_;
+      }
+
+      void AddAttachment(const FileInfo& attachment,
+                         int64_t revision);
+
+      bool LookupAttachment(FileInfo& target,
+                            int64_t& revision,
+                            FileContentType type) const;
+
+      const std::map<FileContentType, FileInfo>& GetAttachments() const
+      {
+        return attachments_;
+      }
+
+      void ListAttachments(std::set<FileContentType>& target) const;
+
+      void SetOneInstanceMetadataAndAttachments(const std::string& instancePublicId,
+                                                const std::map<MetadataType, std::string>& metadata,
+                                                const std::map<FileContentType, FileInfo>& attachments);
+
+      void SetOneInstancePublicId(const std::string& instancePublicId);
+
+      void AddOneInstanceMetadata(MetadataType metadata,
+                                  const std::string& value);
+
+      void AddOneInstanceAttachment(const FileInfo& attachment);
+
+      bool HasOneInstanceMetadataAndAttachments() const
+      {
+        return hasOneInstanceMetadataAndAttachments_;
+      }
+
+      const std::string& GetOneInstancePublicId() const;
+
+      const std::map<MetadataType, std::string>& GetOneInstanceMetadata() const;
+
+      const std::map<FileContentType, FileInfo>& GetOneInstanceAttachments() const;
+
+      void DebugExport(Json::Value& target,
+                       const FindRequest& request) const;
+    };
+
+  private:
+    typedef std::map<std::string, Resource*>  IdentifierIndex;
+    typedef std::map<int64_t, Resource*>      InternalIdIndex;
+
+    std::deque<Resource*>  items_;
+    IdentifierIndex        identifierIndex_;
+    InternalIdIndex        internalIdIndex_;
+
+  public:
+    ~FindResponse();
+
+    void Add(Resource* item /* takes ownership */);
+
+    size_t GetSize() const
+    {
+      return items_.size();
+    }
+
+    const Resource& GetResourceByIndex(size_t index) const;
+
+    Resource& GetResourceByIdentifier(const std::string& id);
+
+    Resource& GetResourceByInternalId(int64_t internalId);
+
+    const Resource& GetResourceByIdentifier(const std::string& id) const
+    {
+      return const_cast<FindResponse&>(*this).GetResourceByIdentifier(id);
+    }
+
+    const Resource& GetResourceByInternalId(int64_t internalId) const
+    {
+      return const_cast<FindResponse&>(*this).GetResourceByInternalId(internalId);
+    }
+
+    bool HasResource(const std::string& id) const
+    {
+      return (identifierIndex_.find(id) != identifierIndex_.end());
+    }
+
+    bool HasResource(int64_t& internalId) const
+    {
+      return (internalIdIndex_.find(internalId) != internalIdIndex_.end());
+    }
+  };
+}
--- a/OrthancServer/Sources/Database/IDatabaseListener.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/IDatabaseListener.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -29,6 +29,8 @@
 #include "../ExportedResource.h"
 #include "../Search/ISqlLookupFormatter.h"
 #include "../ServerIndexChange.h"
+#include "FindRequest.h"
+#include "FindResponse.h"
 #include "IDatabaseListener.h"
 
 #include <list>
@@ -37,7 +39,7 @@
 
 namespace Orthanc
 {
-  class DatabaseConstraint;
+  class DatabaseDicomTagConstraints;
   class ResourcesContent;
 
   class IDatabaseWrapper : public boost::noncopyable
@@ -52,6 +54,8 @@
       bool hasAtomicIncrementGlobalProperty_;
       bool hasUpdateAndGetStatistics_;
       bool hasMeasureLatency_;
+      bool hasFindSupport_;
+      bool hasExtendedChanges_;
 
     public:
       Capabilities() :
@@ -60,7 +64,9 @@
         hasLabelsSupport_(false),
         hasAtomicIncrementGlobalProperty_(false),
         hasUpdateAndGetStatistics_(false),
-        hasMeasureLatency_(false)
+        hasMeasureLatency_(false),
+        hasFindSupport_(false),
+        hasExtendedChanges_(false)
       {
       }
 
@@ -94,6 +100,16 @@
         return hasLabelsSupport_;
       }
 
+      void SetHasExtendedChanges(bool value)
+      {
+        hasExtendedChanges_ = value;
+      }
+
+      bool HasExtendedChanges() const
+      {
+        return hasExtendedChanges_;
+      }
+
       void SetAtomicIncrementGlobalProperty(bool value)
       {
         hasAtomicIncrementGlobalProperty_ = value;
@@ -104,7 +120,7 @@
         return hasAtomicIncrementGlobalProperty_;
       }
 
-      void SetUpdateAndGetStatistics(bool value)
+      void SetHasUpdateAndGetStatistics(bool value)
       {
         hasUpdateAndGetStatistics_ = value;
       }
@@ -123,6 +139,16 @@
       {
         return hasMeasureLatency_;
       }
+
+      void SetHasFindSupport(bool value)
+      {
+        hasFindSupport_ = value;
+      }
+
+      bool HasFindSupport() const
+      {
+        return hasFindSupport_;
+      }
     };
 
 
@@ -176,11 +202,6 @@
       virtual void GetAllPublicIds(std::list<std::string>& target,
                                    ResourceType resourceType) = 0;
 
-      virtual void GetAllPublicIds(std::list<std::string>& target,
-                                   ResourceType resourceType,
-                                   int64_t since,
-                                   uint32_t limit) = 0;
-
       virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
                               bool& done /*out*/,
                               int64_t since,
@@ -189,9 +210,6 @@
       virtual void GetChildrenInternalId(std::list<int64_t>& target,
                                          int64_t id) = 0;
 
-      virtual void GetChildrenPublicId(std::list<std::string>& target,
-                                       int64_t id) = 0;
-
       virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
                                         bool& done /*out*/,
                                         int64_t since,
@@ -280,13 +298,6 @@
     
       virtual bool IsDiskSizeAbove(uint64_t threshold) = 0;
     
-      virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
-                                        std::list<std::string>* instancesId, // Can be NULL if not needed
-                                        const std::vector<DatabaseConstraint>& lookup,
-                                        ResourceType queryLevel,
-                                        const std::set<std::string>& labels,
-                                        LabelsConstraint labelsConstraint,
-                                        uint32_t limit) = 0;
 
       // Returns "true" iff. the instance is new and has been inserted
       // into the database. If "false" is returned, the content of
@@ -315,16 +326,6 @@
 
 
       /**
-       * Primitives introduced in Orthanc 1.5.4
-       **/
-
-      virtual bool LookupResourceAndParent(int64_t& id,
-                                           ResourceType& type,
-                                           std::string& parentPublicId,
-                                           const std::string& publicId) = 0;
-
-
-      /**
        * Primitives introduced in Orthanc 1.12.0
        **/
 
@@ -334,10 +335,6 @@
       virtual void RemoveLabel(int64_t resource,
                                const std::string& label) = 0;
 
-      // List the labels of one single resource
-      virtual void ListLabels(std::set<std::string>& target,
-                              int64_t resource) = 0;
-
       // List all the labels that are present in any resource
       virtual void ListAllLabels(std::set<std::string>& target) = 0;
 
@@ -345,12 +342,101 @@
                                               int64_t increment,
                                               bool shared) = 0;
 
+      // New in Orthanc 1.12.3
       virtual void UpdateAndGetStatistics(int64_t& patientsCount,
                                           int64_t& studiesCount,
                                           int64_t& seriesCount,
                                           int64_t& instancesCount,
                                           int64_t& compressedSize,
                                           int64_t& uncompressedSize) = 0;
+
+      /**
+       * Primitives introduced in Orthanc 1.12.5
+       **/
+
+      // This is only implemented if "HasIntegratedFind()" is "true"
+      virtual void ExecuteCount(uint64_t& count,
+                                const FindRequest& request,
+                                const Capabilities& capabilities) = 0;
+
+      // This is only implemented if "HasIntegratedFind()" is "true"
+      virtual void ExecuteFind(FindResponse& response,
+                               const FindRequest& request,
+                               const Capabilities& capabilities) = 0;
+
+      // This is only implemented if "HasIntegratedFind()" is "false"
+      virtual void ExecuteFind(std::list<std::string>& identifiers,
+                               const Capabilities& capabilities,
+                               const FindRequest& request) = 0;
+
+      /**
+       * This is only implemented if "HasIntegratedFind()" is
+       * "false". In this flavor, the resource of interest might have
+       * been deleted, as the expansion is not done in the same
+       * transaction as the "ExecuteFind()". In such cases, the
+       * wrapper should not throw an exception, but simply ignore the
+       * request to expand the resource (i.e., "response" must not be
+       * modified).
+       **/
+      virtual void ExecuteExpand(FindResponse& response,
+                                 const Capabilities& capabilities,
+                                 const FindRequest& request,
+                                 const std::string& identifier) = 0;
+
+      // New in Orthanc 1.12.5
+      virtual void GetChangesExtended(std::list<ServerIndexChange>& target /*out*/,
+                                      bool& done /*out*/,
+                                      int64_t since,
+                                      int64_t to,
+                                      uint32_t limit,
+                                      const std::set<ChangeType>& filterType) = 0;
+    };
+
+
+    // TODO-FIND: Could this interface be removed?
+    class ICompatibilityTransaction : public boost::noncopyable
+    {
+    public:
+      virtual ~ICompatibilityTransaction()
+      {
+      }
+
+      virtual void GetAllPublicIdsCompatibility(std::list<std::string>& target,
+                                                ResourceType resourceType,
+                                                int64_t since,
+                                                uint32_t limit) = 0;
+
+      virtual void GetChildrenPublicId(std::list<std::string>& target,
+                                       int64_t id) = 0;
+
+      /**
+       * Primitives introduced in Orthanc 1.5.2
+       **/
+
+      virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
+                                        std::list<std::string>* instancesId, // Can be NULL if not needed
+                                        const DatabaseDicomTagConstraints& lookup,
+                                        ResourceType queryLevel,
+                                        const std::set<std::string>& labels,
+                                        LabelsConstraint labelsConstraint,
+                                        uint32_t limit) = 0;
+
+      /**
+       * Primitives introduced in Orthanc 1.5.4
+       **/
+
+      virtual bool LookupResourceAndParent(int64_t& id,
+                                           ResourceType& type,
+                                           std::string& parentPublicId,
+                                           const std::string& publicId) = 0;
+
+      /**
+       * Primitives introduced in Orthanc 1.12.0
+       **/
+
+      // List the labels of one single resource
+      virtual void ListLabels(std::set<std::string>& target,
+                              int64_t resource) = 0;
     };
 
 
@@ -375,5 +461,9 @@
     virtual const Capabilities GetDatabaseCapabilities() const = 0;
 
     virtual uint64_t MeasureLatency() = 0;
+
+    // Returns "true" iff. the database engine supports the
+    // simultaneous find and expansion of resources.
+    virtual bool HasIntegratedFind() const = 0;
   };
 }
--- a/OrthancServer/Sources/Database/InstallLabelsTable.sql	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/InstallLabelsTable.sql	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 -- 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-2024 Orthanc Team SRL, Belgium
--- Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+-- Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+-- Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/Database/InstallTrackAttachmentsSize.sql	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/InstallTrackAttachmentsSize.sql	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 -- 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-2024 Orthanc Team SRL, Belgium
--- Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+-- Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+-- Copyright (C) 2021-2025 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/MainDicomTagsRegistry.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,173 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "MainDicomTagsRegistry.h"
+
+#include "../ServerToolbox.h"
+
+namespace Orthanc
+{
+  void MainDicomTagsRegistry::LoadTags(ResourceType level)
+  {
+    {
+      const DicomTag* tags = NULL;
+      size_t size;
+
+      ServerToolbox::LoadIdentifiers(tags, size, level);
+
+      for (size_t i = 0; i < size; i++)
+      {
+        if (registry_.find(tags[i]) == registry_.end())
+        {
+          registry_[tags[i]] = TagInfo(level, DicomTagType_Identifier);
+        }
+        else
+        {
+          // These patient-level tags are copied in the study level
+          assert(level == ResourceType_Study &&
+                 (tags[i] == DICOM_TAG_PATIENT_ID ||
+                  tags[i] == DICOM_TAG_PATIENT_NAME ||
+                  tags[i] == DICOM_TAG_PATIENT_BIRTH_DATE));
+        }
+      }
+    }
+
+    {
+      std::set<DicomTag> tags;
+      DicomMap::GetMainDicomTags(tags, level);
+
+      for (std::set<DicomTag>::const_iterator
+             tag = tags.begin(); tag != tags.end(); ++tag)
+      {
+        if (registry_.find(*tag) == registry_.end())
+        {
+          registry_[*tag] = TagInfo(level, DicomTagType_Main);
+        }
+      }
+    }
+  }
+
+
+  MainDicomTagsRegistry::MainDicomTagsRegistry()
+  {
+    LoadTags(ResourceType_Patient);
+    LoadTags(ResourceType_Study);
+    LoadTags(ResourceType_Series);
+    LoadTags(ResourceType_Instance);
+  }
+
+
+  void MainDicomTagsRegistry::LookupTag(ResourceType& level,
+                                        DicomTagType& type,
+                                        const DicomTag& tag) const
+  {
+    Registry::const_iterator it = registry_.find(tag);
+
+    if (it == registry_.end())
+    {
+      // Default values
+      level = ResourceType_Instance;
+      type = DicomTagType_Generic;
+    }
+    else
+    {
+      level = it->second.GetLevel();
+      type = it->second.GetType();
+    }
+  }
+
+
+  bool MainDicomTagsRegistry::NormalizeLookup(bool& canBeFullyPerformedInDb,
+                                              DatabaseDicomTagConstraints& target,
+                                              const DatabaseLookup& source,
+                                              ResourceType queryLevel,
+                                              bool allowChildrenExistsQueries) const
+  {
+    bool isEquivalentLookup = true;
+    canBeFullyPerformedInDb = true;
+
+    target.Clear();
+
+    for (size_t i = 0; i < source.GetConstraintsCount(); i++)
+    {
+      ResourceType level;
+      DicomTagType type;
+      const DicomTag& tag = source.GetConstraint(i).GetTag();
+
+      LookupTag(level, type, tag);
+
+
+      if (type == DicomTagType_Identifier ||
+          type == DicomTagType_Main)
+      {
+        // Use the fact that patient-level tags are copied at the study level
+        if (level == ResourceType_Patient &&
+            queryLevel != ResourceType_Patient)
+        {
+          level = ResourceType_Study;
+        }
+
+        bool isEquivalentConstraint;
+        
+        // DicomIdentifiers are stored UPPERCASE -> as soon as a case senstive search happens, it is currently not possible to perform it in DB only
+        if (type == DicomTagType_Identifier && source.GetConstraint(i).IsCaseSensitive())
+        {
+          canBeFullyPerformedInDb = false;
+        }
+
+        target.AddConstraint(source.GetConstraint(i).ConvertToDatabaseConstraint(isEquivalentConstraint, level, type));
+
+        if (!isEquivalentConstraint)
+        {
+          isEquivalentLookup = false;
+        }
+      }
+      else
+      {
+        if (allowChildrenExistsQueries &&
+            tag == DICOM_TAG_MODALITIES_IN_STUDY && queryLevel == ResourceType_Study)
+        {
+          // transform the query into a children query
+          std::vector<std::string> values(source.GetConstraint(i).GetValues().begin(), source.GetConstraint(i).GetValues().end());
+
+          std::unique_ptr<DatabaseDicomTagConstraint> constraint(new DatabaseDicomTagConstraint(ResourceType_Series,
+                                                                 DICOM_TAG_MODALITY,
+                                                                 false,
+                                                                 ConstraintType_List,
+                                                                 values,
+                                                                 false,
+                                                                 true));
+          target.AddConstraint(constraint.release());
+        }
+        else
+        {
+          isEquivalentLookup = false;
+          canBeFullyPerformedInDb = false;
+        }
+      }
+    }
+
+    return isEquivalentLookup;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/MainDicomTagsRegistry.h	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,91 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Search/DatabaseLookup.h"
+#include "../Search/DatabaseDicomTagConstraints.h"
+
+#include <boost/noncopyable.hpp>
+
+
+namespace Orthanc
+{
+  class MainDicomTagsRegistry : public boost::noncopyable
+  {
+  private:
+    class TagInfo
+    {
+    private:
+      ResourceType  level_;
+      DicomTagType  type_;
+
+    public:
+      TagInfo()
+      {
+      }
+
+      TagInfo(ResourceType level,
+              DicomTagType type) :
+        level_(level),
+        type_(type)
+      {
+      }
+
+      ResourceType GetLevel() const
+      {
+        return level_;
+      }
+
+      DicomTagType GetType() const
+      {
+        return type_;
+      }
+    };
+
+    typedef std::map<DicomTag, TagInfo>   Registry;
+
+    Registry  registry_;
+
+    void LoadTags(ResourceType level);
+
+  public:
+    MainDicomTagsRegistry();
+
+    void LookupTag(ResourceType& level,
+                   DicomTagType& type,
+                   const DicomTag& tag) const;
+
+    /**
+     * Returns "true" iff. the normalized lookup is the same as the
+     * original DatabaseLookup. If "false" is returned, the target
+     * constraints are less strict than the original DatabaseLookup,
+     * so more resources will match them.
+     **/
+    bool NormalizeLookup(bool& canBeFullyPerformedInDb,
+                         DatabaseDicomTagConstraints& target,
+                         const DatabaseLookup& source,
+                         ResourceType queryLevel,
+                         bool allowChildrenExistsQueries) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/OrthancIdentifiers.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,248 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "FindRequest.h"
+
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+
+
+namespace Orthanc
+{
+  OrthancIdentifiers::OrthancIdentifiers(const OrthancIdentifiers& other)
+  {
+    if (other.HasPatientId())
+    {
+      SetPatientId(other.GetPatientId());
+    }
+
+    if (other.HasStudyId())
+    {
+      SetStudyId(other.GetStudyId());
+    }
+
+    if (other.HasSeriesId())
+    {
+      SetSeriesId(other.GetSeriesId());
+    }
+
+    if (other.HasInstanceId())
+    {
+      SetInstanceId(other.GetInstanceId());
+    }
+  }
+
+
+  void OrthancIdentifiers::SetPatientId(const std::string& id)
+  {
+    if (HasPatientId())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      patientId_.reset(new std::string(id));
+    }
+  }
+
+
+  const std::string& OrthancIdentifiers::GetPatientId() const
+  {
+    if (HasPatientId())
+    {
+      return *patientId_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void OrthancIdentifiers::SetStudyId(const std::string& id)
+  {
+    if (HasStudyId())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      studyId_.reset(new std::string(id));
+    }
+  }
+
+
+  const std::string& OrthancIdentifiers::GetStudyId() const
+  {
+    if (HasStudyId())
+    {
+      return *studyId_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void OrthancIdentifiers::SetSeriesId(const std::string& id)
+  {
+    if (HasSeriesId())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      seriesId_.reset(new std::string(id));
+    }
+  }
+
+
+  const std::string& OrthancIdentifiers::GetSeriesId() const
+  {
+    if (HasSeriesId())
+    {
+      return *seriesId_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void OrthancIdentifiers::SetInstanceId(const std::string& id)
+  {
+    if (HasInstanceId())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      instanceId_.reset(new std::string(id));
+    }
+  }
+
+
+  const std::string& OrthancIdentifiers::GetInstanceId() const
+  {
+    if (HasInstanceId())
+    {
+      return *instanceId_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  ResourceType OrthancIdentifiers::DetectLevel() const
+  {
+    if (HasPatientId() &&
+        !HasStudyId() &&
+        !HasSeriesId() &&
+        !HasInstanceId())
+    {
+      return ResourceType_Patient;
+    }
+    else if (// HasPatientId() &&
+             HasStudyId() &&
+             !HasSeriesId() &&
+             !HasInstanceId())
+    {
+      return ResourceType_Study;
+    }
+    else if (// HasPatientId() &&
+             // HasStudyId() &&
+             HasSeriesId() &&
+             !HasInstanceId())
+    {
+      return ResourceType_Series;
+    }
+    else if (// HasPatientId() &&
+             // HasStudyId() &&
+             // HasSeriesId() &&
+             HasInstanceId())
+    {
+      return ResourceType_Instance;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InexistentItem);
+    }
+  }
+
+
+  void OrthancIdentifiers::SetLevel(ResourceType level,
+                                    const std::string& id)
+  {
+    switch (level)
+    {
+      case ResourceType_Patient:
+        SetPatientId(id);
+        break;
+
+      case ResourceType_Study:
+        SetStudyId(id);
+        break;
+
+      case ResourceType_Series:
+        SetSeriesId(id);
+        break;
+
+      case ResourceType_Instance:
+        SetInstanceId(id);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  std::string OrthancIdentifiers::GetLevel(ResourceType level) const
+  {
+    switch (level)
+    {
+      case ResourceType_Patient:
+        return GetPatientId();
+
+      case ResourceType_Study:
+        return GetStudyId();
+
+      case ResourceType_Series:
+        return GetSeriesId();
+
+      case ResourceType_Instance:
+        return GetInstanceId();
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  bool OrthancIdentifiers::IsDefined() const
+  {
+    return HasPatientId() || HasStudyId() || HasSeriesId() || HasInstanceId();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/OrthancIdentifiers.h	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,95 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../../OrthancFramework/Sources/Compatibility.h"
+#include "../../../OrthancFramework/Sources/Enumerations.h"
+
+#include <boost/noncopyable.hpp>
+#include <string>
+
+
+namespace Orthanc
+{
+  class OrthancIdentifiers : public boost::noncopyable
+  {
+  private:
+    std::unique_ptr<std::string>  patientId_;
+    std::unique_ptr<std::string>  studyId_;
+    std::unique_ptr<std::string>  seriesId_;
+    std::unique_ptr<std::string>  instanceId_;
+
+  public:
+    OrthancIdentifiers()
+    {
+    }
+
+    OrthancIdentifiers(const OrthancIdentifiers& other);
+
+    void SetPatientId(const std::string& id);
+
+    bool HasPatientId() const
+    {
+      return patientId_.get() != NULL;
+    }
+
+    const std::string& GetPatientId() const;
+
+    void SetStudyId(const std::string& id);
+
+    bool HasStudyId() const
+    {
+      return studyId_.get() != NULL;
+    }
+
+    const std::string& GetStudyId() const;
+
+    void SetSeriesId(const std::string& id);
+
+    bool HasSeriesId() const
+    {
+      return seriesId_.get() != NULL;
+    }
+
+    const std::string& GetSeriesId() const;
+
+    void SetInstanceId(const std::string& id);
+
+    bool HasInstanceId() const
+    {
+      return instanceId_.get() != NULL;
+    }
+
+    const std::string& GetInstanceId() const;
+
+    ResourceType DetectLevel() const;
+
+    void SetLevel(ResourceType level,
+                  const std::string& id);
+
+    std::string GetLevel(ResourceType level) const;
+
+    bool IsDefined() const;
+  };
+}
--- a/OrthancServer/Sources/Database/PrepareDatabase.sql	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/PrepareDatabase.sql	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 -- 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-2024 Orthanc Team SRL, Belgium
--- Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+-- Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+-- Copyright (C) 2021-2025 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
@@ -40,6 +40,9 @@
        );
 
 -- The following table was added in Orthanc 0.8.5 (database v5)
+-- It contains only the DICOM Tags that are commonly used for searches.
+-- All these tags are converted to UPPERCASE !
+-- These tags are also stored in the MainDicomTags table without casing modificiation.
 CREATE TABLE DicomIdentifiers(
        id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
        tagGroup INTEGER,
--- a/OrthancServer/Sources/Database/ResourcesContent.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/ResourcesContent.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/Database/ResourcesContent.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/ResourcesContent.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -29,6 +29,7 @@
 #include "../../../OrthancFramework/Sources/SQLite/Transaction.h"
 #include "../Search/ISqlLookupFormatter.h"
 #include "../ServerToolbox.h"
+#include "Compatibility/GenericFind.h"
 #include "Compatibility/ICreateInstance.h"
 #include "Compatibility/IGetChildrenMetadata.h"
 #include "Compatibility/ILookupResourceAndParent.h"
@@ -42,6 +43,53 @@
 
 namespace Orthanc
 {  
+  static std::string JoinRequestedMetadata(const FindRequest::ChildrenSpecification& childrenSpec)
+  {
+    std::set<std::string> metadataTypes;
+    for (std::set<MetadataType>::const_iterator it = childrenSpec.GetMetadata().begin(); it != childrenSpec.GetMetadata().end(); ++it)
+    {
+      metadataTypes.insert(boost::lexical_cast<std::string>(*it));
+    }
+    std::string joinedMetadataTypes;
+    Orthanc::Toolbox::JoinStrings(joinedMetadataTypes, metadataTypes, ", ");
+
+    return joinedMetadataTypes;
+  }
+
+  static std::string JoinRequestedTags(const FindRequest::ChildrenSpecification& childrenSpec)
+  {
+    // note: SQLite does not seem to support (tagGroup, tagElement) in ((x, y), (z, w)) in complex subqueries.
+    // Therefore, since we expect the requested tag list to be short, we write it as 
+    // ((tagGroup = x AND tagElement = y ) OR (tagGroup = z AND tagElement = w))
+
+    std::string sql = " (";
+    std::set<std::string> tags;
+    for (std::set<DicomTag>::const_iterator it = childrenSpec.GetMainDicomTags().begin(); it != childrenSpec.GetMainDicomTags().end(); ++it)
+    {
+      tags.insert("(tagGroup = " + boost::lexical_cast<std::string>(it->GetGroup()) 
+                  + " AND tagElement = " + boost::lexical_cast<std::string>(it->GetElement()) + ")");
+    }
+    std::string joinedTags;
+    Orthanc::Toolbox::JoinStrings(joinedTags, tags, " OR ");
+
+    sql += joinedTags + ") ";
+    return sql;
+  }
+
+  static std::string JoinChanges(const std::set<ChangeType>& changeTypes)
+  {
+    std::set<std::string> changeTypesString;
+    for (std::set<ChangeType>::const_iterator it = changeTypes.begin(); it != changeTypes.end(); ++it)
+    {
+      changeTypesString.insert(boost::lexical_cast<std::string>(static_cast<uint32_t>(*it)));
+    }
+
+    std::string joinedChangesTypes;
+    Orthanc::Toolbox::JoinStrings(joinedChangesTypes, changeTypesString, ", ");
+
+    return joinedChangesTypes;
+  }
+
   class SQLiteDatabaseWrapper::LookupFormatter : public ISqlLookupFormatter
   {
   private:
@@ -64,6 +112,28 @@
       return "ESCAPE '\\'";
     }
 
+    virtual std::string FormatLimits(uint64_t since, uint64_t count) ORTHANC_OVERRIDE
+    {
+      std::string sql;
+
+      if (count > 0)
+      {
+        sql += " LIMIT " + boost::lexical_cast<std::string>(count);
+      }
+
+      if (since > 0)
+      {
+        if (count == 0)
+        {
+          sql += " LIMIT -1";  // In SQLite, "OFFSET" cannot appear without "LIMIT"
+        }
+
+        sql += " OFFSET " + boost::lexical_cast<std::string>(since);
+      }
+      
+      return sql;
+    }
+
     virtual bool IsEscapeBrackets() const ORTHANC_OVERRIDE
     {
       return false;
@@ -234,11 +304,12 @@
     void GetChangesInternal(std::list<ServerIndexChange>& target,
                             bool& done,
                             SQLite::Statement& s,
-                            uint32_t limit)
+                            uint32_t limit,
+                            bool returnFirstResults) // the statement usually returns limit+1 results while we only need the limit results -> we need to know which ones to return, the firsts or the lasts
     {
       target.clear();
 
-      while (target.size() < limit && s.Step())
+      while (s.Step())
       {
         int64_t seq = s.ColumnInt64(0);
         ChangeType changeType = static_cast<ChangeType>(s.ColumnInt(1));
@@ -251,7 +322,22 @@
         target.push_back(ServerIndexChange(seq, changeType, resourceType, publicId, date));
       }
 
-      done = !(target.size() == limit && s.Step());
+      done = target.size() <= limit;  // 'done' means "there are no more other changes of this type in that direction (depending on since/to)"
+      
+      // if we have retrieved more changes than requested -> cleanup
+      if (target.size() > limit)
+      {
+        assert(target.size() == limit+1); // the statement should only request 1 element more
+
+        if (returnFirstResults)
+        {
+          target.pop_back();
+        }
+        else
+        {
+          target.pop_front();
+        }
+      }
     }
 
 
@@ -340,7 +426,7 @@
 
     virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
                                       std::list<std::string>* instancesId,
-                                      const std::vector<DatabaseConstraint>& lookup,
+                                      const DatabaseDicomTagConstraints& lookup,
                                       ResourceType queryLevel,
                                       const std::set<std::string>& labels,
                                       LabelsConstraint labelsConstraint,
@@ -351,7 +437,7 @@
       std::string sql;
       LookupFormatter::Apply(sql, formatter, lookup, queryLevel, labels, labelsConstraint, limit);
 
-      sql = "CREATE TEMPORARY TABLE Lookup AS " + sql;
+      sql = "CREATE TEMPORARY TABLE Lookup AS " + sql;   // TODO-FIND: use a CTE (or is this method obsolete ?)
     
       {
         SQLite::Statement s(db_, SQLITE_FROM_HERE, "DROP TABLE IF EXISTS Lookup");
@@ -381,6 +467,753 @@
       }
     }
 
+#define C0_QUERY_ID 0
+#define C1_INTERNAL_ID 1
+#define C2_ROW_NUMBER 2
+#define C3_STRING_1 3
+#define C4_STRING_2 4
+#define C5_STRING_3 5
+#define C6_INT_1 6
+#define C7_INT_2 7
+#define C8_BIG_INT_1 8
+#define C9_BIG_INT_2 9
+
+#define QUERY_LOOKUP 1
+#define QUERY_MAIN_DICOM_TAGS 2
+#define QUERY_ATTACHMENTS 3
+#define QUERY_METADATA 4
+#define QUERY_LABELS 5
+#define QUERY_PARENT_MAIN_DICOM_TAGS 10
+#define QUERY_PARENT_IDENTIFIER 11
+#define QUERY_PARENT_METADATA 12
+#define QUERY_GRAND_PARENT_MAIN_DICOM_TAGS 15
+#define QUERY_GRAND_PARENT_METADATA 16
+#define QUERY_CHILDREN_IDENTIFIERS 20
+#define QUERY_CHILDREN_MAIN_DICOM_TAGS 21
+#define QUERY_CHILDREN_METADATA 22
+#define QUERY_CHILDREN_COUNT 23
+#define QUERY_GRAND_CHILDREN_IDENTIFIERS 30
+#define QUERY_GRAND_CHILDREN_MAIN_DICOM_TAGS 31
+#define QUERY_GRAND_CHILDREN_METADATA 32
+#define QUERY_GRAND_CHILDREN_COUNT 33
+#define QUERY_GRAND_GRAND_CHILDREN_IDENTIFIERS 40
+#define QUERY_GRAND_GRAND_CHILDREN_COUNT 41
+#define QUERY_ONE_INSTANCE_IDENTIFIER 50
+#define QUERY_ONE_INSTANCE_METADATA 51
+#define QUERY_ONE_INSTANCE_ATTACHMENTS 52
+
+#define STRINGIFY(x) #x
+#define TOSTRING(x) STRINGIFY(x)
+
+    virtual void ExecuteCount(uint64_t& count,
+                              const FindRequest& request,
+                              const Capabilities& capabilities) ORTHANC_OVERRIDE
+    {
+      LookupFormatter formatter;
+      std::string sql;
+
+      std::string lookupSql;
+      LookupFormatter::Apply(lookupSql, formatter, request);
+
+      // base query, retrieve the ordered internalId and publicId of the selected resources
+      sql = "WITH Lookup AS (" + lookupSql + ") SELECT COUNT(*) FROM Lookup";
+      SQLite::Statement s(db_, SQLITE_FROM_HERE_DYNAMIC(sql), sql);
+      formatter.Bind(s);
+
+      s.Step();
+      count = s.ColumnInt64(0);
+    }
+
+
+    virtual void ExecuteFind(FindResponse& response,
+                             const FindRequest& request,
+                             const Capabilities& capabilities) ORTHANC_OVERRIDE
+    {
+      LookupFormatter formatter;
+      std::string sql;
+      const ResourceType requestLevel = request.GetLevel();
+
+      std::string lookupSql;
+      LookupFormatter::Apply(lookupSql, formatter, request);
+
+      // base query, retrieve the ordered internalId and publicId of the selected resources
+      sql = "WITH Lookup AS (" + lookupSql + ") ";
+
+      // in SQLite, all CTEs must be created at the beginning of the query, you can not define local CTE inside subqueries
+      // need one instance info ? (part 1: create the CTE)
+      if (request.GetLevel() != ResourceType_Instance &&
+          request.IsRetrieveOneInstanceMetadataAndAttachments())
+      {
+        // Here, we create a nested CTE 'OneInstance' with one instance ID to join with metadata and main
+        sql += ", OneInstance AS";
+
+        switch (requestLevel)
+        {
+          case ResourceType_Series:
+          {
+            sql+= "  (SELECT Lookup.internalId AS parentInternalId, childLevel.publicId AS instancePublicId, childLevel.internalId AS instanceInternalId"
+                  "   FROM Resources AS childLevel "
+                  "   INNER JOIN Lookup ON childLevel.parentId = Lookup.internalId GROUP BY Lookup.internalId) ";
+            break;
+          }
+
+          case ResourceType_Study:
+          {
+            sql+= "  (SELECT Lookup.internalId AS parentInternalId, grandChildLevel.publicId AS instancePublicId, grandChildLevel.internalId AS instanceInternalId"
+                  "   FROM Resources AS grandChildLevel "
+                  "   INNER JOIN Resources childLevel ON grandChildLevel.parentId = childLevel.internalId "
+                  "   INNER JOIN Lookup ON childLevel.parentId = Lookup.internalId GROUP BY Lookup.internalId) ";
+            break;
+          }
+
+          case ResourceType_Patient:
+          {
+            sql+= "  (SELECT Lookup.internalId AS parentInternalId, grandGrandChildLevel.publicId AS instancePublicId, grandGrandChildLevel.internalId AS instanceInternalId"
+                  "   FROM Resources AS grandGrandChildLevel "
+                  "   INNER JOIN Resources grandChildLevel ON grandGrandChildLevel.parentId = grandChildLevel.internalId "
+                  "   INNER JOIN Resources childLevel ON grandChildLevel.parentId = childLevel.internalId "
+                  "   INNER JOIN Lookup ON childLevel.parentId = Lookup.internalId GROUP BY Lookup.internalId) ";
+            break;
+          }
+
+          default:
+            throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+
+      sql += "SELECT "
+             "  " TOSTRING(QUERY_LOOKUP) " AS c0_queryId, "
+             "  Lookup.internalId AS c1_internalId, "
+             "  Lookup.rowNumber AS c2_rowNumber, "
+             "  Lookup.publicId AS c3_string1, "
+             "  NULL AS c4_string2, "
+             "  NULL AS c5_string3, "
+             "  NULL AS c6_int1, "
+             "  NULL AS c7_int2, "
+             "  NULL AS c8_big_int1, "
+             "  NULL AS c9_big_int2 "
+             "  FROM Lookup ";
+
+      // need one instance info ? (part 2: execute the queries)
+      if (request.GetLevel() != ResourceType_Instance &&
+          request.IsRetrieveOneInstanceMetadataAndAttachments())
+      {
+        sql += "   UNION SELECT"
+               "    " TOSTRING(QUERY_ONE_INSTANCE_IDENTIFIER) " AS c0_queryId, "
+               "    parentInternalId AS c1_internalId, "
+               "    NULL AS c2_rowNumber, "
+               "    instancePublicId AS c3_string1, "
+               "    NULL AS c4_string2, "
+               "    NULL AS c5_string3, "
+               "    NULL AS c6_int1, "
+               "    NULL AS c7_int2, "
+               "    instanceInternalId AS c8_big_int1, "
+               "    NULL AS c9_big_int2 "
+               "   FROM OneInstance ";
+
+        sql += "   UNION SELECT"
+               "    " TOSTRING(QUERY_ONE_INSTANCE_METADATA) " AS c0_queryId, "
+               "    parentInternalId AS c1_internalId, "
+               "    NULL AS c2_rowNumber, "
+               "    Metadata.value AS c3_string1, "
+               "    NULL AS c4_string2, "
+               "    NULL AS c5_string3, "
+               "    Metadata.type AS c6_int1, "
+               "    NULL AS c7_int2, "
+               "    NULL AS c8_big_int1, "
+               "    NULL AS c9_big_int2 "
+               "   FROM OneInstance "
+               "   INNER JOIN Metadata ON Metadata.id = OneInstance.instanceInternalId ";
+              
+        sql += "   UNION SELECT"
+               "    " TOSTRING(QUERY_ONE_INSTANCE_ATTACHMENTS) " AS c0_queryId, "
+               "    parentInternalId AS c1_internalId, "
+               "    NULL AS c2_rowNumber, "
+               "    uuid AS c3_string1, "
+               "    uncompressedMD5 AS c4_string2, "
+               "    compressedMD5 AS c5_string3, "
+               "    fileType AS c6_int1, "
+               "    compressionType AS c7_int2, "
+               "    compressedSize AS c8_big_int1, "
+               "    uncompressedSize AS c9_big_int2 "
+               "   FROM OneInstance "
+               "   INNER JOIN AttachedFiles ON AttachedFiles.id = OneInstance.instanceInternalId ";
+
+      }
+
+      // need MainDicomTags from resource ?
+      if (request.IsRetrieveMainDicomTags())
+      {
+        sql += "UNION SELECT "
+               "  " TOSTRING(QUERY_MAIN_DICOM_TAGS) " AS c0_queryId, "
+               "  Lookup.internalId AS c1_internalId, "
+               "  NULL AS c2_rowNumber, "
+               "  value AS c3_string1, "
+               "  NULL AS c4_string2, "
+               "  NULL AS c5_string3, "
+               "  tagGroup AS c6_int1, "
+               "  tagElement AS c7_int2, "
+               "  NULL AS c8_big_int1, "
+               "  NULL AS c9_big_int2 "
+               "FROM Lookup "
+               "INNER JOIN MainDicomTags ON MainDicomTags.id = Lookup.internalId ";
+      }
+
+      // need resource metadata ?
+      if (request.IsRetrieveMetadata())
+      {
+        sql += "UNION SELECT "
+               "  " TOSTRING(QUERY_METADATA) " AS c0_queryId, "
+               "  Lookup.internalId AS c1_internalId, "
+               "  NULL AS c2_rowNumber, "
+               "  value AS c3_string1, "
+               "  NULL AS c4_string2, "
+               "  NULL AS c5_string3, "
+               "  type AS c6_int1, "
+               "  NULL AS c7_int2, "
+               "  NULL AS c8_big_int1, "
+               "  NULL AS c9_big_int2 "
+               "FROM Lookup "
+               "INNER JOIN Metadata ON Metadata.id = Lookup.internalId ";
+      }
+
+      // need resource attachments ?
+      if (request.IsRetrieveAttachments())
+      {
+        sql += "UNION SELECT "
+               "  " TOSTRING(QUERY_ATTACHMENTS) " AS c0_queryId, "
+               "  Lookup.internalId AS c1_internalId, "
+               "  NULL AS c2_rowNumber, "
+               "  uuid AS c3_string1, "
+               "  uncompressedMD5 AS c4_string2, "
+               "  compressedMD5 AS c5_string3, "
+               "  fileType AS c6_int1, "
+               "  compressionType AS c7_int2, "
+               "  compressedSize AS c8_big_int1, "
+               "  uncompressedSize AS c9_big_int2 "
+               "FROM Lookup "
+               "INNER JOIN AttachedFiles ON AttachedFiles.id = Lookup.internalId ";
+      }
+
+
+      // need resource labels ?
+      if (request.IsRetrieveLabels())
+      {
+        sql += "UNION SELECT "
+               "  " TOSTRING(QUERY_LABELS) " AS c0_queryId, "
+               "  Lookup.internalId AS c1_internalId, "
+               "  NULL AS c2_rowNumber, "
+               "  label AS c3_string1, "
+               "  NULL AS c4_string2, "
+               "  NULL AS c5_string3, "
+               "  NULL AS c6_int1, "
+               "  NULL AS c7_int2, "
+               "  NULL AS c8_big_int1, "
+               "  NULL AS c9_big_int2 "
+               "FROM Lookup "
+               "INNER JOIN Labels ON Labels.id = Lookup.internalId ";
+      }
+
+      if (requestLevel > ResourceType_Patient)
+      {
+        // need MainDicomTags from parent ?
+        if (request.GetParentSpecification(static_cast<ResourceType>(requestLevel - 1)).IsRetrieveMainDicomTags())
+        {
+          sql += "UNION SELECT "
+                 "  " TOSTRING(QUERY_PARENT_MAIN_DICOM_TAGS) " AS c0_queryId, "
+                 "  Lookup.internalId AS c1_internalId, "
+                 "  NULL AS c2_rowNumber, "
+                 "  value AS c3_string1, "
+                 "  NULL AS c4_string2, "
+                 "  NULL AS c5_string3, "
+                 "  tagGroup AS c6_int1, "
+                 "  tagElement AS c7_int2, "
+                 "  NULL AS c8_big_int1, "
+                 "  NULL AS c9_big_int2 "
+                 "FROM Lookup "
+                 "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
+                 "INNER JOIN MainDicomTags ON MainDicomTags.id = currentLevel.parentId ";
+        }
+
+        // need metadata from parent ?
+        if (request.GetParentSpecification(static_cast<ResourceType>(requestLevel - 1)).IsRetrieveMetadata())
+        {
+          sql += "UNION SELECT "
+                 "  " TOSTRING(QUERY_PARENT_METADATA) " AS c0_queryId, "
+                 "  Lookup.internalId AS c1_internalId, "
+                 "  NULL AS c2_rowNumber, "
+                 "  value AS c3_string1, "
+                 "  NULL AS c4_string2, "
+                 "  NULL AS c5_string3, "
+                 "  type AS c6_int1, "
+                 "  NULL AS c7_int2, "
+                 "  NULL AS c8_big_int1, "
+                 "  NULL AS c9_big_int2 "
+                 "FROM Lookup "
+                 "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
+                 "INNER JOIN Metadata ON Metadata.id = currentLevel.parentId ";        
+        }
+
+        if (requestLevel > ResourceType_Study)
+        {
+          // need MainDicomTags from grandparent ?
+          if (request.GetParentSpecification(static_cast<ResourceType>(requestLevel - 2)).IsRetrieveMainDicomTags())
+          {
+            sql += "UNION SELECT "
+                  "  " TOSTRING(QUERY_GRAND_PARENT_MAIN_DICOM_TAGS) " AS c0_queryId, "
+                  "  Lookup.internalId AS c1_internalId, "
+                  "  NULL AS c2_rowNumber, "
+                  "  value AS c3_string1, "
+                  "  NULL AS c4_string2, "
+                  "  NULL AS c5_string3, "
+                  "  tagGroup AS c6_int1, "
+                  "  tagElement AS c7_int2, "
+                  "  NULL AS c8_big_int1, "
+                  "  NULL AS c9_big_int2 "
+                  "FROM Lookup "
+                  "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
+                  "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId "
+                  "INNER JOIN MainDicomTags ON MainDicomTags.id = parentLevel.parentId ";
+          }
+
+          // need metadata from grandparent ?
+          if (request.GetParentSpecification(static_cast<ResourceType>(requestLevel - 2)).IsRetrieveMetadata())
+          {
+            sql += "UNION SELECT "
+                  "  " TOSTRING(QUERY_GRAND_PARENT_METADATA) " AS c0_queryId, "
+                  "  Lookup.internalId AS c1_internalId, "
+                  "  NULL AS c2_rowNumber, "
+                  "  value AS c3_string1, "
+                  "  NULL AS c4_string2, "
+                  "  NULL AS c5_string3, "
+                  "  type AS c6_int1, "
+                  "  NULL AS c7_int2, "
+                  "  NULL AS c8_big_int1, "
+                  "  NULL AS c9_big_int2 "
+                  "FROM Lookup "
+                  "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
+                  "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId "
+                  "INNER JOIN Metadata ON Metadata.id = parentLevel.parentId ";
+          }
+        }
+      }
+
+      // need MainDicomTags from children ?
+      if (requestLevel <= ResourceType_Series && request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 1)).GetMainDicomTags().size() > 0)
+      {
+        sql += "UNION SELECT "
+               "  " TOSTRING(QUERY_CHILDREN_MAIN_DICOM_TAGS) " AS c0_queryId, "
+               "  Lookup.internalId AS c1_internalId, "
+               "  NULL AS c2_rowNumber, "
+               "  value AS c3_string1, "
+               "  NULL AS c4_string2, "
+               "  NULL AS c5_string3, "
+               "  tagGroup AS c6_int1, "
+               "  tagElement AS c7_int2, "
+               "  NULL AS c8_big_int1, "
+               "  NULL AS c9_big_int2 "
+               "FROM Lookup "
+               "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
+               "  INNER JOIN MainDicomTags ON MainDicomTags.id = childLevel.internalId AND " + JoinRequestedTags(request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 1))); 
+      }
+
+      // need MainDicomTags from grandchildren ?
+      if (requestLevel <= ResourceType_Study && request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 2)).GetMainDicomTags().size() > 0)
+      {
+        sql += "UNION SELECT "
+                "  " TOSTRING(QUERY_GRAND_CHILDREN_MAIN_DICOM_TAGS) " AS c0_queryId, "
+                "  Lookup.internalId AS c1_internalId, "
+                "  NULL AS c2_rowNumber, "
+                "  value AS c3_string1, "
+                "  NULL AS c4_string2, "
+                "  NULL AS c5_string3, "
+                "  tagGroup AS c6_int1, "
+                "  tagElement AS c7_int2, "
+                "  NULL AS c8_big_int1, "
+                "  NULL AS c9_big_int2 "
+                "FROM Lookup "
+                "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
+                "  INNER JOIN Resources grandChildLevel ON grandChildLevel.parentId = childLevel.internalId "
+                "  INNER JOIN MainDicomTags ON MainDicomTags.id = grandChildLevel.internalId AND " + JoinRequestedTags(request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 2))); 
+      }
+
+      // need parent identifier ?
+      if (request.IsRetrieveParentIdentifier())
+      {
+        sql += "UNION SELECT "
+               "  " TOSTRING(QUERY_PARENT_IDENTIFIER) " AS c0_queryId, "
+               "  Lookup.internalId AS c1_internalId, "
+               "  NULL AS c2_rowNumber, "
+               "  parentLevel.publicId AS c3_string1, "
+               "  NULL AS c4_string2, "
+               "  NULL AS c5_string3, "
+               "  NULL AS c6_int1, "
+               "  NULL AS c7_int2, "
+               "  NULL AS c8_big_int1, "
+               "  NULL AS c9_big_int2 "
+               "FROM Lookup "
+               "  INNER JOIN Resources currentLevel ON currentLevel.internalId = Lookup.internalId "
+               "  INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId ";
+      }
+
+      // need children metadata ?
+      if (requestLevel <= ResourceType_Series && request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 1)).GetMetadata().size() > 0)
+      {
+        sql += "UNION SELECT "
+                "  " TOSTRING(QUERY_CHILDREN_METADATA) " AS c0_queryId, "
+                "  Lookup.internalId AS c1_internalId, "
+                "  NULL AS c2_rowNumber, "
+                "  value AS c3_string1, "
+                "  NULL AS c4_string2, "
+                "  NULL AS c5_string3, "
+                "  type AS c6_int1, "
+                "  NULL AS c7_int2, "
+                "  NULL AS c8_big_int1, "
+                "  NULL AS c9_big_int2 "
+                "FROM Lookup "
+                "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
+                "  INNER JOIN Metadata ON Metadata.id = childLevel.internalId AND Metadata.type IN (" + JoinRequestedMetadata(request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 1))) + ") ";
+      }
+
+      // need grandchildren metadata ?
+      if (requestLevel <= ResourceType_Study && request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 2)).GetMetadata().size() > 0)
+      {
+        sql += "UNION SELECT "
+                "  " TOSTRING(QUERY_GRAND_CHILDREN_METADATA) " AS c0_queryId, "
+                "  Lookup.internalId AS c1_internalId, "
+                "  NULL AS c2_rowNumber, "
+                "  value AS c3_string1, "
+                "  NULL AS c4_string2, "
+                "  NULL AS c5_string3, "
+                "  type AS c6_int1, "
+                "  NULL AS c7_int2, "
+                "  NULL AS c8_big_int1, "
+                "  NULL AS c9_big_int2 "
+                "FROM Lookup "
+                "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
+                "  INNER JOIN Resources grandChildLevel ON grandChildLevel.parentId = childLevel.internalId "
+                "  INNER JOIN Metadata ON Metadata.id = grandChildLevel.internalId AND Metadata.type IN (" + JoinRequestedMetadata(request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 2))) + ") ";
+      }
+
+      // need children identifiers ?
+      if ((requestLevel == ResourceType_Patient && request.GetChildrenSpecification(ResourceType_Study).IsRetrieveIdentifiers()) ||
+          (requestLevel == ResourceType_Study && request.GetChildrenSpecification(ResourceType_Series).IsRetrieveIdentifiers()) ||
+          (requestLevel == ResourceType_Series && request.GetChildrenSpecification(ResourceType_Instance).IsRetrieveIdentifiers()))
+      {
+        sql += "UNION SELECT "
+               "  " TOSTRING(QUERY_CHILDREN_IDENTIFIERS) " AS c0_queryId, "
+               "  Lookup.internalId AS c1_internalId, "
+               "  NULL AS c2_rowNumber, "
+               "  childLevel.publicId AS c3_string1, "
+               "  NULL AS c4_string2, "
+               "  NULL AS c5_string3, "
+               "  NULL AS c6_int1, "
+               "  NULL AS c7_int2, "
+               "  NULL AS c8_big_int1, "
+               "  NULL AS c9_big_int2 "
+               "FROM Lookup "
+               "  INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId ";
+      }
+      // no need to count if we have retrieved the list of identifiers
+      else if ((requestLevel == ResourceType_Patient && request.GetChildrenSpecification(ResourceType_Study).IsRetrieveCount()) ||
+          (requestLevel == ResourceType_Study && request.GetChildrenSpecification(ResourceType_Series).IsRetrieveCount()) ||
+          (requestLevel == ResourceType_Series && request.GetChildrenSpecification(ResourceType_Instance).IsRetrieveCount()))
+      {
+        sql += "UNION SELECT "
+               "  " TOSTRING(QUERY_CHILDREN_COUNT) " AS c0_queryId, "
+               "  Lookup.internalId AS c1_internalId, "
+               "  NULL AS c2_rowNumber, "
+               "  NULL AS c3_string1, "
+               "  NULL AS c4_string2, "
+               "  NULL AS c5_string3, "
+               "  COUNT(*) AS c6_int1, "
+               "  NULL AS c7_int2, "
+               "  NULL AS c8_big_int1, "
+               "  NULL AS c9_big_int2 "
+               "FROM Lookup "
+               "  INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId GROUP BY Lookup.internalId ";
+      }
+
+      // need grandchildren identifiers ?
+      if ((requestLevel == ResourceType_Patient && request.GetChildrenSpecification(ResourceType_Series).IsRetrieveIdentifiers()) ||
+          (requestLevel == ResourceType_Study && request.GetChildrenSpecification(ResourceType_Instance).IsRetrieveIdentifiers()))
+      {
+        sql += "UNION SELECT "
+              "  " TOSTRING(QUERY_GRAND_CHILDREN_IDENTIFIERS) " AS c0_queryId, "
+              "  Lookup.internalId AS c1_internalId, "
+              "  NULL AS c2_rowNumber, "
+              "  grandChildLevel.publicId AS c3_string1, "
+              "  NULL AS c4_string2, "
+              "  NULL AS c5_string3, "
+              "  NULL AS c6_int1, "
+              "  NULL AS c7_int2, "
+              "  NULL AS c8_big_int1, "
+              "  NULL AS c9_big_int2 "
+              "FROM Lookup "
+              "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "
+              "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId ";
+      }
+      // no need to count if we have retrieved the list of identifiers
+      else if ((requestLevel == ResourceType_Patient && request.GetChildrenSpecification(ResourceType_Series).IsRetrieveCount()) ||
+          (requestLevel == ResourceType_Study && request.GetChildrenSpecification(ResourceType_Instance).IsRetrieveCount()))
+      {
+        sql += "UNION SELECT "
+              "  " TOSTRING(QUERY_GRAND_CHILDREN_COUNT) " AS c0_queryId, "
+              "  Lookup.internalId AS c1_internalId, "
+              "  NULL AS c2_rowNumber, "
+              "  NULL AS c3_string1, "
+              "  NULL AS c4_string2, "
+              "  NULL AS c5_string3, "
+               "  COUNT(*) AS c6_int1, "
+              "  NULL AS c7_int2, "
+              "  NULL AS c8_big_int1, "
+              "  NULL AS c9_big_int2 "
+              "FROM Lookup "
+              "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "
+              "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId GROUP BY Lookup.internalId ";
+      }
+
+      // need grandgrandchildren identifiers ?
+      if (requestLevel == ResourceType_Patient && request.GetChildrenSpecification(ResourceType_Instance).IsRetrieveIdentifiers())
+      {
+        sql += "UNION SELECT "
+              "  " TOSTRING(QUERY_GRAND_GRAND_CHILDREN_IDENTIFIERS) " AS c0_queryId, "
+              "  Lookup.internalId AS c1_internalId, "
+              "  NULL AS c2_rowNumber, "
+              "  grandGrandChildLevel.publicId AS c3_string1, "
+              "  NULL AS c4_string2, "
+              "  NULL AS c5_string3, "
+              "  NULL AS c6_int1, "
+              "  NULL AS c7_int2, "
+              "  NULL AS c8_big_int1, "
+              "  NULL AS c9_big_int2 "
+              "FROM Lookup "
+              "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "
+              "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId "
+              "INNER JOIN Resources grandGrandChildLevel ON grandChildLevel.internalId = grandGrandChildLevel.parentId ";
+      }
+      // no need to count if we have retrieved the list of identifiers
+      else if (requestLevel == ResourceType_Patient && request.GetChildrenSpecification(ResourceType_Instance).IsRetrieveCount())
+      {
+        sql += "UNION SELECT "
+              "  " TOSTRING(QUERY_GRAND_GRAND_CHILDREN_COUNT) " AS c0_queryId, "
+              "  Lookup.internalId AS c1_internalId, "
+              "  NULL AS c2_rowNumber, "
+              "  NULL AS c3_string1, "
+              "  NULL AS c4_string2, "
+              "  NULL AS c5_string3, "
+               "  COUNT(*) AS c6_int1, "
+              "  NULL AS c7_int2, "
+              "  NULL AS c8_big_int1, "
+              "  NULL AS c9_big_int2 "
+              "FROM Lookup "
+              "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "
+              "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId "
+              "INNER JOIN Resources grandGrandChildLevel ON grandChildLevel.internalId = grandGrandChildLevel.parentId GROUP BY Lookup.internalId ";
+      }
+
+
+      sql += " ORDER BY c0_queryId, c2_rowNumber";  // this is really important to make sure that the Lookup query is the first one to provide results since we use it to create the responses element !
+
+      SQLite::Statement s(db_, SQLITE_FROM_HERE_DYNAMIC(sql), sql);
+      formatter.Bind(s);
+
+      while (s.Step())
+      {
+        int queryId = s.ColumnInt(C0_QUERY_ID);
+        int64_t internalId = s.ColumnInt64(C1_INTERNAL_ID);
+
+        // LOG(INFO) << queryId << ": " << internalId;
+        // continue;
+
+        assert(queryId == QUERY_LOOKUP || response.HasResource(internalId)); // the QUERY_LOOKUP must be read first and must create the response before any other query tries to populate the fields
+
+        switch (queryId)
+        {
+          case QUERY_LOOKUP:
+            response.Add(new FindResponse::Resource(requestLevel, internalId, s.ColumnString(C3_STRING_1)));
+            break;
+
+          case QUERY_LABELS:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddLabel(s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_ATTACHMENTS:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C6_INT_1)),
+                          s.ColumnInt64(C9_BIG_INT_2), s.ColumnString(C4_STRING_2),
+                          static_cast<CompressionType>(s.ColumnInt(C7_INT_2)),
+                          s.ColumnInt64(C8_BIG_INT_1), s.ColumnString(C5_STRING_3));
+            res.AddAttachment(file, 0 /* TODO - REVISIONS */);
+          }; break;
+
+          case QUERY_MAIN_DICOM_TAGS:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddStringDicomTag(requestLevel, 
+                                  static_cast<uint16_t>(s.ColumnInt(C6_INT_1)),
+                                  static_cast<uint16_t>(s.ColumnInt(C7_INT_2)),
+                                  s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_PARENT_MAIN_DICOM_TAGS:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddStringDicomTag(static_cast<ResourceType>(requestLevel - 1), 
+                                  static_cast<uint16_t>(s.ColumnInt(C6_INT_1)),
+                                  static_cast<uint16_t>(s.ColumnInt(C7_INT_2)),
+                                  s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_GRAND_PARENT_MAIN_DICOM_TAGS:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddStringDicomTag(static_cast<ResourceType>(requestLevel - 2), 
+                                  static_cast<uint16_t>(s.ColumnInt(C6_INT_1)),
+                                  static_cast<uint16_t>(s.ColumnInt(C7_INT_2)),
+                                  s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_CHILDREN_MAIN_DICOM_TAGS:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddChildrenMainDicomTagValue(static_cast<ResourceType>(requestLevel + 1), 
+                                             DicomTag(static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), static_cast<uint16_t>(s.ColumnInt(C7_INT_2))),
+                                             s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_GRAND_CHILDREN_MAIN_DICOM_TAGS:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddChildrenMainDicomTagValue(static_cast<ResourceType>(requestLevel + 2), 
+                                             DicomTag(static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), static_cast<uint16_t>(s.ColumnInt(C7_INT_2))),
+                                             s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_METADATA:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddMetadata(static_cast<ResourceType>(requestLevel), 
+                            static_cast<MetadataType>(s.ColumnInt(C6_INT_1)),
+                            s.ColumnString(C3_STRING_1), 0 /* no support for revision */);
+          }; break;
+
+          case QUERY_PARENT_METADATA:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddMetadata(static_cast<ResourceType>(requestLevel - 1), 
+                            static_cast<MetadataType>(s.ColumnInt(C6_INT_1)),
+                            s.ColumnString(C3_STRING_1), 0 /* no support for revision */);
+          }; break;
+
+          case QUERY_GRAND_PARENT_METADATA:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddMetadata(static_cast<ResourceType>(requestLevel - 2), 
+                            static_cast<MetadataType>(s.ColumnInt(C6_INT_1)),
+                            s.ColumnString(C3_STRING_1), 0 /* no support for revision */);
+          }; break;
+
+          case QUERY_CHILDREN_METADATA:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddChildrenMetadataValue(static_cast<ResourceType>(requestLevel + 1), 
+                                         static_cast<MetadataType>(s.ColumnInt(C6_INT_1)),
+                                         s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_GRAND_CHILDREN_METADATA:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddChildrenMetadataValue(static_cast<ResourceType>(requestLevel + 2), 
+                                         static_cast<MetadataType>(s.ColumnInt(C6_INT_1)),
+                                         s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_PARENT_IDENTIFIER:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.SetParentIdentifier(s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_CHILDREN_IDENTIFIERS:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddChildIdentifier(static_cast<ResourceType>(requestLevel + 1),
+                                   s.ColumnString(C3_STRING_1));
+            res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 1),
+                                 res.GetChildrenIdentifiers(static_cast<ResourceType>(requestLevel + 1)).size());
+          }; break;
+
+          case QUERY_GRAND_CHILDREN_IDENTIFIERS:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddChildIdentifier(static_cast<ResourceType>(requestLevel + 2),
+                                   s.ColumnString(C3_STRING_1));
+            res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 2),
+                                 res.GetChildrenIdentifiers(static_cast<ResourceType>(requestLevel + 2)).size());
+          }; break;
+
+          case QUERY_GRAND_GRAND_CHILDREN_IDENTIFIERS:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddChildIdentifier(static_cast<ResourceType>(requestLevel + 3),
+                                   s.ColumnString(C3_STRING_1));
+            res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 3),
+                                 res.GetChildrenIdentifiers(static_cast<ResourceType>(requestLevel + 3)).size());
+          }; break;
+
+          case QUERY_CHILDREN_COUNT:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 1),
+                                 static_cast<uint64_t>(s.ColumnInt64(C6_INT_1)));
+          }; break;
+
+          case QUERY_GRAND_CHILDREN_COUNT:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 2),
+                                 static_cast<uint64_t>(s.ColumnInt64(C6_INT_1)));
+          }; break;
+
+          case QUERY_GRAND_GRAND_CHILDREN_COUNT:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 3),
+                                 static_cast<uint64_t>(s.ColumnInt64(C6_INT_1)));
+          }; break;
+
+          case QUERY_ONE_INSTANCE_IDENTIFIER:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.SetOneInstancePublicId(s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_ONE_INSTANCE_METADATA:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddOneInstanceMetadata(static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_ONE_INSTANCE_ATTACHMENTS:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C6_INT_1)),
+                          s.ColumnInt64(C9_BIG_INT_2), s.ColumnString(C4_STRING_2),
+                          static_cast<CompressionType>(s.ColumnInt(C7_INT_2)),
+                          s.ColumnInt64(C8_BIG_INT_1), s.ColumnString(C5_STRING_3));
+            res.AddOneInstanceAttachment(file);
+          }; break;
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+        }
+      }
+    }
 
     // From the "ICreateInstance" interface
     virtual void AttachChild(int64_t parent,
@@ -510,22 +1343,16 @@
     }
 
 
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType,
-                                 int64_t since,
-                                 uint32_t limit) ORTHANC_OVERRIDE
+    virtual void GetAllPublicIdsCompatibility(std::list<std::string>& target,
+                                              ResourceType resourceType,
+                                              int64_t since,
+                                              uint32_t limit) ORTHANC_OVERRIDE
     {
-      if (limit == 0)
-      {
-        target.clear();
-        return;
-      }
-
       SQLite::Statement s(db_, SQLITE_FROM_HERE,
                           "SELECT publicId FROM Resources WHERE "
                           "resourceType=? LIMIT ? OFFSET ?");
       s.BindInt(0, resourceType);
-      s.BindInt64(1, limit);
+      s.BindInt64(1, limit == 0 ? -1 : limit);  // In SQLite, setting "LIMIT" to "-1" means "no limit"
       s.BindInt64(2, since);
 
       target.clear();
@@ -541,10 +1368,72 @@
                             int64_t since,
                             uint32_t limit) ORTHANC_OVERRIDE
     {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?");
-      s.BindInt64(0, since);
-      s.BindInt(1, limit + 1);
-      GetChangesInternal(target, done, s, limit);
+      std::set<ChangeType> filter;
+      GetChangesExtended(target, done, since, -1, limit, filter);
+    }
+
+    virtual void GetChangesExtended(std::list<ServerIndexChange>& target /*out*/,
+                                    bool& done /*out*/,
+                                    int64_t since,
+                                    int64_t to,
+                                    uint32_t limit,
+                                    const std::set<ChangeType>& filterType) ORTHANC_OVERRIDE
+    {
+      std::vector<std::string> filters;
+      bool hasSince = false;
+      bool hasTo = false;
+
+      if (since > 0)
+      {
+        hasSince = true;
+        filters.push_back("seq>?");
+      }
+      if (to != -1)
+      {
+        hasTo = true;
+        filters.push_back("seq<=?");
+      }
+      if (filterType.size() != 0)
+      {
+        filters.push_back("changeType IN ( " + JoinChanges(filterType) +  " )");
+      }
+
+      std::string filtersString;
+      if (filters.size() > 0)
+      {
+        Toolbox::JoinStrings(filtersString, filters, " AND ");
+        filtersString = "WHERE " + filtersString;
+      }
+
+      std::string sql;
+      bool returnFirstResults;
+      if (hasTo && !hasSince)
+      {
+        // in this case, we want the largest values in the LIMIT clause but we want them ordered in ascending order
+        sql = "SELECT * FROM (SELECT * FROM Changes " + filtersString + " ORDER BY seq DESC LIMIT ?) ORDER BY seq ASC";
+        returnFirstResults = false;
+      }
+      else
+      {
+        // default query: we want the smallest values ordered in ascending order
+        sql = "SELECT * FROM Changes " + filtersString + " ORDER BY seq ASC LIMIT ?";
+        returnFirstResults = true;
+      }
+       
+      SQLite::Statement s(db_, SQLITE_FROM_HERE_DYNAMIC(sql), sql);
+
+      int paramCounter = 0;
+      if (hasSince)
+      {
+        s.BindInt64(paramCounter++, since);
+      }
+      if (hasTo)
+      {
+        s.BindInt64(paramCounter++, to);
+      }
+
+      s.BindInt(paramCounter++, limit + 1); // we take limit+1 because we use the +1 to know if "Done" must be set to true
+      GetChangesInternal(target, done, s, limit, returnFirstResults);
     }
 
 
@@ -605,7 +1494,7 @@
     {
       bool done;  // Ignored
       SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1");
-      GetChangesInternal(target, done, s, 1);
+      GetChangesInternal(target, done, s, 1, true);
     }
 
 
@@ -1328,6 +2217,8 @@
     // TODO: implement revisions in SQLite
     dbCapabilities_.SetFlushToDisk(true);
     dbCapabilities_.SetLabelsSupport(true);
+    dbCapabilities_.SetHasExtendedChanges(true);
+    dbCapabilities_.SetHasFindSupport(HasIntegratedFind());
     db_.Open(path);
   }
 
@@ -1340,6 +2231,8 @@
     // TODO: implement revisions in SQLite
     dbCapabilities_.SetFlushToDisk(true);
     dbCapabilities_.SetLabelsSupport(true);
+    dbCapabilities_.SetHasExtendedChanges(true);
+    dbCapabilities_.SetHasFindSupport(HasIntegratedFind());
     db_.OpenInMemory();
   }
 
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -23,7 +23,7 @@
 
 #pragma once
 
-#include "BaseDatabaseWrapper.h"
+#include "BaseCompatibilityTransaction.h"
 
 #include "../../../OrthancFramework/Sources/SQLite/Connection.h"
 
@@ -36,7 +36,7 @@
    * translates low-level requests into SQL statements. Mutual
    * exclusion MUST be implemented at a higher level.
    **/
-  class SQLiteDatabaseWrapper : public BaseDatabaseWrapper
+  class SQLiteDatabaseWrapper : public IDatabaseWrapper
   {
   private:
     class TransactionBase;
@@ -57,7 +57,8 @@
     void GetChangesInternal(std::list<ServerIndexChange>& target,
                             bool& done,
                             SQLite::Statement& s,
-                            uint32_t maxResults);
+                            uint32_t maxResults,
+                            bool returnFirstResults);
 
     void GetExportedResourcesInternal(std::list<ExportedResource>& target,
                                       bool& done,
@@ -99,13 +100,19 @@
       throw OrthancException(ErrorCode_NotImplemented);
     }
 
+    virtual bool HasIntegratedFind() const ORTHANC_OVERRIDE
+    {
+      return true;   // => This uses specialized SQL commands
+      //return false;   // => This uses Compatibility/GenericFind
+    }
+
     /**
      * The "StartTransaction()" method is guaranteed to return a class
      * derived from "UnitTestsTransaction". The methods of
      * "UnitTestsTransaction" give access to additional information
      * about the underlying SQLite database to be used in unit tests.
      **/
-    class UnitTestsTransaction : public BaseDatabaseWrapper::BaseTransaction
+    class UnitTestsTransaction : public BaseCompatibilityTransaction
     {
     protected:
       SQLite::Connection& db_;
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -281,130 +281,11 @@
     }
     
     target["Last"] = static_cast<int>(last);
-  }
-
-
-  static void CopyListToVector(std::vector<std::string>& target,
-                               const std::list<std::string>& source)
-  {
-    target.resize(source.size());
-
-    size_t pos = 0;
-    
-    for (std::list<std::string>::const_iterator
-           it = source.begin(); it != source.end(); ++it)
-    {
-      target[pos] = *it;
-      pos ++;
-    }      
-  }
-
-
-  class StatelessDatabaseOperations::MainDicomTagsRegistry : public boost::noncopyable
-  {
-  private:
-    class TagInfo
-    {
-    private:
-      ResourceType  level_;
-      DicomTagType  type_;
-
-    public:
-      TagInfo()
-      {
-      }
-
-      TagInfo(ResourceType level,
-              DicomTagType type) :
-        level_(level),
-        type_(type)
-      {
-      }
-
-      ResourceType GetLevel() const
-      {
-        return level_;
-      }
-
-      DicomTagType GetType() const
-      {
-        return type_;
-      }
-    };
-      
-    typedef std::map<DicomTag, TagInfo>   Registry;
-
-
-    Registry  registry_;
-      
-    void LoadTags(ResourceType level)
+    if (!log.empty())
     {
-      {
-        const DicomTag* tags = NULL;
-        size_t size;
-  
-        ServerToolbox::LoadIdentifiers(tags, size, level);
-  
-        for (size_t i = 0; i < size; i++)
-        {
-          if (registry_.find(tags[i]) == registry_.end())
-          {
-            registry_[tags[i]] = TagInfo(level, DicomTagType_Identifier);
-          }
-          else
-          {
-            // These patient-level tags are copied in the study level
-            assert(level == ResourceType_Study &&
-                   (tags[i] == DICOM_TAG_PATIENT_ID ||
-                    tags[i] == DICOM_TAG_PATIENT_NAME ||
-                    tags[i] == DICOM_TAG_PATIENT_BIRTH_DATE));
-          }
-        }
-      }
-
-      {
-        std::set<DicomTag> tags;
-        DicomMap::GetMainDicomTags(tags, level);
-
-        for (std::set<DicomTag>::const_iterator
-               tag = tags.begin(); tag != tags.end(); ++tag)
-        {
-          if (registry_.find(*tag) == registry_.end())
-          {
-            registry_[*tag] = TagInfo(level, DicomTagType_Main);
-          }
-        }
-      }
+      target["First"] = static_cast<int>(log.front().GetSeq());
     }
-
-  public:
-    MainDicomTagsRegistry()
-    {
-      LoadTags(ResourceType_Patient);
-      LoadTags(ResourceType_Study);
-      LoadTags(ResourceType_Series);
-      LoadTags(ResourceType_Instance); 
-    }
-
-    void LookupTag(ResourceType& level,
-                   DicomTagType& type,
-                   const DicomTag& tag) const
-    {
-      Registry::const_iterator it = registry_.find(tag);
-
-      if (it == registry_.end())
-      {
-        // Default values
-        level = ResourceType_Instance;
-        type = DicomTagType_Generic;
-      }
-      else
-      {
-        level = it->second.GetLevel();
-        type = it->second.GetType();
-      }
-    }
-  };
+  }
 
 
   void StatelessDatabaseOperations::ReadWriteTransaction::LogChange(int64_t internalId,
@@ -471,38 +352,6 @@
   }
 
 
-  void StatelessDatabaseOperations::NormalizeLookup(std::vector<DatabaseConstraint>& target,
-                                                    const DatabaseLookup& source,
-                                                    ResourceType queryLevel) const
-  {
-    assert(mainDicomTagsRegistry_.get() != NULL);
-
-    target.clear();
-    target.reserve(source.GetConstraintsCount());
-
-    for (size_t i = 0; i < source.GetConstraintsCount(); i++)
-    {
-      ResourceType level;
-      DicomTagType type;
-      
-      mainDicomTagsRegistry_->LookupTag(level, type, source.GetConstraint(i).GetTag());
-
-      if (type == DicomTagType_Identifier ||
-          type == DicomTagType_Main)
-      {
-        // Use the fact that patient-level tags are copied at the study level
-        if (level == ResourceType_Patient &&
-            queryLevel != ResourceType_Patient)
-        {
-          level = ResourceType_Study;
-        }
-        
-        target.push_back(source.GetConstraint(i).ConvertToDatabaseConstraint(level, type));
-      }
-    }
-  }
-
-
   class StatelessDatabaseOperations::Transaction : public boost::noncopyable
   {
   private:
@@ -616,6 +465,10 @@
         else
         {
           assert(writeOperations != NULL);
+          if (readOnly_)
+          {
+            throw OrthancException(ErrorCode_ReadOnly, "The DB is trying to execute a ReadWrite transaction while Orthanc has been started in ReadOnly mode.");
+          }
           
           Transaction transaction(db_, *factory_, TransactionType_ReadWrite);
           {
@@ -653,10 +506,11 @@
   }
 
   
-  StatelessDatabaseOperations::StatelessDatabaseOperations(IDatabaseWrapper& db) : 
+  StatelessDatabaseOperations::StatelessDatabaseOperations(IDatabaseWrapper& db, bool readOnly) : 
     db_(db),
     mainDicomTagsRegistry_(new MainDicomTagsRegistry),
-    maxRetries_(0)
+    maxRetries_(0),
+    readOnly_(readOnly)
   {
   }
 
@@ -710,284 +564,32 @@
   {
     ApplyInternal(NULL, &operations);
   }
-  
-
-  bool StatelessDatabaseOperations::ExpandResource(ExpandedResource& target,
-                                                   const std::string& publicId,
-                                                   ResourceType level,
-                                                   const std::set<DicomTag>& requestedTags,
-                                                   ExpandResourceFlags expandFlags)
-  {    
-    class Operations : public ReadOnlyOperationsT6<
-      bool&, ExpandedResource&, const std::string&, ResourceType, const std::set<DicomTag>&, ExpandResourceFlags>
+
+
+  const FindResponse::Resource& StatelessDatabaseOperations::ExecuteSingleResource(FindResponse& response,
+                                                                                   const FindRequest& request)
+  {
+    ExecuteFind(response, request);
+
+    if (response.GetSize() == 0)
     {
-    private:
-      bool hasLabelsSupport_;
-
-      static bool LookupStringMetadata(std::string& result,
-                                       const std::map<MetadataType, std::string>& metadata,
-                                       MetadataType type)
-      {
-        std::map<MetadataType, std::string>::const_iterator found = metadata.find(type);
-
-        if (found == metadata.end())
-        {
-          return false;
-        }
-        else
-        {
-          result = found->second;
-          return true;
-        }
-      }
-
-
-      static bool LookupIntegerMetadata(int64_t& result,
-                                        const std::map<MetadataType, std::string>& metadata,
-                                        MetadataType type)
-      {
-        std::string s;
-        if (!LookupStringMetadata(s, metadata, type))
-        {
-          return false;
-        }
-
-        try
-        {
-          result = boost::lexical_cast<int64_t>(s);
-          return true;
-        }
-        catch (boost::bad_lexical_cast&)
-        {
-          return false;
-        }
-      }
-
-
-    public:
-      explicit Operations(bool hasLabelsSupport) :
-        hasLabelsSupport_(hasLabelsSupport)
-      {
-      }
-
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+    else if (response.GetSize() == 1)
+    {
+      if (response.GetResourceByIndex(0).GetLevel() != request.GetLevel())
       {
-        // Lookup for the requested resource
-        int64_t internalId;
-        ResourceType type;
-        std::string parent;
-        if (!transaction.LookupResourceAndParent(internalId, type, parent, tuple.get<2>()) ||
-            type != tuple.get<3>())
-        {
-          tuple.get<0>() = false;
-        }
-        else
-        {
-          ExpandedResource& target = tuple.get<1>();
-          ExpandResourceFlags expandFlags = tuple.get<5>();
-
-          // Set information about the parent resource (if it exists)
-          if (type == ResourceType_Patient)
-          {
-            if (!parent.empty())
-            {
-              throw OrthancException(ErrorCode_DatabasePlugin);
-            }
-          }
-          else
-          {
-            if (parent.empty())
-            {
-              throw OrthancException(ErrorCode_DatabasePlugin);
-            }
-
-            target.parentId_ = parent;
-          }
-
-          target.SetResource(type, tuple.get<2>());
-
-          if (expandFlags & ExpandResourceFlags_IncludeChildren)
-          {
-            // List the children resources
-            transaction.GetChildrenPublicId(target.childrenIds_, internalId);
-          }
-
-          if (expandFlags & ExpandResourceFlags_IncludeMetadata)
-          {
-            // Extract the metadata
-            transaction.GetAllMetadata(target.metadata_, internalId);
-
-            switch (type)
-            {
-              case ResourceType_Patient:
-              case ResourceType_Study:
-                break;
-
-              case ResourceType_Series:
-              {
-                int64_t i;
-                if (LookupIntegerMetadata(i, target.metadata_, MetadataType_Series_ExpectedNumberOfInstances))
-                {
-                  target.expectedNumberOfInstances_ = static_cast<int>(i);
-                  target.status_ = EnumerationToString(transaction.GetSeriesStatus(internalId, i));
-                }
-                else
-                {
-                  target.expectedNumberOfInstances_ = -1;
-                  target.status_ = EnumerationToString(SeriesStatus_Unknown);
-                }
-
-                break;
-              }
-
-              case ResourceType_Instance:
-              {
-                FileInfo attachment;
-                int64_t revision;  // ignored
-                if (!transaction.LookupAttachment(attachment, revision, internalId, FileContentType_Dicom))
-                {
-                  throw OrthancException(ErrorCode_InternalError);
-                }
-
-                target.fileSize_ = static_cast<unsigned int>(attachment.GetUncompressedSize());
-                target.fileUuid_ = attachment.GetUuid();
-
-                int64_t i;
-                if (LookupIntegerMetadata(i, target.metadata_, MetadataType_Instance_IndexInSeries))
-                {
-                  target.indexInSeries_ = static_cast<int>(i);
-                }
-                else
-                {
-                  target.indexInSeries_ = -1;
-                }
-
-                break;
-              }
-
-              default:
-                throw OrthancException(ErrorCode_InternalError);
-            }
-
-            // check the main dicom tags list has not changed since the resource was stored
-            target.mainDicomTagsSignature_ = DicomMap::GetDefaultMainDicomTagsSignature(type);
-            LookupStringMetadata(target.mainDicomTagsSignature_, target.metadata_, MetadataType_MainDicomTagsSignature);
-          }
-
-          if (expandFlags & ExpandResourceFlags_IncludeMainDicomTags)
-          {
-            // read all tags from DB
-            transaction.GetMainDicomTags(target.GetMainDicomTags(), internalId);
-
-            // read all main sequences from DB
-            std::string serializedSequences;
-            if (LookupStringMetadata(serializedSequences, target.metadata_, MetadataType_MainDicomSequences))
-            {
-              Json::Value jsonMetadata;
-              Toolbox::ReadJson(jsonMetadata, serializedSequences);
-
-              assert(jsonMetadata["Version"].asInt() == 1);
-              target.GetMainDicomTags().FromDicomAsJson(jsonMetadata["Sequences"], true /* append */, true /* parseSequences */);
-            }
-
-            // check if we have access to all requestedTags or if we must get tags from parents
-            const std::set<DicomTag>& requestedTags = tuple.get<4>();
-
-            if (requestedTags.size() > 0)
-            {
-              std::set<DicomTag> savedMainDicomTags;
-              
-              FromDcmtkBridge::ParseListOfTags(savedMainDicomTags, target.mainDicomTagsSignature_);
-
-              // read parent main dicom tags as long as we have not gathered all requested tags
-              ResourceType currentLevel = target.GetLevel();
-              int64_t currentInternalId = internalId;
-              Toolbox::GetMissingsFromSet(target.missingRequestedTags_, requestedTags, savedMainDicomTags);
-
-              while ((target.missingRequestedTags_.size() > 0)
-                    && currentLevel != ResourceType_Patient)
-              {
-                currentLevel = GetParentResourceType(currentLevel);
-
-                int64_t currentParentId;
-                if (!transaction.LookupParent(currentParentId, currentInternalId))
-                {
-                  break;
-                }
-
-                std::map<MetadataType, std::string> parentMetadata;
-                transaction.GetAllMetadata(parentMetadata, currentParentId);
-
-                std::string parentMainDicomTagsSignature = DicomMap::GetDefaultMainDicomTagsSignature(currentLevel);
-                LookupStringMetadata(parentMainDicomTagsSignature, parentMetadata, MetadataType_MainDicomTagsSignature);
-
-                std::set<DicomTag> parentSavedMainDicomTags;
-                FromDcmtkBridge::ParseListOfTags(parentSavedMainDicomTags, parentMainDicomTagsSignature);
-                
-                size_t previousMissingCount = target.missingRequestedTags_.size();
-                Toolbox::AppendSets(savedMainDicomTags, parentSavedMainDicomTags);
-                Toolbox::GetMissingsFromSet(target.missingRequestedTags_, requestedTags, savedMainDicomTags);
-
-                // read the parent tags from DB only if it reduces the number of missing tags
-                if (target.missingRequestedTags_.size() < previousMissingCount)
-                { 
-                  Toolbox::AppendSets(savedMainDicomTags, parentSavedMainDicomTags);
-
-                  DicomMap parentTags;
-                  transaction.GetMainDicomTags(parentTags, currentParentId);
-
-                  target.GetMainDicomTags().Merge(parentTags);
-                }
-
-                currentInternalId = currentParentId;
-              }
-            }
-          }
-
-          if ((expandFlags & ExpandResourceFlags_IncludeLabels) &&
-              hasLabelsSupport_)
-          {
-            transaction.ListLabels(target.labels_, internalId);
-          }
-
-          std::string tmp;
-
-          if (LookupStringMetadata(tmp, target.metadata_, MetadataType_AnonymizedFrom))
-          {
-            target.anonymizedFrom_ = tmp;
-          }
-
-          if (LookupStringMetadata(tmp, target.metadata_, MetadataType_ModifiedFrom))
-          {
-            target.modifiedFrom_ = tmp;
-          }
-
-          if (type == ResourceType_Patient ||
-              type == ResourceType_Study ||
-              type == ResourceType_Series)
-          {
-            target.isStable_ = !transaction.GetTransactionContext().IsUnstableResource(type, internalId);
-
-            if (LookupStringMetadata(tmp, target.metadata_, MetadataType_LastUpdate))
-            {
-              target.lastUpdate_ = tmp;
-            }
-          }
-          else
-          {
-            target.isStable_ = false;
-          }
-
-          tuple.get<0>() = true;
-        }
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+      else
+      {
+        return response.GetResourceByIndex(0);
       }
-    };
-
-    bool found;
-    Operations operations(db_.GetDatabaseCapabilities().HasLabelsSupport());
-    operations.Apply(*this, found, target, publicId, level, requestedTags, expandFlags);
-    return found;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin);
+    }
   }
 
 
@@ -995,110 +597,50 @@
                                                    const std::string& publicId,
                                                    ResourceType level)
   {
-    class Operations : public ReadOnlyOperationsT3<std::map<MetadataType, std::string>&, const std::string&, ResourceType>
+    FindRequest request(level);
+    request.SetOrthancId(level, publicId);
+    request.SetRetrieveMetadata(true);
+
+    FindResponse response;
+    std::map<MetadataType, FindResponse::MetadataContent> metadata = ExecuteSingleResource(response, request).GetMetadata(level);
+
+    target.clear();
+    for (std::map<MetadataType, FindResponse::MetadataContent>::const_iterator
+         it = metadata.begin(); it != metadata.end(); ++it)
     {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        ResourceType type;
-        int64_t id;
-        if (!transaction.LookupResource(id, type, tuple.get<1>()) ||
-            tuple.get<2>() != type)
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-        else
-        {
-          transaction.GetAllMetadata(tuple.get<0>(), id);
-        }
-      }
-    };
-
-    Operations operations;
-    operations.Apply(*this, target, publicId, level);
+      target[it->first] = it->second.GetValue();
+    }
   }
 
 
   bool StatelessDatabaseOperations::LookupAttachment(FileInfo& attachment,
                                                      int64_t& revision,
-                                                     const std::string& instancePublicId,
+                                                     ResourceType level,
+                                                     const std::string& publicId,
                                                      FileContentType contentType)
   {
-    class Operations : public ReadOnlyOperationsT5<bool&, FileInfo&, int64_t&, const std::string&, FileContentType>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        int64_t internalId;
-        ResourceType type;
-        if (!transaction.LookupResource(internalId, type, tuple.get<3>()))
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-        else if (transaction.LookupAttachment(tuple.get<1>(), tuple.get<2>(), internalId, tuple.get<4>()))
-        {
-          assert(tuple.get<1>().GetContentType() == tuple.get<4>());
-          tuple.get<0>() = true;
-        }
-        else
-        {
-          tuple.get<0>() = false;
-        }
-      }
-    };
-
-    bool found;
-    Operations operations;
-    operations.Apply(*this, found, attachment, revision, instancePublicId, contentType);
-    return found;
+    FindRequest request(level);
+    request.SetOrthancId(level, publicId);
+    request.SetRetrieveAttachments(true);
+
+    FindResponse response;
+    return ExecuteSingleResource(response, request).LookupAttachment(attachment, revision, contentType);
   }
 
 
   void StatelessDatabaseOperations::GetAllUuids(std::list<std::string>& target,
                                                 ResourceType resourceType)
   {
-    class Operations : public ReadOnlyOperationsT2<std::list<std::string>&, ResourceType>
+    // This method is tested by "orthanc-tests/Plugins/WebDav/Run.py"
+    FindRequest request(resourceType);
+
+    FindResponse response;
+    ExecuteFind(response, request);
+
+    target.clear();
+    for (size_t i = 0; i < response.GetSize(); i++)
     {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        // TODO - CANDIDATE FOR "TransactionType_Implicit"
-        transaction.GetAllPublicIds(tuple.get<0>(), tuple.get<1>());
-      }
-    };
-
-    Operations operations;
-    operations.Apply(*this, target, resourceType);
-  }
-
-
-  void StatelessDatabaseOperations::GetAllUuids(std::list<std::string>& target,
-                                                ResourceType resourceType,
-                                                size_t since,
-                                                uint32_t limit)
-  {
-    if (limit == 0)
-    {
-      target.clear();
-    }
-    else
-    {
-      class Operations : public ReadOnlyOperationsT4<std::list<std::string>&, ResourceType, size_t, size_t>
-      {
-      public:
-        virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                                const Tuple& tuple) ORTHANC_OVERRIDE
-        {
-          // TODO - CANDIDATE FOR "TransactionType_Implicit"
-          transaction.GetAllPublicIds(tuple.get<0>(), tuple.get<1>(), tuple.get<2>(), tuple.get<3>());
-        }
-      };
-
-      Operations operations;
-      operations.Apply(*this, target, resourceType, since, limit);
+      target.push_back(response.GetResourceByIndex(i).GetIdentifier());
     }
   }
 
@@ -1172,7 +714,7 @@
       }
     };
 
-    if (GetDatabaseCapabilities().HasUpdateAndGetStatistics())
+    if (GetDatabaseCapabilities().HasUpdateAndGetStatistics() && !IsReadOnly())
     {
       Operations operations;
       Apply(operations);
@@ -1222,6 +764,39 @@
   }
 
 
+  void StatelessDatabaseOperations::GetChangesExtended(Json::Value& target,
+                                                       int64_t since,
+                                                       int64_t to,                               
+                                                       unsigned int maxResults,
+                                                       const std::set<ChangeType>& changeType)
+  {
+    class Operations : public ReadOnlyOperationsT5<Json::Value&, int64_t, int64_t, unsigned int, const std::set<ChangeType>&>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        std::list<ServerIndexChange> changes;
+        bool done;
+        bool hasLast = false;
+        int64_t last = 0;
+
+        transaction.GetChangesExtended(changes, done, tuple.get<1>(), tuple.get<2>(), tuple.get<3>(), tuple.get<4>());
+        if (changes.empty())
+        {
+          last = transaction.GetLastChangeIndex();
+          hasLast = true;
+        }
+
+        FormatLog(tuple.get<0>(), changes, "Changes", done, tuple.get<1>(), hasLast, last);
+      }
+    };
+    
+    Operations operations;
+    operations.Apply(*this, target, since, to, maxResults, changeType);
+  }
+
+
   void StatelessDatabaseOperations::GetLastChange(Json::Value& target)
   {
     class Operations : public ReadOnlyOperationsT1<Json::Value&>
@@ -1329,104 +904,85 @@
 
 
   void StatelessDatabaseOperations::GetChildren(std::list<std::string>& result,
+                                                ResourceType level,
                                                 const std::string& publicId)
   {
-    class Operations : public ReadOnlyOperationsT2<std::list<std::string>&, const std::string&>
+    const ResourceType childLevel = GetChildResourceType(level);
+
+    FindRequest request(level);
+    request.SetOrthancId(level, publicId);
+    request.GetChildrenSpecification(childLevel).SetRetrieveIdentifiers(true);
+
+    FindResponse response;
+    ExecuteFind(response, request);
+
+    result.clear();
+
+    for (size_t i = 0; i < response.GetSize(); i++)
     {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
+      const std::set<std::string>& children = response.GetResourceByIndex(i).GetChildrenIdentifiers(childLevel);
+
+      for (std::set<std::string>::const_iterator it = children.begin(); it != children.end(); ++it)
       {
-        ResourceType type;
-        int64_t resource;
-        if (!transaction.LookupResource(resource, type, tuple.get<1>()))
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-        else if (type == ResourceType_Instance)
-        {
-          // An instance cannot have a child
-          throw OrthancException(ErrorCode_BadParameterType);
-        }
-        else
-        {
-          std::list<int64_t> tmp;
-          transaction.GetChildrenInternalId(tmp, resource);
-
-          tuple.get<0>().clear();
-
-          for (std::list<int64_t>::const_iterator 
-                 it = tmp.begin(); it != tmp.end(); ++it)
-          {
-            tuple.get<0>().push_back(transaction.GetPublicId(*it));
-          }
-        }
+        result.push_back(*it);
       }
-    };
-    
-    Operations operations;
-    operations.Apply(*this, result, publicId);
+    }
+  }
+
+
+  void StatelessDatabaseOperations::GetChildInstances(std::list<std::string>& result,
+                                                      const std::string& publicId,
+                                                      ResourceType level)
+  {
+    result.clear();
+    if (level == ResourceType_Instance)
+    {
+      result.push_back(publicId);
+    }
+    else
+    {
+      FindRequest request(level);
+      request.SetOrthancId(level, publicId);
+      request.GetChildrenSpecification(ResourceType_Instance).SetRetrieveIdentifiers(true);
+
+      FindResponse response;
+      const std::set<std::string>& instances = ExecuteSingleResource(response, request).GetChildrenIdentifiers(ResourceType_Instance);
+
+      for (std::set<std::string>::const_iterator it = instances.begin(); it != instances.end(); ++it)
+      {
+        result.push_back(*it);
+      }
+    }
   }
 
 
   void StatelessDatabaseOperations::GetChildInstances(std::list<std::string>& result,
                                                       const std::string& publicId)
   {
-    class Operations : public ReadOnlyOperationsT2<std::list<std::string>&, const std::string&>
+    ResourceType level;
+    if (LookupResourceType(level, publicId))
+    {
+      GetChildInstances(result, publicId, level);
+    }
+    else
     {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        tuple.get<0>().clear();
-        
-        ResourceType type;
-        int64_t top;
-        if (!transaction.LookupResource(top, type, tuple.get<1>()))
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-        else if (type == ResourceType_Instance)
-        {
-          // The resource is already an instance: Do not go down the hierarchy
-          tuple.get<0>().push_back(tuple.get<1>());
-        }
-        else
-        {
-          std::stack<int64_t> toExplore;
-          toExplore.push(top);
-
-          std::list<int64_t> tmp;
-          while (!toExplore.empty())
-          {
-            // Get the internal ID of the current resource
-            int64_t resource = toExplore.top();
-            toExplore.pop();
-
-            // TODO - This could be optimized by seeing how many
-            // levels "type == transaction.GetResourceType(top)" is
-            // above the "instances level"
-            if (transaction.GetResourceType(resource) == ResourceType_Instance)
-            {
-              tuple.get<0>().push_back(transaction.GetPublicId(resource));
-            }
-            else
-            {
-              // Tag all the children of this resource as to be explored
-              transaction.GetChildrenInternalId(tmp, resource);
-              for (std::list<int64_t>::const_iterator 
-                     it = tmp.begin(); it != tmp.end(); ++it)
-              {
-                toExplore.push(*it);
-              }
-            }
-          }
-        }
-      }
-    };
-    
-    Operations operations;
-    operations.Apply(*this, result, publicId);
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+  }
+
+
+  bool StatelessDatabaseOperations::LookupMetadata(std::string& target,
+                                                   const std::string& publicId,
+                                                   ResourceType expectedType,
+                                                   MetadataType type)
+  {
+    FindRequest request(expectedType);
+    request.SetOrthancId(expectedType, publicId);
+    request.SetRetrieveMetadata(true);
+    request.SetRetrieveMetadataRevisions(false);  // No need to retrieve revisions
+
+    FindResponse response;
+    return ExecuteSingleResource(response, request).LookupMetadata(target, expectedType, type);
   }
 
 
@@ -1436,31 +992,13 @@
                                                    ResourceType expectedType,
                                                    MetadataType type)
   {
-    class Operations : public ReadOnlyOperationsT6<bool&, std::string&, int64_t&,
-                                                   const std::string&, ResourceType, MetadataType>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        ResourceType resourceType;
-        int64_t id;
-        if (!transaction.LookupResource(id, resourceType, tuple.get<3>()) ||
-            resourceType != tuple.get<4>())
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-        else
-        {
-          tuple.get<0>() = transaction.LookupMetadata(tuple.get<1>(), tuple.get<2>(), id, tuple.get<5>());
-        }
-      }
-    };
-
-    bool found;
-    Operations operations;
-    operations.Apply(*this, found, target, revision, publicId, expectedType, type);
-    return found;
+    FindRequest request(expectedType);
+    request.SetOrthancId(expectedType, publicId);
+    request.SetRetrieveMetadata(true);
+    request.SetRetrieveMetadataRevisions(true);  // We are asked to retrieve revisions
+
+    FindResponse response;
+    return ExecuteSingleResource(response, request).LookupMetadata(target, revision, expectedType, type);
   }
 
 
@@ -1468,28 +1006,12 @@
                                                              const std::string& publicId,
                                                              ResourceType expectedType)
   {
-    class Operations : public ReadOnlyOperationsT3<std::set<FileContentType>&, const std::string&, ResourceType>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        ResourceType type;
-        int64_t id;
-        if (!transaction.LookupResource(id, type, tuple.get<1>()) ||
-            tuple.get<2>() != type)
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-        else
-        {
-          transaction.ListAvailableAttachments(tuple.get<0>(), id);
-        }
-      }
-    };
-    
-    Operations operations;
-    operations.Apply(*this, target, publicId, expectedType);
+    FindRequest request(expectedType);
+    request.SetOrthancId(expectedType, publicId);
+    request.SetRetrieveAttachments(true);
+
+    FindResponse response;
+    ExecuteSingleResource(response, request).ListAttachments(target);
   }
 
 
@@ -1685,44 +1207,24 @@
            (level == ResourceType_Study && tag == DICOM_TAG_ACCESSION_NUMBER) ||
            (level == ResourceType_Series && tag == DICOM_TAG_SERIES_INSTANCE_UID) ||
            (level == ResourceType_Instance && tag == DICOM_TAG_SOP_INSTANCE_UID));
-    
-    result.clear();
+
+    FindRequest request(level);
 
     DicomTagConstraint c(tag, ConstraintType_Equal, value, true, true);
 
-    std::vector<DatabaseConstraint> query;
-    query.push_back(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
-
-
-    class Operations : public IReadOnlyOperations
+    bool isIdentical;  // unused
+    request.GetDicomTagConstraints().AddConstraint(c.ConvertToDatabaseConstraint(isIdentical, level, DicomTagType_Identifier));
+
+    FindResponse response;
+    ExecuteFind(response, request);
+
+    result.clear();
+    result.reserve(response.GetSize());
+
+    for (size_t i = 0; i < response.GetSize(); i++)
     {
-    private:
-      std::vector<std::string>&               result_;
-      const std::vector<DatabaseConstraint>&  query_;
-      ResourceType                            level_;
-      
-    public:
-      Operations(std::vector<std::string>& result,
-                 const std::vector<DatabaseConstraint>& query,
-                 ResourceType level) :
-        result_(result),
-        query_(query),
-        level_(level)
-      {
-      }
-
-      virtual void Apply(ReadOnlyTransaction& transaction) ORTHANC_OVERRIDE
-      {
-        // TODO - CANDIDATE FOR "TransactionType_Implicit"
-        std::list<std::string> tmp;
-        std::set<std::string> labels;
-        transaction.ApplyLookupResources(tmp, NULL, query_, level_, labels, LabelsConstraint_Any, 0);
-        CopyListToVector(result_, tmp);
-      }
-    };
-
-    Operations operations(result, query, level);
-    Apply(operations);
+      result.push_back(response.GetResourceByIndex(i).GetIdentifier());
+    }
   }
 
 
@@ -1779,139 +1281,116 @@
       throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
 
-
-    class Operations : public ReadOnlyOperationsT5<bool&, DicomMap&, const std::string&, ResourceType, ResourceType>
+    FindRequest request(expectedType);
+    request.SetOrthancId(expectedType, publicId);
+    request.SetRetrieveMainDicomTags(true);
+
+    FindResponse response;
+    ExecuteFind(response, request);
+
+    if (response.GetSize() == 0)
     {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
+      return false;
+    }
+    else if (response.GetSize() > 1)
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin);
+    }
+    else
+    {
+      result.Clear();
+      if (expectedType == ResourceType_Study)
       {
-        // Lookup for the requested resource
-        int64_t id;
-        ResourceType type;
-        if (!transaction.LookupResource(id, type, tuple.get<2>()) ||
-            type != tuple.get<3>())
-        {
-          tuple.get<0>() = false;
-        }
-        else if (type == ResourceType_Study)
+        DicomMap tmp;
+        response.GetResourceByIndex(0).GetMainDicomTags(tmp, expectedType);
+
+        switch (levelOfInterest)
         {
-          DicomMap tmp;
-          transaction.GetMainDicomTags(tmp, id);
-
-          switch (tuple.get<4>())
-          {
-            case ResourceType_Patient:
-              tmp.ExtractPatientInformation(tuple.get<1>());
-              tuple.get<0>() = true;
-              break;
-
-            case ResourceType_Study:
-              tmp.ExtractStudyInformation(tuple.get<1>());
-              tuple.get<0>() = true;
-              break;
-
-            default:
-              throw OrthancException(ErrorCode_InternalError);
-          }
+          case ResourceType_Study:
+            tmp.ExtractStudyInformation(result);
+            break;
+
+          case ResourceType_Patient:
+            tmp.ExtractPatientInformation(result);
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_InternalError);
         }
-        else
-        {
-          transaction.GetMainDicomTags(tuple.get<1>(), id);
-          tuple.get<0>() = true;
-        }    
       }
-    };
-
-    result.Clear();
-
-    bool found;
-    Operations operations;
-    operations.Apply(*this, found, result, publicId, expectedType, levelOfInterest);
-    return found;
+      else
+      {
+        assert(expectedType == levelOfInterest);
+        response.GetResourceByIndex(0).GetMainDicomTags(result, expectedType);
+      }
+      return true;
+    }
   }
 
 
   bool StatelessDatabaseOperations::GetAllMainDicomTags(DicomMap& result,
                                                         const std::string& instancePublicId)
   {
-    class Operations : public ReadOnlyOperationsT3<bool&, DicomMap&, const std::string&>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        // Lookup for the requested resource
-        int64_t instance;
-        ResourceType type;
-        if (!transaction.LookupResource(instance, type, tuple.get<2>()) ||
-            type != ResourceType_Instance)
-        {
-          tuple.get<0>() =  false;
-        }
-        else
-        {
-          DicomMap tmp;
-
-          transaction.GetMainDicomTags(tmp, instance);
-          tuple.get<1>().Merge(tmp);
-
-          int64_t series;
-          if (!transaction.LookupParent(series, instance))
-          {
-            throw OrthancException(ErrorCode_InternalError);
-          }
-
-          tmp.Clear();
-          transaction.GetMainDicomTags(tmp, series);
-          tuple.get<1>().Merge(tmp);
-
-          int64_t study;
-          if (!transaction.LookupParent(study, series))
-          {
-            throw OrthancException(ErrorCode_InternalError);
-          }
-
-          tmp.Clear();
-          transaction.GetMainDicomTags(tmp, study);
-          tuple.get<1>().Merge(tmp);
+    FindRequest request(ResourceType_Instance);
+    request.SetOrthancId(ResourceType_Instance, instancePublicId);
+    request.GetParentSpecification(ResourceType_Study).SetRetrieveMainDicomTags(true);
+    request.GetParentSpecification(ResourceType_Series).SetRetrieveMainDicomTags(true);
+    request.SetRetrieveMainDicomTags(true);
 
 #ifndef NDEBUG
-          {
-            // Sanity test to check that all the main DICOM tags from the
-            // patient level are copied at the study level
-        
-            int64_t patient;
-            if (!transaction.LookupParent(patient, study))
-            {
-              throw OrthancException(ErrorCode_InternalError);
-            }
-
-            tmp.Clear();
-            transaction.GetMainDicomTags(tmp, study);
-
-            std::set<DicomTag> patientTags;
-            tmp.GetTags(patientTags);
-
-            for (std::set<DicomTag>::const_iterator
-                   it = patientTags.begin(); it != patientTags.end(); ++it)
-            {
-              assert(tuple.get<1>().HasTag(*it));
-            }
-          }
+    // For sanity check below
+    request.GetParentSpecification(ResourceType_Patient).SetRetrieveMainDicomTags(true);
 #endif
-      
-          tuple.get<0>() =  true;
+
+    FindResponse response;
+    ExecuteFind(response, request);
+
+    if (response.GetSize() == 0)
+    {
+      return false;
+    }
+    else if (response.GetSize() > 1)
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin);
+    }
+    else
+    {
+      const FindResponse::Resource& resource = response.GetResourceByIndex(0);
+
+      result.Clear();
+
+      DicomMap tmp;
+      resource.GetMainDicomTags(tmp, ResourceType_Instance);
+      result.Merge(tmp);
+
+      tmp.Clear();
+      resource.GetMainDicomTags(tmp, ResourceType_Series);
+      result.Merge(tmp);
+
+      tmp.Clear();
+      resource.GetMainDicomTags(tmp, ResourceType_Study);
+      result.Merge(tmp);
+
+#ifndef NDEBUG
+      {
+        // Sanity test to check that all the main DICOM tags from the
+        // patient level are copied at the study level
+        tmp.Clear();
+        resource.GetMainDicomTags(tmp, ResourceType_Patient);
+
+        std::set<DicomTag> patientTags;
+        tmp.GetTags(patientTags);
+
+        for (std::set<DicomTag>::const_iterator
+               it = patientTags.begin(); it != patientTags.end(); ++it)
+        {
+          assert(result.HasTag(*it));
         }
       }
-    };
-
-    result.Clear();
-    
-    bool found;
-    Operations operations;
-    operations.Apply(*this, found, result, instancePublicId);
-    return found;
+#endif
+
+      return true;
+    }
   }
 
 
@@ -1941,111 +1420,27 @@
                                                  const std::string& publicId,
                                                  ResourceType parentType)
   {
-    class Operations : public ReadOnlyOperationsT4<bool&, std::string&, const std::string&, ResourceType>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        ResourceType type;
-        int64_t id;
-        if (!transaction.LookupResource(id, type, tuple.get<2>()))
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-
-        while (type != tuple.get<3>())
-        {
-          int64_t parentId;
-
-          if (type == ResourceType_Patient ||    // Cannot further go up in hierarchy
-              !transaction.LookupParent(parentId, id))
-          {
-            tuple.get<0>() = false;
-            return;
-          }
-
-          id = parentId;
-          type = GetParentResourceType(type);
-        }
-
-        tuple.get<0>() = true;
-        tuple.get<1>() = transaction.GetPublicId(id);
-      }
-    };
-
-    bool found;
-    Operations operations;
-    operations.Apply(*this, found, target, publicId, parentType);
-    return found;
-  }
-
-
-  void StatelessDatabaseOperations::ApplyLookupResources(std::vector<std::string>& resourcesId,
-                                                         std::vector<std::string>* instancesId,
-                                                         const DatabaseLookup& lookup,
-                                                         ResourceType queryLevel,
-                                                         const std::set<std::string>& labels,
-                                                         LabelsConstraint labelsConstraint,
-                                                         uint32_t limit)
-  {
-    class Operations : public ReadOnlyOperationsT6<bool, const std::vector<DatabaseConstraint>&, ResourceType,
-                                                   const std::set<std::string>&, LabelsConstraint, size_t>
+    const ResourceType level = GetChildResourceType(parentType);
+
+    FindRequest request(level);
+    request.SetOrthancId(level, publicId);
+    request.SetRetrieveParentIdentifier(true);
+
+    FindResponse response;
+    ExecuteFind(response, request);
+
+    if (response.GetSize() == 0)
     {
-    private:
-      std::list<std::string>  resourcesList_;
-      std::list<std::string>  instancesList_;
-      
-    public:
-      const std::list<std::string>& GetResourcesList() const
-      {
-        return resourcesList_;
-      }
-
-      const std::list<std::string>& GetInstancesList() const
-      {
-        return instancesList_;
-      }
-
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        // TODO - CANDIDATE FOR "TransactionType_Implicit"
-        if (tuple.get<0>())
-        {
-          transaction.ApplyLookupResources(
-            resourcesList_, &instancesList_, tuple.get<1>(), tuple.get<2>(), tuple.get<3>(), tuple.get<4>(), tuple.get<5>());
-        }
-        else
-        {
-          transaction.ApplyLookupResources(
-            resourcesList_, NULL, tuple.get<1>(), tuple.get<2>(), tuple.get<3>(), tuple.get<4>(), tuple.get<5>());
-        }
-      }
-    };
-
-    if (!labels.empty() &&
-        !db_.GetDatabaseCapabilities().HasLabelsSupport())
+      return false;
+    }
+    else if (response.GetSize() > 1)
     {
-      throw OrthancException(ErrorCode_NotImplemented, "The database backend doesn't support labels");
-    }
-
-    for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
-    {
-      ServerToolbox::CheckValidLabel(*it);
+      throw OrthancException(ErrorCode_DatabasePlugin);
     }
-
-    std::vector<DatabaseConstraint> normalized;
-    NormalizeLookup(normalized, lookup, queryLevel);
-
-    Operations operations;
-    operations.Apply(*this, (instancesId != NULL), normalized, queryLevel, labels, labelsConstraint, limit);
-    
-    CopyListToVector(resourcesId, operations.GetResourcesList());
-
-    if (instancesId != NULL)
-    { 
-      CopyListToVector(*instancesId, operations.GetInstancesList());
+    else
+    {
+      target = response.GetResourceByIndex(0).GetParentIdentifier();
+      return true;
     }
   }
 
@@ -2517,7 +1912,7 @@
             catch (boost::bad_lexical_cast&)
             {
               LOG(ERROR) << "Cannot read the global sequence "
-                        << boost::lexical_cast<std::string>(sequence_) << ", resetting it";
+                         << boost::lexical_cast<std::string>(sequence_) << ", resetting it";
               oldValue = 0;
             }
 
@@ -2816,8 +2211,8 @@
 
     public:
       explicit Operations(const ParsedDicomFile& dicom, bool limitToThisLevelDicomTags, ResourceType limitToLevel)
-      : limitToThisLevelDicomTags_(limitToThisLevelDicomTags),
-        limitToLevel_(limitToLevel)
+        : limitToThisLevelDicomTags_(limitToThisLevelDicomTags),
+          limitToLevel_(limitToLevel)
       {
         OrthancConfiguration::DefaultExtractDicomSummary(summary_, dicom);
         hasher_.reset(new DicomInstanceHasher(summary_));
@@ -2919,8 +2314,8 @@
   }
 
 
-  bool StatelessDatabaseOperations::ReadWriteTransaction::HasReachedMaxStorageSize(uint64_t maximumStorageSize,
-                                                                                   uint64_t addedInstanceSize)
+  bool StatelessDatabaseOperations::ReadOnlyTransaction::HasReachedMaxStorageSize(uint64_t maximumStorageSize,
+                                                                                  uint64_t addedInstanceSize)
   {
     if (maximumStorageSize != 0)
     {
@@ -2941,7 +2336,7 @@
     return false;
   }                                                                           
 
-  bool StatelessDatabaseOperations::ReadWriteTransaction::HasReachedMaxPatientCount(unsigned int maximumPatientCount,
+  bool StatelessDatabaseOperations::ReadOnlyTransaction::HasReachedMaxPatientCount(unsigned int maximumPatientCount,
                                                                                    const std::string& patientId)
   {
     if (maximumPatientCount != 0)
@@ -3044,7 +2439,7 @@
     };
 
     if (maximumStorageMode == MaxStorageMode_Recycle 
-      && (maximumStorageSize != 0 || maximumPatientCount != 0))
+        && (maximumStorageSize != 0 || maximumPatientCount != 0))
     {
       Operations operations(maximumStorageSize, maximumPatientCount);
       Apply(operations);
@@ -3108,9 +2503,9 @@
       }
 
       static void SetMainDicomSequenceMetadata(ResourcesContent& content,
-                                                int64_t resource,
-                                                const DicomMap& dicomSummary,
-                                                ResourceType level)
+                                               int64_t resource,
+                                               const DicomMap& dicomSummary,
+                                               ResourceType level)
       {
         std::string serialized;
         GetMainDicomSequenceMetadataContent(serialized, dicomSummary, level);
@@ -3303,7 +2698,7 @@
         // Ensure there is enough room in the storage for the new instance
         uint64_t instanceSize = 0;
         for (Attachments::const_iterator it = attachments_.begin();
-              it != attachments_.end(); ++it)
+             it != attachments_.end(); ++it)
         {
           instanceSize += it->GetCompressedSize();
         }
@@ -3332,7 +2727,7 @@
     
         // Attach the files to the newly created instance
         for (Attachments::const_iterator it = attachments_.begin();
-              it != attachments_.end(); ++it)
+             it != attachments_.end(); ++it)
         {
           if (isReconstruct_)
           {
@@ -3690,28 +3085,12 @@
                                                const std::string& publicId,
                                                ResourceType level)
   {
-    class Operations : public ReadOnlyOperationsT3<std::set<std::string>&, const std::string&, ResourceType>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        ResourceType type;
-        int64_t id;
-        if (!transaction.LookupResource(id, type, tuple.get<1>()) ||
-            tuple.get<2>() != type)
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-        else
-        {
-          transaction.ListLabels(tuple.get<0>(), id);
-        }
-      }
-    };
-
-    Operations operations;
-    operations.Apply(*this, target, publicId, level);
+    FindRequest request(level);
+    request.SetOrthancId(level, publicId);
+    request.SetRetrieveLabels(true);
+
+    FindResponse response;
+    target = ExecuteSingleResource(response, request).GetLabels();
   }
 
 
@@ -3808,4 +3187,137 @@
     boost::shared_lock<boost::shared_mutex> lock(mutex_);
     return db_.GetDatabaseCapabilities().HasLabelsSupport();
   }
+
+  bool StatelessDatabaseOperations::HasExtendedChanges()
+  {
+    boost::shared_lock<boost::shared_mutex> lock(mutex_);
+    return db_.GetDatabaseCapabilities().HasExtendedChanges();
+  }
+
+  bool StatelessDatabaseOperations::HasFindSupport()
+  {
+    boost::shared_lock<boost::shared_mutex> lock(mutex_);
+    return db_.GetDatabaseCapabilities().HasFindSupport();
+  }
+
+  void StatelessDatabaseOperations::ExecuteCount(uint64_t& count,
+                                                 const FindRequest& request)
+  {
+    class IntegratedCount : public ReadOnlyOperationsT3<uint64_t&, const FindRequest&,
+                                                       const IDatabaseWrapper::Capabilities&>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        transaction.ExecuteCount(tuple.get<0>(), tuple.get<1>(), tuple.get<2>());
+      }
+    };
+
+    class Compatibility : public ReadOnlyOperationsT3<uint64_t&, const FindRequest&, const IDatabaseWrapper::Capabilities&>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        std::list<std::string> identifiers;
+        transaction.ExecuteFind(identifiers, tuple.get<2>(), tuple.get<1>());
+        tuple.get<0>() = identifiers.size();
+      }
+    };
+
+    IDatabaseWrapper::Capabilities capabilities = db_.GetDatabaseCapabilities();
+
+    if (db_.HasIntegratedFind())
+    {
+      IntegratedCount operations;
+      operations.Apply(*this, count, request, capabilities);
+    }
+    else
+    {
+      Compatibility operations;
+      operations.Apply(*this, count, request, capabilities);
+    }
+  }
+
+  void StatelessDatabaseOperations::ExecuteFind(FindResponse& response,
+                                                const FindRequest& request)
+  {
+    class IntegratedFind : public ReadOnlyOperationsT3<FindResponse&, const FindRequest&,
+                                                       const IDatabaseWrapper::Capabilities&>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        transaction.ExecuteFind(tuple.get<0>(), tuple.get<1>(), tuple.get<2>());
+      }
+    };
+
+    class FindStage : public ReadOnlyOperationsT3<std::list<std::string>&, const IDatabaseWrapper::Capabilities&, const FindRequest& >
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        transaction.ExecuteFind(tuple.get<0>(), tuple.get<1>(), tuple.get<2>());
+      }
+    };
+
+    class ExpandStage : public ReadOnlyOperationsT4<FindResponse&, const IDatabaseWrapper::Capabilities&, const FindRequest&, const std::string&>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        transaction.ExecuteExpand(tuple.get<0>(), tuple.get<1>(), tuple.get<2>(), tuple.get<3>());
+      }
+    };
+
+    IDatabaseWrapper::Capabilities capabilities = db_.GetDatabaseCapabilities();
+
+    if (db_.HasIntegratedFind())
+    {
+      /**
+       * In this flavor, the "find" and the "expand" phases are
+       * executed in one single transaction.
+       **/
+      IntegratedFind operations;
+      operations.Apply(*this, response, request, capabilities);
+    }
+    else
+    {
+      /**
+       * In this flavor, the "find" and the "expand" phases for each
+       * found resource are executed in distinct transactions. This is
+       * the compatibility mode equivalent to Orthanc <= 1.12.3.
+       **/
+      std::list<std::string> identifiers;
+
+      std::string publicId;
+      if (request.IsTrivialFind(publicId))
+      {
+        // This is a trivial case for which no transaction is needed
+        identifiers.push_back(publicId);
+      }
+      else
+      {
+        // Non-trival case, a transaction is needed
+        FindStage find;
+        find.Apply(*this, identifiers, capabilities, request);
+      }
+
+      ExpandStage expand;
+
+      for (std::list<std::string>::const_iterator it = identifiers.begin(); it != identifiers.end(); ++it)
+      {
+        /**
+         * Note that the resource might have been deleted (as we are in
+         * another transaction). The database engine must ignore such
+         * error cases.
+         **/
+        expand.Apply(*this, response, capabilities, request, *it);
+      }
+    }
+  }
 }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -25,8 +25,9 @@
 
 #include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h"
 
+#include "../DicomInstanceOrigin.h"
 #include "IDatabaseWrapper.h"
-#include "../DicomInstanceOrigin.h"
+#include "MainDicomTagsRegistry.h"
 
 #include <boost/shared_ptr.hpp>
 #include <boost/thread/shared_mutex.hpp>
@@ -38,91 +39,6 @@
   class ParsedDicomFile;
   struct ServerIndexChange;
 
-  class ExpandedResource : public boost::noncopyable
-  {
-  private:
-    std::string                         id_;
-    ResourceType                        level_;
-    DicomMap                            tags_;  // all main tags and main sequences from DB
-
-  public:
-    std::string                         mainDicomTagsSignature_;
-    std::string                         parentId_;
-    std::list<std::string>              childrenIds_;
-    std::map<MetadataType, std::string> metadata_;
-    std::string                         anonymizedFrom_;
-    std::string                         modifiedFrom_;
-    std::string                         lastUpdate_;
-    std::set<DicomTag>                  missingRequestedTags_;
-
-    // for patients/studies/series
-    bool                                isStable_;
-
-    // for series only
-    int                                 expectedNumberOfInstances_;
-    std::string                         status_;
-
-    // for instances only
-    size_t                              fileSize_;
-    std::string                         fileUuid_;
-    int                                 indexInSeries_;
-
-    // New in Orthanc 1.12.0
-    std::set<std::string>               labels_;
-
-  public:
-    // TODO - Cleanup
-    ExpandedResource() :
-      level_(ResourceType_Instance),
-      isStable_(false),
-      expectedNumberOfInstances_(0),
-      fileSize_(0),
-      indexInSeries_(0)
-    {
-    }
-    
-    void SetResource(ResourceType level,
-                     const std::string& id)
-    {
-      level_ = level;
-      id_ = id;
-    }
-
-    const std::string& GetPublicId() const
-    {
-      return id_;
-    }
-
-    ResourceType GetLevel() const
-    {
-      return level_;
-    }
-
-    DicomMap& GetMainDicomTags()
-    {
-      return tags_;
-    }
-
-    const DicomMap& GetMainDicomTags() const
-    {
-      return tags_;
-    }
-  };
-
-  enum ExpandResourceFlags
-  {
-    ExpandResourceFlags_None                    = 0,
-    ExpandResourceFlags_IncludeMetadata         = (1 << 0),
-    ExpandResourceFlags_IncludeChildren         = (1 << 1),
-    ExpandResourceFlags_IncludeMainDicomTags    = (1 << 2),
-    ExpandResourceFlags_IncludeLabels           = (1 << 3),
-
-    ExpandResourceFlags_Default = (ExpandResourceFlags_IncludeMetadata |
-                                     ExpandResourceFlags_IncludeChildren |
-                                     ExpandResourceFlags_IncludeMainDicomTags |
-                                     ExpandResourceFlags_IncludeLabels)
-  };
-
   class StatelessDatabaseOperations : public boost::noncopyable
   {
   public:
@@ -207,38 +123,12 @@
        * Read-only methods from "IDatabaseWrapper"
        **/
 
-      void ApplyLookupResources(std::list<std::string>& resourcesId,
-                                std::list<std::string>* instancesId, // Can be NULL if not needed
-                                const std::vector<DatabaseConstraint>& lookup,
-                                ResourceType queryLevel,
-                                const std::set<std::string>& labels,  // New in Orthanc 1.12.0
-                                LabelsConstraint labelsConstraint,    // New in Orthanc 1.12.0
-                                uint32_t limit)
-      {
-        return transaction_.ApplyLookupResources(resourcesId, instancesId, lookup, queryLevel,
-                                                 labels, labelsConstraint, limit);
-      }
-
       void GetAllMetadata(std::map<MetadataType, std::string>& target,
                           int64_t id)
       {
         transaction_.GetAllMetadata(target, id);
       }
 
-      void GetAllPublicIds(std::list<std::string>& target,
-                           ResourceType resourceType)
-      {
-        return transaction_.GetAllPublicIds(target, resourceType);
-      }
-
-      void GetAllPublicIds(std::list<std::string>& target,
-                           ResourceType resourceType,
-                           size_t since,
-                           uint32_t limit)
-      {
-        return transaction_.GetAllPublicIds(target, resourceType, since, limit);
-      }  
-
       void GetChanges(std::list<ServerIndexChange>& target /*out*/,
                       bool& done /*out*/,
                       int64_t since,
@@ -247,18 +137,22 @@
         transaction_.GetChanges(target, done, since, limit);
       }
 
+      void GetChangesExtended(std::list<ServerIndexChange>& target /*out*/,
+                              bool& done /*out*/,
+                              int64_t since,
+                              int64_t to,
+                              uint32_t limit,
+                              const std::set<ChangeType>& filterType)
+      {
+        transaction_.GetChangesExtended(target, done, since, to, limit, filterType);
+      }
+
       void GetChildrenInternalId(std::list<int64_t>& target,
                                  int64_t id)
       {
         transaction_.GetChildrenInternalId(target, id);
       }
 
-      void GetChildrenPublicId(std::list<std::string>& target,
-                               int64_t id)
-      {
-        transaction_.GetChildrenPublicId(target, id);
-      }
-
       void GetExportedResources(std::list<ExportedResource>& target /*out*/,
                                 bool& done /*out*/,
                                 int64_t since,
@@ -359,25 +253,46 @@
       {
         return transaction_.LookupResource(id, type, publicId);
       }
-      
-      bool LookupResourceAndParent(int64_t& id,
-                                   ResourceType& type,
-                                   std::string& parentPublicId,
-                                   const std::string& publicId)
-      {
-        return transaction_.LookupResourceAndParent(id, type, parentPublicId, publicId);
-      }
-
-      void ListLabels(std::set<std::string>& target,
-                      int64_t id)
-      {
-        transaction_.ListLabels(target, id);
-      }
 
       void ListAllLabels(std::set<std::string>& target)
       {
         transaction_.ListAllLabels(target);
       }
+
+      bool HasReachedMaxStorageSize(uint64_t maximumStorageSize,
+                                    uint64_t addedInstanceSize);
+
+      bool HasReachedMaxPatientCount(unsigned int maximumPatientCount,
+                                     const std::string& patientId);
+
+      void ExecuteCount(uint64_t& count,
+                        const FindRequest& request,
+                        const IDatabaseWrapper::Capabilities& capabilities)
+      {
+        transaction_.ExecuteCount(count, request, capabilities);
+      }
+
+      void ExecuteFind(FindResponse& response,
+                       const FindRequest& request,
+                       const IDatabaseWrapper::Capabilities& capabilities)
+      {
+        transaction_.ExecuteFind(response, request, capabilities);
+      }
+
+      void ExecuteFind(std::list<std::string>& identifiers,
+                       const IDatabaseWrapper::Capabilities& capabilities,
+                       const FindRequest& request)
+      {
+        transaction_.ExecuteFind(identifiers, capabilities, request);
+      }
+
+      void ExecuteExpand(FindResponse& response,
+                         const IDatabaseWrapper::Capabilities& capabilities,
+                         const FindRequest& request,
+                         const std::string& identifier)
+      {
+        transaction_.ExecuteExpand(response, capabilities, request, identifier);
+      }
     };
 
 
@@ -497,12 +412,6 @@
                    uint64_t addedInstanceSize,
                    const std::string& newPatientId);
 
-      bool HasReachedMaxStorageSize(uint64_t maximumStorageSize,
-                                    uint64_t addedInstanceSize);
-
-      bool HasReachedMaxPatientCount(unsigned int maximumPatientCount,
-                                     const std::string& patientId);
-      
       bool IsRecyclingNeeded(uint64_t maximumStorageSize,
                              unsigned int maximumPatients,
                              uint64_t addedInstanceSize,
@@ -545,7 +454,6 @@
     
 
   private:
-    class MainDicomTagsRegistry;
     class Transaction;
 
     IDatabaseWrapper&                            db_;
@@ -555,21 +463,26 @@
     boost::shared_mutex                          mutex_;
     std::unique_ptr<ITransactionContextFactory>  factory_;
     unsigned int                                 maxRetries_;
-
-    void NormalizeLookup(std::vector<DatabaseConstraint>& target,
-                         const DatabaseLookup& source,
-                         ResourceType level) const;
+    bool                                         readOnly_;
 
     void ApplyInternal(IReadOnlyOperations* readOperations,
                        IReadWriteOperations* writeOperations);
 
+    const FindResponse::Resource &ExecuteSingleResource(FindResponse &response,
+                                                        const FindRequest &request);
+
   protected:
     void StandaloneRecycling(MaxStorageMode maximumStorageMode,
                              uint64_t maximumStorageSize,
                              unsigned int maximumPatientCount);
 
+    bool IsReadOnly()
+    {
+      return readOnly_;
+    }
+
   public:
-    explicit StatelessDatabaseOperations(IDatabaseWrapper& database);
+    explicit StatelessDatabaseOperations(IDatabaseWrapper& database, bool readOnly);
 
     void SetTransactionContextFactory(ITransactionContextFactory* factory /* takes ownership */);
 
@@ -596,12 +509,6 @@
   
     void Apply(IReadWriteOperations& operations);
 
-    bool ExpandResource(ExpandedResource& target,
-                        const std::string& publicId,
-                        ResourceType level,
-                        const std::set<DicomTag>& requestedTags,
-                        ExpandResourceFlags expandFlags);
-
     void GetAllMetadata(std::map<MetadataType, std::string>& target,
                         const std::string& publicId,
                         ResourceType level);
@@ -609,11 +516,6 @@
     void GetAllUuids(std::list<std::string>& target,
                      ResourceType resourceType);
 
-    void GetAllUuids(std::list<std::string>& target,
-                     ResourceType resourceType,
-                     size_t since,
-                     uint32_t limit);
-
     void GetGlobalStatistics(/* out */ uint64_t& diskSize,
                              /* out */ uint64_t& uncompressedSize,
                              /* out */ uint64_t& countPatients, 
@@ -623,15 +525,26 @@
 
     bool LookupAttachment(FileInfo& attachment,
                           int64_t& revision,
-                          const std::string& instancePublicId,
+                          ResourceType level,
+                          const std::string& publicId,
                           FileContentType contentType);
 
     void GetChanges(Json::Value& target,
                     int64_t since,
                     uint32_t limit);
 
+    void GetChangesExtended(Json::Value& target,
+                            int64_t since,
+                            int64_t to,
+                            uint32_t limit,
+                            const std::set<ChangeType>& filterType);
+
     void GetLastChange(Json::Value& target);
 
+    bool HasExtendedChanges();
+
+    bool HasFindSupport();
+    
     void GetExportedResources(Json::Value& target,
                               int64_t since,
                               uint32_t limit);
@@ -641,12 +554,23 @@
     bool IsProtectedPatient(const std::string& publicId);
 
     void GetChildren(std::list<std::string>& result,
+                     ResourceType level,
                      const std::string& publicId);
 
+    // Always prefer this flavor, which is more efficient than the flavor without "level"
+    void GetChildInstances(std::list<std::string>& result,
+                           const std::string& publicId,
+                           ResourceType level);
+
     void GetChildInstances(std::list<std::string>& result,
                            const std::string& publicId);
 
     bool LookupMetadata(std::string& target,
+                        const std::string& publicId,
+                        ResourceType expectedType,
+                        MetadataType type);
+
+    bool LookupMetadata(std::string& target,
                         int64_t& revision,
                         const std::string& publicId,
                         ResourceType expectedType,
@@ -687,7 +611,7 @@
                           ResourceType expectedType,
                           ResourceType levelOfInterest);
 
-    // Only applicable at the instance level
+    // Only applicable at the instance level, retrieves tags from patient/study/series levels
     bool GetAllMainDicomTags(DicomMap& result,
                              const std::string& instancePublicId);
 
@@ -698,14 +622,6 @@
                       const std::string& publicId,
                       ResourceType parentType);
 
-    void ApplyLookupResources(std::vector<std::string>& resourcesId,
-                              std::vector<std::string>* instancesId,  // Can be NULL if not needed
-                              const DatabaseLookup& lookup,
-                              ResourceType queryLevel,
-                              const std::set<std::string>& labels,
-                              LabelsConstraint labelsConstraint,
-                              uint32_t limit);
-
     bool DeleteResource(Json::Value& remainingAncestor /* out */,
                         const std::string& uuid,
                         ResourceType expectedType);
@@ -802,5 +718,11 @@
                    const std::set<std::string>& labels);
 
     bool HasLabelsSupport();
+
+    void ExecuteFind(FindResponse& response,
+                     const FindRequest& request);
+
+    void ExecuteCount(uint64_t& count,
+                      const FindRequest& request);
   };
 }
--- a/OrthancServer/Sources/Database/Upgrade3To4.sql	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/Upgrade3To4.sql	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 -- 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-2024 Orthanc Team SRL, Belgium
--- Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+-- Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+-- Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/Database/Upgrade4To5.sql	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/Upgrade4To5.sql	Tue Mar 18 13:37:18 2025 +0100
@@ -2,8 +2,8 @@
 -- 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-2024 Orthanc Team SRL, Belgium
--- Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+-- Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+-- Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/Database/VoidDatabaseListener.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/VoidDatabaseListener.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/Database/VoidDatabaseListener.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Database/VoidDatabaseListener.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/DicomInstanceOrigin.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/DicomInstanceOrigin.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/DicomInstanceOrigin.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/DicomInstanceOrigin.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/DicomInstanceToStore.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/DicomInstanceToStore.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/DicomInstanceToStore.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/DicomInstanceToStore.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/EmbeddedResourceHttpHandler.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/EmbeddedResourceHttpHandler.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/EmbeddedResourceHttpHandler.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/EmbeddedResourceHttpHandler.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/ExportedResource.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ExportedResource.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/ExportedResource.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ExportedResource.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/IDicomImageDecoder.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/IDicomImageDecoder.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/IServerListener.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/IServerListener.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/JobEvent.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/JobEvent.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/LuaScripting.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/LuaScripting.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/LuaScripting.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/LuaScripting.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/OrthancConfiguration.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/OrthancConfiguration.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -46,6 +46,7 @@
 static const char* const DATABASE_SERVER_IDENTIFIER = "DatabaseServerIdentifier";
 static const char* const WARNINGS = "Warnings";
 static const char* const JOBS_ENGINE_THREADS_COUNT = "JobsEngineThreadsCount";
+static const char* const DICOM_LOSSY_TRANSCODING_QUALITY = "DicomLossyTranscodingQuality";
 
 namespace Orthanc
 {
@@ -700,6 +701,11 @@
     }
   }
 
+  unsigned int OrthancConfiguration::GetDicomLossyTranscodingQuality() const
+  {
+    return GetUnsignedIntegerParameter(DICOM_LOSSY_TRANSCODING_QUALITY, 90);
+  }
+
 
   bool OrthancConfiguration::SetupRegisteredUsers(HttpServer& httpServer) const
   {
@@ -1157,6 +1163,26 @@
         {
           warning = Warnings_002_InconsistentDicomTagsInDb;
         }
+        else if (name == "W003_DecoderFailure")
+        {
+          warning = Warnings_003_DecoderFailure;
+        }
+        else if (name == "W004_NoMainDicomTagsSignature")
+        {
+          warning = Warnings_004_NoMainDicomTagsSignature;
+        }
+        else if (name == "W005_RequestingTagFromLowerResourceLevel")
+        {
+          warning = Warnings_005_RequestingTagFromLowerResourceLevel;
+        }
+        else if (name == "W006_RequestingTagFromMetaHeader")
+        {
+          warning = Warnings_006_RequestingTagFromMetaHeader;
+        }
+        else if (name == "W007_MissingRequestedTagsNotReadFromDisk")
+        {
+          warning = Warnings_007_MissingRequestedTagsNotReadFromDisk;
+        }
         else
         {
           throw OrthancException(ErrorCode_BadFileFormat, name + " is not recognized as a valid warning name");
--- a/OrthancServer/Sources/OrthancConfiguration.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/OrthancConfiguration.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -197,6 +197,8 @@
 
     void GetListOfOrthancPeers(std::set<std::string>& target) const;
 
+    unsigned int GetDicomLossyTranscodingQuality() const;
+    
     // Returns "true" iff. at least one user is registered
     bool SetupRegisteredUsers(HttpServer& httpServer) const;
 
--- a/OrthancServer/Sources/OrthancFindRequestHandler.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/OrthancFindRequestHandler.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -30,6 +30,7 @@
 #include "../../OrthancFramework/Sources/Lua/LuaFunctionCall.h"
 #include "../../OrthancFramework/Sources/MetricsRegistry.h"
 #include "OrthancConfiguration.h"
+#include "ResourceFinder.h"
 #include "Search/DatabaseLookup.h"
 #include "ServerContext.h"
 #include "ServerToolbox.h"
@@ -39,134 +40,48 @@
 
 namespace Orthanc
 {
-  static void AddAnswer(DicomFindAnswers& answers,
-                        ServerContext& context,
-                        const std::string& publicId,
-                        const std::string& instanceId,
-                        const DicomMap& mainDicomTags,
-                        const Json::Value* dicomAsJson,
-                        ResourceType level,
-                        const DicomArray& query,
-                        const std::list<DicomTag>& sequencesToReturn,
-                        const std::string& defaultPrivateCreator,
-                        const std::map<uint16_t, std::string>& privateCreators,
-                        const std::string& retrieveAet,
-                        bool allowStorageAccess)
+  static void CopySequence(ParsedDicomFile& dicom,
+                           const DicomTag& tag,
+                           const Json::Value& source,
+                           const std::string& defaultPrivateCreator,
+                           const std::map<uint16_t, std::string>& privateCreators)
   {
-    ExpandedResource resource;
-    std::set<DicomTag> requestedTags;
-    
-    query.GetTags(requestedTags);
-    requestedTags.erase(DICOM_TAG_QUERY_RETRIEVE_LEVEL); // this is not part of the answer
-
-    // reuse ExpandResource to get missing tags and computed tags (ModalitiesInStudy ...).  This code is therefore shared between C-Find, tools/find, list-resources and QIDO-RS
-    context.ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson,
-                           level, requestedTags, ExpandResourceFlags_IncludeMainDicomTags, allowStorageAccess);
-
-    DicomMap result;
+    if (source.type() == Json::objectValue &&
+        source.isMember("Type") &&
+        source.isMember("Value") &&
+        source["Type"].asString() == "Sequence" &&
+        source["Value"].type() == Json::arrayValue)
+    {
+      Json::Value content = Json::arrayValue;
 
-    /**
-     * Add the mandatory "Retrieve AE Title (0008,0054)" tag, which was missing in Orthanc <= 1.7.2.
-     * http://dicom.nema.org/medical/dicom/current/output/html/part04.html#sect_C.4.1.1.3.2
-     * https://groups.google.com/g/orthanc-users/c/-7zNTKR_PMU/m/kfjwzEVNAgAJ
-     **/
-    result.SetValue(DICOM_TAG_RETRIEVE_AE_TITLE, retrieveAet, false /* not binary */);
-
-    for (size_t i = 0; i < query.GetSize(); i++)
-    {
-      if (query.GetElement(i).GetTag() == DICOM_TAG_QUERY_RETRIEVE_LEVEL)
+      for (Json::Value::ArrayIndex i = 0; i < source["Value"].size(); i++)
       {
-        // Fix issue 30 on Google Code (QR response missing "Query/Retrieve Level" (008,0052))
-        result.SetValue(query.GetElement(i).GetTag(), query.GetElement(i).GetValue());
+        Json::Value item;
+        Toolbox::SimplifyDicomAsJson(item, source["Value"][i], DicomToJsonFormat_Short);
+        content.append(item);
       }
-      else if (query.GetElement(i).GetTag() == DICOM_TAG_SPECIFIC_CHARACTER_SET)
-      {
-        // Do not include the encoding, this is handled by class ParsedDicomFile
-      }
-      else
+
+      if (tag.IsPrivate())
       {
-        const DicomTag& tag = query.GetElement(i).GetTag();
-        const DicomValue* value = resource.GetMainDicomTags().TestAndGetValue(tag);
+        std::map<uint16_t, std::string>::const_iterator found = privateCreators.find(tag.GetGroup());
 
-        if (value != NULL &&
-            !value->IsNull() &&
-            !value->IsBinary())
+        if (found != privateCreators.end())
         {
-          result.SetValue(tag, value->GetContent(), false);
+          dicom.Replace(tag, content, false, DicomReplaceMode_InsertIfAbsent, found->second.c_str());
         }
         else
         {
-          result.SetValue(tag, "", false);
+          dicom.Replace(tag, content, false, DicomReplaceMode_InsertIfAbsent, defaultPrivateCreator);
         }
       }
+      else
+      {
+        dicom.Replace(tag, content, false, DicomReplaceMode_InsertIfAbsent, "" /* no private creator */);
+      }
     }
-
-    if (result.GetSize() == 0 &&
-        sequencesToReturn.empty())
-    {
-      CLOG(WARNING, DICOM) << "The C-FIND request does not return any DICOM tag";
-    }
-    else if (sequencesToReturn.empty())
-    {
-      answers.Add(result);
-    }
-    else if (dicomAsJson == NULL)
-    {
-      CLOG(WARNING, DICOM) << "C-FIND query requesting a sequence, but reading JSON from disk is disabled";
-      answers.Add(result);
-    }
-    else
-    {
-      ParsedDicomFile dicom(result, GetDefaultDicomEncoding(),
-                            true /* be permissive, cf. issue #136 */, defaultPrivateCreator, privateCreators);
-
-      for (std::list<DicomTag>::const_iterator tag = sequencesToReturn.begin();
-           tag != sequencesToReturn.end(); ++tag)
-      {
-        assert(dicomAsJson != NULL);
-        const Json::Value& source = (*dicomAsJson) [tag->Format()];
-
-        if (source.type() == Json::objectValue &&
-            source.isMember("Type") &&
-            source.isMember("Value") &&
-            source["Type"].asString() == "Sequence" &&
-            source["Value"].type() == Json::arrayValue)
-        {
-          Json::Value content = Json::arrayValue;
-
-          for (Json::Value::ArrayIndex i = 0; i < source["Value"].size(); i++)
-          {
-            Json::Value item;
-            Toolbox::SimplifyDicomAsJson(item, source["Value"][i], DicomToJsonFormat_Short);
-            content.append(item);
-          }
-
-          if (tag->IsPrivate())
-          {
-            std::map<uint16_t, std::string>::const_iterator found = privateCreators.find(tag->GetGroup());
-            
-            if (found != privateCreators.end())
-            {
-              dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, found->second.c_str());
-            }
-            else
-            {
-              dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, defaultPrivateCreator);
-            }
-          }
-          else
-          {
-            dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, "" /* no private creator */);
-          }
-        }
-      }
-
-      answers.Add(dicom);
-    }
   }
 
 
-
   bool OrthancFindRequestHandler::FilterQueryTag(std::string& value /* can be modified */,
                                                  ResourceType level,
                                                  const DicomTag& tag,
@@ -237,86 +152,122 @@
   }
 
 
-  class OrthancFindRequestHandler::LookupVisitor : public ServerContext::ILookupVisitor
+  namespace
   {
-  private:
-    DicomFindAnswers&           answers_;
-    ServerContext&              context_;
-    ResourceType                level_;
-    const DicomMap&             query_;
-    DicomArray                  queryAsArray_;
-    const std::list<DicomTag>&  sequencesToReturn_;
-    std::string                 defaultPrivateCreator_;       // the private creator to use if the group is not defined in the query itself
-    const std::map<uint16_t, std::string>& privateCreators_;  // the private creators defined in the query itself
-    std::string                 retrieveAet_;
-    FindStorageAccessMode       findStorageAccessMode_;
+    class LookupVisitorV2 : public ResourceFinder::IVisitor
+    {
+    private:
+      DicomFindAnswers&           answers_;
+      DicomArray                  queryAsArray_;
+      const std::list<DicomTag>&  sequencesToReturn_;
+      std::string                 defaultPrivateCreator_;       // the private creator to use if the group is not defined in the query itself
+      const std::map<uint16_t, std::string>& privateCreators_;  // the private creators defined in the query itself
+      std::string                 retrieveAet_;
+
+    public:
+      LookupVisitorV2(DicomFindAnswers& answers,
+                      const DicomMap& query,
+                      const std::list<DicomTag>& sequencesToReturn,
+                      const std::map<uint16_t, std::string>& privateCreators) :
+        answers_(answers),
+        queryAsArray_(query),
+        sequencesToReturn_(sequencesToReturn),
+        privateCreators_(privateCreators)
+      {
+        answers_.SetComplete(false);
 
-  public:
-    LookupVisitor(DicomFindAnswers&  answers,
-                  ServerContext& context,
-                  ResourceType level,
-                  const DicomMap& query,
-                  const std::list<DicomTag>& sequencesToReturn,
-                  const std::map<uint16_t, std::string>& privateCreators,
-                  FindStorageAccessMode findStorageAccessMode) :
-      answers_(answers),
-      context_(context),
-      level_(level),
-      query_(query),
-      queryAsArray_(query),
-      sequencesToReturn_(sequencesToReturn),
-      privateCreators_(privateCreators),
-      findStorageAccessMode_(findStorageAccessMode)
-    {
-      answers_.SetComplete(false);
+        {
+          OrthancConfiguration::ReaderLock lock;
+          defaultPrivateCreator_ = lock.GetConfiguration().GetDefaultPrivateCreator();
+          retrieveAet_ = lock.GetConfiguration().GetOrthancAET();
+        }
+      }
 
+      virtual void Apply(const FindResponse::Resource& resource,
+                         const DicomMap& requestedTags) ORTHANC_OVERRIDE
       {
-        OrthancConfiguration::ReaderLock lock;
-        defaultPrivateCreator_ = lock.GetConfiguration().GetDefaultPrivateCreator();
-        retrieveAet_ = lock.GetConfiguration().GetOrthancAET();
-      }
-    }
+        DicomMap resourceTags;
+        resource.GetAllMainDicomTags(resourceTags);
+        resourceTags.Merge(requestedTags);
+
+        DicomMap result;
+
+        /**
+         * Add the mandatory "Retrieve AE Title (0008,0054)" tag, which was missing in Orthanc <= 1.7.2.
+         * http://dicom.nema.org/medical/dicom/current/output/html/part04.html#sect_C.4.1.1.3.2
+         * https://groups.google.com/g/orthanc-users/c/-7zNTKR_PMU/m/kfjwzEVNAgAJ
+         **/
+        result.SetValue(DICOM_TAG_RETRIEVE_AE_TITLE, retrieveAet_, false /* not binary */);
+
+        for (size_t i = 0; i < queryAsArray_.GetSize(); i++)
+        {
+          const DicomTag tag = queryAsArray_.GetElement(i).GetTag();
 
-    virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
-    {
-      // Ask the "DICOM-as-JSON" attachment only if sequences are to
-      // be returned OR if "query_" contains non-main DICOM tags!
-
-      DicomMap withoutSpecialTags;
-      withoutSpecialTags.Assign(query_);
+          if (tag == DICOM_TAG_QUERY_RETRIEVE_LEVEL)
+          {
+            // Fix issue 30 on Google Code (QR response missing "Query/Retrieve Level" (008,0052))
+            result.SetValue(tag, queryAsArray_.GetElement(i).GetValue());
+          }
+          else if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET)
+          {
+            // Do not include the encoding, this is handled by class ParsedDicomFile
+          }
+          else
+          {
+            const DicomValue* value = resourceTags.TestAndGetValue(tag);
 
-      // Check out "ComputeCounters()"
-      withoutSpecialTags.Remove(DICOM_TAG_MODALITIES_IN_STUDY);
-      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES);
-      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES);
-      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES);
-      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES);
-      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES);
-      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES);
-      withoutSpecialTags.Remove(DICOM_TAG_SOP_CLASSES_IN_STUDY);
+            if (value == NULL ||
+                value->IsNull() ||
+                value->IsBinary())
+            {
+              result.SetValue(tag, "", false);
+            }
+            else
+            {
+              result.SetValue(tag, value->GetContent(), false);
+            }
+          }
+        }
 
-      // Check out "AddAnswer()"
-      withoutSpecialTags.Remove(DICOM_TAG_SPECIFIC_CHARACTER_SET);
-      withoutSpecialTags.Remove(DICOM_TAG_QUERY_RETRIEVE_LEVEL);
-      
-      return (!sequencesToReturn_.empty() ||
-              !withoutSpecialTags.HasOnlyMainDicomTags());
-    }
-      
-    virtual void MarkAsComplete() ORTHANC_OVERRIDE
-    {
-      answers_.SetComplete(true);
-    }
+        if (result.GetSize() == 0 &&
+            sequencesToReturn_.empty())
+        {
+          CLOG(WARNING, DICOM) << "The C-FIND request does not return any DICOM tag";
+        }
+        else if (sequencesToReturn_.empty())
+        {
+          answers_.Add(result);
+        }
+        else
+        {
+          ParsedDicomFile dicom(result, GetDefaultDicomEncoding(),
+                                true /* be permissive, cf. issue #136 */, defaultPrivateCreator_, privateCreators_);
 
-    virtual void Visit(const std::string& publicId,
-                       const std::string& instanceId,
-                       const DicomMap& mainDicomTags,
-                       const Json::Value* dicomAsJson) ORTHANC_OVERRIDE
-    {
-      AddAnswer(answers_, context_, publicId, instanceId, mainDicomTags, dicomAsJson, level_, queryAsArray_, sequencesToReturn_,
-                defaultPrivateCreator_, privateCreators_, retrieveAet_, IsStorageAccessAllowedForAnswers(findStorageAccessMode_));
-    }
-  };
+          for (std::list<DicomTag>::const_iterator tag = sequencesToReturn_.begin();
+               tag != sequencesToReturn_.end(); ++tag)
+          {
+            const DicomValue* value = resourceTags.TestAndGetValue(*tag);
+            if (value != NULL &&
+                value->IsSequence())
+            {
+              CopySequence(dicom, *tag, value->GetSequenceContent(), defaultPrivateCreator_, privateCreators_);
+            }
+            else
+            {
+              dicom.Replace(*tag, std::string(""), false, DicomReplaceMode_InsertIfAbsent, defaultPrivateCreator_);
+            }
+          }
+
+          answers_.Add(dicom);
+        }
+      }
+
+      virtual void MarkAsComplete() ORTHANC_OVERRIDE
+      {
+        answers_.SetComplete(true);
+      }
+    };
+  }
 
 
   void OrthancFindRequestHandler::Handle(DicomFindAnswers& answers,
@@ -396,7 +347,6 @@
       throw OrthancException(ErrorCode_NotImplemented);
     }
 
-
     DicomArray query(*filteredInput);
     CLOG(INFO, DICOM) << "DICOM C-Find request at level: " << EnumerationToString(level);
 
@@ -410,9 +360,12 @@
       }
     }
 
+    std::set<DicomTag> requestedTags;
+
     for (std::list<DicomTag>::const_iterator it = sequencesToReturn.begin();
          it != sequencesToReturn.end(); ++it)
     {
+      requestedTags.insert(*it);
       CLOG(INFO, DICOM) << "  (" << it->Format()
                         << ")  " << FromDcmtkBridge::GetTagName(*it, "")
                         << " : sequence tag whose content will be copied";
@@ -441,18 +394,26 @@
       const DicomTag tag = element.GetTag();
 
       // remove tags that are not used for matching
-      if (element.GetValue().IsNull() ||
-          tag == DICOM_TAG_QUERY_RETRIEVE_LEVEL ||
+      if (tag == DICOM_TAG_QUERY_RETRIEVE_LEVEL ||
           tag == DICOM_TAG_SPECIFIC_CHARACTER_SET ||
           tag == DICOM_TAG_TIMEZONE_OFFSET_FROM_UTC)  // time zone is not directly used for matching.  Once we support "Timezone query adjustment", we may use it to adjust date-time filters but for now, just ignore it 
       {
         continue;
       }
 
+      requestedTags.insert(tag);
+
+      if (element.GetValue().IsNull())
+      {
+        // There is no constraint on this tag
+        continue;
+      }
+
       std::string value = element.GetValue().GetContent();
       if (value.size() == 0)
       {
         // An empty string corresponds to an universal constraint, so we ignore it
+        requestedTags.insert(tag);
         continue;
       }
 
@@ -483,11 +444,12 @@
      * Run the query.
      **/
 
-    size_t limit = (level == ResourceType_Instance) ? maxInstances_ : maxResults_;
-
+    ResourceFinder finder(level, ResponseContentFlags_ID, context_.GetFindStorageAccessMode(), context_.GetIndex().HasFindSupport());
+    finder.SetDatabaseLookup(lookup);
+    finder.AddRequestedTags(requestedTags);
 
-    LookupVisitor visitor(answers, context_, level, *filteredInput, sequencesToReturn, privateCreators, context_.GetFindStorageAccessMode());
-    context_.Apply(visitor, lookup, level, 0 /* "since" is not relevant to C-FIND */, limit);
+    LookupVisitorV2 visitor(answers, *filteredInput, sequencesToReturn, privateCreators);
+    finder.Execute(visitor, context_);
   }
 
 
--- a/OrthancServer/Sources/OrthancFindRequestHandler.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/OrthancFindRequestHandler.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/OrthancGetRequestHandler.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/OrthancGetRequestHandler.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -165,6 +165,7 @@
      * 2. Select the preferred transfer syntaxes, which corresponds to
      * the source transfer syntax, plus all the uncompressed transfer
      * syntaxes if transcoding is enabled.
+     * This way, we minimize the transcoding on our side.
      **/
     
     std::list<DicomTransferSyntax> preferred;
@@ -208,7 +209,16 @@
       }
     }
 
-    // No preferred syntax was accepted
+    // No preferred syntax was accepted but, if a PC has been accepted, it means that we have accepted a TS.
+    // This maybe means that we need to transcode twice on our side (from a compressed format to another compressed format).
+    if (allowTranscoding && accepted.size() >  0)
+    {
+      Accepted::const_iterator it = accepted.begin();
+      selectedPresentationId = it->second;
+      selectedSyntax = it->first;
+      return true;
+    }
+
     return false;
   }                                                           
 
--- a/OrthancServer/Sources/OrthancGetRequestHandler.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/OrthancGetRequestHandler.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/OrthancHttpHandler.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/OrthancHttpHandler.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/OrthancHttpHandler.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/OrthancHttpHandler.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/OrthancInitialization.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/OrthancInitialization.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -59,6 +59,7 @@
 #    undef __FILE__
 #    define __FILE__ __ORTHANC_FILE__
 #  endif
+#  include <google/protobuf/stubs/common.h>
 #  include <google/protobuf/any.h>
 #endif
 
@@ -261,7 +262,17 @@
             {
               LOG(INFO) << "  - " << tagName;
             }
-            DicomMap::AddMainDicomTag(tag, level);
+
+            try
+            {
+              DicomMap::AddMainDicomTag(tag, level);
+            }
+            catch(OrthancException& e)
+            {
+              LOG(WARNING) << "  - !!! " << tagName << " is already defined as a standard MainDicomTags, it is useless to include it in the ExtraMainDicomTags";
+            }
+            
+            
           }
         }
       }
--- a/OrthancServer/Sources/OrthancInitialization.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/OrthancInitialization.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/OrthancMoveRequestHandler.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/OrthancMoveRequestHandler.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/OrthancMoveRequestHandler.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/OrthancMoveRequestHandler.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -58,6 +58,8 @@
 static const char* const SERIES = "Series";
 static const char* const TAGS = "Tags";
 static const char* const TRANSCODE = "Transcode";
+static const char* const LOSSY_QUALITY = "LossyQuality";
+
 
 
 namespace Orthanc
@@ -88,6 +90,10 @@
       .SetRequestField(TRANSCODE, RestApiCallDocumentation::Type_String,
                        "Transcode the DICOM instances to the provided DICOM transfer syntax: "
                        "https://orthanc.uclouvain.be/book/faq/transcoding.html", false)
+      .SetRequestField(LOSSY_QUALITY, RestApiCallDocumentation::Type_Number,
+                        "If transcoding to a lossy transfer syntax, this entry defines the quality "
+                        "as an integer between 1 and 100.  If not provided, the value is defined "
+                        "by the \"DicomLossyTranscodingQuality\" configuration. (new in v1.12.7)", false)
       .SetRequestField(FORCE, RestApiCallDocumentation::Type_Boolean,
                        "Allow the modification of tags related to DICOM identifiers, at the risk of "
                        "breaking the DICOM model of the real world", false)
@@ -116,6 +122,10 @@
       .SetRequestField(TRANSCODE, RestApiCallDocumentation::Type_String,
                        "Transcode the DICOM instances to the provided DICOM transfer syntax: "
                        "https://orthanc.uclouvain.be/book/faq/transcoding.html", false)
+      .SetRequestField(LOSSY_QUALITY, RestApiCallDocumentation::Type_Number,
+                        "If transcoding to a lossy transfer syntax, this entry defines the quality "
+                        "as an integer between 1 and 100.  If not provided, the value is defined "
+                        "by the \"DicomLossyTranscodingQuality\" configuration. (new in v1.12.7)", false)
       .SetRequestField(FORCE, RestApiCallDocumentation::Type_Boolean,
                        "Allow the modification of tags related to DICOM identifiers, at the risk of "
                        "breaking the DICOM model of the real world", false)
@@ -196,7 +206,8 @@
   static void AnonymizeOrModifyInstance(DicomModification& modification,
                                         RestApiPostCall& call,
                                         bool transcode,
-                                        DicomTransferSyntax targetSyntax)
+                                        DicomTransferSyntax targetSyntax,
+                                        unsigned int lossyQuality)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
     std::string id = call.GetUriComponent("id", "");
@@ -220,7 +231,7 @@
       std::set<DicomTransferSyntax> s;
       s.insert(targetSyntax);
       
-      if (context.Transcode(transcoded, source, s, true))
+      if (context.Transcode(transcoded, source, s, true, lossyQuality))
       {      
         call.GetOutput().AnswerBuffer(transcoded.GetBufferData(),
                                       transcoded.GetBufferSize(), MimeType_Dicom);
@@ -259,6 +270,20 @@
     }
   }
 
+  static unsigned int GetLossyQuality(const Json::Value& request)
+  {
+    unsigned int lossyQuality;
+    {
+      OrthancConfiguration::ReaderLock lock;
+      lossyQuality = lock.GetConfiguration().GetDicomLossyTranscodingQuality();
+    }
+
+    if (request.isMember(LOSSY_QUALITY)) 
+    {
+      lossyQuality = SerializationToolbox::ReadUnsignedInteger(request, LOSSY_QUALITY);
+    }
+    return lossyQuality;
+}
 
   static void ModifyInstance(RestApiPostCall& call)
   {
@@ -286,11 +311,11 @@
     if (request.isMember(TRANSCODE))
     {
       std::string s = SerializationToolbox::ReadString(request, TRANSCODE);
-      
+
       DicomTransferSyntax syntax;
       if (LookupTransferSyntax(syntax, s))
       {
-        AnonymizeOrModifyInstance(modification, call, true, syntax);
+        AnonymizeOrModifyInstance(modification, call, true, syntax, GetLossyQuality(request));
       }
       else
       {
@@ -300,7 +325,7 @@
     else
     {
       AnonymizeOrModifyInstance(modification, call, false /* no transcoding */,
-                                DicomTransferSyntax_LittleEndianImplicit /* unused */);
+                                DicomTransferSyntax_LittleEndianImplicit /* unused */, 0 /* unused */);
     }
   }
 
@@ -326,8 +351,25 @@
     Json::Value request;
     ParseAnonymizationRequest(request, modification, call);
 
-    AnonymizeOrModifyInstance(modification, call, false /* no transcoding */,
-                              DicomTransferSyntax_LittleEndianImplicit /* unused */);
+    if (request.isMember(TRANSCODE))
+    {
+      std::string s = SerializationToolbox::ReadString(request, TRANSCODE);
+
+      DicomTransferSyntax syntax;
+      if (LookupTransferSyntax(syntax, s))
+      {
+        AnonymizeOrModifyInstance(modification, call, true, syntax, GetLossyQuality(request));
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange, "Unknown transfer syntax: " + s);
+      }
+    }
+    else
+    {
+      AnonymizeOrModifyInstance(modification, call, false /* no transcoding */,
+                                DicomTransferSyntax_LittleEndianImplicit /* unused */, 0 /* unused */);
+    }
   }
 
 
@@ -807,7 +849,7 @@
       {
         // Retrieve all the instances of the parent resource
         std::list<std::string>  siblingInstances;
-        context.GetIndex().GetChildInstances(siblingInstances, parent);
+        context.GetIndex().GetChildInstances(siblingInstances, parent, parentType);
 
         if (siblingInstances.empty())
 	{
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -270,7 +270,16 @@
     RegisterAnonymizeModify();
     RegisterArchive();
 
-    Register("/instances", UploadDicomFile);
+    if (!context_.IsReadOnly())
+    {
+      Register("/instances", UploadDicomFile);
+    }
+    else
+    {
+      LOG(WARNING) << "READ-ONLY SYSTEM: deactivating POST /instances route";
+    }
+
+    
 
     // Auto-generated directories
     Register("/tools", RestApi::AutoListChildren);
@@ -472,7 +481,7 @@
                        "If `true`, run the job in asynchronous mode, which means that the REST API call will immediately "
                        "return, reporting the identifier of a job. Prefer this flavor wherever possible.", false)
       .SetRequestField(KEY_PRIORITY, RestApiCallDocumentation::Type_Number,
-                       "In asynchronous mode, the priority of the job. The lower the value, the higher the priority.", false)
+                       "In asynchronous mode, the priority of the job. The higher the value, the higher the priority.", false)
       .SetAnswerField("ID", RestApiCallDocumentation::Type_String, "In asynchronous mode, identifier of the job")
       .SetAnswerField("Path", RestApiCallDocumentation::Type_String, "In asynchronous mode, path to access the job in the REST API");
   }
@@ -492,11 +501,15 @@
   static const std::string GET_SHORT = "short";
   static const std::string GET_REQUESTED_TAGS_OLD = "requestedTags";  // This was the only option in Orthanc <= 1.12.3
   static const std::string GET_REQUESTED_TAGS = "requested-tags";
+  static const std::string GET_RESPONSE_CONTENT = "response-content";
+  static const std::string GET_EXPAND = "expand";
 
   static const std::string POST_SIMPLIFY = "Simplify";
   static const std::string POST_FULL = "Full";
   static const std::string POST_SHORT = "Short";
   static const std::string POST_REQUESTED_TAGS = "RequestedTags";
+  static const std::string POST_RESPONSE_CONTENT = "ResponseContent";
+  static const std::string POST_EXPAND = "Expand";
 
   static const std::string DOCUMENT_SIMPLIFY =
     "report the DICOM tags in human-readable format (using the symbolic name of the tags)";
@@ -625,19 +638,100 @@
       }
       catch (OrthancException& ex)
       {
-        throw OrthancException(ErrorCode_BadRequest, std::string("Invalid requestedTags argument: ") + ex.What() + " " + ex.GetDetails());
+        throw OrthancException(ErrorCode_BadRequest, std::string("Invalid requested-tags argument: ") + ex.What() + " " + ex.GetDetails());
       }
     }
   }
 
-  void OrthancRestApi::DocumentRequestedTags(RestApiGetCall& call)
+  void OrthancRestApi::DocumentRequestedTags(RestApiCall& call)
   {
+    if (call.GetMethod() == HttpMethod_Get)
+    {
       call.GetDocumentation().SetHttpGetArgument(GET_REQUESTED_TAGS, RestApiCallDocumentation::Type_String,
                           "If present, list the DICOM Tags you want to list in the response.  This argument is a semi-column separated list "
                           "of DICOM Tags identifiers; e.g: '" + GET_REQUESTED_TAGS + "=0010,0010;PatientBirthDate'.  "
                           "The tags requested tags are returned in the 'RequestedTags' field in the response.  "
                           "Note that, if you are requesting tags that are not listed in the Main Dicom Tags stored in DB, building the response "
-                          "might be slow since Orthanc will need to access the DICOM files.  If not specified, Orthanc will return ", false);
+                          "might be slow since Orthanc will need to access the DICOM files.  If not specified, Orthanc will return "
+                          "all Main Dicom Tags to keep backward compatibility with Orthanc prior to 1.11.0.", false);
+    }
+    else if (call.GetMethod() == HttpMethod_Post)
+    {
+      call.GetDocumentation().SetRequestField(POST_REQUESTED_TAGS, RestApiCallDocumentation::Type_JsonListOfStrings,
+                            "A list of DICOM tags to include in the response (applicable only if \"Expand\" is set to true).  "
+                            "The tags requested tags are returned in the 'RequestedTags' field in the response.  "
+                            "Note that, if you are requesting tags that are not listed in the Main Dicom Tags stored in DB, building the response "
+                            "might be slow since Orthanc will need to access the DICOM files.  If not specified, Orthanc will return "
+                            "all Main Dicom Tags to keep backward compatibility with Orthanc prior to 1.11.0.", false);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
   }
 
+  void OrthancRestApi::GetResponseContentAndExpand(ResponseContentFlags& responseContent,
+                                                   const RestApiGetCall& call)
+  {
+    if (call.HasArgument(GET_RESPONSE_CONTENT))
+    {
+      std::string s = call.GetArgument(GET_RESPONSE_CONTENT, "");
+      responseContent = ResponseContentFlags_Default;
+
+      if (!s.empty())
+      {
+        std::set<std::string> splitResponseContent;
+        Toolbox::SplitString(splitResponseContent, s, ';');
+
+        for (std::set<std::string>::const_iterator it = splitResponseContent.begin(); it != splitResponseContent.end(); ++it)
+        {
+          responseContent = static_cast<ResponseContentFlags>(static_cast<uint32_t>(responseContent) | StringToResponseContent(*it));
+        }
+      }
+    }
+    else if (call.HasArgument(GET_EXPAND) && call.GetBooleanArgument("expand", true))
+    {
+      responseContent = ResponseContentFlags_ExpandTrue;
+    }
+    else
+    {
+      responseContent = ResponseContentFlags_ID;
+    }
+  }
+
+  void OrthancRestApi::DocumentResponseContentAndExpand(RestApiCall& call)
+  {
+    if (call.GetMethod() == HttpMethod_Get)
+    {
+      call.GetDocumentation().SetHttpGetArgument(GET_RESPONSE_CONTENT, RestApiCallDocumentation::Type_String,
+                            "Defines the content of response for each returned resource.  Allowed values are `MainDicomTags`, "
+                            "`Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `Attachments`.  If not specified, Orthanc "
+                            "will return `MainDicomTags`, `Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`."
+                            "e.g: '" + GET_RESPONSE_CONTENT + "=MainDicomTags;Children "
+                            "(new in Orthanc 1.12.5 - overrides `expand`)", false);
+
+      call.GetDocumentation().SetHttpGetArgument(GET_EXPAND, RestApiCallDocumentation::Type_String,
+                            "If present, retrieve detailed information about the individual resources, not only their Orthanc identifiers", false);
+
+    }
+    else if (call.GetMethod() == HttpMethod_Post)
+    {
+      call.GetDocumentation().SetRequestField(POST_RESPONSE_CONTENT, RestApiCallDocumentation::Type_JsonListOfStrings,
+                            "Defines the content of response for each returned resource. (this field, if present, overrides the \"Expand\" field).  "
+                            "Allowed values are `MainDicomTags`, "
+                            "`Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `Attachments`.  If not specified, Orthanc "
+                            "will return `MainDicomTags`, `Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`."
+                            "(new in Orthanc 1.12.5)", false);
+
+      call.GetDocumentation().SetRequestField(POST_EXPAND, RestApiCallDocumentation::Type_Boolean,
+                            "If set to \"true\", retrieve detailed information about the individual resources, not only their Orthanc identifiers", false);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+  }
+
+
 }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -157,6 +157,11 @@
     static void GetRequestedTags(std::set<DicomTag>& requestedTags,
                                  const RestApiGetCall& call);
 
-    static void DocumentRequestedTags(RestApiGetCall& call);
+    static void DocumentRequestedTags(RestApiCall& call);
+
+    static void GetResponseContentAndExpand(ResponseContentFlags& responseContent,
+                                            const RestApiGetCall& call);
+
+    static void DocumentResponseContentAndExpand(RestApiCall& call);
   };
 }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -42,6 +42,13 @@
   static const char* const KEY_RESOURCES = "Resources";
   static const char* const KEY_EXTENDED = "Extended";
   static const char* const KEY_TRANSCODE = "Transcode";
+  static const char* const KEY_LOSSY_QUALITY = "LossyQuality";
+  static const char* const KEY_FILENAME = "Filename";
+  
+  static const char* const GET_TRANSCODE = "transcode";
+  static const char* const GET_LOSSY_QUALITY = "lossy-quality";
+  static const char* const GET_FILENAME = "filename";
+  static const char* const GET_RESOURCES = "resources";
 
   static const char* const CONFIG_LOADER_THREADS = "ZipLoaderThreads";
 
@@ -113,10 +120,13 @@
                                bool& extended,               /* out */
                                bool& transcode,              /* out */
                                DicomTransferSyntax& syntax,  /* out */
+                               unsigned int& lossyQuality,   /* out */
                                int& priority,                /* out */
                                unsigned int& loaderThreads,  /* out */
+                               std::string& filename,        /* out */
                                const Json::Value& body,      /* in */
-                               const bool defaultExtended    /* in */)
+                               const bool defaultExtended    /* in */,
+                               const std::string& defaultFilename /* in */)
   {
     synchronous = OrthancRestApi::IsSynchronousJobRequest
       (true /* synchronous by default */, body);
@@ -138,12 +148,32 @@
     {
       transcode = true;
       syntax = Orthanc::GetTransferSyntax(SerializationToolbox::ReadString(body, KEY_TRANSCODE));
+      
+      {
+        OrthancConfiguration::ReaderLock lock;
+        lossyQuality = lock.GetConfiguration().GetDicomLossyTranscodingQuality();
+      }
+
+      if (body.isMember(KEY_LOSSY_QUALITY)) 
+      {
+        lossyQuality = SerializationToolbox::ReadUnsignedInteger(body, KEY_LOSSY_QUALITY);
+      }
     }
     else
     {
       transcode = false;
     }
 
+    if (body.type() == Json::objectValue &&
+      body.isMember(KEY_FILENAME) && body[KEY_FILENAME].isString())
+    {
+      filename = body[KEY_FILENAME].asString();
+    }
+    else
+    {
+      filename = defaultFilename;
+    }
+
     {
       OrthancConfiguration::ReaderLock lock;
       loaderThreads = lock.GetConfiguration().GetUnsignedIntegerParameter(CONFIG_LOADER_THREADS, 0);  // New in Orthanc 1.10.0
@@ -487,6 +517,7 @@
     }
     else
     {
+      job->SetFilename(filename);
       OrthancRestApi::SubmitGenericJob(output, context, job.release(), false, priority);
     }
   }
@@ -508,8 +539,15 @@
       .SetRequestField(KEY_TRANSCODE, RestApiCallDocumentation::Type_String,
                        "If present, the DICOM files in the archive will be transcoded to the provided "
                        "transfer syntax: https://orthanc.uclouvain.be/book/faq/transcoding.html", false)
+      .SetRequestField(KEY_LOSSY_QUALITY, RestApiCallDocumentation::Type_Number,
+                        "If transcoding to a lossy transfer syntax, this entry defines the quality "
+                        "as an integer between 1 and 100.  If not provided, the value is defined "
+                        "by the \"DicomLossyTranscodingQuality\" configuration. (new in v1.12.7)", false)
+      .SetRequestField(KEY_FILENAME, RestApiCallDocumentation::Type_String,
+                        "Filename to set in the \"Content-Disposition\" HTTP header "
+                        "(including file extension)", false)
       .SetRequestField("Priority", RestApiCallDocumentation::Type_Number,
-                       "In asynchronous mode, the priority of the job. The lower the value, the higher the priority.", false)
+                       "In asynchronous mode, the priority of the job. The higher the value, the higher the priority.", false)
       .AddAnswerType(MimeType_Zip, "In synchronous mode, the ZIP file containing the archive")
       .AddAnswerType(MimeType_Json, "In asynchronous mode, information about the job that has been submitted to "
                      "generate the archive: https://orthanc.uclouvain.be/book/users/advanced-rest.html#jobs")
@@ -539,8 +577,11 @@
         .SetSummary("Create " + m)
         .SetDescription("Create a " + m + " containing the DICOM resources (patients, studies, series, or instances) "
                         "whose Orthanc identifiers are provided in the body")
-        .SetRequestField("Resources", RestApiCallDocumentation::Type_JsonListOfStrings,
-                         "The list of Orthanc identifiers of interest.", false);
+        .SetRequestField(KEY_RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings,
+                         "The list of Orthanc identifiers of interest.", false)
+        .SetRequestField(KEY_FILENAME, RestApiCallDocumentation::Type_String,
+                         "Filename to set in the \"Content-Disposition\" HTTP header "
+                         "(including file extension)", false);
       return;
     }
 
@@ -553,8 +594,11 @@
       DicomTransferSyntax transferSyntax;
       int priority;
       unsigned int loaderThreads;
-      GetJobParameters(synchronous, extended, transcode, transferSyntax,
-                       priority, loaderThreads, body, DEFAULT_IS_EXTENDED);
+      std::string filename;
+      unsigned int lossyQuality;
+
+      GetJobParameters(synchronous, extended, transcode, transferSyntax, lossyQuality,
+                       priority, loaderThreads, filename, body, DEFAULT_IS_EXTENDED, "Archive.zip");
       
       std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended, ResourceType_Patient));
       AddResourcesOfInterest(*job, body);
@@ -562,11 +606,12 @@
       if (transcode)
       {
         job->SetTranscode(transferSyntax);
+        job->SetLossyQuality(lossyQuality);
       }
       
       job->SetLoaderThreads(loaderThreads);
 
-      SubmitJob(call.GetOutput(), context, job, priority, synchronous, "Archive.zip");
+      SubmitJob(call.GetOutput(), context, job, priority, synchronous, filename);
     }
     else
     {
@@ -574,15 +619,23 @@
                              "Expected a list of resources to archive in the body");
     }
   }
-  
+
+  static unsigned int GetLossyQuality(const RestApiGetCall& call)
+  {
+    unsigned int lossyQuality;
+
+    OrthancConfiguration::ReaderLock lock;
+    lossyQuality = lock.GetConfiguration().GetDicomLossyTranscodingQuality();
+    lossyQuality = call.GetUnsignedInteger32Argument(GET_LOSSY_QUALITY, lossyQuality);
+    
+    return lossyQuality;
+  }
+
 
   template <bool IS_MEDIA,
             bool DEFAULT_IS_EXTENDED  /* only makes sense for media (i.e. not ZIP archives) */ >
   static void CreateBatchGet(RestApiGetCall& call)
   {
-    static const char* const TRANSCODE = "transcode";
-    static const char* const RESOURCES = "resources";
-
     if (call.IsDocumentation())
     {
       std::string m = (IS_MEDIA ? "DICOMDIR media" : "ZIP archive");
@@ -591,10 +644,17 @@
         .SetSummary("Create " + m)
         .SetDescription("Create a " + m + " containing the DICOM resources (patients, studies, series, or instances) "
                         "whose Orthanc identifiers are provided in the 'resources' argument")
-        .SetHttpGetArgument(TRANSCODE, RestApiCallDocumentation::Type_String,
+        .SetHttpGetArgument(GET_FILENAME, RestApiCallDocumentation::Type_String,
+                          "Filename to set in the \"Content-Disposition\" HTTP header "
+                          "(including file extension)", false)
+        .SetHttpGetArgument(GET_TRANSCODE, RestApiCallDocumentation::Type_String,
                             "If present, the DICOM files will be transcoded to the provided "
                             "transfer syntax: https://orthanc.uclouvain.be/book/faq/transcoding.html", false)
-        .SetHttpGetArgument(RESOURCES, RestApiCallDocumentation::Type_String,
+        .SetHttpGetArgument(GET_LOSSY_QUALITY, RestApiCallDocumentation::Type_Number,
+                            "If transcoding to a lossy transfer syntax, this entry defines the quality "
+                            "as an integer between 1 and 100.  If not provided, the value is defined "
+                            "by the \"DicomLossyTranscodingQuality\" configuration. (new in v1.12.7)", false)
+        .SetHttpGetArgument(GET_RESOURCES, RestApiCallDocumentation::Type_String,
                             "A comma separated list of Orthanc resource identifiers to include in the " + m + ".", true);
       return;
     }
@@ -602,27 +662,32 @@
     ServerContext& context = OrthancRestApi::GetContext(call);
     bool transcode = false;
     DicomTransferSyntax transferSyntax = DicomTransferSyntax_LittleEndianImplicit;  // Initialize variable to avoid warnings
+    unsigned int lossyQuality;
 
-    if (call.HasArgument(TRANSCODE))
+    if (call.HasArgument(GET_TRANSCODE))
     {
       transcode = true;
-      transferSyntax = GetTransferSyntax(call.GetArgument(TRANSCODE, ""));
+      transferSyntax = GetTransferSyntax(call.GetArgument(GET_TRANSCODE, ""));
+      lossyQuality = GetLossyQuality(call);
     }
     
-    if (!call.HasArgument(RESOURCES))
+    if (!call.HasArgument(GET_RESOURCES))
     {
-      throw OrthancException(Orthanc::ErrorCode_BadRequest, std::string("Missing ") + RESOURCES + " argument");
+      throw OrthancException(Orthanc::ErrorCode_BadRequest, std::string("Missing ") + GET_RESOURCES + " argument");
     }
 
     std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, DEFAULT_IS_EXTENDED, ResourceType_Patient));
-    AddResourcesOfInterestFromString(*job, call.GetArgument(RESOURCES, ""));
+    AddResourcesOfInterestFromString(*job, call.GetArgument(GET_RESOURCES, ""));
 
     if (transcode)
     {
       job->SetTranscode(transferSyntax);
+      job->SetLossyQuality(lossyQuality);
     }
 
-    SubmitJob(call.GetOutput(), context, job, 0, true, "Archive.zip");
+    const std::string filename = call.GetArgument(GET_FILENAME, "Archive.zip");  // New in Orthanc 1.12.7
+
+    SubmitJob(call.GetOutput(), context, job, 0, true, filename);
   }
 
 
@@ -630,8 +695,6 @@
             bool IS_MEDIA>
   static void CreateSingleGet(RestApiGetCall& call)
   {
-    static const char* const TRANSCODE = "transcode";
-    static const char* const FILENAME = "filename";
 
     if (call.IsDocumentation())
     {
@@ -646,12 +709,16 @@
                         "which might *not* be desirable to archive large amount of data, as it might "
                         "lead to network timeouts. Prefer the asynchronous version using `POST` method.")
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
-        .SetHttpGetArgument(FILENAME, RestApiCallDocumentation::Type_String,
+        .SetHttpGetArgument(GET_FILENAME, RestApiCallDocumentation::Type_String,
                             "Filename to set in the \"Content-Disposition\" HTTP header "
                             "(including file extension)", false)
-        .SetHttpGetArgument(TRANSCODE, RestApiCallDocumentation::Type_String,
+        .SetHttpGetArgument(GET_TRANSCODE, RestApiCallDocumentation::Type_String,
                             "If present, the DICOM files in the archive will be transcoded to the provided "
                             "transfer syntax: https://orthanc.uclouvain.be/book/faq/transcoding.html", false)
+        .SetHttpGetArgument(GET_LOSSY_QUALITY, RestApiCallDocumentation::Type_Number,
+                            "If transcoding to a lossy transfer syntax, this entry defines the quality "
+                            "as an integer between 1 and 100.  If not provided, the value is defined "
+                            "by the \"DicomLossyTranscodingQuality\" configuration. (new in v1.12.7)", false)
         .AddAnswerType(MimeType_Zip, "ZIP file containing the archive");
       if (IS_MEDIA)
       {
@@ -665,7 +732,7 @@
     ServerContext& context = OrthancRestApi::GetContext(call);
 
     const std::string id = call.GetUriComponent("id", "");
-    const std::string filename = call.GetArgument(FILENAME, id + ".zip");  // New in Orthanc 1.11.0
+    const std::string filename = call.GetArgument(GET_FILENAME, id + ".zip");  // New in Orthanc 1.11.0
 
     bool extended;
     if (IS_MEDIA)
@@ -680,9 +747,10 @@
     std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended, (LEVEL == ResourceType_Patient ? ResourceType_Patient : ResourceType_Study))); // use patient info from study except when exporting a patient
     job->AddResource(id, true, LEVEL);
 
-    if (call.HasArgument(TRANSCODE))
+    if (call.HasArgument(GET_TRANSCODE))
     {
-      job->SetTranscode(GetTransferSyntax(call.GetArgument(TRANSCODE, "")));
+      job->SetTranscode(GetTransferSyntax(call.GetArgument(GET_TRANSCODE, "")));
+      job->SetLossyQuality(GetLossyQuality(call));
     }
 
     {
@@ -726,8 +794,10 @@
       DicomTransferSyntax transferSyntax;
       int priority;
       unsigned int loaderThreads;
-      GetJobParameters(synchronous, extended, transcode, transferSyntax,
-                       priority, loaderThreads, body, false /* by default, not extented */);
+      std::string filename;
+      unsigned int lossyQuality;
+      GetJobParameters(synchronous, extended, transcode, transferSyntax, lossyQuality,
+                       priority, loaderThreads, filename, body, false /* by default, not extented */, id + ".zip");
       
       std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended, LEVEL));
       job->AddResource(id, true, LEVEL);
@@ -735,11 +805,12 @@
       if (transcode)
       {
         job->SetTranscode(transferSyntax);
+        job->SetLossyQuality(lossyQuality);
       }
 
       job->SetLoaderThreads(loaderThreads);
 
-      SubmitJob(call.GetOutput(), context, job, priority, synchronous, id + ".zip");
+      SubmitJob(call.GetOutput(), context, job, priority, synchronous, filename);
     }
     else
     {
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestChanges.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestChanges.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -29,14 +29,15 @@
 namespace Orthanc
 {
   // Changes API --------------------------------------------------------------
+  static const unsigned int DEFAULT_LIMIT = 100;
+  static const int64_t DEFAULT_TO = -1;
  
-  static void GetSinceAndLimit(int64_t& since,
-                               unsigned int& limit,
-                               bool& last,
-                               const RestApiGetCall& call)
+  static void GetSinceToAndLimit(int64_t& since,
+                                 int64_t& to,
+                                 unsigned int& limit,
+                                 bool& last,
+                                 const RestApiGetCall& call)
   {
-    static const unsigned int DEFAULT_LIMIT = 100;
-    
     if (call.HasArgument("last"))
     {
       last = true;
@@ -48,11 +49,13 @@
     try
     {
       since = boost::lexical_cast<int64_t>(call.GetArgument("since", "0"));
+      to = boost::lexical_cast<int64_t>(call.GetArgument("to", boost::lexical_cast<std::string>(DEFAULT_TO)));
       limit = boost::lexical_cast<unsigned int>(call.GetArgument("limit", boost::lexical_cast<std::string>(DEFAULT_LIMIT)));
     }
     catch (boost::bad_lexical_cast&)
     {
       since = 0;
+      to = DEFAULT_TO;
       limit = DEFAULT_LIMIT;
       return;
     }
@@ -65,34 +68,66 @@
       call.GetDocumentation()
         .SetTag("Tracking changes")
         .SetSummary("List changes")
-        .SetDescription("Whenever Orthanc receives a new DICOM instance, this event is recorded in the so-called _Changes Log_. This enables remote scripts to react to the arrival of new DICOM resources. A typical application is auto-routing, where an external script waits for a new DICOM instance to arrive into Orthanc, then forward this instance to another modality.")
+        .SetDescription("Whenever Orthanc receives a new DICOM instance, this event is recorded in the so-called _Changes Log_. This enables remote scripts to react to the arrival of new DICOM resources. A typical application is auto-routing, where an external script waits for a new DICOM instance to arrive into Orthanc, then forward this instance to another modality. Please note that, when resources are deleted, their corresponding change entries are also removed from the Changes Log, which helps ensuring that this log does not grow indefinitely.")
+        .SetHttpGetArgument("last", RestApiCallDocumentation::Type_Number, "Request only the last change id (this argument must be used alone)", false)
         .SetHttpGetArgument("limit", RestApiCallDocumentation::Type_Number, "Limit the number of results", false)
-        .SetHttpGetArgument("since", RestApiCallDocumentation::Type_Number, "Show only the resources since the provided index", false)
+        .SetHttpGetArgument("since", RestApiCallDocumentation::Type_Number, "Show only the resources since the provided index excluded", false)
+        .SetHttpGetArgument("to", RestApiCallDocumentation::Type_Number, "Show only the resources till the provided index included (only available if your DB backend supports ExtendedChanges)", false)
+        .SetHttpGetArgument("type", RestApiCallDocumentation::Type_String, "Show only the changes of the provided type (only available if your DB backend supports ExtendedChanges).  Multiple values can be provided and must be separated by a ';'.", false)
         .AddAnswerType(MimeType_Json, "The list of changes")
         .SetAnswerField("Changes", RestApiCallDocumentation::Type_JsonListOfObjects, "The individual changes")
         .SetAnswerField("Done", RestApiCallDocumentation::Type_Boolean,
-                        "Whether the last reported change is the last of the full history")
+                        "Whether the last reported change is the last of the full history.")
         .SetAnswerField("Last", RestApiCallDocumentation::Type_Number,
                         "The index of the last reported change, can be used for the `since` argument in subsequent calls to this route")
+        .SetAnswerField("First", RestApiCallDocumentation::Type_Number,
+                        "The index of the first reported change, its value-1 can be used for the `to` argument in subsequent calls to this route when browsing the changes in reverse order")
         .SetHttpGetSample("https://orthanc.uclouvain.be/demo/changes?since=0&limit=2", true);
       return;
     }
     
     ServerContext& context = OrthancRestApi::GetContext(call);
 
-    //std::string filter = GetArgument(getArguments, "filter", "");
-    int64_t since;
+    int64_t since, to;
+    std::set<ChangeType> filterType;
+
     unsigned int limit;
     bool last;
-    GetSinceAndLimit(since, limit, last, call);
+    GetSinceToAndLimit(since, to, limit, last, call);
+
+    std::string filterArgument = call.GetArgument("type", "all");
+    if (filterArgument != "all" && filterArgument != "All")
+    {
+      std::set<std::string> filterTypeStrings;
+      Toolbox::SplitString(filterTypeStrings, filterArgument, ';');
+
+      for (std::set<std::string>::const_iterator it = filterTypeStrings.begin(); it != filterTypeStrings.end(); ++it)
+      {
+        filterType.insert(StringToChangeType(*it));
+      }
+    }
 
     Json::Value result;
     if (last)
     {
       context.GetIndex().GetLastChange(result);
     }
+    else if (context.GetIndex().HasExtendedChanges())
+    {
+      context.GetIndex().GetChangesExtended(result, since, to, limit, filterType);
+    }
     else
     {
+      if (filterType.size() > 0)
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange, "CAPABILITIES: Trying to filter changes while the Database backend does not support it (requires a DB backend with support for ExtendedChanges)");
+      }
+
+      if (to != DEFAULT_TO)
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange, "CAPABILITIES: Trying to use the 'to' parameter in /changes while the Database backend does not support it (requires a DB backend with support for ExtendedChanges)");
+      }
+
       context.GetIndex().GetChanges(result, since, limit);
     }
 
@@ -139,10 +174,10 @@
 
     ServerContext& context = OrthancRestApi::GetContext(call);
 
-    int64_t since;
+    int64_t since, to;
     unsigned int limit;
     bool last;
-    GetSinceAndLimit(since, limit, last, call);
+    GetSinceToAndLimit(since, to, limit, last, call);
 
     Json::Value result;
     if (last)
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -36,6 +36,7 @@
 #include "../ServerContext.h"
 #include "../ServerJobs/DicomModalityStoreJob.h"
 #include "../ServerJobs/DicomMoveScuJob.h"
+#include "../ServerJobs/DicomGetScuJob.h"
 #include "../ServerJobs/OrthancPeerStoreJob.h"
 #include "../ServerToolbox.h"
 #include "../StorageCommitmentReports.h"
@@ -56,6 +57,7 @@
   static const char* const KEY_CHECK_FIND = "CheckFind";
   static const char* const SOP_CLASS_UID = "SOPClassUID";
   static const char* const SOP_INSTANCE_UID = "SOPInstanceUID";
+  static const char* const KEY_RETRIEVE_METHOD = "RetrieveMethod";
   
   static RemoteModalityParameters MyGetModalityUsingSymbolicName(const std::string& name)
   {
@@ -155,24 +157,31 @@
                           const DicomAssociationParameters& parameters,
                           const Json::Value& body)
   {
-    DicomControlUserConnection connection(parameters);
-
+    bool checkFind = false;
+    
+    if (body.type() == Json::objectValue &&
+        body.isMember(KEY_CHECK_FIND))
+    {
+      checkFind = SerializationToolbox::ReadBoolean(body, KEY_CHECK_FIND);
+    }
+    else
+    {
+      OrthancConfiguration::ReaderLock lock;
+      checkFind = lock.GetConfiguration().GetBooleanParameter("DicomEchoChecksFind", false);
+    }
+
+    ScuOperationFlags operations = ScuOperationFlags_Echo;
+    
+    if (checkFind)
+    {
+      operations = static_cast<ScuOperationFlags>(operations | ScuOperationFlags_Find);
+    }
+
+    DicomControlUserConnection connection(parameters, operations);
     if (connection.Echo())
     {
-      bool find = false;
-      
-      if (body.type() == Json::objectValue &&
-          body.isMember(KEY_CHECK_FIND))
-      {
-        find = SerializationToolbox::ReadBoolean(body, KEY_CHECK_FIND);
-      }
-      else
-      {
-        OrthancConfiguration::ReaderLock lock;
-        find = lock.GetConfiguration().GetBooleanParameter("DicomEchoChecksFind", false);
-      }
-
-      if (find)
+
+      if (checkFind)
       {
         // Issue a C-FIND request at the study level about a random Study Instance UID
         const std::string studyInstanceUid = FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study);
@@ -385,7 +394,7 @@
     DicomFindAnswers answers(false);
 
     {
-      DicomControlUserConnection connection(GetAssociationParameters(call));
+      DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_FindPatient);
       FindPatient(answers, connection, fields);
     }
 
@@ -428,7 +437,7 @@
     DicomFindAnswers answers(false);
 
     {
-      DicomControlUserConnection connection(GetAssociationParameters(call));
+      DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_FindStudy);
       FindStudy(answers, connection, fields);
     }
 
@@ -472,7 +481,7 @@
     DicomFindAnswers answers(false);
 
     {
-      DicomControlUserConnection connection(GetAssociationParameters(call));
+      DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_FindStudy);
       FindSeries(answers, connection, fields);
     }
 
@@ -517,7 +526,7 @@
     DicomFindAnswers answers(false);
 
     {
-      DicomControlUserConnection connection(GetAssociationParameters(call));
+      DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_FindStudy);
       FindInstance(answers, connection, fields);
     }
 
@@ -566,7 +575,7 @@
       return;
     }
  
-    DicomControlUserConnection connection(GetAssociationParameters(call));
+    DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_Find);
     
     DicomFindAnswers patients(false);
     FindPatient(patients, connection, m);
@@ -914,12 +923,25 @@
 
     std::string targetAet;
     int timeout = -1;
-    
+
+    QueryAccessor query(call);
+
+    RetrieveMethod retrieveMethod = query.GetHandler().GetRemoteModality().GetRetrieveMethod();
+
     Json::Value body;
     if (call.ParseJsonRequest(body))
     {
+      OrthancConfiguration::ReaderLock lock;
+
       targetAet = Toolbox::GetJsonStringField(body, KEY_TARGET_AET, context.GetDefaultLocalApplicationEntityTitle());
       timeout = Toolbox::GetJsonIntegerField(body, KEY_TIMEOUT, -1);
+      
+      std::string strRetrieveMethod = SerializationToolbox::ReadString(body, KEY_RETRIEVE_METHOD, "");
+      
+      if (!strRetrieveMethod.empty())
+      {
+        retrieveMethod = StringToRetrieveMethod(strRetrieveMethod);
+      }
     }
     else
     {
@@ -934,45 +956,67 @@
       }
     }
     
-    std::unique_ptr<DicomMoveScuJob> job(new DicomMoveScuJob(context));
-    job->SetQueryFormat(OrthancRestApi::GetDicomFormat(body, DicomToJsonFormat_Short));
-    
+    if (retrieveMethod == RetrieveMethod_SystemDefault)
+    {
+      retrieveMethod = context.GetDefaultDicomRetrieveMethod();
+    }
+
+    std::unique_ptr<DicomRetrieveScuBaseJob> job;
+
+
+    switch (retrieveMethod)
     {
-      QueryAccessor query(call);
-      job->SetTargetAet(targetAet);
-      job->SetLocalAet(query.GetHandler().GetLocalAet());
-      job->SetRemoteModality(query.GetHandler().GetRemoteModality());
-
-      if (timeout >= 0)
+      case RetrieveMethod_Move:
       {
-        // New in Orthanc 1.7.0
-        job->SetTimeout(static_cast<uint32_t>(timeout));
-      }
-      else if (query.GetHandler().HasTimeout())
+        job.reset(new DicomMoveScuJob(context));
+        (dynamic_cast<DicomMoveScuJob*>(job.get()))->SetTargetAet(targetAet);
+
+        LOG(WARNING) << "Driving C-Move SCU on remote modality "
+                    << query.GetHandler().GetRemoteModality().GetApplicationEntityTitle()
+                    << " to target modality " << targetAet;
+      }; break;
+      case RetrieveMethod_Get:
       {
-        // New in Orthanc 1.9.1
-        job->SetTimeout(query.GetHandler().GetTimeout());
-      }
-
-      LOG(WARNING) << "Driving C-Move SCU on remote modality "
-                   << query.GetHandler().GetRemoteModality().GetApplicationEntityTitle()
-                   << " to target modality " << targetAet;
-
-      if (allAnswers)
+        job.reset(new DicomGetScuJob(context));
+
+        LOG(WARNING) << "Driving C-Get SCU on remote modality "
+                    << query.GetHandler().GetRemoteModality().GetApplicationEntityTitle();
+      }; break;
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    job->SetQueryFormat(OrthancRestApi::GetDicomFormat(body, DicomToJsonFormat_Short));
+
+    job->SetLocalAet(query.GetHandler().GetLocalAet());
+    job->SetRemoteModality(query.GetHandler().GetRemoteModality());
+
+    if (timeout >= 0)
+    {
+      // New in Orthanc 1.7.0
+      job->SetTimeout(static_cast<uint32_t>(timeout));
+    }
+    else if (query.GetHandler().HasTimeout())
+    {
+      // New in Orthanc 1.9.1
+      job->SetTimeout(query.GetHandler().GetTimeout());
+    }
+
+    if (allAnswers)
+    {
+      for (size_t i = 0; i < query.GetHandler().GetAnswersCount(); i++)
       {
-        for (size_t i = 0; i < query.GetHandler().GetAnswersCount(); i++)
-        {
-          job->AddFindAnswer(query.GetHandler(), i);
-        }
+        job->AddFindAnswer(query.GetHandler(), i);
       }
-      else
-      {
-        job->AddFindAnswer(query.GetHandler(), index);
-      }
+    }
+    else
+    {
+      job->AddFindAnswer(query.GetHandler(), index);
     }
 
     OrthancRestApi::GetApi(call).SubmitCommandsJob
       (call, job.release(), true /* synchronous by default */, body);
+
   }
 
 
@@ -989,6 +1033,10 @@
                        "`DicomAet` configuration option.", false)
       .SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number,
                        "Timeout for the C-MOVE command, in seconds", false)
+      .SetRequestField(KEY_RETRIEVE_METHOD, RestApiCallDocumentation::Type_String,
+                        "Force usage of C-MOVE or C-GET to retrieve the resource.  If note defined in the payload, "
+                        "the retrieve method is defined in the DicomDefaultRetrieveMethod configuration or in "
+                        "DicomModalities->..->RetrieveMethod", false)
       .AddRequestType(MimeType_PlainText, "AET of the target modality");
   }
   
@@ -999,8 +1047,8 @@
     {
       DocumentRetrieveShared(call);
       call.GetDocumentation()
-        .SetSummary("Retrieve one answer")
-        .SetDescription("Start a C-MOVE SCU command as a job, in order to retrieve one answer associated with the "
+        .SetSummary("Retrieve one answer with a C-MOVE or a C-GET SCU")
+        .SetDescription("Start a C-MOVE or a C-GET SCU command as a job, in order to retrieve one answer associated with the "
                         "query/retrieve operation whose identifiers are provided in the URL: "
                         "https://orthanc.uclouvain.be/book/users/rest.html#performing-retrieve-c-move")
         .SetUriArgument("index", "Index of the answer");
@@ -1012,13 +1060,15 @@
   }
 
 
+
+
   static void RetrieveAllAnswers(RestApiPostCall& call)
   {
     if (call.IsDocumentation())
     {
       DocumentRetrieveShared(call);
       call.GetDocumentation()
-        .SetSummary("Retrieve all answers")
+        .SetSummary("Retrieve all answers with C-MOVE SCU")
         .SetDescription("Start a C-MOVE SCU command as a job, in order to retrieve all the answers associated with the "
                         "query/retrieve operation whose identifier is provided in the URL: "
                         "https://orthanc.uclouvain.be/book/users/rest.html#performing-retrieve-c-move");
@@ -1536,6 +1586,48 @@
     call.GetOutput().AnswerJson(answer);
   }
 
+  void ParseMoveGetJob(DicomRetrieveScuBaseJob& job, Json::Value& request, RestApiPostCall& call)
+  {
+    const ServerContext& context = OrthancRestApi::GetContext(call);
+
+    if (!call.ParseJsonRequest(request) ||
+        request.type() != Json::objectValue ||
+        !request.isMember(KEY_RESOURCES) ||
+        !request.isMember(KEY_LEVEL) ||
+        request[KEY_RESOURCES].type() != Json::arrayValue ||
+        request[KEY_LEVEL].type() != Json::stringValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON body containing fields " +
+                             std::string(KEY_RESOURCES) + " and " + std::string(KEY_LEVEL));
+    }
+
+    ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString());
+    
+    std::string localAet = Toolbox::GetJsonStringField
+      (request, KEY_LOCAL_AET, context.GetDefaultLocalApplicationEntityTitle());
+
+    const RemoteModalityParameters source =
+      MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+
+    job.SetQueryFormat(DicomToJsonFormat_Short);
+    job.SetLocalAet(localAet);
+    job.SetRemoteModality(source);
+
+    if (request.isMember(KEY_TIMEOUT))
+    {
+      job.SetTimeout(SerializationToolbox::ReadUnsignedInteger(request, KEY_TIMEOUT));
+    }
+
+    for (Json::Value::ArrayIndex i = 0; i < request[KEY_RESOURCES].size(); i++)
+    {
+      DicomMap resource;
+      FromDcmtkBridge::FromJson(resource, request[KEY_RESOURCES][i], "Resources elements");
+
+      resource.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, std::string(ResourceTypeToDicomQueryRetrieveLevel(level)), false);
+
+      job.AddQuery(resource);      
+    }
+  }
 
   /***************************************************************************
    * DICOM C-Move SCU
@@ -1569,53 +1661,15 @@
     }
 
     ServerContext& context = OrthancRestApi::GetContext(call);
-
     Json::Value request;
 
-    if (!call.ParseJsonRequest(request) ||
-        request.type() != Json::objectValue ||
-        !request.isMember(KEY_RESOURCES) ||
-        !request.isMember(KEY_LEVEL) ||
-        request[KEY_RESOURCES].type() != Json::arrayValue ||
-        request[KEY_LEVEL].type() != Json::stringValue)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON body containing fields " +
-                             std::string(KEY_RESOURCES) + " and " + std::string(KEY_LEVEL));
-    }
-
-    ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString());
-    
-    std::string localAet = Toolbox::GetJsonStringField
-      (request, KEY_LOCAL_AET, context.GetDefaultLocalApplicationEntityTitle());
+    std::unique_ptr<DicomMoveScuJob> job(new DicomMoveScuJob(context));
+
+    ParseMoveGetJob(*job, request, call);
+
     std::string targetAet = Toolbox::GetJsonStringField
       (request, KEY_TARGET_AET, context.GetDefaultLocalApplicationEntityTitle());
-
-    const RemoteModalityParameters source =
-      MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
-
-    std::unique_ptr<DicomMoveScuJob> job(new DicomMoveScuJob(context));
-
-    job->SetQueryFormat(DicomToJsonFormat_Short);
-    
-    // QueryAccessor query(call);
     job->SetTargetAet(targetAet);
-    job->SetLocalAet(localAet);
-    job->SetRemoteModality(source);
-
-    if (request.isMember(KEY_TIMEOUT))
-    {
-      job->SetTimeout(SerializationToolbox::ReadUnsignedInteger(request, KEY_TIMEOUT));
-    }
-
-    for (Json::Value::ArrayIndex i = 0; i < request[KEY_RESOURCES].size(); i++)
-    {
-      DicomMap resource;
-      FromDcmtkBridge::FromJson(resource, request[KEY_RESOURCES][i], "Resources elements");
-
-      resource.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, std::string(ResourceTypeToDicomQueryRetrieveLevel(level)), false);
-
-      job->AddQuery(resource);      
-    }
 
     OrthancRestApi::GetApi(call).SubmitCommandsJob
       (call, job.release(), true /* synchronous by default */, request);
@@ -1623,6 +1677,51 @@
   }
 
 
+  /***************************************************************************
+   * DICOM C-Get SCU
+   ***************************************************************************/
+  
+  static void DicomGet(RestApiPostCall& call)
+  {
+    if (call.IsDocumentation())
+    {
+      OrthancRestApi::DocumentSubmitCommandsJob(call);
+      call.GetDocumentation()
+        .SetTag("Networking")
+        .SetSummary("Trigger C-GET SCU")
+        .SetDescription("Start a C-GET SCU command as a job, in order to retrieve DICOM resources "
+                        "from a remote DICOM modality whose identifier is provided in the URL: ")
+                        // "https://orthanc.uclouvain.be/book/users/rest.html#performing-c-move")   // TODO-GET
+        .SetRequestField(KEY_RESOURCES, RestApiCallDocumentation::Type_JsonListOfObjects,
+                         "List of queries identifying all the DICOM resources to be sent.  "
+                         "Usage of wildcards is prohibited and the query shall only contain DICOM ID tags.  "
+                         "Additionally, you may provide SOPClassesInStudy to limit the scope of the DICOM "
+                         "negotiation to certain SOPClassUID or to present uncommon SOPClassUID during "
+                         "the DICOM negotiation.  By default, "
+                         "Orhanc will propose the most 120 common SOPClassUIDs.", true)
+        .SetRequestField(KEY_QUERY, RestApiCallDocumentation::Type_JsonObject,
+                         "A query object identifying all the DICOM resources to be retrieved", true)
+        .SetRequestField(KEY_LOCAL_AET, RestApiCallDocumentation::Type_String,
+                         "Local AET that is used for this commands, defaults to `DicomAet` configuration option. "
+                         "Ignored if `DicomModalities` already sets `LocalAet` for this modality.", false)
+        .SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number,
+                         "Timeout for the C-GET command, in seconds", false)
+        .SetUriArgument("id", "Identifier of the modality of interest");
+      return;
+    }
+
+    ServerContext& context = OrthancRestApi::GetContext(call);
+    Json::Value request;
+
+    std::unique_ptr<DicomGetScuJob> job(new DicomGetScuJob(context));
+
+    ParseMoveGetJob(*job, request, call);
+
+    OrthancRestApi::GetApi(call).SubmitCommandsJob
+      (call, job.release(), true /* synchronous by default */, request);
+    return;
+  }
+
 
   /***************************************************************************
    * Orthanc Peers => Store client
@@ -2213,7 +2312,7 @@
       DicomFindAnswers answers(true);
 
       {
-        DicomControlUserConnection connection(GetAssociationParameters(call, json));
+        DicomControlUserConnection connection(GetAssociationParameters(call, json), ScuOperationFlags_FindWorklist);
         connection.FindWorklist(answers, *query);
       }
 
@@ -2544,6 +2643,7 @@
     Register("/modalities/{id}/store", DicomStore);
     Register("/modalities/{id}/store-straight", DicomStoreStraight);  // New in 1.6.1
     Register("/modalities/{id}/move", DicomMove);
+    Register("/modalities/{id}/get", DicomGet);
     Register("/modalities/{id}/configuration", GetModalityConfiguration);  // New in 1.8.1
 
     // For Query/Retrieve
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -22,6 +22,8 @@
 
 
 #include "../PrecompiledHeadersServer.h"
+#include "../ResourceFinder.h"
+
 #include "OrthancRestApi.h"
 
 #include "../../../OrthancFramework/Sources/Compression/GzipCompressor.h"
@@ -39,6 +41,7 @@
 
 #include "../OrthancConfiguration.h"
 #include "../Search/DatabaseLookup.h"
+#include "../Search/DatabaseMetadataConstraint.h"
 #include "../ServerContext.h"
 #include "../ServerToolbox.h"
 #include "../SliceOrdering.h"
@@ -47,6 +50,8 @@
 #include <boost/math/special_functions/round.hpp>
 #include <boost/shared_ptr.hpp>
 
+#include "../../../OrthancFramework/Sources/FileStorage/StorageAccessor.h"
+
 /**
  * This semaphore is used to limit the number of concurrent HTTP
  * requests on CPU-intensive routes of the REST API, in order to
@@ -65,6 +70,14 @@
 
 namespace Orthanc
 {
+  static ResourceType GetResourceTypeFromUri(const RestApiCall& call)
+  {
+    assert(!call.GetFullUri().empty());
+    const std::string resourceType = call.GetFullUri() [0];
+    return StringToResourceType(resourceType.c_str());
+  }
+
+
   static std::string GetDocumentationSampleResource(ResourceType type)
   {
     switch (type)
@@ -127,79 +140,29 @@
   }
 
 
+  static bool ExpandResource(Json::Value& target,
+                             ServerContext& context,
+                             ResourceType level,
+                             const std::string& identifier,
+                             DicomToJsonFormat format,
+                             bool retrieveMetadata)
+  {
+    ResponseContentFlags responseContent = ResponseContentFlags_ExpandTrue;
+    
+    if (retrieveMetadata)
+    {
+      responseContent = static_cast<ResponseContentFlags>(static_cast<uint32_t>(responseContent) | ResponseContentFlags_Metadata);
+    }
+
+    ResourceFinder finder(level, responseContent, context.GetFindStorageAccessMode(), context.GetIndex().HasFindSupport());
+    finder.SetOrthancId(level, identifier);
+
+    return finder.ExecuteOneResource(target, context, format, retrieveMetadata);
+  }
+
+
   // List all the patients, studies, series or instances ----------------------
  
-  static void AnswerListOfResources(RestApiOutput& output,
-                                    ServerContext& context,
-                                    const std::list<std::string>& resources,
-                                    const std::map<std::string, std::string>& instancesIds, // optional: the id of an instance for each found resource.
-                                    const std::map<std::string, boost::shared_ptr<DicomMap> >& resourcesMainDicomTags,  // optional: all tags read from DB for a resource (current level and upper levels)
-                                    const std::map<std::string, boost::shared_ptr<Json::Value> >& resourcesDicomAsJson, // optional: the dicom-as-json for each resource
-                                    ResourceType level,
-                                    bool expand,
-                                    DicomToJsonFormat format,
-                                    const std::set<DicomTag>& requestedTags,
-                                    bool allowStorageAccess)
-  {
-    Json::Value answer = Json::arrayValue;
-
-    for (std::list<std::string>::const_iterator
-           resource = resources.begin(); resource != resources.end(); ++resource)
-    {
-      if (expand)
-      {
-        Json::Value expanded;
-
-        std::map<std::string, std::string>::const_iterator instanceId = instancesIds.find(*resource);
-        if (instanceId != instancesIds.end())  // if it is found in instancesIds, it is also in resourcesDicomAsJson and mainDicomTags
-        {
-          // reuse data already collected before (e.g during lookup)
-          std::map<std::string, boost::shared_ptr<DicomMap> >::const_iterator mainDicomTags = resourcesMainDicomTags.find(*resource);
-          std::map<std::string, boost::shared_ptr<Json::Value> >::const_iterator dicomAsJson = resourcesDicomAsJson.find(*resource);
-
-          context.ExpandResource(expanded, *resource, 
-                                 *(mainDicomTags->second.get()),
-                                 instanceId->second,
-                                 dicomAsJson->second.get(),
-                                 level, format, requestedTags, allowStorageAccess);
-        }
-        else
-        {
-          context.ExpandResource(expanded, *resource, level, format, requestedTags, allowStorageAccess);
-        }
-
-        if (expanded.type() == Json::objectValue)
-        {
-          answer.append(expanded);
-        }
-      }
-      else
-      {
-        answer.append(*resource);
-      }
-    }
-
-    output.AnswerJson(answer);
-  }
-
-
-  static void AnswerListOfResources(RestApiOutput& output,
-                                    ServerContext& context,
-                                    const std::list<std::string>& resources,
-                                    ResourceType level,
-                                    bool expand,
-                                    DicomToJsonFormat format,
-                                    const std::set<DicomTag>& requestedTags,
-                                    bool allowStorageAccess)
-  {
-    std::map<std::string, std::string> unusedInstancesIds;
-    std::map<std::string, boost::shared_ptr<DicomMap> > unusedResourcesMainDicomTags;
-    std::map<std::string, boost::shared_ptr<Json::Value> > unusedResourcesDicomAsJson;
-
-    AnswerListOfResources(output, context, resources, unusedInstancesIds, unusedResourcesMainDicomTags, unusedResourcesDicomAsJson, level, expand, format, requestedTags, allowStorageAccess);
-  }
-
-
   template <enum ResourceType resourceType>
   static void ListResources(RestApiGetCall& call)
   {
@@ -215,21 +178,26 @@
         .SetDescription("List the Orthanc identifiers of all the available DICOM " + resources)
         .SetHttpGetArgument("limit", RestApiCallDocumentation::Type_Number, "Limit the number of results", false)
         .SetHttpGetArgument("since", RestApiCallDocumentation::Type_Number, "Show only the resources since the provided index", false)
-        .SetHttpGetArgument("expand", RestApiCallDocumentation::Type_String,
-                            "If present, retrieve detailed information about the individual " + resources, false)
         .AddAnswerType(MimeType_Json, "JSON array containing either the Orthanc identifiers, or detailed information "
                        "about the reported " + resources + " (if `expand` argument is provided)")
         .SetHttpGetSample("https://orthanc.uclouvain.be/demo/" + resources + "?since=0&limit=2", true);
+      OrthancRestApi::DocumentResponseContentAndExpand(call);
       return;
     }
-    
-    ServerIndex& index = OrthancRestApi::GetIndex(call);
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    std::list<std::string> result;
+
+    // TODO-FIND: include the FindRequest options parsing like since, limit in a method (parse from get-arguments and from post payload)
 
     std::set<DicomTag> requestedTags;
+    ResponseContentFlags responseContent;
+    
     OrthancRestApi::GetRequestedTags(requestedTags, call);
+    OrthancRestApi::GetResponseContentAndExpand(responseContent, call);
+
+    ResourceFinder finder(resourceType, 
+                          responseContent, 
+                          OrthancRestApi::GetContext(call).GetFindStorageAccessMode(),
+                          OrthancRestApi::GetContext(call).GetIndex().HasFindSupport());
+    finder.AddRequestedTags(requestedTags);
 
     if (call.HasArgument("limit") ||
         call.HasArgument("since"))
@@ -248,19 +216,16 @@
                                call.FlattenUri());
       }
 
-      size_t since = boost::lexical_cast<size_t>(call.GetArgument("since", ""));
-      size_t limit = boost::lexical_cast<size_t>(call.GetArgument("limit", ""));
-      index.GetAllUuids(result, resourceType, since, limit);
+      uint64_t since = boost::lexical_cast<uint64_t>(call.GetArgument("since", ""));
+      uint64_t limit = boost::lexical_cast<uint64_t>(call.GetArgument("limit", ""));
+      finder.SetLimitsSince(since);
+      finder.SetLimitsCount(limit);
     }
-    else
-    {
-      index.GetAllUuids(result, resourceType);
-    }
-
-    AnswerListOfResources(call.GetOutput(), context, result, resourceType, call.HasArgument("expand") && call.GetBooleanArgument("expand", true),
-                          OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human),
-                          requestedTags,
-                          true /* allowStorageAccess */);
+
+    Json::Value answer;
+    finder.Execute(answer, OrthancRestApi::GetContext(call),
+                   OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human), false /* no "Metadata" field */);
+    call.GetOutput().AnswerJson(answer);
   }
 
 
@@ -284,14 +249,20 @@
       return;
     }
 
-    const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human);
-
     std::set<DicomTag> requestedTags;
     OrthancRestApi::GetRequestedTags(requestedTags, call);
 
+    const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human);
+
+    ResourceFinder finder(resourceType, 
+                          ResponseContentFlags_ExpandTrue, 
+                          OrthancRestApi::GetContext(call).GetFindStorageAccessMode(),
+                          OrthancRestApi::GetContext(call).GetIndex().HasFindSupport());
+    finder.AddRequestedTags(requestedTags);
+    finder.SetOrthancId(resourceType, call.GetUriComponent("id", ""));
+
     Json::Value json;
-    if (OrthancRestApi::GetContext(call).ExpandResource(
-          json, call.GetUriComponent("id", ""), resourceType, format, requestedTags, true /* allowStorageAccess */))
+    if (finder.ExecuteOneResource(json, OrthancRestApi::GetContext(call), format, false /* no "Metadata" field */))
     {
       call.GetOutput().AnswerJson(json);
     }
@@ -366,7 +337,9 @@
  
   static void GetInstanceFile(RestApiGetCall& call)
   {
-    static const char* const TRANSCODE = "transcode";
+    static const char* const GET_TRANSCODE = "transcode";
+    static const char* const GET_LOSSY_QUALITY = "lossy-quality";
+    static const char* const GET_FILENAME = "filename";
 
     if (call.IsDocumentation())
     {
@@ -376,9 +349,16 @@
         .SetDescription("Download one DICOM instance")
         .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest")
         .SetHttpHeader("Accept", "This HTTP header can be set to retrieve the DICOM instance in DICOMweb format")
-        .SetHttpGetArgument(TRANSCODE, RestApiCallDocumentation::Type_String,
+        .SetHttpGetArgument(GET_TRANSCODE, RestApiCallDocumentation::Type_String,
                             "If present, the DICOM file will be transcoded to the provided "
                             "transfer syntax: https://orthanc.uclouvain.be/book/faq/transcoding.html", false)
+        .SetHttpGetArgument(GET_LOSSY_QUALITY, RestApiCallDocumentation::Type_Number,
+                            "If transcoding to a lossy transfer syntax, this entry defines the quality "
+                            "as an integer between 1 and 100.  If not provided, the value is defined "
+                            "by the \"DicomLossyTranscodingQuality\" configuration. (new in v1.12.7)", false)
+        .SetHttpGetArgument(GET_FILENAME, RestApiCallDocumentation::Type_String,
+                              "Filename to set in the \"Content-Disposition\" HTTP header "
+                              "(including file extension)", false)
         .AddAnswerType(MimeType_Dicom, "The DICOM instance")
         .AddAnswerType(MimeType_DicomWebJson, "The DICOM instance, in DICOMweb JSON format")
         .AddAnswerType(MimeType_DicomWebXml, "The DICOM instance, in DICOMweb XML format");
@@ -427,22 +407,56 @@
       }
     }
 
-    if (call.HasArgument(TRANSCODE))
+    const std::string filename = call.GetArgument(GET_FILENAME, publicId + ".dcm");  // New in Orthanc 1.12.7
+
+    if (call.HasArgument(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 (context.TranscodeWithCache(transcoded, source, publicId, attachmentId, GetTransferSyntax(call.GetArgument(TRANSCODE, ""))))
+      if (lossyQuality != defaultLossyQuality) // we can't use the cache if the lossy quality is not the default one
       {
+        IDicomTranscoder::DicomImage targetImage;
+        IDicomTranscoder::DicomImage sourceImage;
+        sourceImage.SetExternalBuffer(source);
+        std::set<DicomTransferSyntax> allowedSyntaxes;
+        allowedSyntaxes.insert(GetTransferSyntax(call.GetArgument(GET_TRANSCODE, "")));
+
+        if (context.Transcode(targetImage, sourceImage, allowedSyntaxes, true, 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);
       }
     }
     else
     {
       // return the attachment without any transcoding
-      context.AnswerAttachment(call.GetOutput(), publicId, FileContentType_Dicom);
+      FileInfo info;
+      int64_t revision;
+      if (!context.GetIndex().LookupAttachment(info, revision, ResourceType_Instance, publicId, FileContentType_Dicom))
+      {
+        throw OrthancException(ErrorCode_UnknownResource);
+      }
+      else
+      {
+        context.AnswerAttachment(call.GetOutput(), info, filename);
+      }
     }
   }
 
@@ -869,7 +883,7 @@
             .SetTag("Instances")
             .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest")
             .SetHttpGetArgument("quality", RestApiCallDocumentation::Type_Number, "Quality for JPEG images (between 1 and 100, defaults to 90)", false)
-            .SetHttpGetArgument("returnUnsupportedImage", RestApiCallDocumentation::Type_Boolean, "Returns an unsupported.png placeholder image if unable to provide the image instead of returning a 415 HTTP error (defaults to false)", false)
+            .SetHttpGetArgument("returnUnsupportedImage", RestApiCallDocumentation::Type_Boolean, "Returns an unsupported.png placeholder image if unable to provide the image instead of returning a 415 HTTP error (value is true if option is present)", false)
             .SetHttpHeader("Accept", "Format of the resulting image. Can be `image/png` (default), `image/jpeg` or `image/x-portable-arbitrarymap`")
             .AddAnswerType(MimeType_Png, "PNG image")
             .AddAnswerType(MimeType_Jpeg, "JPEG image")
@@ -932,7 +946,8 @@
           }
           else
           {
-            if (call.HasArgument("returnUnsupportedImage"))
+            // if present and not explicitly set to false
+            if (call.HasArgument("returnUnsupportedImage") && call.GetBooleanArgument("returnUnsupportedImage", true))
             {
               std::string root = "";
               for (size_t i = 1; i < call.GetFullUri().size(); i++)
@@ -1619,12 +1634,13 @@
 
   static void GetResourceStatistics(RestApiGetCall& call)
   {
+    const ResourceType level = GetResourceTypeFromUri(call);
+
     if (call.IsDocumentation())
     {
-      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
-      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */);
       call.GetDocumentation()
-        .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
+        .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */))
         .SetSummary("Get " + r + " statistics")
         .SetDescription("Get statistics about the given " + r)
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
@@ -1645,9 +1661,9 @@
                         "Size on the disk of the uncompressed DICOM instances associated with the " + r + ", expressed in bytes")
         .SetAnswerField("DicomUncompressedSizeMB", RestApiCallDocumentation::Type_Number,
                         "Size on the disk of the uncompressed DICOM instances associated with the " + r + ", expressed in megabytes (MB)")
-        .SetHttpGetSample(GetDocumentationSampleResource(t) + "/statistics", true);
-
-      switch (t)
+        .SetHttpGetSample(GetDocumentationSampleResource(level) + "/statistics", true);
+
+      switch (level)
       {
         // Do NOT add "break" below this point!
         case ResourceType_Patient:
@@ -1716,22 +1732,15 @@
 
   // Handling of metadata -----------------------------------------------------
 
-  static void CheckValidResourceType(const RestApiCall& call)
-  {
-    assert(!call.GetFullUri().empty());
-    const std::string resourceType = call.GetFullUri() [0];
-    StringToResourceType(resourceType.c_str());
-  }
-
-
   static void ListMetadata(RestApiGetCall& call)
   {
+    const ResourceType level = GetResourceTypeFromUri(call);
+
     if (call.IsDocumentation())
     {
-      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
-      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */);
       call.GetDocumentation()
-        .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
+        .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */))
         .SetSummary("List metadata")
         .SetDescription("Get the list of metadata that are associated with the given " + r)
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
@@ -1741,13 +1750,12 @@
                             "If present, use the numeric identifier of the metadata instead of its symbolic name", false)
         .AddAnswerType(MimeType_Json, "JSON array containing the names of the available metadata, "
                        "or JSON associative array mapping metadata to their values (if `expand` argument is provided)")
-        .SetHttpGetSample(GetDocumentationSampleResource(t) + "/metadata", true);
+        .SetHttpGetSample(GetDocumentationSampleResource(level) + "/metadata", true);
       return;
     }
 
     assert(!call.GetFullUri().empty());
     const std::string publicId = call.GetUriComponent("id", "");
-    ResourceType level = StringToResourceType(call.GetFullUri() [0].c_str());
 
     typedef std::map<MetadataType, std::string>  Metadata;
 
@@ -1879,12 +1887,13 @@
 
   static void GetMetadata(RestApiGetCall& call)
   {
+    const ResourceType level = GetResourceTypeFromUri(call);
+
     if (call.IsDocumentation())
     {
-      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
-      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */);
       call.GetDocumentation()
-        .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
+        .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */))
         .SetSummary("Get metadata")
         .SetDescription("Get the value of a metadata that is associated with the given " + r)
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
@@ -1897,7 +1906,6 @@
 
     assert(!call.GetFullUri().empty());
     const std::string publicId = call.GetUriComponent("id", "");
-    const ResourceType level = StringToResourceType(call.GetFullUri() [0].c_str());
 
     std::string name = call.GetUriComponent("name", "");
     MetadataType metadata = StringToMetadata(name);
@@ -1926,12 +1934,13 @@
 
   static void DeleteMetadata(RestApiDeleteCall& call)
   {
+    const ResourceType level = GetResourceTypeFromUri(call);
+
     if (call.IsDocumentation())
     {
-      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
-      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */);
       call.GetDocumentation()
-        .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
+        .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */))
         .SetSummary("Delete metadata")
         .SetDescription("Delete some metadata associated with the given DICOM " + r +
                         ". This call will fail if trying to delete a system metadata (i.e. whose index is < 1024).")
@@ -1942,7 +1951,6 @@
       return;
     }
 
-    CheckValidResourceType(call);
     const std::string publicId = call.GetUriComponent("id", "");
 
     std::string name = call.GetUriComponent("name", "");
@@ -1990,12 +1998,13 @@
 
   static void SetMetadata(RestApiPutCall& call)
   {
+    const ResourceType level = GetResourceTypeFromUri(call);
+
     if (call.IsDocumentation())
     {
-      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
-      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */);
       call.GetDocumentation()
-        .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
+        .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */))
         .SetSummary("Set metadata")
         .SetDescription("Set the value of some metadata in the given DICOM " + r +
                         ". This call will fail if trying to modify a system metadata (i.e. whose index is < 1024).")
@@ -2006,8 +2015,6 @@
       return;
     }
 
-    CheckValidResourceType(call);
-
     std::string publicId = call.GetUriComponent("id", "");
     std::string name = call.GetUriComponent("name", "");
     MetadataType metadata = StringToMetadata(name);
@@ -2055,23 +2062,22 @@
 
   static void ListLabels(RestApiGetCall& call)
   {
+    const ResourceType level = GetResourceTypeFromUri(call);
+
     if (call.IsDocumentation())
     {
-      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
-      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */);
       call.GetDocumentation()
-        .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
+        .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */))
         .SetSummary("List labels")
         .SetDescription("Get the labels that are associated with the given " + r + " (new in Orthanc 1.12.0)")
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
         .AddAnswerType(MimeType_Json, "JSON array containing the names of the labels")
-        .SetHttpGetSample(GetDocumentationSampleResource(t) + "/labels", true);
+        .SetHttpGetSample(GetDocumentationSampleResource(level) + "/labels", true);
       return;
     }
 
-    assert(!call.GetFullUri().empty());
     const std::string publicId = call.GetUriComponent("id", "");
-    ResourceType level = StringToResourceType(call.GetFullUri() [0].c_str());
 
     std::set<std::string> labels;
     OrthancRestApi::GetIndex(call).ListLabels(labels, publicId, level);
@@ -2089,12 +2095,13 @@
 
   static void GetLabel(RestApiGetCall& call)
   {
+    const ResourceType level = GetResourceTypeFromUri(call);
+
     if (call.IsDocumentation())
     {
-      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
-      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */);
       call.GetDocumentation()
-        .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
+        .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */))
         .SetSummary("Test label")
         .SetDescription("Test whether the " + r + " is associated with the given label")
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
@@ -2103,11 +2110,7 @@
       return;
     }
 
-    CheckValidResourceType(call);
-
-    assert(!call.GetFullUri().empty());
     const std::string publicId = call.GetUriComponent("id", "");
-    const ResourceType level = StringToResourceType(call.GetFullUri() [0].c_str());
 
     std::string label = call.GetUriComponent("label", "");
 
@@ -2123,12 +2126,13 @@
 
   static void AddLabel(RestApiPutCall& call)
   {
+    const ResourceType level = GetResourceTypeFromUri(call);
+
     if (call.IsDocumentation())
     {
-      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
-      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */);
       call.GetDocumentation()
-        .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
+        .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */))
         .SetSummary("Add label")
         .SetDescription("Associate a label with a " + r)
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
@@ -2136,10 +2140,7 @@
       return;
     }
 
-    CheckValidResourceType(call);
-
     std::string publicId = call.GetUriComponent("id", "");
-    const ResourceType level = StringToResourceType(call.GetFullUri() [0].c_str());
 
     std::string label = call.GetUriComponent("label", "");
     OrthancRestApi::GetIndex(call).ModifyLabel(publicId, level, label, StatelessDatabaseOperations::LabelOperation_Add);
@@ -2150,12 +2151,13 @@
 
   static void RemoveLabel(RestApiDeleteCall& call)
   {
+    const ResourceType level = GetResourceTypeFromUri(call);
+
     if (call.IsDocumentation())
     {
-      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
-      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */);
       call.GetDocumentation()
-        .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
+        .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */))
         .SetSummary("Remove label")
         .SetDescription("Remove a label associated with a " + r)
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
@@ -2163,10 +2165,7 @@
       return;
     }
 
-    CheckValidResourceType(call);
-
     std::string publicId = call.GetUriComponent("id", "");
-    const ResourceType level = StringToResourceType(call.GetFullUri() [0].c_str());
 
     std::string label = call.GetUriComponent("label", "");
     OrthancRestApi::GetIndex(call).ModifyLabel(publicId, level, label, StatelessDatabaseOperations::LabelOperation_Remove);
@@ -2179,26 +2178,26 @@
 
   static void ListAttachments(RestApiGetCall& call)
   {
+    const ResourceType level = GetResourceTypeFromUri(call);
+
     if (call.IsDocumentation())
     {
-      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
-      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */);
       call.GetDocumentation()
-        .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
+        .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */))
         .SetSummary("List attachments")
         .SetDescription("Get the list of attachments that are associated with the given " + r)
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
         .SetHttpGetArgument("full", RestApiCallDocumentation::Type_String,
                             "If present, retrieve the attachments list and their numerical ids", false)
         .AddAnswerType(MimeType_Json, "JSON array containing the names of the attachments")
-        .SetHttpGetSample(GetDocumentationSampleResource(t) + "/attachments", true);
+        .SetHttpGetSample(GetDocumentationSampleResource(level) + "/attachments", true);
       return;
     }
 
-    const std::string resourceType = call.GetFullUri() [0];
     const std::string publicId = call.GetUriComponent("id", "");
     std::set<FileContentType> attachments;
-    OrthancRestApi::GetIndex(call).ListAvailableAttachments(attachments, publicId, StringToResourceType(resourceType.c_str()));
+    OrthancRestApi::GetIndex(call).ListAvailableAttachments(attachments, publicId, level);
 
     Json::Value result;
 
@@ -2207,7 +2206,7 @@
       result = Json::objectValue;
       
       for (std::set<FileContentType>::const_iterator 
-            it = attachments.begin(); it != attachments.end(); ++it)
+             it = attachments.begin(); it != attachments.end(); ++it)
       {
         std::string key = EnumerationToString(*it);
         result[key] = static_cast<uint16_t>(*it);
@@ -2218,7 +2217,7 @@
       result = Json::arrayValue;
       
       for (std::set<FileContentType>::const_iterator 
-            it = attachments.begin(); it != attachments.end(); ++it)
+             it = attachments.begin(); it != attachments.end(); ++it)
       {
         result.append(EnumerationToString(*it));
       }
@@ -2239,17 +2238,15 @@
   }
 
   
-  static bool GetAttachmentInfo(FileInfo& info,
+  static bool GetAttachmentInfo(FileInfo& info /* out */,
+                                int64_t& revision /* out */,
+                                ResourceType level,
                                 RestApiGetCall& call)
   {
-    CheckValidResourceType(call);
- 
     const std::string publicId = call.GetUriComponent("id", "");
-    const std::string name = call.GetUriComponent("name", "");
-    FileContentType contentType = StringToContentType(name);
-
-    int64_t revision;
-    if (OrthancRestApi::GetIndex(call).LookupAttachment(info, revision, publicId, contentType))
+    FileContentType contentType = StringToContentType(call.GetUriComponent("name", ""));
+
+    if (OrthancRestApi::GetIndex(call).LookupAttachment(info, revision, level, publicId, contentType))
     {
       SetAttachmentETag(call.GetOutput(), revision, info);  // New in Orthanc 1.9.2
 
@@ -2276,10 +2273,11 @@
 
   static void GetAttachmentOperations(RestApiGetCall& call)
   {
+    const ResourceType level = GetResourceTypeFromUri(call);
+
     if (call.IsDocumentation())
     {
-      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
-      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */);
       AddAttachmentDocumentation(call, r);
       call.GetDocumentation()
         .SetTag("Other")
@@ -2291,7 +2289,8 @@
     }
 
     FileInfo info;
-    if (GetAttachmentInfo(info, call))
+    int64_t revision;
+    if (GetAttachmentInfo(info, revision, level, call))
     {
       Json::Value operations = Json::arrayValue;
 
@@ -2330,59 +2329,82 @@
   template <int uncompress>
   static void GetAttachmentData(RestApiGetCall& call)
   {
+    const ResourceType level = GetResourceTypeFromUri(call);
+
+    static const char* const GET_FILENAME = "filename";
+
     if (call.IsDocumentation())
     {
-      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
-      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */);
       call.GetDocumentation()
-        .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
+        .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */))
         .SetSummary("Get attachment" + std::string(uncompress ? "" : " (no decompression)"))
         .SetDescription("Get the (binary) content of one attachment associated with the given " + r +
                         std::string(uncompress ? "" : ". The attachment will not be decompressed if `StorageCompression` is `true`."))
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
         .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)")
+        .SetHttpGetArgument(GET_FILENAME, RestApiCallDocumentation::Type_String,
+          "Filename to set in the \"Content-Disposition\" HTTP header "
+          "(including file extension)", false)
         .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 metadata, to check if its content has changed");
-      return;
+        .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)");
+    
+        return;
     }
 
     ServerContext& context = OrthancRestApi::GetContext(call);
 
-    CheckValidResourceType(call);
- 
-    std::string publicId = call.GetUriComponent("id", "");
-    FileContentType type = StringToContentType(call.GetUriComponent("name", ""));
+    bool hasRangeHeader = false;
+    StorageAccessor::Range range;
+
+    HttpToolbox::Arguments::const_iterator rangeHeader = call.GetHttpHeaders().find("range");
+    if (rangeHeader != call.GetHttpHeaders().end())
+    {
+      hasRangeHeader = true;
+      range = StorageAccessor::Range::ParseHttpRange(rangeHeader->second);
+    }
 
     FileInfo info;
-    if (GetAttachmentInfo(info, call))
+    int64_t revision;
+    if (GetAttachmentInfo(info, revision, level, call))
     {
       // NB: "SetAttachmentETag()" is already invoked by "GetAttachmentInfo()"
 
-      if (uncompress)
+      int64_t userRevision;
+      std::string userMD5;
+      if (GetRevisionHeader(userRevision, userMD5, call, "If-None-Match") &&
+          revision == userRevision &&
+          info.GetUncompressedMD5() == userMD5)
       {
-        context.AnswerAttachment(call.GetOutput(), publicId, type);
+        call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified);
+        return;
+      }
+
+      const std::string filename = call.GetArgument(GET_FILENAME, info.GetUuid());  // New in Orthanc 1.12.7
+
+      if (hasRangeHeader)
+      {
+        std::string fragment;
+        context.ReadAttachmentRange(fragment, 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);
+      }
+      else if (uncompress ||
+               info.GetCompressionType() == CompressionType_None)
+      {
+        context.AnswerAttachment(call.GetOutput(), info, filename);
       }
       else
       {
-        // Return the raw data (possibly compressed), as stored on the filesystem
+        // Access to the raw attachment (which is compressed)
         std::string content;
-        std::string attachmentId;
-        int64_t revision;
-        context.ReadAttachment(content, revision, attachmentId, publicId, type, false, true /* skipCache when you absolutely need the compressed data */);
-
-        int64_t userRevision;
-        std::string userMD5;
-        if (GetRevisionHeader(userRevision, userMD5, call, "If-None-Match") &&
-            revision == userRevision &&
-            info.GetUncompressedMD5() == userMD5)
-        {
-          call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified);
-        }
-        else
-        {
-          call.GetOutput().AnswerBuffer(content, MimeType_Binary);
-        }
+        context.ReadAttachment(content, info, false /* don't uncompress */, true /* skip cache */);
+        call.GetOutput().AnswerBuffer(content, MimeType_Binary);
       }
     }
   }
@@ -2390,13 +2412,14 @@
 
   static void GetAttachmentSize(RestApiGetCall& call)
   {
+    const ResourceType level = GetResourceTypeFromUri(call);
+
     if (call.IsDocumentation())
     {
-      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
-      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */);
       AddAttachmentDocumentation(call, r);
       call.GetDocumentation()
-        .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
+        .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */))
         .SetSummary("Get size of attachment")
         .SetDescription("Get the size of one attachment associated with the given " + r)
         .AddAnswerType(MimeType_PlainText, "The size of the attachment");
@@ -2404,7 +2427,8 @@
     }
 
     FileInfo info;
-    if (GetAttachmentInfo(info, call))
+    int64_t revision;
+    if (GetAttachmentInfo(info, revision, level, call))
     {
       call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetUncompressedSize()), MimeType_PlainText);
     }
@@ -2412,13 +2436,14 @@
 
   static void GetAttachmentInfo(RestApiGetCall& call)
   {
+    const ResourceType level = GetResourceTypeFromUri(call);
+
     if (call.IsDocumentation())
     {
-      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
-      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */);
       AddAttachmentDocumentation(call, r);
       call.GetDocumentation()
-        .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
+        .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */))
         .SetSummary("Get info about the attachment")
         .SetDescription("Get all the information about the attachment associated with the given " + r)
         .AddAnswerType(MimeType_Json, "JSON object containing the information about the attachment")
@@ -2427,7 +2452,8 @@
     }
 
     FileInfo info;
-    if (GetAttachmentInfo(info, call))
+    int64_t revision;
+    if (GetAttachmentInfo(info, revision, level, call))
     {
       Json::Value result = Json::objectValue;    
       result["Uuid"] = info.GetUuid();
@@ -2443,13 +2469,14 @@
 
   static void GetAttachmentCompressedSize(RestApiGetCall& call)
   {
+    const ResourceType level = GetResourceTypeFromUri(call);
+
     if (call.IsDocumentation())
     {
-      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
-      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */);
       AddAttachmentDocumentation(call, r);
       call.GetDocumentation()
-        .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
+        .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */))
         .SetSummary("Get size of attachment on disk")
         .SetDescription("Get the size of one attachment associated with the given " + r + ", as stored on the disk. "
                         "This is different from `.../size` iff `EnableStorage` is `true`.")
@@ -2458,7 +2485,8 @@
     }
 
     FileInfo info;
-    if (GetAttachmentInfo(info, call))
+    int64_t revision;
+    if (GetAttachmentInfo(info, revision, level, call))
     {
       call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetCompressedSize()), MimeType_PlainText);
     }
@@ -2467,13 +2495,14 @@
 
   static void GetAttachmentMD5(RestApiGetCall& call)
   {
+    const ResourceType level = GetResourceTypeFromUri(call);
+
     if (call.IsDocumentation())
     {
-      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
-      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */);
       AddAttachmentDocumentation(call, r);
       call.GetDocumentation()
-        .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
+        .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */))
         .SetSummary("Get MD5 of attachment")
         .SetDescription("Get the MD5 hash of one attachment associated with the given " + r)
         .AddAnswerType(MimeType_PlainText, "The MD5 of the attachment");
@@ -2481,7 +2510,8 @@
     }
 
     FileInfo info;
-    if (GetAttachmentInfo(info, call) &&
+    int64_t revision;
+    if (GetAttachmentInfo(info, revision, level, call) &&
         info.GetUncompressedMD5() != "")
     {
       call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetUncompressedMD5()), MimeType_PlainText);
@@ -2491,13 +2521,14 @@
 
   static void GetAttachmentCompressedMD5(RestApiGetCall& call)
   {
+    const ResourceType level = GetResourceTypeFromUri(call);
+
     if (call.IsDocumentation())
     {
-      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
-      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */);
       AddAttachmentDocumentation(call, r);
       call.GetDocumentation()
-        .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
+        .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */))
         .SetSummary("Get MD5 of attachment on disk")
         .SetDescription("Get the MD5 hash of one attachment associated with the given " + r + ", as stored on the disk. "
                         "This is different from `.../md5` iff `EnableStorage` is `true`.")
@@ -2506,7 +2537,8 @@
     }
 
     FileInfo info;
-    if (GetAttachmentInfo(info, call) &&
+    int64_t revision;
+    if (GetAttachmentInfo(info, revision, level, call) &&
         info.GetCompressedMD5() != "")
     {
       call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetCompressedMD5()), MimeType_PlainText);
@@ -2516,12 +2548,13 @@
 
   static void VerifyAttachment(RestApiPostCall& call)
   {
+    const ResourceType level = GetResourceTypeFromUri(call);
+
     if (call.IsDocumentation())
     {
-      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
-      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */);
       call.GetDocumentation()
-        .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
+        .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */))
         .SetSummary("Verify attachment")
         .SetDescription("Verify that the attachment is not corrupted, by validating its MD5 hash")
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
@@ -2531,7 +2564,6 @@
     }
 
     ServerContext& context = OrthancRestApi::GetContext(call);
-    CheckValidResourceType(call);
 
     std::string publicId = call.GetUriComponent("id", "");
     std::string name = call.GetUriComponent("name", "");
@@ -2539,7 +2571,7 @@
 
     FileInfo info;
     int64_t revision;  // Ignored
-    if (!OrthancRestApi::GetIndex(call).LookupAttachment(info, revision, publicId, contentType) ||
+    if (!OrthancRestApi::GetIndex(call).LookupAttachment(info, revision, level, publicId, contentType) ||
         info.GetCompressedMD5() == "" ||
         info.GetUncompressedMD5() == "")
     {
@@ -2551,9 +2583,7 @@
 
     // First check whether the compressed data is correctly stored in the disk
     std::string data;
-    std::string attachmentId;
-
-    context.ReadAttachment(data, revision, attachmentId, publicId, StringToContentType(name), false, true /* skipCache when you absolutely need the compressed data */);
+    context.ReadAttachment(data, info, false, true /* skipCache when you absolutely need the compressed data */);
 
     std::string actualMD5;
     Toolbox::ComputeMD5(actualMD5, data);
@@ -2568,7 +2598,7 @@
       }
       else
       {
-        context.ReadAttachment(data, revision, attachmentId, publicId, StringToContentType(name), true, true /* skipCache when you absolutely need the compressed data */);
+        context.ReadAttachment(data, info, true, true /* skipCache when you absolutely need the compressed data */);
         Toolbox::ComputeMD5(actualMD5, data);
         ok = (actualMD5 == info.GetUncompressedMD5());
       }
@@ -2588,12 +2618,13 @@
 
   static void UploadAttachment(RestApiPutCall& call)
   {
+    const ResourceType level = GetResourceTypeFromUri(call);
+
     if (call.IsDocumentation())
     {
-      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
-      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */);
       call.GetDocumentation()
-        .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
+        .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */))
         .SetSummary("Set attachment")
         .SetDescription("Attach a file to the given DICOM " + r +
                         ". This call will fail if trying to modify a system attachment (i.e. whose index is < 1024).")
@@ -2606,7 +2637,6 @@
     }
 
     ServerContext& context = OrthancRestApi::GetContext(call);
-    CheckValidResourceType(call);
  
     std::string publicId = call.GetUriComponent("id", "");
     std::string name = call.GetUriComponent("name", "");
@@ -2649,12 +2679,13 @@
 
   static void DeleteAttachment(RestApiDeleteCall& call)
   {
+    const ResourceType level = GetResourceTypeFromUri(call);
+
     if (call.IsDocumentation())
     {
-      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
-      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */);
       call.GetDocumentation()
-        .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
+        .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */))
         .SetSummary("Delete attachment")
         .SetDescription("Delete an attachment associated with the given DICOM " + r +
                         ". This call will fail if trying to delete a system attachment (i.e. whose index is < 1024).")
@@ -2665,8 +2696,6 @@
       return;
     }
 
-    CheckValidResourceType(call);
-
     std::string publicId = call.GetUriComponent("id", "");
     std::string name = call.GetUriComponent("name", "");
     FileContentType contentType = StringToContentType(name);
@@ -2738,12 +2767,13 @@
   template <enum CompressionType compression>
   static void ChangeAttachmentCompression(RestApiPostCall& call)
   {
+    const ResourceType level = GetResourceTypeFromUri(call);
+
     if (call.IsDocumentation())
     {
-      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
-      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */);
       call.GetDocumentation()
-        .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
+        .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */))
         .SetSummary(compression == CompressionType_None ? "Uncompress attachment" : "Compress attachment")
         .SetDescription("Change the compression scheme that is used to store an attachment.")
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
@@ -2751,26 +2781,25 @@
       return;
     }
 
-    CheckValidResourceType(call);
-
     std::string publicId = call.GetUriComponent("id", "");
     std::string name = call.GetUriComponent("name", "");
     FileContentType contentType = StringToContentType(name);
 
-    OrthancRestApi::GetContext(call).ChangeAttachmentCompression(publicId, contentType, compression);
+    OrthancRestApi::GetContext(call).ChangeAttachmentCompression(level, publicId, contentType, compression);
     call.GetOutput().AnswerBuffer("{}", MimeType_Json);
   }
 
 
   static void IsAttachmentCompressed(RestApiGetCall& call)
   {
+    const ResourceType level = GetResourceTypeFromUri(call);
+
     if (call.IsDocumentation())
     {
-      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
-      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */);
       AddAttachmentDocumentation(call, r);
       call.GetDocumentation()
-        .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
+        .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */))
         .SetSummary("Is attachment compressed?")
         .SetDescription("Test whether the attachment has been stored as a compressed file on the disk.")
         .AddAnswerType(MimeType_PlainText, "`0` if the attachment was stored uncompressed, `1` if it was compressed");
@@ -2778,7 +2807,8 @@
     }
 
     FileInfo info;
-    if (GetAttachmentInfo(info, call))
+    int64_t revision;
+    if (GetAttachmentInfo(info, revision, level, call))
     {
       std::string answer = (info.GetCompressionType() == CompressionType_None) ? "0" : "1";
       call.GetOutput().AnswerBuffer(answer, MimeType_PlainText);
@@ -2816,12 +2846,13 @@
 
   static bool ExtractSharedTags(Json::Value& shared,
                                 ServerContext& context,
-                                const std::string& publicId)
+                                const std::string& publicId,
+                                ResourceType level)
   {
     // Retrieve all the instances of this patient/study/series
     typedef std::list<std::string> Instances;
     Instances instances;
-    context.GetIndex().GetChildInstances(instances, publicId);  // (*)
+    context.GetIndex().GetChildInstances(instances, publicId, level);  // (*)
 
     // Loop over the instances
     bool isFirst = true;
@@ -2891,20 +2922,21 @@
 
   static void GetSharedTags(RestApiGetCall& call)
   {
+    const ResourceType level = GetResourceTypeFromUri(call);
+
     if (call.IsDocumentation())
     {
       OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Full);
 
-      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
-      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */);
       call.GetDocumentation()
-        .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
+        .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */))
         .SetSummary("Get shared tags")
         .SetDescription("Extract the DICOM tags whose value is constant across all the child instances of "
                         "the DICOM " + r + " whose Orthanc identifier is provided in the URL")
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
         .AddAnswerType(MimeType_Json, "JSON object containing the values of the DICOM tags")
-        .SetTruncatedJsonHttpGetSample(GetDocumentationSampleResource(t) + "/shared-tags", 5);
+        .SetTruncatedJsonHttpGetSample(GetDocumentationSampleResource(level) + "/shared-tags", 5);
       return;
     }
 
@@ -2912,7 +2944,7 @@
     std::string publicId = call.GetUriComponent("id", "");
 
     Json::Value sharedTags;
-    if (ExtractSharedTags(sharedTags, context, publicId))
+    if (ExtractSharedTags(sharedTags, context, publicId, level))
     {
       // Success: Send the value of the shared tags
       AnswerDicomAsJson(call, sharedTags, OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Full));
@@ -2987,7 +3019,7 @@
       // Retrieve all the instances of this patient/study/series
       typedef std::list<std::string> Instances;
       Instances instances;
-      context.GetIndex().GetChildInstances(instances, publicId);
+      context.GetIndex().GetChildInstances(instances, publicId, resourceType);
 
       if (instances.empty())
       {
@@ -3082,70 +3114,14 @@
   }
 
 
-  namespace 
+  enum FindType
   {
-    class FindVisitor : public ServerContext::ILookupVisitor
-    {
-    private:
-      bool                    isComplete_;
-      std::list<std::string>  resources_;
-      FindStorageAccessMode   findStorageAccessMode_;
-      
-      // cache the data we used during lookup and that we could reuse when building the answers
-      std::map<std::string, std::string> instancesIds_;         // the id of an instance for each found resource.
-      std::map<std::string, boost::shared_ptr<DicomMap> > resourcesMainDicomTags_;  // all tags read from DB for a resource (current level and upper levels)
-      std::map<std::string, boost::shared_ptr<Json::Value> > resourcesDicomAsJson_; // the dicom-as-json for a resource
-
-      DicomToJsonFormat       format_;
-
-    public:
-      explicit FindVisitor(DicomToJsonFormat format, FindStorageAccessMode findStorageAccessMode) :
-        isComplete_(false),
-        findStorageAccessMode_(findStorageAccessMode),
-        format_(format)
-      {
-      }
-      
-      virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
-      {
-        return false;   // (*)
-      }
-      
-      virtual void MarkAsComplete() ORTHANC_OVERRIDE
-      {
-        isComplete_ = true;  // Unused information as of Orthanc 1.5.0
-      }
-
-      virtual void Visit(const std::string& publicId,
-                         const std::string& instanceId,
-                         const DicomMap& mainDicomTags,
-                         const Json::Value* dicomAsJson)  ORTHANC_OVERRIDE
-      {
-        resources_.push_back(publicId);
-        instancesIds_[publicId] = instanceId;
-        resourcesMainDicomTags_[publicId].reset(mainDicomTags.Clone());
-        if (dicomAsJson != NULL)
-        {
-          resourcesDicomAsJson_[publicId].reset(new Json::Value(*dicomAsJson));  // keep our own copy because we might reuse it between lookup and answers
-        }
-        else
-        {
-          resourcesDicomAsJson_[publicId] = boost::shared_ptr<Json::Value>();
-        }
-      }
-
-      void Answer(RestApiOutput& output,
-                  ServerContext& context,
-                  ResourceType level,
-                  bool expand,
-                  const std::set<DicomTag>& requestedTags) const
-      {
-        AnswerListOfResources(output, context, resources_, instancesIds_, resourcesMainDicomTags_, resourcesDicomAsJson_, level, expand, format_, requestedTags, IsStorageAccessAllowedForAnswers(findStorageAccessMode_));
-      }
-    };
-  }
-
-
+    FindType_Find,
+    FindType_Count
+  };
+
+
+  template <enum FindType requestType>
   static void Find(RestApiPostCall& call)
   {
     static const char* const KEY_CASE_SENSITIVE = "CaseSensitive";
@@ -3157,41 +3133,71 @@
     static const char* const KEY_SINCE = "Since";
     static const char* const KEY_LABELS = "Labels";                       // New in Orthanc 1.12.0
     static const char* const KEY_LABELS_CONSTRAINT = "LabelsConstraint";  // New in Orthanc 1.12.0
+    static const char* const KEY_ORDER_BY = "OrderBy";                    // New in Orthanc 1.12.5
+    static const char* const KEY_ORDER_BY_KEY = "Key";                    // New in Orthanc 1.12.5
+    static const char* const KEY_ORDER_BY_TYPE = "Type";                  // New in Orthanc 1.12.5
+    static const char* const KEY_ORDER_BY_DIRECTION = "Direction";        // New in Orthanc 1.12.5
+    static const char* const KEY_PARENT_PATIENT = "ParentPatient";        // New in Orthanc 1.12.5
+    static const char* const KEY_PARENT_STUDY = "ParentStudy";            // New in Orthanc 1.12.5
+    static const char* const KEY_PARENT_SERIES = "ParentSeries";          // New in Orthanc 1.12.5
+    static const char* const KEY_METADATA_QUERY = "MetadataQuery";        // New in Orthanc 1.12.5
+    static const char* const KEY_RESPONSE_CONTENT = "ResponseContent";    // New in Orthanc 1.12.5
 
     if (call.IsDocumentation())
     {
       OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Human);
 
-      call.GetDocumentation()
-        .SetTag("System")
-        .SetSummary("Look for local resources")
-        .SetDescription("This URI can be used to perform a search on the content of the local Orthanc server, "
-                        "in a way that is similar to querying remote DICOM modalities using C-FIND SCU: "
-                        "https://orthanc.uclouvain.be/book/users/rest.html#performing-finds-within-orthanc")
-        .SetRequestField(KEY_CASE_SENSITIVE, RestApiCallDocumentation::Type_Boolean,
-                         "Enable case-sensitive search for PN value representations (defaults to configuration option `CaseSensitivePN`)", false)
-        .SetRequestField(KEY_EXPAND, RestApiCallDocumentation::Type_Boolean,
-                         "Also retrieve the content of the matching resources, not only their Orthanc identifiers", false)
+      RestApiCallDocumentation& doc = call.GetDocumentation();
+
+      doc.SetTag("System")
         .SetRequestField(KEY_LEVEL, RestApiCallDocumentation::Type_String,
                          "Level of the query (`Patient`, `Study`, `Series` or `Instance`)", true)
-        .SetRequestField(KEY_LIMIT, RestApiCallDocumentation::Type_Number,
-                         "Limit the number of reported resources", false)
-        .SetRequestField(KEY_SINCE, RestApiCallDocumentation::Type_Number,
-                         "Show only the resources since the provided index (in conjunction with `Limit`)", false)
-        .SetRequestField(KEY_REQUESTED_TAGS, RestApiCallDocumentation::Type_JsonListOfStrings,
-                         "A list of DICOM tags to include in the response (applicable only if \"Expand\" is set to true).  "
-                         "The tags requested tags are returned in the 'RequestedTags' field in the response.  "
-                         "Note that, if you are requesting tags that are not listed in the Main Dicom Tags stored in DB, building the response "
-                         "might be slow since Orthanc will need to access the DICOM files.  If not specified, Orthanc will return "
-                         "all Main Dicom Tags to keep backward compatibility with Orthanc prior to 1.11.0.", false)
         .SetRequestField(KEY_QUERY, RestApiCallDocumentation::Type_JsonObject,
                          "Associative array containing the filter on the values of the DICOM tags", true)
         .SetRequestField(KEY_LABELS, RestApiCallDocumentation::Type_JsonListOfStrings,
                          "List of strings specifying which labels to look for in the resources (new in Orthanc 1.12.0)", true)
         .SetRequestField(KEY_LABELS_CONSTRAINT, RestApiCallDocumentation::Type_String,
                          "Constraint on the labels, can be `All`, `Any`, or `None` (defaults to `All`, new in Orthanc 1.12.0)", true)
-        .AddAnswerType(MimeType_Json, "JSON array containing either the Orthanc identifiers, or detailed information "
-                       "about the reported resources (if `Expand` argument is `true`)");
+        .SetRequestField(KEY_PARENT_PATIENT, RestApiCallDocumentation::Type_String,
+                         "Limit the reported resources to descendants of this patient (new in Orthanc 1.12.5)", true)
+        .SetRequestField(KEY_PARENT_STUDY, RestApiCallDocumentation::Type_String,
+                         "Limit the reported resources to descendants of this study (new in Orthanc 1.12.5)", true)
+        .SetRequestField(KEY_PARENT_SERIES, RestApiCallDocumentation::Type_String,
+                         "Limit the reported resources to descendants of this series (new in Orthanc 1.12.5)", true)
+        .SetRequestField(KEY_METADATA_QUERY, RestApiCallDocumentation::Type_JsonObject,
+                         "Associative array containing the filter on the values of the metadata (new in Orthanc 1.12.5)", true);
+      
+      switch (requestType)
+      {
+        case FindType_Find:
+          doc.SetSummary("Look for local resources")
+          .SetRequestField(KEY_CASE_SENSITIVE, RestApiCallDocumentation::Type_Boolean,
+                           "Enable case-sensitive search for PN value representations (defaults to configuration option `CaseSensitivePN`)", false)
+          .SetDescription("This URI can be used to perform a search on the content of the local Orthanc server, "
+                          "in a way that is similar to querying remote DICOM modalities using C-FIND SCU: "
+                          "https://orthanc.uclouvain.be/book/users/rest.html#performing-finds-within-orthanc")
+          .SetRequestField(KEY_LIMIT, RestApiCallDocumentation::Type_Number,
+                          "Limit the number of reported resources", false)
+          .SetRequestField(KEY_SINCE, RestApiCallDocumentation::Type_Number,
+                          "Show only the resources since the provided index (in conjunction with `Limit`)", false)
+          .SetRequestField(KEY_ORDER_BY, RestApiCallDocumentation::Type_JsonListOfObjects,
+                          "Array of associative arrays containing the requested ordering (new in Orthanc 1.12.5)", true)
+          .AddAnswerType(MimeType_Json, "JSON array containing either the Orthanc identifiers, or detailed information "
+                        "about the reported resources (if `Expand` argument is `true`)");
+
+          OrthancRestApi::DocumentRequestedTags(call);
+          OrthancRestApi::DocumentResponseContentAndExpand(call);
+          break;
+        case FindType_Count:
+          doc.SetSummary("Count local resources")
+          .SetDescription("This URI can be used to count the resources that are matching criteria on the content of the local Orthanc server, "
+                          "in a way that is similar to tools/find")
+          .AddAnswerType(MimeType_Json, "A JSON object with the `Count` of matching resources");
+          break;
+        default:
+          throw OrthancException(ErrorCode_NotImplemented);
+      }
+        
       return;
     }
 
@@ -3222,24 +3228,6 @@
       throw OrthancException(ErrorCode_BadRequest, 
                              "Field \"" + std::string(KEY_CASE_SENSITIVE) + "\" must be a Boolean");
     }
-    else if (request.isMember(KEY_LIMIT) && 
-             request[KEY_LIMIT].type() != Json::intValue)
-    {
-      throw OrthancException(ErrorCode_BadRequest, 
-                             "Field \"" + std::string(KEY_LIMIT) + "\" must be an integer");
-    }
-    else if (request.isMember(KEY_SINCE) &&
-             request[KEY_SINCE].type() != Json::intValue)
-    {
-      throw OrthancException(ErrorCode_BadRequest, 
-                             "Field \"" + std::string(KEY_SINCE) + "\" must be an integer");
-    }
-    else if (request.isMember(KEY_REQUESTED_TAGS) &&
-             request[KEY_REQUESTED_TAGS].type() != Json::arrayValue)
-    {
-      throw OrthancException(ErrorCode_BadRequest, 
-                             "Field \"" + std::string(KEY_REQUESTED_TAGS) + "\" must be an array");
-    }
     else if (request.isMember(KEY_LABELS) &&
              request[KEY_LABELS].type() != Json::arrayValue)
     {
@@ -3252,119 +3240,377 @@
       throw OrthancException(ErrorCode_BadRequest, 
                              "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be an array of strings");
     }
-    else
+    else if (request.isMember(KEY_METADATA_QUERY) &&
+             request[KEY_METADATA_QUERY].type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_METADATA_QUERY) + "\" must be an JSON object");
+    }
+    else if (request.isMember(KEY_PARENT_PATIENT) &&
+             request[KEY_PARENT_PATIENT].type() != Json::stringValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_PARENT_PATIENT) + "\" must be a string");
+    }
+    else if (request.isMember(KEY_PARENT_STUDY) &&
+             request[KEY_PARENT_STUDY].type() != Json::stringValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_PARENT_STUDY) + "\" must be a string");
+    }
+    else if (request.isMember(KEY_PARENT_SERIES) &&
+             request[KEY_PARENT_SERIES].type() != Json::stringValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_PARENT_SERIES) + "\" must be a string");
+    }
+    else if (requestType == FindType_Find && request.isMember(KEY_LIMIT) && 
+             request[KEY_LIMIT].type() != Json::intValue)
     {
-      bool expand = false;
-      if (request.isMember(KEY_EXPAND))
-      {
-        expand = request[KEY_EXPAND].asBool();
-      }
-
-      bool caseSensitive = false;
-      if (request.isMember(KEY_CASE_SENSITIVE))
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_LIMIT) + "\" must be an integer");
+    }
+    else if (requestType == FindType_Find && request.isMember(KEY_SINCE) &&
+             request[KEY_SINCE].type() != Json::intValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_SINCE) + "\" must be an integer");
+    }
+    else if (requestType == FindType_Find && request.isMember(KEY_REQUESTED_TAGS) &&
+             request[KEY_REQUESTED_TAGS].type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_REQUESTED_TAGS) + "\" must be an array");
+    }
+    else if (requestType == FindType_Find && request.isMember(KEY_RESPONSE_CONTENT) &&
+             request[KEY_RESPONSE_CONTENT].type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_RESPONSE_CONTENT) + "\" must be an array");
+    }
+    else if (requestType == FindType_Find && request.isMember(KEY_ORDER_BY) &&
+             request[KEY_ORDER_BY].type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_ORDER_BY) + "\" must be an array");
+    }
+    else if (true)
+    {
+      ResponseContentFlags responseContent = ResponseContentFlags_ID;
+      
+      if (requestType == FindType_Find)
       {
-        caseSensitive = request[KEY_CASE_SENSITIVE].asBool();
-      }
-
-      size_t limit = 0;
-      if (request.isMember(KEY_LIMIT))
-      {
-        int tmp = request[KEY_LIMIT].asInt();
-        if (tmp < 0)
-        {
-          throw OrthancException(ErrorCode_ParameterOutOfRange,
-                                 "Field \"" + std::string(KEY_LIMIT) + "\" must be a positive integer");
-        }
-
-        limit = static_cast<size_t>(tmp);
-      }
-
-      size_t since = 0;
-      if (request.isMember(KEY_SINCE))
-      {
-        int tmp = request[KEY_SINCE].asInt();
-        if (tmp < 0)
+        if (request.isMember(KEY_RESPONSE_CONTENT))
         {
-          throw OrthancException(ErrorCode_ParameterOutOfRange,
-                                 "Field \"" + std::string(KEY_SINCE) + "\" must be a positive integer");
+          responseContent = ResponseContentFlags_Default;
+
+          for (Json::ArrayIndex i = 0; i < request[KEY_RESPONSE_CONTENT].size(); ++i)
+          {
+            responseContent = static_cast<ResponseContentFlags>(static_cast<uint32_t>(responseContent) | StringToResponseContent(request[KEY_RESPONSE_CONTENT][i].asString()));
+          }
         }
-
-        since = static_cast<size_t>(tmp);
-      }
-
-      std::set<DicomTag> requestedTags;
-
-      if (request.isMember(KEY_REQUESTED_TAGS))
-      {
-        FromDcmtkBridge::ParseListOfTags(requestedTags, request[KEY_REQUESTED_TAGS]);
-      }
-
-      ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString());
-
-      DatabaseLookup query;
-
-      Json::Value::Members members = request[KEY_QUERY].getMemberNames();
-      for (size_t i = 0; i < members.size(); i++)
-      {
-        if (request[KEY_QUERY][members[i]].type() != Json::stringValue)
+        else if (request.isMember(KEY_EXPAND) && request[KEY_EXPAND].asBool())
         {
-          throw OrthancException(ErrorCode_BadRequest,
-                                 "Tag \"" + members[i] + "\" must be associated with a string");
-        }
-
-        const std::string value = request[KEY_QUERY][members[i]].asString();
-
-        if (!value.empty())
-        {
-          // An empty string corresponds to an universal constraint,
-          // so we ignore it. This mimics the behavior of class
-          // "OrthancFindRequestHandler"
-          query.AddRestConstraint(FromDcmtkBridge::ParseTag(members[i]), 
-                                  value, caseSensitive, true);
+          responseContent = ResponseContentFlags_ExpandTrue;
         }
       }
-
-      if (request.isMember(KEY_LABELS))  // New in Orthanc 1.12.0
+      else if (requestType == FindType_Count)
       {
-        for (Json::Value::ArrayIndex i = 0; i < request[KEY_LABELS].size(); i++)
+        responseContent = ResponseContentFlags_INTERNAL_CountResources;
+      }
+
+      const ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString());
+
+      ResourceFinder finder(level, responseContent, context.GetFindStorageAccessMode(), context.GetIndex().HasFindSupport());
+
+      DatabaseLookup dicomTagLookup;
+
+      { // common query code
+        bool caseSensitive = false;
+        if (request.isMember(KEY_CASE_SENSITIVE))
         {
-          if (request[KEY_LABELS][i].type() != Json::stringValue)
+          caseSensitive = request[KEY_CASE_SENSITIVE].asBool();
+
+          if (requestType == FindType_Count && caseSensitive)
+          {
+            /**
+             * Explanation: "/tools/find" uses "lookup_->IsMatch(tags)" in "ResourceFinder::Execute()"
+             * to apply case sensitiveness (as the database stores tags with PN VR in lower case).
+             * But, the purpose of "/tools/count-resources" is to speed up the counting the number of
+             * matching resources: Calling "lookup_->IsMatch(tags)" would require gathering the main
+             * DICOM tags, which would lead to no speedup wrt. "/tools/find".
+             **/
+            throw OrthancException(ErrorCode_ParameterOutOfRange, "Setting \"" + std::string(KEY_CASE_SENSITIVE) +
+                                   "\" to \"true\" is not supported by /tools/count-resources");
+          }
+        }
+
+        { // DICOM Tag query
+
+          Json::Value::Members members = request[KEY_QUERY].getMemberNames();
+          for (size_t i = 0; i < members.size(); i++)
+          {
+            if (request[KEY_QUERY][members[i]].type() != Json::stringValue)
+            {
+              throw OrthancException(ErrorCode_BadRequest,
+                                    "Tag \"" + members[i] + "\" must be associated with a string");
+            }
+
+            const std::string value = request[KEY_QUERY][members[i]].asString();
+
+            if (!value.empty())
+            {
+              // An empty string corresponds to an universal constraint,
+              // so we ignore it. This mimics the behavior of class
+              // "OrthancFindRequestHandler"
+              dicomTagLookup.AddRestConstraint(FromDcmtkBridge::ParseTag(members[i]),
+                                      value, caseSensitive, true);
+            }
+          }
+
+          finder.SetDatabaseLookup(dicomTagLookup);
+        }
+
+        { // Metadata query
+          Json::Value::Members members = request[KEY_METADATA_QUERY].getMemberNames();
+          for (size_t i = 0; i < members.size(); i++)
+          {
+            if (request[KEY_METADATA_QUERY][members[i]].type() != Json::stringValue)
+            {
+              throw OrthancException(ErrorCode_BadRequest,
+                                    "Tag \"" + members[i] + "\" must be associated with a string");
+            }
+            MetadataType metadata = StringToMetadata(members[i]);
+
+            const std::string value = request[KEY_METADATA_QUERY][members[i]].asString();
+
+            if (!value.empty())
+            {
+              if (value.find('\\') != std::string::npos)
+              {
+                std::vector<std::string> items;
+                Toolbox::TokenizeString(items, value, '\\');
+                
+                finder.AddMetadataConstraint(new DatabaseMetadataConstraint(metadata, ConstraintType_List, items, caseSensitive));
+              }
+              else if (value.find('*') != std::string::npos || value.find('?') != std::string::npos)
+              {
+                finder.AddMetadataConstraint(new DatabaseMetadataConstraint(metadata, ConstraintType_Wildcard, value, caseSensitive));
+              }
+              else
+              {
+                finder.AddMetadataConstraint(new DatabaseMetadataConstraint(metadata, ConstraintType_Equal, value, caseSensitive));
+              }
+            }
+          }
+        }
+
+        { // labels query
+          if (request.isMember(KEY_LABELS))  // New in Orthanc 1.12.0
           {
-            throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS) + "\" must contain strings");
+            for (Json::Value::ArrayIndex i = 0; i < request[KEY_LABELS].size(); i++)
+            {
+              if (request[KEY_LABELS][i].type() != Json::stringValue)
+              {
+                throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS) + "\" must contain strings");
+              }
+              else
+              {
+                finder.AddLabel(request[KEY_LABELS][i].asString());
+              }
+            }
+          }
+
+          finder.SetLabelsConstraint(LabelsConstraint_All);
+
+          if (request.isMember(KEY_LABELS_CONSTRAINT))
+          {
+            const std::string& s = request[KEY_LABELS_CONSTRAINT].asString();
+            if (s == "All")
+            {
+              finder.SetLabelsConstraint(LabelsConstraint_All);
+            }
+            else if (s == "Any")
+            {
+              finder.SetLabelsConstraint(LabelsConstraint_Any);
+            }
+            else if (s == "None")
+            {
+              finder.SetLabelsConstraint(LabelsConstraint_None);
+            }
+            else
+            {
+              throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be \"All\", \"Any\", or \"None\"");
+            }
+          }
+        }
+
+        // parents query
+        if (request.isMember(KEY_PARENT_PATIENT)) // New in Orthanc 1.12.5
+        {
+          finder.SetOrthancId(ResourceType_Patient, request[KEY_PARENT_PATIENT].asString());
+        }
+        else if (request.isMember(KEY_PARENT_STUDY))
+        {
+          finder.SetOrthancId(ResourceType_Study, request[KEY_PARENT_STUDY].asString());
+        }
+        else if (request.isMember(KEY_PARENT_SERIES))
+        {
+          finder.SetOrthancId(ResourceType_Series, request[KEY_PARENT_SERIES].asString());
+        }
+      }
+
+      // response
+      if (requestType == FindType_Find)
+      {
+        const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(request, DicomToJsonFormat_Human);
+
+        finder.SetDatabaseLimits(context.GetDatabaseLimits(level));
+
+        if ((request.isMember(KEY_SINCE) && request[KEY_SINCE].asInt64() != 0) &&
+            !finder.CanBeFullyPerformedInDb())
+        {
+            throw OrthancException(ErrorCode_BadRequest,
+                                  "Unable to use '" + std::string(KEY_SINCE) + "' in tools/find when querying tags that are not stored as MainDicomTags in the Database");
+        }
+
+        if (request.isMember(KEY_LIMIT))
+        {
+          int64_t tmp = request[KEY_LIMIT].asInt64();
+          if (tmp < 0)
+          {
+            throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                  "Field \"" + std::string(KEY_LIMIT) + "\" must be a positive integer");
+          }
+          else if (tmp != 0)  // This is for compatibility with Orthanc 1.12.4
+          {
+            finder.SetLimitsCount(static_cast<uint64_t>(tmp));
+          }
+        }
+
+        if (request.isMember(KEY_SINCE))
+        {
+          int64_t tmp = request[KEY_SINCE].asInt64();
+          if (tmp < 0)
+          {
+            throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                  "Field \"" + std::string(KEY_SINCE) + "\" must be a positive integer");
           }
           else
           {
-            query.AddLabel(request[KEY_LABELS][i].asString());
+            finder.SetLimitsSince(static_cast<uint64_t>(tmp));
           }
         }
-      }
-
-      query.SetLabelsConstraint(LabelsConstraint_All);
-      
-      if (request.isMember(KEY_LABELS_CONSTRAINT))
-      {
-        const std::string& s = request[KEY_LABELS_CONSTRAINT].asString();
-        if (s == "All")
+
+        if (request.isMember(KEY_REQUESTED_TAGS))
         {
-          query.SetLabelsConstraint(LabelsConstraint_All);
+          std::set<DicomTag> requestedTags;
+          FromDcmtkBridge::ParseListOfTags(requestedTags, request[KEY_REQUESTED_TAGS]);
+          finder.AddRequestedTags(requestedTags);
         }
-        else if (s == "Any")
+
+        if (request.isMember(KEY_ORDER_BY))  // New in Orthanc 1.12.5
         {
-          query.SetLabelsConstraint(LabelsConstraint_Any);
-        }
-        else if (s == "None")
-        {
-          query.SetLabelsConstraint(LabelsConstraint_None);
+          for (Json::Value::ArrayIndex i = 0; i < request[KEY_ORDER_BY].size(); i++)
+          {
+            if (request[KEY_ORDER_BY][i].type() != Json::objectValue)
+            {
+              throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY) + "\" must contain objects");
+            }
+            else
+            {
+              const Json::Value& order = request[KEY_ORDER_BY][i];
+              FindRequest::OrderingDirection direction;
+              std::string directionString;
+              std::string typeString;
+
+              if (!order.isMember(KEY_ORDER_BY_KEY) || order[KEY_ORDER_BY_KEY].type() != Json::stringValue)
+              {
+                throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_KEY) + "\" must be a string");
+              }
+
+              if (!order.isMember(KEY_ORDER_BY_DIRECTION) || order[KEY_ORDER_BY_DIRECTION].type() != Json::stringValue)
+              {
+                throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_DIRECTION) + "\" must be \"ASC\" or \"DESC\"");
+              }
+
+              Toolbox::ToLowerCase(directionString,  order[KEY_ORDER_BY_DIRECTION].asString());
+              if (directionString == "asc")
+              {
+                direction = FindRequest::OrderingDirection_Ascending;
+              }
+              else if (directionString == "desc")
+              {
+                direction = FindRequest::OrderingDirection_Descending;
+              }
+              else
+              {
+                throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_DIRECTION) + "\" must be \"ASC\" or \"DESC\"");
+              }
+
+              if (!order.isMember(KEY_ORDER_BY_TYPE) || order[KEY_ORDER_BY_TYPE].type() != Json::stringValue)
+              {
+                throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_TYPE) + "\" must be \"DicomTag\" or \"Metadata\"");
+              }
+
+              Toolbox::ToLowerCase(typeString, order[KEY_ORDER_BY_TYPE].asString());
+              if (Toolbox::StartsWith(typeString, "dicomtag"))
+              {
+                DicomTag tag = FromDcmtkBridge::ParseTag(order[KEY_ORDER_BY_KEY].asString());
+
+                if (typeString == "dicomtagasint")
+                {
+                  finder.AddOrdering(tag, FindRequest::OrderingCast_Int, direction);
+                }
+                else if (typeString == "dicomtagasfloat")
+                {
+                  finder.AddOrdering(tag, FindRequest::OrderingCast_Float, direction);
+                }
+                else
+                {
+                  finder.AddOrdering(tag, FindRequest::OrderingCast_String, direction);
+                }
+              }
+              else if (Toolbox::StartsWith(typeString, "metadata"))
+              {
+                MetadataType metadata = StringToMetadata(order[KEY_ORDER_BY_KEY].asString());
+
+                if (typeString == "metadataasint")
+                {
+                  finder.AddOrdering(metadata, FindRequest::OrderingCast_Int, direction);
+                }
+                else if (typeString == "dicomtagasfloat")
+                {
+                  finder.AddOrdering(metadata, FindRequest::OrderingCast_Float, direction);
+                }
+                else
+                {
+                  finder.AddOrdering(metadata, FindRequest::OrderingCast_String, direction);
+                }
+              }
+              else
+              {
+                throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_TYPE) + "\" must be \"DicomTag\", \"DicomTagAsInt\", \"DicomTagAsFloat\" or \"Metadata\", \"MetadataAsInt\", \"MetadataAsFloat\"");
+              }
+            }
+          }
         }
-        else
-        {
-          throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be \"All\", \"Any\", or \"None\"");
-        }
+
+        Json::Value answer;
+        finder.Execute(answer, context, format, false /* no "Metadata" field */);
+        call.GetOutput().AnswerJson(answer);
       }
-      
-      FindVisitor visitor(OrthancRestApi::GetDicomFormat(request, DicomToJsonFormat_Human), context.GetFindStorageAccessMode());
-      context.Apply(visitor, query, level, since, limit);
-      visitor.Answer(call.GetOutput(), context, level, expand, requestedTags);
+      else if (requestType == FindType_Count)
+      {
+        uint64_t count = finder.Count(context);
+        Json::Value answer;
+        answer["Count"] = Json::Value::UInt64(count);
+        call.GetOutput().AnswerJson(answer);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
     }
   }
 
@@ -3393,50 +3639,46 @@
       return;
     }
 
-    ServerIndex& index = OrthancRestApi::GetIndex(call);
-    ServerContext& context = OrthancRestApi::GetContext(call);
+    const bool expand = (!call.HasArgument("expand") ||
+                         // this "expand" is the only one to have a false default value to keep backward compatibility
+                         call.GetBooleanArgument("expand", false));
+    const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human);
 
     std::set<DicomTag> requestedTags;
     OrthancRestApi::GetRequestedTags(requestedTags, call);
 
-    std::list<std::string> a, b, c;
-    a.push_back(call.GetUriComponent("id", ""));
-
-    ResourceType type = start;
-    while (type != end)
+    ResourceFinder finder(end, 
+                          (expand ? ResponseContentFlags_ExpandTrue : ResponseContentFlags_ID), 
+                          OrthancRestApi::GetContext(call).GetFindStorageAccessMode(),
+                          OrthancRestApi::GetContext(call).GetIndex().HasFindSupport());
+    finder.SetOrthancId(start, call.GetUriComponent("id", ""));
+    finder.AddRequestedTags(requestedTags);
+
+    Json::Value answer;
+    finder.Execute(answer, OrthancRestApi::GetContext(call), format, false /* no "Metadata" field */);
+    
+    // Given the data model, if there are no children, it means there is no parent.
+    // https://discourse.orthanc-server.org/t/patients-id-instances-quirk/5498
+    if (answer.size() == 0) 
     {
-      b.clear();
-
-      for (std::list<std::string>::const_iterator
-             it = a.begin(); it != a.end(); ++it)
-      {
-        index.GetChildren(c, *it);
-        b.splice(b.begin(), c);
-      }
-
-      type = GetChildResourceType(type);
-
-      a.clear();
-      a.splice(a.begin(), b);
+      throw OrthancException(ErrorCode_UnknownResource);
     }
 
-    AnswerListOfResources(call.GetOutput(), context, a, type, !call.HasArgument("expand") || call.GetBooleanArgument("expand", false),  // this "expand" is the only one to have a false default value to keep backward compatibility
-                          OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human),
-                          requestedTags,
-                          true /* allowStorageAccess */);
+    call.GetOutput().AnswerJson(answer);
   }
 
 
   static void GetChildInstancesTags(RestApiGetCall& call)
   {
+    const ResourceType level = GetResourceTypeFromUri(call);
+
     if (call.IsDocumentation())
     {
       OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Full);
 
-      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
-      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */);
       call.GetDocumentation()
-        .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
+        .SetTag(GetResourceTypeText(level, true /* plural */, true /* upper case */))
         .SetSummary("Get tags of instances")
         .SetDescription("Get the tags of all the child instances of the DICOM " + r +
                         " whose Orthanc identifier is provided in the URL")
@@ -3444,7 +3686,7 @@
         .SetHttpGetArgument(IGNORE_LENGTH, RestApiCallDocumentation::Type_JsonListOfStrings,
                             "Also include the DICOM tags that are provided in this list, even if their associated value is long", false)
         .AddAnswerType(MimeType_Json, "JSON object associating the Orthanc identifiers of the instances, with the values of their DICOM tags")
-        .SetTruncatedJsonHttpGetSample(GetDocumentationSampleResource(t) + "/instances-tags", 5);
+        .SetTruncatedJsonHttpGetSample(GetDocumentationSampleResource(level) + "/instances-tags", 5);
       return;
     }
 
@@ -3459,7 +3701,7 @@
     typedef std::list<std::string> Instances;
     Instances instances;
 
-    context.GetIndex().GetChildInstances(instances, publicId);  // (*)
+    context.GetIndex().GetChildInstances(instances, publicId, level);  // (*)
 
     Json::Value result = Json::objectValue;
 
@@ -3536,7 +3778,7 @@
     const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human);
 
     Json::Value resource;
-    if (OrthancRestApi::GetContext(call).ExpandResource(resource, current, end, format, requestedTags, true /* allowStorageAccess */))
+    if (ExpandResource(resource, OrthancRestApi::GetContext(call), currentType, current, format, false))
     {
       call.GetOutput().AnswerJson(resource);
     }
@@ -3665,7 +3907,7 @@
            study = studies.begin(); study != studies.end(); ++study)
     {
       std::list<std::string> instances;
-      index.GetChildInstances(instances, *study);
+      index.GetChildInstances(instances, *study, ResourceType_Study);
 
       for (std::list<std::string>::const_iterator 
              instance = instances.begin(); instance != instances.end(); ++instance)
@@ -3689,7 +3931,7 @@
       call.GetDocumentation()
         .SetRequestField(LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS, RestApiCallDocumentation::Type_Boolean,
                         "Only reconstruct this level MainDicomTags by re-reading them from a random child instance of the resource. "
-                        "This option is much faster than a full reconstruct and is usefull e.g. if you have modified the "
+                        "This option is much faster than a full reconstruct and is useful e.g. if you have modified the "
                         "'ExtraMainDicomTags' at the Study level to optimize the speed of some C-Find. "
                         "'false' by default. (New in Orthanc 1.12.4)", false);
     }
@@ -3796,6 +4038,7 @@
 
   static void GetBulkChildren(std::set<std::string>& target,
                               ServerIndex& index,
+                              ResourceType level,
                               const std::set<std::string>& source)
   {
     target.clear();
@@ -3804,7 +4047,7 @@
            it = source.begin(); it != source.end(); ++it)
     {
       std::list<std::string> children;
-      index.GetChildren(children, *it);
+      index.GetChildren(children, level, *it);
 
       for (std::list<std::string>::const_iterator
              child = children.begin(); child != children.end(); ++child)
@@ -3815,24 +4058,6 @@
   }
 
 
-  static void AddMetadata(Json::Value& target,
-                          ServerIndex& index,
-                          const std::string& resource,
-                          ResourceType level)
-  {
-    target = Json::objectValue;
-    
-    std::map<MetadataType, std::string> content;
-    index.GetAllMetadata(content, resource, level);
-    
-    for (std::map<MetadataType, std::string>::const_iterator
-           it = content.begin(); it != content.end(); ++it)
-    {
-      target[EnumerationToString(it->first)] = it->second;
-    }
-  }
-
-
   static void BulkContent(RestApiPostCall& call)
   {
     static const char* const LEVEL = "Level";
@@ -3909,11 +4134,11 @@
               // Need to explore children
               std::set<std::string> current;
               current.insert(*it);
-              
+
               for (;;)
               {
                 std::set<std::string> children;
-                GetBulkChildren(children, index, current);
+                GetBulkChildren(children, index, type, current);
 
                 type = GetChildResourceType(type);
                 if (type == level)
@@ -3970,15 +4195,8 @@
                it = interest.begin(); it != interest.end(); ++it)
         {
           Json::Value item;
-          std::set<DicomTag> emptyRequestedTags;  // not supported for bulk content
-
-          if (OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format, emptyRequestedTags, true /* allowStorageAccess */))
+          if (ExpandResource(item, OrthancRestApi::GetContext(call), level, *it, format, metadata))
           {
-            if (metadata)
-            {
-              AddMetadata(item[METADATA], index, *it, level);
-            }
-
             answer.append(item);
           }
         }
@@ -3994,16 +4212,10 @@
         {
           ResourceType level;
           Json::Value item;
-          std::set<DicomTag> emptyRequestedTags;  // not supported for bulk content
 
           if (index.LookupResourceType(level, *it) &&
-              OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format, emptyRequestedTags, true /* allowStorageAccess */))
+              ExpandResource(item, OrthancRestApi::GetContext(call), level, *it, format, metadata))
           {
-            if (metadata)
-            {
-              AddMetadata(item[METADATA], index, *it, level);
-            }
-
             answer.append(item);
           }
           else
@@ -4071,13 +4283,23 @@
     Register("/series", ListResources<ResourceType_Series>);
     Register("/studies", ListResources<ResourceType_Study>);
 
-    Register("/instances/{id}", DeleteSingleResource<ResourceType_Instance>);
+    if (!context_.IsReadOnly())
+    {
+      Register("/instances/{id}", DeleteSingleResource<ResourceType_Instance>);
+      Register("/patients/{id}", DeleteSingleResource<ResourceType_Patient>);
+      Register("/series/{id}", DeleteSingleResource<ResourceType_Series>);
+      Register("/studies/{id}", DeleteSingleResource<ResourceType_Study>);
+
+      Register("/tools/bulk-delete", BulkDelete);
+    }
+    else
+    {
+      LOG(WARNING) << "READ-ONLY SYSTEM: deactivating DELETE routes";
+    }
+
     Register("/instances/{id}", GetSingleResource<ResourceType_Instance>);
-    Register("/patients/{id}", DeleteSingleResource<ResourceType_Patient>);
     Register("/patients/{id}", GetSingleResource<ResourceType_Patient>);
-    Register("/series/{id}", DeleteSingleResource<ResourceType_Series>);
     Register("/series/{id}", GetSingleResource<ResourceType_Series>);
-    Register("/studies/{id}", DeleteSingleResource<ResourceType_Study>);
     Register("/studies/{id}", GetSingleResource<ResourceType_Study>);
 
     Register("/instances/{id}/statistics", GetResourceStatistics);
@@ -4122,7 +4344,16 @@
     Register("/instances/{id}/numpy", GetNumpyInstance);  // New in Orthanc 1.10.0
 
     Register("/patients/{id}/protected", IsProtectedPatient);
-    Register("/patients/{id}/protected", SetPatientProtection);
+  
+    if (!context_.IsReadOnly())
+    {
+      Register("/patients/{id}/protected", SetPatientProtection);
+    }
+    else
+    {
+      LOG(WARNING) << "READ-ONLY SYSTEM: deactivating PUT /patients/{id}/protected route";
+    }
+
 
     std::vector<std::string> resourceTypes;
     resourceTypes.push_back("patients");
@@ -4140,14 +4371,15 @@
       // New in Orthanc 1.12.0
       Register("/" + resourceTypes[i] + "/{id}/labels", ListLabels);
       Register("/" + resourceTypes[i] + "/{id}/labels/{label}", GetLabel);
-      Register("/" + resourceTypes[i] + "/{id}/labels/{label}", RemoveLabel);
-      Register("/" + resourceTypes[i] + "/{id}/labels/{label}", AddLabel);
+
+      if (!context_.IsReadOnly())
+      {
+        Register("/" + resourceTypes[i] + "/{id}/labels/{label}", RemoveLabel);
+        Register("/" + resourceTypes[i] + "/{id}/labels/{label}", AddLabel);
+      }
 
       Register("/" + resourceTypes[i] + "/{id}/attachments", ListAttachments);
-      Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", DeleteAttachment);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", GetAttachmentOperations);
-      Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", UploadAttachment);
-      Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/compress", ChangeAttachmentCompression<CompressionType_ZlibWithSize>);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/compressed-data", GetAttachmentData<0>);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/compressed-md5", GetAttachmentCompressedMD5);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/compressed-size", GetAttachmentCompressedSize);
@@ -4155,14 +4387,32 @@
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/is-compressed", IsAttachmentCompressed);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/md5", GetAttachmentMD5);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/size", GetAttachmentSize);
-      Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/uncompress", ChangeAttachmentCompression<CompressionType_None>);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/info", GetAttachmentInfo);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/verify-md5", VerifyAttachment);
+
+      if (!context_.IsReadOnly())
+      {
+        Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", DeleteAttachment);
+        Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", UploadAttachment);
+        Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/compress", ChangeAttachmentCompression<CompressionType_ZlibWithSize>);
+        Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/uncompress", ChangeAttachmentCompression<CompressionType_None>);
+      }
     }
 
-    Register("/tools/invalidate-tags", InvalidateTags);
+    if (context_.IsReadOnly())
+    {
+      LOG(WARNING) << "READ-ONLY SYSTEM: deactivating PUT, POST and DELETE attachments routes";
+      LOG(WARNING) << "READ-ONLY SYSTEM: deactivating PUT and DELETE labels routes";
+    }
+
+    if (!context_.IsReadOnly())
+    {
+      Register("/tools/invalidate-tags", InvalidateTags);
+    }
+
     Register("/tools/lookup", Lookup);
-    Register("/tools/find", Find);
+    Register("/tools/find", Find<FindType_Find>);
+    Register("/tools/count-resources", Find<FindType_Count>);
 
     Register("/patients/{id}/studies", GetChildResources<ResourceType_Patient, ResourceType_Study>);
     Register("/patients/{id}/series", GetChildResources<ResourceType_Patient, ResourceType_Series>);
@@ -4187,13 +4437,19 @@
     Register("/series/{id}/ordered-slices", OrderSlices);
     Register("/series/{id}/numpy", GetNumpySeries);  // New in Orthanc 1.10.0
 
-    Register("/patients/{id}/reconstruct", ReconstructResource<ResourceType_Patient>);
-    Register("/studies/{id}/reconstruct", ReconstructResource<ResourceType_Study>);
-    Register("/series/{id}/reconstruct", ReconstructResource<ResourceType_Series>);
-    Register("/instances/{id}/reconstruct", ReconstructResource<ResourceType_Instance>);
-    Register("/tools/reconstruct", ReconstructAllResources);
+    if (!context_.IsReadOnly())
+    {
+      Register("/patients/{id}/reconstruct", ReconstructResource<ResourceType_Patient>);
+      Register("/studies/{id}/reconstruct", ReconstructResource<ResourceType_Study>);
+      Register("/series/{id}/reconstruct", ReconstructResource<ResourceType_Series>);
+      Register("/instances/{id}/reconstruct", ReconstructResource<ResourceType_Instance>);
+      Register("/tools/reconstruct", ReconstructAllResources);
+    }
+    else
+    {
+      LOG(WARNING) << "READ-ONLY SYSTEM: deactivating /reconstruct routes";
+    }
 
     Register("/tools/bulk-content", BulkContent);
-    Register("/tools/bulk-delete", BulkDelete);
-  }
+   }
 }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -75,6 +75,7 @@
     static const char* const DATABASE_BACKEND_PLUGIN = "DatabaseBackendPlugin";
     static const char* const DATABASE_VERSION = "DatabaseVersion";
     static const char* const DATABASE_SERVER_IDENTIFIER = "DatabaseServerIdentifier";
+    static const char* const DEFAULT_RETRIEVE_METHOD = "DefaultRetrieveMethod";
     static const char* const DICOM_AET = "DicomAet";
     static const char* const DICOM_PORT = "DicomPort";
     static const char* const HTTP_PORT = "HttpPort";
@@ -92,6 +93,10 @@
     static const char* const MAXIMUM_STORAGE_MODE = "MaximumStorageMode";
     static const char* const USER_METADATA = "UserMetadata";
     static const char* const HAS_LABELS = "HasLabels";
+    static const char* const CAPABILITIES = "Capabilities";
+    static const char* const HAS_EXTENDED_CHANGES = "HasExtendedChanges";
+    static const char* const HAS_EXTENDED_FIND = "HasExtendedFind";
+    static const char* const READ_ONLY = "ReadOnly";
 
     if (call.IsDocumentation())
     {
@@ -111,6 +116,7 @@
                         "Information about the installed storage area plugin (`null` if no such plugin is installed)")
         .SetAnswerField(DATABASE_BACKEND_PLUGIN, RestApiCallDocumentation::Type_String,
                         "Information about the installed database index plugin (`null` if no such plugin is installed)")
+        .SetAnswerField(DEFAULT_RETRIEVE_METHOD, RestApiCallDocumentation::Type_String, "The DefaultRetrieveMethod configuration")
         .SetAnswerField(DICOM_AET, RestApiCallDocumentation::Type_String, "The DICOM AET of Orthanc")
         .SetAnswerField(DICOM_PORT, RestApiCallDocumentation::Type_Number, "The port to the DICOM server of Orthanc")
         .SetAnswerField(HTTP_PORT, RestApiCallDocumentation::Type_Number, "The port to the HTTP server of Orthanc")
@@ -138,6 +144,10 @@
                         "The configured UserMetadata (new in Orthanc 1.12.0)")
         .SetAnswerField(HAS_LABELS, RestApiCallDocumentation::Type_Boolean,
                         "Whether the database back-end supports labels (new in Orthanc 1.12.0)")
+        .SetAnswerField(CAPABILITIES, RestApiCallDocumentation::Type_JsonObject,
+                        "Whether the back-end supports optional features like 'HasExtendedChanges', 'HasExtendedFind' (new in Orthanc 1.12.5) ")
+        .SetAnswerField(READ_ONLY, RestApiCallDocumentation::Type_Boolean,
+                        "Whether Orthanc is running in read only mode (new in Orthanc 1.12.5)")
         .SetHttpGetSample("https://orthanc.uclouvain.be/demo/system", true);
       return;
     }
@@ -165,10 +175,12 @@
       result[MAXIMUM_STORAGE_SIZE] = lock.GetConfiguration().GetUnsignedIntegerParameter(MAXIMUM_STORAGE_SIZE, 0); // New in Orthanc 1.11.3
       result[MAXIMUM_PATIENT_COUNT] = lock.GetConfiguration().GetUnsignedIntegerParameter(MAXIMUM_PATIENT_COUNT, 0); // New in Orthanc 1.12.4
       result[MAXIMUM_STORAGE_MODE] = lock.GetConfiguration().GetStringParameter(MAXIMUM_STORAGE_MODE, "Recycle"); // New in Orthanc 1.11.3
+      result[DEFAULT_RETRIEVE_METHOD] = lock.GetConfiguration().GetStringParameter(DEFAULT_RETRIEVE_METHOD, "C-MOVE");
     }
 
     result[STORAGE_AREA_PLUGIN] = Json::nullValue;
     result[DATABASE_BACKEND_PLUGIN] = Json::nullValue;
+    result[READ_ONLY] = context.IsReadOnly();
 
 #if ORTHANC_ENABLE_PLUGINS == 1
     result[PLUGINS_ENABLED] = true;
@@ -196,6 +208,9 @@
     GetUserMetadataConfiguration(result[USER_METADATA]);
 
     result[HAS_LABELS] = OrthancRestApi::GetIndex(call).HasLabelsSupport();
+    result[CAPABILITIES] = Json::objectValue;
+    result[CAPABILITIES][HAS_EXTENDED_CHANGES] = OrthancRestApi::GetIndex(call).HasExtendedChanges();
+    result[CAPABILITIES][HAS_EXTENDED_FIND] = OrthancRestApi::GetIndex(call).HasFindSupport();
     
     call.GetOutput().AnswerJson(result);
   }
@@ -466,6 +481,33 @@
     AnswerAcceptedTransferSyntaxes(call);
   }
 
+  static void GetAcceptedSopClasses(RestApiGetCall& call)
+  {
+    if (call.IsDocumentation())
+    {
+      call.GetDocumentation()
+        .SetTag("System")
+        .SetSummary("Get accepted SOPClassUID")
+        .SetDescription("Get the list of SOP Class UIDs that are accepted "
+                        "by Orthanc C-STORE SCP. This corresponds to the configuration options "
+                        "`AcceptedSopClasses` and `RejectedSopClasses`.")
+        .AddAnswerType(MimeType_Json, "JSON array containing the SOP Class UIDs");
+      return;
+    }
+
+    std::set<std::string> sopClasses;
+    OrthancRestApi::GetContext(call).GetAcceptedSopClasses(sopClasses, 0);
+    
+    Json::Value json = Json::arrayValue;
+    for (std::set<std::string>::const_iterator
+           sop = sopClasses.begin(); sop != sopClasses.end(); ++sop)
+    {
+      json.append(*sop);
+    }
+    
+    call.GetOutput().AnswerJson(json);
+  }
+
 
   static void GetUnknownSopClassAccepted(RestApiGetCall& call)
   {
@@ -1188,5 +1230,8 @@
     Register("/tools/unknown-sop-class-accepted", SetUnknownSopClassAccepted);
 
     Register("/tools/labels", ListAllLabels);  // New in Orthanc 1.12.0
+
+    Register("/tools/accepted-sop-classes", GetAcceptedSopClasses);
+
   }
 }
--- a/OrthancServer/Sources/OrthancWebDav.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/OrthancWebDav.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -28,6 +28,7 @@
 #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
 #include "../../OrthancFramework/Sources/HttpServer/WebDavStorage.h"
 #include "../../OrthancFramework/Sources/Logging.h"
+#include "ResourceFinder.h"
 #include "Search/DatabaseLookup.h"
 #include "ServerContext.h"
 
@@ -50,6 +51,20 @@
   {
     return boost::posix_time::second_clock::universal_time();
   }
+
+
+  static void ParseTime(boost::posix_time::ptime& target,
+                        const std::string& value)
+  {
+    try
+    {
+      target = boost::posix_time::from_iso_string(value);
+    }
+    catch (std::exception& e)
+    {
+      target = GetNow();
+    }
+  }
   
 
   static void LookupTime(boost::posix_time::ptime& target,
@@ -59,117 +74,111 @@
                          MetadataType metadata)
   {
     std::string value;
-    int64_t revision;  // Ignored
-    if (context.GetIndex().LookupMetadata(value, revision, publicId, level, metadata))
+    if (context.GetIndex().LookupMetadata(value, publicId, level, metadata))
     {
-      try
-      {
-        target = boost::posix_time::from_iso_string(value);
-        return;
-      }
-      catch (std::exception& e)
-      {
-      }
+      ParseTime(target, value);
     }
-
-    target = GetNow();
+    else
+    {
+      target = GetNow();
+    }
   }
 
   
-  class OrthancWebDav::DicomIdentifiersVisitor : public ServerContext::ILookupVisitor
+  class OrthancWebDav::DicomIdentifiersVisitorV2 : public ResourceFinder::IVisitor
   {
   private:
-    ServerContext&  context_;
-    bool            isComplete_;
-    Collection&     target_;
-    ResourceType    level_;
+    bool         isComplete_;
+    Collection&  target_;
 
   public:
-    DicomIdentifiersVisitor(ServerContext& context,
-                            Collection&  target,
-                            ResourceType level) :
-      context_(context),
+    explicit DicomIdentifiersVisitorV2(Collection& target) :
       isComplete_(false),
-      target_(target),
-      level_(level)
+      target_(target)
     {
     }
-      
-    virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
-    {
-      return false;   // (*)
-    }
-      
+
     virtual void MarkAsComplete() ORTHANC_OVERRIDE
     {
       isComplete_ = true;  // TODO
     }
 
-    virtual void Visit(const std::string& publicId,
-                       const std::string& instanceId   /* unused     */,
-                       const DicomMap& mainDicomTags,
-                       const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
+    virtual void Apply(const FindResponse::Resource& resource,
+                       const DicomMap& requestedTags)  ORTHANC_OVERRIDE
     {
-      DicomTag tag(0, 0);
-      MetadataType timeMetadata;
+      DicomMap resourceTags;
+      resource.GetMainDicomTags(resourceTags, resource.GetLevel());
 
-      switch (level_)
+      std::string uid;
+      bool hasUid;
+
+      std::string time;
+      bool hasTime;
+
+      switch (resource.GetLevel())
       {
         case ResourceType_Study:
-          tag = DICOM_TAG_STUDY_INSTANCE_UID;
-          timeMetadata = MetadataType_LastUpdate;
+          hasUid = resourceTags.LookupStringValue(uid, DICOM_TAG_STUDY_INSTANCE_UID, false);
+          hasTime = resource.LookupMetadata(time, resource.GetLevel(), MetadataType_LastUpdate);
           break;
 
         case ResourceType_Series:
-          tag = DICOM_TAG_SERIES_INSTANCE_UID;
-          timeMetadata = MetadataType_LastUpdate;
+          hasUid = resourceTags.LookupStringValue(uid, DICOM_TAG_SERIES_INSTANCE_UID, false);
+          hasTime = resource.LookupMetadata(time, resource.GetLevel(), MetadataType_LastUpdate);
           break;
-        
+
         case ResourceType_Instance:
-          tag = DICOM_TAG_SOP_INSTANCE_UID;
-          timeMetadata = MetadataType_Instance_ReceptionDate;
+          hasUid = resourceTags.LookupStringValue(uid, DICOM_TAG_SOP_INSTANCE_UID, false);
+          hasTime = resource.LookupMetadata(time, resource.GetLevel(), MetadataType_Instance_ReceptionDate);
           break;
 
         default:
           throw OrthancException(ErrorCode_InternalError);
       }
-        
-      std::string s;
-      if (mainDicomTags.LookupStringValue(s, tag, false) &&
-          !s.empty())
+
+      if (hasUid &&
+          !uid.empty())
       {
-        std::unique_ptr<Resource> resource;
+        std::unique_ptr<Resource> item;
 
-        if (level_ == ResourceType_Instance)
+        if (resource.GetLevel() == ResourceType_Instance)
         {
           FileInfo info;
-          int64_t revision;  // Ignored
-          if (context_.GetIndex().LookupAttachment(info, revision, publicId, FileContentType_Dicom))
+          int64_t revision;
+          if (resource.LookupAttachment(info, revision, FileContentType_Dicom))
           {
-            std::unique_ptr<File> f(new File(s + ".dcm"));
+            std::unique_ptr<File> f(new File(uid + ".dcm"));
             f->SetMimeType(MimeType_Dicom);
             f->SetContentLength(info.GetUncompressedSize());
-            resource.reset(f.release());
+            item.reset(f.release());
           }
         }
         else
         {
-          resource.reset(new Folder(s));
+          item.reset(new Folder(uid));
         }
 
-        if (resource.get() != NULL)
+        if (item.get() != NULL)
         {
-          boost::posix_time::ptime t;
-          LookupTime(t, context_, publicId, level_, timeMetadata);
-          resource->SetCreationTime(t);
-          target_.AddResource(resource.release());
+          if (hasTime)
+          {
+            boost::posix_time::ptime t;
+            ParseTime(t, time);
+            item->SetCreationTime(t);
+          }
+          else
+          {
+            item->SetCreationTime(GetNow());
+          }
+
+          target_.AddResource(item.release());
         }
       }
     }
   };
 
-  
-  class OrthancWebDav::DicomFileVisitor : public ServerContext::ILookupVisitor
+
+  class OrthancWebDav::DicomFileVisitorV2 : public ResourceFinder::IVisitor
   {
   private:
     ServerContext&  context_;
@@ -178,9 +187,9 @@
     boost::posix_time::ptime&  time_;
 
   public:
-    DicomFileVisitor(ServerContext& context,
-                     std::string& target,
-                     boost::posix_time::ptime& time) :
+    DicomFileVisitorV2(ServerContext& context,
+                       std::string& target,
+                       boost::posix_time::ptime& time) :
       context_(context),
       success_(false),
       target_(target),
@@ -193,19 +202,12 @@
       return success_;
     }
 
-    virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
-    {
-      return false;   // (*)
-    }
-      
     virtual void MarkAsComplete() ORTHANC_OVERRIDE
     {
     }
 
-    virtual void Visit(const std::string& publicId,
-                       const std::string& instanceId   /* unused     */,
-                       const DicomMap& mainDicomTags,
-                       const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
+    virtual void Apply(const FindResponse::Resource& resource,
+                       const DicomMap& requestedTags) ORTHANC_OVERRIDE
     {
       if (success_)
       {
@@ -213,70 +215,18 @@
       }
       else
       {
-        LookupTime(time_, context_, publicId, ResourceType_Instance, MetadataType_Instance_ReceptionDate);
-        context_.ReadDicom(target_, publicId);
-        success_ = true;
-      }
-    }
-  };
-  
-
-  class OrthancWebDav::OrthancJsonVisitor : public ServerContext::ILookupVisitor
-  {
-  private:
-    ServerContext&  context_;
-    bool            success_;
-    std::string&    target_;
-    ResourceType    level_;
-
-  public:
-    OrthancJsonVisitor(ServerContext& context,
-                       std::string& target,
-                       ResourceType level) :
-      context_(context),
-      success_(false),
-      target_(target),
-      level_(level)
-    {
-    }
-
-    bool IsSuccess() const
-    {
-      return success_;
-    }
-
-    virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
-    {
-      return false;   // (*)
-    }
-      
-    virtual void MarkAsComplete() ORTHANC_OVERRIDE
-    {
-    }
-
-    virtual void Visit(const std::string& publicId,
-                       const std::string& instanceId   /* unused     */,
-                       const DicomMap& mainDicomTags,
-                       const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
-    {
-      Json::Value resource;
-      std::set<DicomTag> emptyRequestedTags;  // not supported for webdav
-
-      if (context_.ExpandResource(resource, publicId, level_, DicomToJsonFormat_Human, emptyRequestedTags, true /* allowStorageAccess */))
-      {
-        if (success_)
+        std::string s;
+        if (resource.LookupMetadata(s, ResourceType_Instance, MetadataType_Instance_ReceptionDate))
         {
-          success_ = false;  // Two matches => Error
+          ParseTime(time_, s);
         }
         else
         {
-          target_ = resource.toStyledString();
+          time_ = GetNow();
+        }
 
-          // Replace UNIX newlines with DOS newlines 
-          boost::replace_all(target_, "\n", "\r\n");
-
-          success_ = true;
-        }
+        context_.ReadDicom(target_, resource.GetIdentifier());
+        success_ = true;
       }
     }
   };
@@ -486,7 +436,7 @@
         std::list<std::string> resources;
         try
         {
-          context_.GetIndex().GetChildren(resources, parentSeries_);
+          context_.GetIndex().GetChildren(resources, ResourceType_Series, parentSeries_);
         }
         catch (OrthancException&)
         {
@@ -502,7 +452,7 @@
 
           FileInfo info;
           int64_t revision;  // Ignored
-          if (context_.GetIndex().LookupAttachment(info, revision, *it, FileContentType_Dicom))
+          if (context_.GetIndex().LookupAttachment(info, revision, ResourceType_Instance, *it, FileContentType_Dicom))
           {
             std::unique_ptr<File> resource(new File(*it + ".dcm"));
             resource->SetMimeType(MimeType_Dicom);
@@ -555,7 +505,7 @@
         std::list<std::string> resources;
         try
         {
-          context_.GetIndex().GetChildren(resources, parentSeries_);
+          context_.GetIndex().GetChildren(resources, ResourceType_Series, parentSeries_);
         }
         catch (OrthancException&)
         {
@@ -873,6 +823,7 @@
   class OrthancWebDav::SingleDicomResource : public ListOfResources
   {
   private:
+    ResourceType parentLevel_;
     std::string  parentId_;
     
   protected: 
@@ -880,7 +831,7 @@
     {
       try
       {
-        GetContext().GetIndex().GetChildren(resources, parentId_);
+        GetContext().GetIndex().GetChildren(resources, parentLevel_, parentId_);
       }
       catch (OrthancException&)
       {
@@ -912,6 +863,7 @@
                         const std::string& parentId,
                         const Templates& templates) :
       ListOfResources(context, level, templates),
+      parentLevel_(GetParentResourceType(level)),
       parentId_(parentId)
     {
     }
@@ -955,7 +907,7 @@
     std::string  year_;
     std::string  month_;
 
-    class Visitor : public ServerContext::ILookupVisitor
+    class Visitor : public ResourceFinder::IVisitor
     {
     private:
       std::list<std::string>&  resources_;
@@ -966,21 +918,14 @@
       {
       }
 
-      virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
-      {
-        return false;   // (*)
-      }
-      
       virtual void MarkAsComplete() ORTHANC_OVERRIDE
       {
       }
 
-      virtual void Visit(const std::string& publicId,
-                         const std::string& instanceId   /* unused     */,
-                         const DicomMap& mainDicomTags,
-                         const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
+      virtual void Apply(const FindResponse::Resource& resource,
+                         const DicomMap& requestedTags) ORTHANC_OVERRIDE
       {
-        resources_.push_back(publicId);
+        resources_.push_back(resource.GetIdentifier());
       }
     };
     
@@ -992,7 +937,10 @@
                               true /* case sensitive */, true /* mandatory tag */);
 
       Visitor visitor(resources);
-      GetContext().Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */);
+
+      ResourceFinder finder(ResourceType_Study, ResponseContentFlags_ID, GetContext().GetFindStorageAccessMode(), GetContext().GetIndex().HasFindSupport());
+      finder.SetDatabaseLookup(query);
+      finder.Execute(visitor, GetContext());
     }
 
     virtual INode* CreateResourceNode(const std::string& resource) ORTHANC_OVERRIDE
@@ -1025,7 +973,7 @@
     std::string       year_;
     const Templates&  templates_;
 
-    class Visitor : public ServerContext::ILookupVisitor
+    class Visitor : public ResourceFinder::IVisitor
     {
     private:
       std::set<std::string> months_;
@@ -1036,20 +984,16 @@
         return months_;
       }
       
-      virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
-      {
-        return false;   // (*)
-      }
-      
       virtual void MarkAsComplete() ORTHANC_OVERRIDE
       {
       }
 
-      virtual void Visit(const std::string& publicId,
-                         const std::string& instanceId   /* unused     */,
-                         const DicomMap& mainDicomTags,
-                         const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
+      virtual void Apply(const FindResponse::Resource& resource,
+                         const DicomMap& requestedTags) ORTHANC_OVERRIDE
       {
+        DicomMap mainDicomTags;
+        resource.GetMainDicomTags(mainDicomTags, ResourceType_Study);
+
         std::string s;
         if (mainDicomTags.LookupStringValue(s, DICOM_TAG_STUDY_DATE, false) &&
             s.size() == 8)
@@ -1071,7 +1015,10 @@
                               true /* case sensitive */, true /* mandatory tag */);
 
       Visitor visitor;
-      context_.Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */);
+
+      ResourceFinder finder(ResourceType_Study, ResponseContentFlags_ID, context_.GetFindStorageAccessMode(), context_.GetIndex().HasFindSupport());
+      finder.SetDatabaseLookup(query);
+      finder.Execute(visitor, context_);
 
       for (std::set<std::string>::const_iterator it = visitor.GetMonths().begin();
            it != visitor.GetMonths().end(); ++it)
@@ -1165,7 +1112,7 @@
   };
 
 
-  class OrthancWebDav::DicomDeleteVisitor : public ServerContext::ILookupVisitor
+  class OrthancWebDav::DicomDeleteVisitor : public ResourceFinder::IVisitor
   {
   private:
     ServerContext&  context_;
@@ -1179,22 +1126,15 @@
     {
     }
 
-    virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
-    {
-      return false;   // (*)
-    }
-      
     virtual void MarkAsComplete() ORTHANC_OVERRIDE
     {
     }
 
-    virtual void Visit(const std::string& publicId,
-                       const std::string& instanceId   /* unused     */,
-                       const DicomMap& mainDicomTags   /* unused     */,
-                       const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
+    virtual void Apply(const FindResponse::Resource& resource,
+                       const DicomMap& requestedTags) ORTHANC_OVERRIDE
     {
       Json::Value info;
-      context_.DeleteResource(info, publicId, level_);
+      context_.DeleteResource(info, resource.GetIdentifier(), level_);
     }
   };
   
@@ -1425,12 +1365,11 @@
     {
       DatabaseLookup query;
       ResourceType level;
-      size_t limit = 0;  // By default, no limits
 
       if (path.size() == 1)
       {
         level = ResourceType_Study;
-        limit = 0;  // TODO - Should we limit here?
+        // TODO - Should we limit here?
       }
       else if (path.size() == 2)
       {
@@ -1455,9 +1394,32 @@
         return false;
       }
 
-      DicomIdentifiersVisitor visitor(context_, collection, level);
-      context_.Apply(visitor, query, level, 0 /* since */, limit);
-      
+      ResourceFinder finder(level, ResponseContentFlags_ID, context_.GetFindStorageAccessMode(), context_.GetIndex().HasFindSupport());
+      finder.SetDatabaseLookup(query);
+      finder.SetRetrieveMetadata(true);
+
+      switch (level)
+      {
+        case ResourceType_Study:
+          finder.AddRequestedTag(DICOM_TAG_STUDY_INSTANCE_UID);
+          break;
+
+        case ResourceType_Series:
+          finder.AddRequestedTag(DICOM_TAG_SERIES_INSTANCE_UID);
+          break;
+
+        case ResourceType_Instance:
+          finder.AddRequestedTag(DICOM_TAG_SOP_INSTANCE_UID);
+          finder.SetRetrieveAttachments(true);
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      DicomIdentifiersVisitorV2 visitor(collection);
+      finder.Execute(visitor, context_);
+
       return true;
     }
     else if (path[0] == BY_PATIENTS ||
@@ -1478,6 +1440,33 @@
   }
 
   
+  static bool GetOrthancJson(std::string& target,
+                             ServerContext& context,
+                             ResourceType level,
+                             const DatabaseLookup& query)
+  {
+    ResourceFinder finder(level, ResponseContentFlags_ExpandTrue, context.GetFindStorageAccessMode(), context.GetIndex().HasFindSupport());
+    finder.SetDatabaseLookup(query);
+
+    Json::Value expanded;
+    finder.Execute(expanded, context, DicomToJsonFormat_Human, false /* don't add "Metadata" */);
+
+    if (expanded.size() != 1)
+    {
+      return false;
+    }
+    else
+    {
+      target = expanded[0].toStyledString();
+
+      // Replace UNIX newlines with DOS newlines
+      boost::replace_all(target, "\n", "\r\n");
+
+      return true;
+    }
+  }
+
+
   bool OrthancWebDav::GetFileContent(MimeType& mime,
                                      std::string& content,
                                      boost::posix_time::ptime& modificationTime, 
@@ -1495,12 +1484,9 @@
         DatabaseLookup query;
         query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
                                 true /* case sensitive */, true /* mandatory tag */);
-      
-        OrthancJsonVisitor visitor(context_, content, ResourceType_Study);
-        context_.Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */);
 
         mime = MimeType_Json;
-        return visitor.IsSuccess();
+        return GetOrthancJson(content, context_, ResourceType_Study, query);
       }
       else if (path.size() == 4 &&
                path[3] == SERIES_INFO)
@@ -1511,11 +1497,8 @@
         query.AddRestConstraint(DICOM_TAG_SERIES_INSTANCE_UID, path[2],
                                 true /* case sensitive */, true /* mandatory tag */);
       
-        OrthancJsonVisitor visitor(context_, content, ResourceType_Series);
-        context_.Apply(visitor, query, ResourceType_Series, 0 /* since */, 0 /* no limit */);
-
         mime = MimeType_Json;
-        return visitor.IsSuccess();
+        return GetOrthancJson(content, context_, ResourceType_Series, query);
       }
       else if (path.size() == 4 &&
                boost::ends_with(path[3], ".dcm"))
@@ -1530,10 +1513,16 @@
         query.AddRestConstraint(DICOM_TAG_SOP_INSTANCE_UID, sopInstanceUid,
                                 true /* case sensitive */, true /* mandatory tag */);
       
-        DicomFileVisitor visitor(context_, content, modificationTime);
-        context_.Apply(visitor, query, ResourceType_Instance, 0 /* since */, 0 /* no limit */);
-        
         mime = MimeType_Dicom;
+
+        ResourceFinder finder(ResourceType_Instance, ResponseContentFlags_ID, context_.GetFindStorageAccessMode(), context_.GetIndex().HasFindSupport());
+        finder.SetDatabaseLookup(query);
+        finder.SetRetrieveMetadata(true);
+        finder.SetRetrieveAttachments(true);
+
+        DicomFileVisitorV2 visitor(context_, content, modificationTime);
+        finder.Execute(visitor, context_);
+
         return visitor.IsSuccess();
       }
       else
@@ -1655,7 +1644,10 @@
         }
 
         DicomDeleteVisitor visitor(context_, level);
-        context_.Apply(visitor, query, level, 0 /* since */, 0 /* no limit */);
+
+        ResourceFinder finder(level, ResponseContentFlags_ID, context_.GetFindStorageAccessMode(), context_.GetIndex().HasFindSupport());
+        finder.SetDatabaseLookup(query);
+        finder.Execute(visitor, context_);
         return true;
       }
       else
--- a/OrthancServer/Sources/OrthancWebDav.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/OrthancWebDav.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -38,8 +38,8 @@
     typedef std::map<ResourceType, std::string>  Templates;
 
     class DicomDeleteVisitor;
-    class DicomFileVisitor;
-    class DicomIdentifiersVisitor;  
+    class DicomFileVisitorV2;
+    class DicomIdentifiersVisitorV2;
     class InstancesOfSeries;
     class InternalNode;
     class ListOfResources;
@@ -47,6 +47,7 @@
     class ListOfStudiesByMonth;
     class ListOfStudiesByYear;
     class OrthancJsonVisitor;
+    class OrthancJsonVisitorV2;
     class ResourcesIndex;
     class RootNode;
     class SingleDicomResource;
--- a/OrthancServer/Sources/PrecompiledHeadersServer.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/PrecompiledHeadersServer.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/PrecompiledHeadersServer.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/PrecompiledHeadersServer.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/QueryRetrieveHandler.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/QueryRetrieveHandler.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -79,7 +79,7 @@
           params.SetTimeout(timeout_);
         }
         
-        DicomControlUserConnection connection(params);
+        DicomControlUserConnection connection(params, static_cast<ScuOperationFlags>(ScuOperationFlags_Find));
         connection.Find(answers_, level_, fixed, findNormalized_);
       }
 
--- a/OrthancServer/Sources/QueryRetrieveHandler.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/QueryRetrieveHandler.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ResourceFinder.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,1356 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PrecompiledHeadersServer.h"
+#include "ResourceFinder.h"
+
+#include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
+#include "../../OrthancFramework/Sources/Logging.h"
+#include "../../OrthancFramework/Sources/OrthancException.h"
+#include "../../OrthancFramework/Sources/SerializationToolbox.h"
+#include "OrthancConfiguration.h"
+#include "Search/DatabaseLookup.h"
+#include "ServerContext.h"
+#include "ServerIndex.h"
+
+
+namespace Orthanc
+{
+  static bool IsComputedTag(const DicomTag& tag)
+  {
+    return (tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES ||
+            tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES ||
+            tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES ||
+            tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES ||
+            tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES ||
+            tag == DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES ||
+            tag == DICOM_TAG_SOP_CLASSES_IN_STUDY ||
+            tag == DICOM_TAG_MODALITIES_IN_STUDY ||
+            tag == DICOM_TAG_INSTANCE_AVAILABILITY);
+  }
+
+  void ResourceFinder::ConfigureChildrenCountComputedTag(DicomTag tag,
+                                                         ResourceType parentLevel,
+                                                         ResourceType childLevel)
+  {
+    if (request_.GetLevel() == parentLevel)
+    {
+      requestedComputedTags_.insert(tag);
+      request_.GetChildrenSpecification(childLevel).SetRetrieveCount(true);
+    }
+  }
+
+
+  void ResourceFinder::InjectChildrenCountComputedTag(DicomMap& requestedTags,
+                                                      DicomTag tag,
+                                                      const FindResponse::Resource& resource,
+                                                      ResourceType level) const
+  {
+    if (IsRequestedComputedTag(tag))
+    {
+      requestedTags.SetValue(tag, boost::lexical_cast<std::string>(resource.GetChildrenCount(level)), false);
+    }
+  }
+
+
+  void ResourceFinder::InjectComputedTags(DicomMap& requestedTags,
+                                          const FindResponse::Resource& resource) const
+  {
+    switch (resource.GetLevel())
+    {
+      case ResourceType_Patient:
+        InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES, resource, ResourceType_Study);
+        InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES, resource, ResourceType_Series);
+        InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES, resource, ResourceType_Instance);
+        break;
+
+      case ResourceType_Study:
+        InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES, resource, ResourceType_Series);
+        InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES, resource, ResourceType_Instance);
+
+        if (IsRequestedComputedTag(DICOM_TAG_MODALITIES_IN_STUDY))
+        {
+          std::set<std::string> modalities;
+          resource.GetChildrenMainDicomTagValues(modalities, ResourceType_Series, DICOM_TAG_MODALITY);
+
+          std::string s;
+          Toolbox::JoinStrings(s, modalities, "\\");
+
+          requestedTags.SetValue(DICOM_TAG_MODALITIES_IN_STUDY, s, false);
+        }
+
+        if (IsRequestedComputedTag(DICOM_TAG_SOP_CLASSES_IN_STUDY))
+        {
+          std::set<std::string> classes;
+          resource.GetChildrenMetadataValues(classes, ResourceType_Instance, MetadataType_Instance_SopClassUid);
+
+          std::string s;
+          Toolbox::JoinStrings(s, classes, "\\");
+
+          requestedTags.SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, s, false);
+        }
+
+        break;
+
+      case ResourceType_Series:
+        InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES, resource, ResourceType_Instance);
+        break;
+
+      case ResourceType_Instance:
+        if (IsRequestedComputedTag(DICOM_TAG_INSTANCE_AVAILABILITY))
+        {
+          requestedTags.SetValue(DICOM_TAG_INSTANCE_AVAILABILITY, "ONLINE", false);
+        }
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  SeriesStatus ResourceFinder::GetSeriesStatus(uint32_t& expectedNumberOfInstances,
+                                               const FindResponse::Resource& resource)
+  {
+    if (resource.GetLevel() != ResourceType_Series)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    std::string s;
+    if (!resource.LookupMetadata(s, ResourceType_Series, MetadataType_Series_ExpectedNumberOfInstances) ||
+        !SerializationToolbox::ParseUnsignedInteger32(expectedNumberOfInstances, s))
+    {
+      return SeriesStatus_Unknown;
+    }
+
+    std::set<std::string> values;
+    resource.GetChildrenMetadataValues(values, ResourceType_Instance, MetadataType_Instance_IndexInSeries);
+
+    std::set<int64_t> instances;
+
+    for (std::set<std::string>::const_iterator
+           it = values.begin(); it != values.end(); ++it)
+    {
+      int64_t index;
+
+      if (!SerializationToolbox::ParseInteger64(index, *it))
+      {
+        return SeriesStatus_Unknown;
+      }
+
+      if (index <= 0 ||
+          index > static_cast<int64_t>(expectedNumberOfInstances))
+      {
+        // Out-of-range instance index
+        return SeriesStatus_Inconsistent;
+      }
+
+      if (instances.find(index) != instances.end())
+      {
+        // Twice the same instance index
+        return SeriesStatus_Inconsistent;
+      }
+
+      instances.insert(index);
+    }
+
+    if (instances.size() == static_cast<size_t>(expectedNumberOfInstances))
+    {
+      return SeriesStatus_Complete;
+    }
+    else
+    {
+      return SeriesStatus_Missing;
+    }
+  }
+
+  static void GetMainDicomSequencesFromMetadata(DicomMap& target, const FindResponse::Resource& resource, ResourceType level)
+  {
+    // read all main sequences from DB
+    std::string serializedSequences;
+    if (resource.LookupMetadata(serializedSequences, level, MetadataType_MainDicomSequences))
+    {
+      Json::Value jsonMetadata;
+      Toolbox::ReadJson(jsonMetadata, serializedSequences);
+
+      if (jsonMetadata["Version"].asInt() == 1)
+      {
+        target.FromDicomAsJson(jsonMetadata["Sequences"], true /* append */, true /* parseSequences */);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_NotImplemented);
+      }
+    }
+  }
+
+
+  void ResourceFinder::Expand(Json::Value& target,
+                              const FindResponse::Resource& resource,
+                              ServerIndex& index,
+                              DicomToJsonFormat format) const
+  {
+    /**
+     * This method closely follows "SerializeExpandedResource()" in
+     * "ServerContext.cpp" from Orthanc 1.12.4.
+     **/
+
+    if (responseContent_ == ResponseContentFlags_ID)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    if (resource.GetLevel() != request_.GetLevel())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    target = Json::objectValue;
+
+    target["Type"] = GetResourceTypeText(resource.GetLevel(), false, true);
+    target["ID"] = resource.GetIdentifier();
+
+    if (responseContent_ & ResponseContentFlags_Parent)
+    {
+      switch (resource.GetLevel())
+      {
+        case ResourceType_Patient:
+          break;
+
+        case ResourceType_Study:
+          target["ParentPatient"] = resource.GetParentIdentifier();
+          break;
+
+        case ResourceType_Series:
+          target["ParentStudy"] = resource.GetParentIdentifier();
+          break;
+
+        case ResourceType_Instance:
+          target["ParentSeries"] = resource.GetParentIdentifier();
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    if ((responseContent_ & ResponseContentFlags_Children) && (resource.GetLevel() != ResourceType_Instance))
+    {
+      const std::set<std::string>& children = resource.GetChildrenIdentifiers(GetChildResourceType(resource.GetLevel()));
+
+      Json::Value c = Json::arrayValue;
+      for (std::set<std::string>::const_iterator
+             it = children.begin(); it != children.end(); ++it)
+      {
+        c.append(*it);
+      }
+
+      switch (resource.GetLevel())
+      {
+        case ResourceType_Patient:
+          target["Studies"] = c;
+          break;
+
+        case ResourceType_Study:
+          target["Series"] = c;
+          break;
+
+        case ResourceType_Series:
+          target["Instances"] = c;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    switch (resource.GetLevel())
+    {
+      case ResourceType_Patient:
+      case ResourceType_Study:
+        break;
+
+      case ResourceType_Series:
+      {
+        if ((responseContent_ & ResponseContentFlags_Status) || (responseContent_ & ResponseContentFlags_MetadataLegacy) )
+        {
+          uint32_t expectedNumberOfInstances;
+          SeriesStatus status = GetSeriesStatus(expectedNumberOfInstances, resource);
+          
+          if (responseContent_ & ResponseContentFlags_Status )
+          {
+            target["Status"] = EnumerationToString(status);
+          }
+
+          if (responseContent_ & ResponseContentFlags_MetadataLegacy)
+          {
+            static const char* const EXPECTED_NUMBER_OF_INSTANCES = "ExpectedNumberOfInstances";
+
+            if (status == SeriesStatus_Unknown)
+            {
+              target[EXPECTED_NUMBER_OF_INSTANCES] = Json::nullValue;
+            }
+            else
+            {
+              target[EXPECTED_NUMBER_OF_INSTANCES] = expectedNumberOfInstances;
+            }
+          }
+        }
+        break;
+      }
+
+      case ResourceType_Instance:
+      {
+        if (responseContent_ & ResponseContentFlags_AttachmentsLegacy)
+        {
+          FileInfo info;
+          int64_t revision;
+          if (resource.LookupAttachment(info, revision, FileContentType_Dicom))
+          {
+            target["FileSize"] = static_cast<Json::UInt64>(info.GetUncompressedSize());
+            target["FileUuid"] = info.GetUuid();
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+        }
+
+        if (responseContent_ & ResponseContentFlags_MetadataLegacy)
+        {
+          static const char* const INDEX_IN_SERIES = "IndexInSeries";
+
+          std::string s;
+          uint32_t indexInSeries;
+          if (resource.LookupMetadata(s, ResourceType_Instance, MetadataType_Instance_IndexInSeries) &&
+              SerializationToolbox::ParseUnsignedInteger32(indexInSeries, s))
+          {
+            target[INDEX_IN_SERIES] = indexInSeries;
+          }
+          else
+          {
+            target[INDEX_IN_SERIES] = Json::nullValue;
+          }
+        }
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    std::string s;
+    if (responseContent_ & ResponseContentFlags_MetadataLegacy)
+    {
+      if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_AnonymizedFrom))
+      {
+        target["AnonymizedFrom"] = s;
+      }
+
+      if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_ModifiedFrom))
+      {
+        target["ModifiedFrom"] = s;
+      }
+
+      if (resource.GetLevel() == ResourceType_Patient ||
+          resource.GetLevel() == ResourceType_Study ||
+          resource.GetLevel() == ResourceType_Series)
+      {
+        if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_LastUpdate))
+        {
+          target["LastUpdate"] = s;
+        }
+      }
+    }
+
+    if (responseContent_ & ResponseContentFlags_IsStable)
+    {
+      if (resource.GetLevel() == ResourceType_Patient ||
+          resource.GetLevel() == ResourceType_Study ||
+          resource.GetLevel() == ResourceType_Series)
+      {
+        target["IsStable"] = !index.IsUnstableResource(resource.GetLevel(), resource.GetInternalId());
+      }
+    }
+
+    if (responseContent_ & ResponseContentFlags_MainDicomTags)
+    {
+      DicomMap allMainDicomTags;
+      resource.GetMainDicomTags(allMainDicomTags, resource.GetLevel());
+
+      // read all main sequences from DB
+      GetMainDicomSequencesFromMetadata(allMainDicomTags, resource, resource.GetLevel());
+
+      static const char* const MAIN_DICOM_TAGS = "MainDicomTags";
+      static const char* const PATIENT_MAIN_DICOM_TAGS = "PatientMainDicomTags";
+
+      // TODO-FIND : Ignore "null" values
+
+      DicomMap levelMainDicomTags;
+      allMainDicomTags.ExtractResourceInformation(levelMainDicomTags, resource.GetLevel());
+
+      target[MAIN_DICOM_TAGS] = Json::objectValue;
+      FromDcmtkBridge::ToJson(target[MAIN_DICOM_TAGS], levelMainDicomTags, format);
+
+      if (resource.GetLevel() == ResourceType_Study)
+      {
+        DicomMap patientMainDicomTags;
+        allMainDicomTags.ExtractPatientInformation(patientMainDicomTags);
+
+        target[PATIENT_MAIN_DICOM_TAGS] = Json::objectValue;
+        FromDcmtkBridge::ToJson(target[PATIENT_MAIN_DICOM_TAGS], patientMainDicomTags, format);
+      }
+    }
+
+    if (responseContent_ & ResponseContentFlags_Labels)
+    {
+      Json::Value labels = Json::arrayValue;
+
+      for (std::set<std::string>::const_iterator
+             it = resource.GetLabels().begin(); it != resource.GetLabels().end(); ++it)
+      {
+        labels.append(*it);
+      }
+
+      target["Labels"] = labels;
+    }
+
+    if (responseContent_ & ResponseContentFlags_Metadata)  // new in Orthanc 1.12.4
+    {
+      const std::map<MetadataType, FindResponse::MetadataContent>& m = resource.GetMetadata(resource.GetLevel());
+
+      Json::Value metadata = Json::objectValue;
+
+      for (std::map<MetadataType, FindResponse::MetadataContent>::const_iterator it = m.begin(); it != m.end(); ++it)
+      {
+        metadata[EnumerationToString(it->first)] = it->second.GetValue();
+      }
+
+      target["Metadata"] = metadata;
+    }
+
+    if (responseContent_ & ResponseContentFlags_Attachments)  // new in Orthanc 1.12.5
+    {
+      const std::map<FileContentType, FileInfo>& attachments = resource.GetAttachments();
+
+      target["Attachments"] = Json::arrayValue;
+
+      for (std::map<FileContentType, FileInfo>::const_iterator it = attachments.begin(); it != attachments.end(); ++it)
+      {
+        Json::Value attachment = Json::objectValue;    
+        attachment["Uuid"] = it->second.GetUuid();
+        attachment["ContentType"] = it->second.GetContentType();
+        attachment["UncompressedSize"] = Json::Value::UInt64(it->second.GetUncompressedSize());
+        attachment["CompressedSize"] = Json::Value::UInt64(it->second.GetCompressedSize());
+        attachment["UncompressedMD5"] = it->second.GetUncompressedMD5();
+        attachment["CompressedMD5"] = it->second.GetCompressedMD5();
+
+        target["Attachments"].append(attachment);
+      }
+    }
+  }
+
+
+  void ResourceFinder::UpdateRequestLimits(ServerContext& context)
+  {
+    if (context.GetIndex().HasFindSupport())  // in this case, limits are fully implemented in DB
+    {
+      pagingMode_ = PagingMode_FullDatabase;
+
+      if (hasLimitsSince_ || hasLimitsCount_)
+      {
+        pagingMode_ = PagingMode_FullDatabase;
+        if (databaseLimits_ != 0 && limitsCount_ > databaseLimits_)
+        {
+          LOG(WARNING) << "ResourceFinder: \"Limit\" is larger than LimitFindResults/LimitFindInstances configurations, using limit from the configuration file";
+          limitsCount_ = databaseLimits_;
+        }
+
+        request_.SetLimits(limitsSince_, limitsCount_);
+      }
+      else if (databaseLimits_ != 0)
+      {
+        request_.SetLimits(0, databaseLimits_);
+      }
+
+    }
+    else
+    {
+      // By default, use manual paging
+      pagingMode_ = PagingMode_FullManual;
+
+      if (databaseLimits_ != 0)
+      {
+        request_.SetLimits(0, databaseLimits_ + 1);
+      }
+      else
+      {
+        request_.ClearLimits();
+      }
+
+      if (lookup_.get() == NULL &&
+          (hasLimitsSince_ || hasLimitsCount_))
+      {
+        pagingMode_ = PagingMode_FullDatabase;
+        request_.SetLimits(limitsSince_, limitsCount_);
+      }
+
+      if (lookup_.get() != NULL &&
+          canBeFullyPerformedInDb_ &&
+          (hasLimitsSince_ || hasLimitsCount_))
+      {
+        /**
+         * TODO-FIND: "IDatabaseWrapper::ApplyLookupResources()" only
+         * accept the "limit" argument.  The "since" must be implemented
+         * manually.
+         **/
+
+        if (hasLimitsSince_ &&
+            limitsSince_ != 0)
+        {
+          pagingMode_ = PagingMode_ManualSkip;
+          request_.SetLimits(0, limitsCount_ + limitsSince_);
+        }
+        else
+        {
+          pagingMode_ = PagingMode_FullDatabase;
+          request_.SetLimits(0, limitsCount_);
+        }
+      }
+    }
+  }
+
+
+  ResourceFinder::ResourceFinder(ResourceType level,
+                                 ResponseContentFlags responseContent,
+                                 FindStorageAccessMode storageAccessMode,
+                                 bool supportsChildExistQueries) :
+    request_(level),
+    databaseLimits_(0),
+    isSimpleLookup_(true),
+    canBeFullyPerformedInDb_(true),
+    pagingMode_(PagingMode_FullManual),
+    hasLimitsSince_(false),
+    hasLimitsCount_(false),
+    limitsSince_(0),
+    limitsCount_(0),
+    responseContent_(responseContent),
+    storageAccessMode_(storageAccessMode),
+    supportsChildExistQueries_(supportsChildExistQueries),
+    isWarning002Enabled_(false),
+    isWarning004Enabled_(false),
+    isWarning005Enabled_(false)
+  {
+    {
+      OrthancConfiguration::ReaderLock lock;
+      isWarning002Enabled_ = lock.GetConfiguration().IsWarningEnabled(Warnings_002_InconsistentDicomTagsInDb);
+      isWarning004Enabled_ = lock.GetConfiguration().IsWarningEnabled(Warnings_004_NoMainDicomTagsSignature);
+      isWarning005Enabled_ = lock.GetConfiguration().IsWarningEnabled(Warnings_005_RequestingTagFromLowerResourceLevel);
+    }
+
+    request_.SetRetrieveMainDicomTags(responseContent_ & ResponseContentFlags_MainDicomTags);
+    request_.SetRetrieveMetadata((responseContent_ & ResponseContentFlags_Metadata) || (responseContent_ & ResponseContentFlags_MetadataLegacy));
+    request_.SetRetrieveLabels(responseContent_ & ResponseContentFlags_Labels);
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        request_.GetChildrenSpecification(ResourceType_Study).SetRetrieveIdentifiers(responseContent_ & ResponseContentFlags_Children);
+        request_.SetRetrieveAttachments(responseContent_ & ResponseContentFlags_Attachments); 
+        break;
+
+      case ResourceType_Study:
+        request_.GetChildrenSpecification(ResourceType_Series).SetRetrieveIdentifiers(responseContent_ & ResponseContentFlags_Children);
+        request_.SetRetrieveParentIdentifier(responseContent_ & ResponseContentFlags_Parent);
+        request_.SetRetrieveAttachments(responseContent_ & ResponseContentFlags_Attachments); 
+        break;
+
+      case ResourceType_Series:
+        if (responseContent_ & ResponseContentFlags_Status)
+        {
+          request_.GetChildrenSpecification(ResourceType_Instance).AddMetadata(MetadataType_Instance_IndexInSeries); // required for the SeriesStatus
+        }
+        request_.GetChildrenSpecification(ResourceType_Instance).SetRetrieveIdentifiers(responseContent_ & ResponseContentFlags_Children);
+        request_.SetRetrieveParentIdentifier(responseContent_ & ResponseContentFlags_Parent);
+        request_.SetRetrieveAttachments(responseContent_ & ResponseContentFlags_Attachments); 
+        break;
+
+      case ResourceType_Instance:
+        request_.SetRetrieveAttachments((responseContent_ & ResponseContentFlags_AttachmentsLegacy) // for FileSize & FileUuid
+                                        || (responseContent_ & ResponseContentFlags_Attachments)); 
+        request_.SetRetrieveParentIdentifier(true);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  void ResourceFinder::SetDatabaseLimits(uint64_t limits)
+  {
+    databaseLimits_ = limits;
+  }
+
+
+  void ResourceFinder::SetLimitsSince(uint64_t since)
+  {
+    if (hasLimitsSince_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      hasLimitsSince_ = true;
+      limitsSince_ = since;
+    }
+  }
+
+
+  void ResourceFinder::SetLimitsCount(uint64_t count)
+  {
+    if (hasLimitsCount_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      hasLimitsCount_ = true;
+      limitsCount_ = count;
+    }
+  }
+
+
+  void ResourceFinder::SetDatabaseLookup(const DatabaseLookup& lookup)
+  {
+    MainDicomTagsRegistry registry;
+
+    lookup_.reset(lookup.Clone());
+
+    for (size_t i = 0; i < lookup.GetConstraintsCount(); i++)
+    {
+      DicomTag tag = lookup.GetConstraint(i).GetTag();
+      if (IsComputedTag(tag))
+      {
+        AddRequestedTag(tag);
+      }
+      else
+      {
+        ResourceType level;
+        DicomTagType tagType;
+        registry.LookupTag(level, tagType, tag);
+        if (tagType == DicomTagType_Generic)
+        {
+          AddRequestedTag(tag);
+        }
+      }
+    }
+
+    isSimpleLookup_ = registry.NormalizeLookup(canBeFullyPerformedInDb_, request_.GetDicomTagConstraints(), lookup, request_.GetLevel(), supportsChildExistQueries_);
+
+    // "request_.GetDicomTagConstraints()" only contains constraints on main DICOM tags
+
+    for (size_t i = 0; i < request_.GetDicomTagConstraints().GetSize(); i++)
+    {
+      const DatabaseDicomTagConstraint& constraint = request_.GetDicomTagConstraints().GetConstraint(i);
+      if (constraint.GetLevel() == request_.GetLevel())
+      {
+        request_.SetRetrieveMainDicomTags(true);
+      }
+      else if (IsResourceLevelAboveOrEqual(constraint.GetLevel(), request_.GetLevel()))
+      {
+        request_.GetParentSpecification(constraint.GetLevel()).SetRetrieveMainDicomTags(true);
+      }
+      else
+      {
+        if (!supportsChildExistQueries_ || (constraint.GetLevel() != ResourceType_Series && constraint.GetTag() != DICOM_TAG_MODALITIES_IN_STUDY))
+        {
+          LOG(WARNING) << "Executing a database lookup at level " << EnumerationToString(request_.GetLevel())
+                      << " on main DICOM tag " << constraint.GetTag().Format() << " from an inferior level ("
+                      << EnumerationToString(constraint.GetLevel()) << "), this will return no result";
+        }
+      }
+
+      if (IsComputedTag(constraint.GetTag()) && constraint.GetTag() != DICOM_TAG_MODALITIES_IN_STUDY)
+      {
+        // Sanity check
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+  }
+
+
+  void ResourceFinder::AddRequestedTag(const DicomTag& tag)
+  {
+    requestedTags_.insert(tag);
+
+    if (DicomMap::IsMainDicomTag(tag, ResourceType_Patient))
+    {
+      if (request_.GetLevel() == ResourceType_Patient)
+      {
+        request_.SetRetrieveMainDicomTags(true);
+      }
+      else
+      {
+        /**
+         * This comes from the fact that patient-level tags are copied
+         * at the study level, as implemented by "ResourcesContent::AddResource()".
+         **/
+        if (request_.GetLevel() == ResourceType_Study)
+        {
+          request_.SetRetrieveMainDicomTags(true);
+        }
+        else
+        {
+          request_.GetParentSpecification(ResourceType_Study).SetRetrieveMainDicomTags(true);
+          request_.GetParentSpecification(ResourceType_Study).SetRetrieveMetadata(true);  // to get the MainDicomSequences
+        }
+      }
+    }
+    else if (DicomMap::IsMainDicomTag(tag, ResourceType_Study))
+    {
+      if (request_.GetLevel() == ResourceType_Patient)
+      {
+        if (isWarning005Enabled_)
+        {
+          LOG(WARNING) << "W005: Requested tag " << tag.Format()
+                       << " should only be read at the study, series, or instance level";
+        }
+        request_.SetRetrieveOneInstanceMetadataAndAttachments(true); // we might need to get it from one instance
+      }
+      else
+      {
+        if (request_.GetLevel() == ResourceType_Study)
+        {
+          request_.SetRetrieveMainDicomTags(true);
+        }
+        else
+        {
+          request_.GetParentSpecification(ResourceType_Study).SetRetrieveMainDicomTags(true);
+          request_.GetParentSpecification(ResourceType_Study).SetRetrieveMetadata(true);  // to get the MainDicomSequences
+        }
+      }
+    }
+    else if (DicomMap::IsMainDicomTag(tag, ResourceType_Series))
+    {
+      if (request_.GetLevel() == ResourceType_Patient ||
+          request_.GetLevel() == ResourceType_Study)
+      {
+        if (isWarning005Enabled_)
+        {
+          LOG(WARNING) << "W005: Requested tag " << tag.Format()
+                      << " should only be read at the series or instance level";
+        }
+        request_.SetRetrieveOneInstanceMetadataAndAttachments(true); // we might need to get it from one instance
+      }
+      else
+      {
+        if (request_.GetLevel() == ResourceType_Series)
+        {
+          request_.SetRetrieveMainDicomTags(true);
+        }
+        else
+        {
+          request_.GetParentSpecification(ResourceType_Series).SetRetrieveMainDicomTags(true);
+          request_.GetParentSpecification(ResourceType_Series).SetRetrieveMetadata(true);  // to get the MainDicomSequences
+        }
+      }
+    }
+    else if (DicomMap::IsMainDicomTag(tag, ResourceType_Instance))
+    {
+      if (request_.GetLevel() == ResourceType_Patient ||
+          request_.GetLevel() == ResourceType_Study ||
+          request_.GetLevel() == ResourceType_Series)
+      {
+        if (isWarning005Enabled_)
+        {
+          LOG(WARNING) << "W005: Requested tag " << tag.Format()
+                       << " should only be read at the instance level";
+        }
+        request_.SetRetrieveOneInstanceMetadataAndAttachments(true); // we might need to get it from one instance
+      }
+      else
+      {
+        request_.SetRetrieveMainDicomTags(true);
+      }
+    }
+    else if (tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES)
+    {
+      ConfigureChildrenCountComputedTag(tag, ResourceType_Patient, ResourceType_Study);
+    }
+    else if (tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES)
+    {
+      ConfigureChildrenCountComputedTag(tag, ResourceType_Patient, ResourceType_Series);
+    }
+    else if (tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES)
+    {
+      ConfigureChildrenCountComputedTag(tag, ResourceType_Patient, ResourceType_Instance);
+    }
+    else if (tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES)
+    {
+      ConfigureChildrenCountComputedTag(tag, ResourceType_Study, ResourceType_Series);
+    }
+    else if (tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES)
+    {
+      ConfigureChildrenCountComputedTag(tag, ResourceType_Study, ResourceType_Instance);
+    }
+    else if (tag == DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES)
+    {
+      ConfigureChildrenCountComputedTag(tag, ResourceType_Series, ResourceType_Instance);
+    }
+    else if (tag == DICOM_TAG_SOP_CLASSES_IN_STUDY)
+    {
+      requestedComputedTags_.insert(tag);
+      request_.GetChildrenSpecification(ResourceType_Instance).AddMetadata(MetadataType_Instance_SopClassUid);
+    }
+    else if (tag == DICOM_TAG_MODALITIES_IN_STUDY)
+    {
+      requestedComputedTags_.insert(tag);
+      if (request_.GetLevel() < ResourceType_Series)
+      {
+        request_.GetChildrenSpecification(ResourceType_Series).AddMainDicomTag(DICOM_TAG_MODALITY);
+      }
+      else if (request_.GetLevel() == ResourceType_Instance)  // this happens in QIDO-RS when searching for instances without specifying a StudyInstanceUID -> all Study level tags must be included in the response
+      {
+        request_.GetParentSpecification(ResourceType_Series).SetRetrieveMainDicomTags(true);
+      }
+    }
+    else if (tag == DICOM_TAG_INSTANCE_AVAILABILITY)
+    {
+      requestedComputedTags_.insert(tag);
+    }
+    else
+    {
+      // This is neither a main DICOM tag, nor a computed DICOM tag:
+      // We might need to access a DICOM file or the MainDicomSequences metadata
+      
+      request_.SetRetrieveMetadata(true);
+
+      if (request_.GetLevel() != ResourceType_Instance)
+      {
+        // only retrieve the instance attachments and metadata if we have allowed access to the storage
+        request_.SetRetrieveOneInstanceMetadataAndAttachments(IsStorageAccessAllowed());
+      }
+    }
+  }
+
+
+  void ResourceFinder::AddRequestedTags(const std::set<DicomTag>& tags)
+  {
+    for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
+    {
+      AddRequestedTag(*it);
+    }
+  }
+
+
+  static void InjectRequestedTags(DicomMap& target,
+                                  std::set<DicomTag>& remainingRequestedTags /* in & out */,
+                                  const FindResponse::Resource& resource,
+                                  ResourceType level/*,
+                                  const std::set<DicomTag>& tags*/)
+  {
+    if (!remainingRequestedTags.empty() && level <= resource.GetLevel())
+    {
+      std::set<DicomTag> savedMainDicomTags;
+
+      DicomMap m;
+      resource.GetMainDicomTags(m, level);                          // read DicomTags from DB
+
+      if (resource.GetMetadata(level).size() > 0)
+      {
+        GetMainDicomSequencesFromMetadata(m, resource, level);        // read DicomSequences from metadata
+      
+        // check which tags have been saved in DB; that's the way to know if they are missing because they were not saved or because they have no value
+        
+        std::string signature = DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(level); // default signature in case it's not in the metadata (= the signature for 1.11.0)
+        if (resource.LookupMetadata(signature, level, MetadataType_MainDicomTagsSignature))
+        {
+          if (level == ResourceType_Study) // when we retrieve the study tags, we actually also get the patient tags that are also saved at study level but not included in the signature
+          {
+            signature += ";" + DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType_Patient); // append the default signature (from before 1.11.0)
+          }
+
+          FromDcmtkBridge::ParseListOfTags(savedMainDicomTags, signature);
+        }
+      }
+
+      std::set<DicomTag> copiedTags;
+      for (std::set<DicomTag>::const_iterator it = remainingRequestedTags.begin(); it != remainingRequestedTags.end(); ++it)
+      {
+        if (target.CopyTagIfExists(m, *it))
+        {
+          copiedTags.insert(*it);
+        }
+        else if (savedMainDicomTags.find(*it) != savedMainDicomTags.end())  // the tag should have been saved in DB but has no value so we consider it has been copied
+        {
+          copiedTags.insert(*it);
+        }
+      }
+
+      Toolbox::RemoveSets(remainingRequestedTags, copiedTags);
+    }
+  }
+
+
+  static void ConvertMetadata(std::map<MetadataType, std::string>& converted,
+                              const FindResponse::Resource& resource)
+  {
+    const std::map<MetadataType, FindResponse::MetadataContent> metadata = resource.GetMetadata(ResourceType_Instance);
+
+    for (std::map<MetadataType, FindResponse::MetadataContent>::const_iterator
+           it = metadata.begin(); it != metadata.end(); ++it)
+    {
+      converted[it->first] = it->second.GetValue();
+    }
+  }
+
+
+  static void ReadMissingTagsFromStorageArea(DicomMap& requestedTags,
+                                             ServerContext& context,
+                                             const FindRequest& request,
+                                             const FindResponse::Resource& resource,
+                                             const std::set<DicomTag>& missingTags)
+  {
+    OrthancConfiguration::ReaderLock lock;
+    if (lock.GetConfiguration().IsWarningEnabled(Warnings_001_TagsBeingReadFromStorage))
+    {
+      std::string missings;
+      FromDcmtkBridge::FormatListOfTags(missings, missingTags);
+
+      LOG(WARNING) << "W001: Accessing DICOM tags from storage when accessing "
+                   << Orthanc::GetResourceTypeText(resource.GetLevel(), false, false)
+                   << " " << resource.GetIdentifier()
+                   << ": " << missings;
+    }
+
+    // TODO-FIND: What do we do if the DICOM has been removed since the request?
+    // Do we fail, or do we skip the resource?
+
+    Json::Value tmpDicomAsJson;
+
+    if (request.GetLevel() == ResourceType_Instance &&
+        request.IsRetrieveMetadata() &&
+        request.IsRetrieveAttachments())
+    {
+      LOG(INFO) << "Will retrieve missing DICOM tags from instance: " << resource.GetIdentifier();
+
+      std::map<MetadataType, std::string> converted;
+      ConvertMetadata(converted, resource);
+
+      context.ReadDicomAsJson(tmpDicomAsJson, resource.GetIdentifier(), converted,
+                              resource.GetAttachments(), missingTags /* ignoreTagLength */);
+    }
+    else if (request.GetLevel() != ResourceType_Instance &&
+             request.IsRetrieveOneInstanceMetadataAndAttachments())
+    {
+      LOG(INFO) << "Will retrieve missing DICOM tags from instance: " << resource.GetOneInstancePublicId();
+
+      context.ReadDicomAsJson(tmpDicomAsJson, resource.GetOneInstancePublicId(), resource.GetOneInstanceMetadata(),
+                              resource.GetOneInstanceAttachments(), missingTags /* ignoreTagLength */);
+    }
+    else
+    {
+      // TODO-FIND: This fallback shouldn't be necessary
+
+      FindRequest requestDicomAttachment(request.GetLevel());
+      requestDicomAttachment.SetOrthancId(request.GetLevel(), resource.GetIdentifier());
+
+      if (request.GetLevel() == ResourceType_Instance)
+      {
+        requestDicomAttachment.SetRetrieveMetadata(true);
+        requestDicomAttachment.SetRetrieveAttachments(true);
+      }
+      else
+      {
+        requestDicomAttachment.SetRetrieveOneInstanceMetadataAndAttachments(true);
+      }
+
+      FindResponse responseDicomAttachment;
+      context.GetIndex().ExecuteFind(responseDicomAttachment, requestDicomAttachment);
+
+      if (responseDicomAttachment.GetSize() != 1)
+      {
+        throw OrthancException(ErrorCode_InexistentFile);
+      }
+      else
+      {
+        const FindResponse::Resource& response = responseDicomAttachment.GetResourceByIndex(0);
+        const std::string instancePublicId = response.GetIdentifier();
+        LOG(INFO) << "Will retrieve missing DICOM tags from instance: " << instancePublicId;
+
+        if (request.GetLevel() == ResourceType_Instance)
+        {
+          std::map<MetadataType, std::string> converted;
+          ConvertMetadata(converted, resource);
+
+          context.ReadDicomAsJson(tmpDicomAsJson, response.GetIdentifier(), converted,
+                                  response.GetAttachments(), missingTags /* ignoreTagLength */);
+        }
+        else
+        {
+          context.ReadDicomAsJson(tmpDicomAsJson, response.GetOneInstancePublicId(), response.GetOneInstanceMetadata(),
+                                  response.GetOneInstanceAttachments(), missingTags /* ignoreTagLength */);
+        }
+      }
+    }
+
+    DicomMap tmpDicomMap;
+    tmpDicomMap.FromDicomAsJson(tmpDicomAsJson, false /* append */, true /* parseSequences*/);
+
+    for (std::set<DicomTag>::const_iterator it = missingTags.begin(); it != missingTags.end(); ++it)
+    {
+      assert(!requestedTags.HasTag(*it));
+      if (tmpDicomMap.HasTag(*it))
+      {
+        requestedTags.SetValue(*it, tmpDicomMap.GetValue(*it));
+      }
+      else
+      {
+        requestedTags.SetNullValue(*it);  // TODO-FIND: Is this compatible with Orthanc <= 1.12.3?
+      }
+    }
+  }
+
+  uint64_t ResourceFinder::Count(ServerContext& context) const
+  {
+    if (!canBeFullyPerformedInDb_)
+    {
+      throw OrthancException(ErrorCode_BadRequest,
+                            "Unable to count resources when querying tags that are not stored as MainDicomTags in the Database or when using case sensitive queries.");
+    }
+
+    uint64_t count = 0;
+    context.GetIndex().ExecuteCount(count, request_);
+    return count;
+  }
+
+
+  void ResourceFinder::Execute(IVisitor& visitor,
+                               ServerContext& context)
+  {
+    UpdateRequestLimits(context);
+
+    if ((request_.HasLimits() && request_.GetLimitsSince() > 0) &&
+        !canBeFullyPerformedInDb_)
+    {
+      throw OrthancException(ErrorCode_BadRequest,
+                             "nable to use 'Since' when finding resources when querying against Dicom Tags that are not in the MainDicomTags or when using CaseSenstive queries.");
+    }
+
+    bool isWarning002Enabled = false;
+    bool isWarning004Enabled = false;
+    bool isWarning006Enabled = false;
+    bool isWarning007Enabled = false;
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      isWarning002Enabled = lock.GetConfiguration().IsWarningEnabled(Warnings_002_InconsistentDicomTagsInDb);
+      isWarning004Enabled = lock.GetConfiguration().IsWarningEnabled(Warnings_004_NoMainDicomTagsSignature);
+      isWarning006Enabled = lock.GetConfiguration().IsWarningEnabled(Warnings_006_RequestingTagFromMetaHeader);
+      isWarning007Enabled = lock.GetConfiguration().IsWarningEnabled(Warnings_007_MissingRequestedTagsNotReadFromDisk);
+    }
+
+    FindResponse response;
+    context.GetIndex().ExecuteFind(response, request_);
+
+    bool complete;
+
+    switch (pagingMode_)
+    {
+      case PagingMode_FullDatabase:
+      case PagingMode_ManualSkip:
+        complete = true;
+        break;
+
+      case PagingMode_FullManual:
+        complete = (databaseLimits_ == 0 ||
+                    response.GetSize() <= databaseLimits_);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    if (lookup_.get() != NULL)
+    {
+      LOG(INFO) << "Number of candidate resources after fast DB filtering on main DICOM tags: " << response.GetSize();
+    }
+
+    size_t countResults = 0;
+    size_t skipped = 0;
+
+    for (size_t i = 0; i < response.GetSize(); i++)
+    {
+      const FindResponse::Resource& resource = response.GetResourceByIndex(i);
+
+#if 0
+      {
+        Json::Value v;
+        resource.DebugExport(v, request_);
+        std::cout << v.toStyledString();
+      }
+#endif
+
+      DicomMap outRequestedTags;
+
+      if (HasRequestedTags())
+      {
+        std::set<DicomTag> remainingRequestedTags = requestedTags_; // at this point, all requested tags are "missing"
+
+        InjectComputedTags(outRequestedTags, resource);
+        Toolbox::RemoveSets(remainingRequestedTags, requestedComputedTags_);
+
+        InjectRequestedTags(outRequestedTags, remainingRequestedTags, resource, ResourceType_Patient);
+        InjectRequestedTags(outRequestedTags, remainingRequestedTags, resource, ResourceType_Study);
+        InjectRequestedTags(outRequestedTags, remainingRequestedTags, resource, ResourceType_Series);
+        InjectRequestedTags(outRequestedTags, remainingRequestedTags, resource, ResourceType_Instance);
+
+        if (DicomMap::HasMetaInformationTags(remainingRequestedTags)) // we are not able to retrieve meta information in RequestedTags
+        {
+          std::set<DicomTag> metaTagsToRemove;
+          for (std::set<DicomTag>::const_iterator it = remainingRequestedTags.begin(); it != remainingRequestedTags.end(); ++it)
+          {
+            if (it->GetGroup() == 0x0002)
+            {
+              metaTagsToRemove.insert(*it);
+            }
+          }
+
+          if (isWarning006Enabled)
+          {
+            std::string joinedMetaTags;
+            FromDcmtkBridge::FormatListOfTags(joinedMetaTags, metaTagsToRemove);
+            LOG(WARNING) << "W006: Unable to include tags from the Meta Header in \"RequestedTags\".  Skipping them: " << joinedMetaTags;
+          }
+
+          Toolbox::RemoveSets(remainingRequestedTags, metaTagsToRemove);
+        }
+
+        // if SOPClassUID has been requested, we might still find it at metadata level (useful e.g. for StoneViewer)
+        std::string sopClassUidFromMetadata;
+        if (resource.GetLevel() == ResourceType_Instance &&
+            remainingRequestedTags.find(DICOM_TAG_SOP_CLASS_UID) != remainingRequestedTags.end() && 
+            resource.LookupMetadata(sopClassUidFromMetadata, ResourceType_Instance, MetadataType_Instance_SopClassUid))
+        {
+          outRequestedTags.SetValue(DICOM_TAG_SOP_CLASS_UID, sopClassUidFromMetadata, false);
+          remainingRequestedTags.erase(DICOM_TAG_SOP_CLASS_UID);
+        }
+
+        if (!remainingRequestedTags.empty() && 
+            !DicomMap::HasOnlyComputedTags(remainingRequestedTags)) // if the only remaining tags are computed tags, it is worthless to read them from disk
+        {
+          // If a lookup tag is not available from DB, it is included in remainingRequestedTags and it will always be included in the answer too
+          // -> from 1.12.5, "StorageAccessOnFind": "Always" is actually equivalent to "StorageAccessOnFind": "Answers"
+          if (IsStorageAccessAllowed())
+          {
+            ReadMissingTagsFromStorageArea(outRequestedTags, context, request_, resource, remainingRequestedTags);
+          }
+          else if (isWarning007Enabled)
+          {
+            std::string joinedTags;
+            FromDcmtkBridge::FormatListOfTags(joinedTags, remainingRequestedTags);
+            LOG(WARNING) << "W007: Unable to include requested tags since \"StorageAccessOnFind\" does not allow accessing the storage to build answers: " << joinedTags;
+          }
+        }
+
+        std::string mainDicomTagsSignature;
+        if (isWarning002Enabled &&
+            resource.LookupMetadata(mainDicomTagsSignature, resource.GetLevel(), MetadataType_MainDicomTagsSignature) &&
+            mainDicomTagsSignature != DicomMap::GetMainDicomTagsSignature(resource.GetLevel()))
+        {
+          LOG(WARNING) << "W002: " << Orthanc::GetResourceTypeText(resource.GetLevel(), false , false)
+                       << " has been stored with another version of Main Dicom Tags list, you should POST to /"
+                       << Orthanc::GetResourceTypeText(resource.GetLevel(), true, false)
+                       << "/" << resource.GetIdentifier()
+                       << "/reconstruct to update the list of tags saved in DB or run the Housekeeper plugin.  Some MainDicomTags might be missing from this answer.";
+        }
+        else if (isWarning004Enabled && 
+                 request_.IsRetrieveMetadata() &&
+                 !resource.LookupMetadata(mainDicomTagsSignature, resource.GetLevel(), MetadataType_MainDicomTagsSignature))
+        {
+          LOG(WARNING) << "W004: " << Orthanc::GetResourceTypeText(resource.GetLevel(), false , false)
+                       << " has been stored with an old Orthanc version and does not have a MainDicomTagsSignature, you should POST to /"
+                       << Orthanc::GetResourceTypeText(resource.GetLevel(), true, false)
+                       << "/" << resource.GetIdentifier()
+                       << "/reconstruct to update the list of tags saved in DB or run the Housekeeper plugin.  Some MainDicomTags might be missing from this answer.";
+        }
+
+      }
+
+      bool match = true;
+
+      if (lookup_.get() != NULL)
+      {
+        DicomMap tags;
+        resource.GetAllMainDicomTags(tags);
+        tags.Merge(outRequestedTags);
+        match = lookup_->IsMatch(tags);
+      }
+
+      if (match)
+      {
+        if (pagingMode_ == PagingMode_FullDatabase)
+        {
+          visitor.Apply(resource, outRequestedTags);
+        }
+        else
+        {
+          if (hasLimitsSince_ &&
+              skipped < limitsSince_)
+          {
+            skipped++;
+          }
+          else if (hasLimitsCount_ &&
+                   countResults >= limitsCount_)
+          {
+            // Too many results, don't mark as complete
+            complete = false;
+            break;
+          }
+          else
+          {
+            visitor.Apply(resource, outRequestedTags);
+            countResults++;
+          }
+        }
+      }
+    }
+
+    if (complete)
+    {
+      visitor.MarkAsComplete();
+    }
+  }
+
+  bool ResourceFinder::IsStorageAccessAllowed()
+  {
+    switch (storageAccessMode_)
+    {
+      case FindStorageAccessMode_DiskOnAnswer:
+      case FindStorageAccessMode_DiskOnLookupAndAnswer:
+        return true;
+      case FindStorageAccessMode_DatabaseOnly:
+        return false;
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+  void ResourceFinder::Execute(Json::Value& target,
+                               ServerContext& context,
+                               DicomToJsonFormat format,
+                               bool includeAllMetadata)
+  {
+    class Visitor : public IVisitor
+    {
+    private:
+      const ResourceFinder&  that_;
+      ServerIndex&           index_;
+      Json::Value&           target_;
+      DicomToJsonFormat      format_;
+      bool                   hasRequestedTags_;
+      bool                   includeAllMetadata_;
+
+    public:
+      Visitor(const ResourceFinder& that,
+              ServerIndex& index,
+              Json::Value& target,
+              DicomToJsonFormat format,
+              bool hasRequestedTags,
+              bool includeAllMetadata) :
+        that_(that),
+        index_(index),
+        target_(target),
+        format_(format),
+        hasRequestedTags_(hasRequestedTags),
+        includeAllMetadata_(includeAllMetadata)
+      {
+      }
+
+      virtual void Apply(const FindResponse::Resource& resource,
+                         const DicomMap& requestedTags) ORTHANC_OVERRIDE
+      {
+        if (that_.responseContent_ != ResponseContentFlags_ID)
+        {
+          Json::Value item;
+          that_.Expand(item, resource, index_, format_);
+
+          if (hasRequestedTags_)
+          {
+            static const char* const REQUESTED_TAGS = "RequestedTags";
+            item[REQUESTED_TAGS] = Json::objectValue;
+            FromDcmtkBridge::ToJson(item[REQUESTED_TAGS], requestedTags, format_);
+          }
+
+          target_.append(item);
+        }
+        else
+        {
+          target_.append(resource.GetIdentifier());
+        }
+      }
+
+      virtual void MarkAsComplete() ORTHANC_OVERRIDE
+      {
+      }
+    };
+
+    UpdateRequestLimits(context);
+
+    target = Json::arrayValue;
+
+    Visitor visitor(*this, context.GetIndex(), target, format, HasRequestedTags(), includeAllMetadata);
+    Execute(visitor, context);
+  }
+
+
+  bool ResourceFinder::ExecuteOneResource(Json::Value& target,
+                                          ServerContext& context,
+                                          DicomToJsonFormat format,
+                                          bool includeAllMetadata)
+  {
+    Json::Value answer;
+    Execute(answer, context, format, includeAllMetadata);
+
+    if (answer.type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else if (answer.size() > 1)
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin);
+    }
+    else if (answer.empty())
+    {
+      // Inexistent resource (or was deleted between the first and second phases)
+      return false;
+    }
+    else
+    {
+      target = answer[0];
+      return true;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ResourceFinder.h	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,207 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "Database/FindRequest.h"
+#include "Database/FindResponse.h"
+
+namespace Orthanc
+{
+  class DatabaseLookup;
+  class ServerContext;
+  class ServerIndex;
+
+  class ResourceFinder : public boost::noncopyable
+  {
+  public:
+    class IVisitor : public boost::noncopyable
+    {
+    public:
+      virtual ~IVisitor()
+      {
+      }
+
+      virtual void Apply(const FindResponse::Resource& resource,
+                         const DicomMap& requestedTags) = 0;
+
+      virtual void MarkAsComplete() = 0;
+    };
+
+  private:
+    enum PagingMode
+    {
+      PagingMode_FullDatabase,
+      PagingMode_FullManual,
+      PagingMode_ManualSkip
+    };
+
+    FindRequest                      request_;
+    uint64_t                         databaseLimits_;
+    std::unique_ptr<DatabaseLookup>  lookup_;
+    bool                             isSimpleLookup_;
+    bool                             canBeFullyPerformedInDb_;
+    PagingMode                       pagingMode_;
+    bool                             hasLimitsSince_;
+    bool                             hasLimitsCount_;
+    uint64_t                         limitsSince_;
+    uint64_t                         limitsCount_;
+    ResponseContentFlags             responseContent_;
+    FindStorageAccessMode            storageAccessMode_;
+    bool                             supportsChildExistQueries_;
+    std::set<DicomTag>               requestedTags_;
+    std::set<DicomTag>               requestedComputedTags_;
+
+    bool                             isWarning002Enabled_;
+    bool                             isWarning004Enabled_;
+    bool                             isWarning005Enabled_;
+
+    bool IsRequestedComputedTag(const DicomTag& tag) const
+    {
+      return requestedComputedTags_.find(tag) != requestedComputedTags_.end();
+    }
+
+    void ConfigureChildrenCountComputedTag(DicomTag tag,
+                                           ResourceType parentLevel,
+                                           ResourceType childLevel);
+
+    void InjectChildrenCountComputedTag(DicomMap& requestedTags,
+                                        DicomTag tag,
+                                        const FindResponse::Resource& resource,
+                                        ResourceType level) const;
+
+    static SeriesStatus GetSeriesStatus(uint32_t& expectedNumberOfInstances,
+                                        const FindResponse::Resource& resource);
+
+    void InjectComputedTags(DicomMap& requestedTags,
+                            const FindResponse::Resource& resource) const;
+
+    void UpdateRequestLimits(ServerContext& context);
+
+    bool HasRequestedTags() const
+    {
+      return requestedTags_.size() > 0;
+    }
+
+    bool IsStorageAccessAllowed();
+
+  public:
+    ResourceFinder(ResourceType level,
+                   ResponseContentFlags responseContent,
+                   FindStorageAccessMode storageAccessMode,
+                   bool supportsChildExistQueries);
+
+    void SetDatabaseLimits(uint64_t limits);
+
+    void SetOrthancId(ResourceType level,
+                      const std::string& id)
+    {
+      request_.SetOrthancId(level, id);
+    }
+
+    void SetLimitsSince(uint64_t since);
+
+    void SetLimitsCount(uint64_t count);
+
+    void SetDatabaseLookup(const DatabaseLookup& lookup);
+
+    void AddRequestedTag(const DicomTag& tag);
+
+    void AddRequestedTags(const std::set<DicomTag>& tags);
+
+    void AddOrdering(const DicomTag& tag,
+                     FindRequest::OrderingCast cast,
+                     FindRequest::OrderingDirection direction)
+    {
+      request_.AddOrdering(tag, cast, direction);
+    }
+
+    void AddOrdering(MetadataType metadataType,
+                     FindRequest::OrderingCast cast,
+                     FindRequest::OrderingDirection direction)
+    {
+      request_.AddOrdering(metadataType, cast, direction);
+    }
+
+    void AddMetadataConstraint(DatabaseMetadataConstraint* constraint)
+    {
+      request_.AddMetadataConstraint(constraint);
+    }
+
+    void SetLabels(const std::set<std::string>& labels)
+    {
+      request_.SetLabels(labels);
+    }
+
+    void AddLabel(const std::string& label)
+    {
+      request_.AddLabel(label);
+    }
+
+    void SetLabelsConstraint(LabelsConstraint constraint)
+    {
+      request_.SetLabelsConstraint(constraint);
+    }
+
+    void SetRetrieveOneInstanceMetadataAndAttachments(bool retrieve)
+    {
+      request_.SetRetrieveOneInstanceMetadataAndAttachments(retrieve);
+    }
+
+    void SetRetrieveMetadata(bool retrieve)
+    {
+      request_.SetRetrieveMetadata(retrieve);
+    }
+
+    void SetRetrieveAttachments(bool retrieve)
+    {
+      request_.SetRetrieveAttachments(retrieve);
+    }
+
+    // NB: "index" is only used in this method to fill the "IsStable" information
+    void Expand(Json::Value& target,
+                const FindResponse::Resource& resource,
+                ServerIndex& index,
+                DicomToJsonFormat format) const;
+
+    void Execute(IVisitor& visitor,
+                 ServerContext& context);
+
+    void Execute(Json::Value& target,
+                 ServerContext& context,
+                 DicomToJsonFormat format,
+                 bool includeAllMetadata);
+
+    bool ExecuteOneResource(Json::Value& target,
+                            ServerContext& context,
+                            DicomToJsonFormat format,
+                            bool includeAllMetadata);
+
+    uint64_t Count(ServerContext& context) const;
+
+    bool CanBeFullyPerformedInDb() const
+    {
+      return canBeFullyPerformedInDb_;
+    }
+  };
+}
--- a/OrthancServer/Sources/Search/DatabaseConstraint.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,248 +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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#if !defined(ORTHANC_BUILDING_SERVER_LIBRARY)
-#  error Macro ORTHANC_BUILDING_SERVER_LIBRARY must be defined
-#endif
-
-#if ORTHANC_BUILDING_SERVER_LIBRARY == 1
-#  include "../PrecompiledHeadersServer.h"
-#endif
-
-#include "DatabaseConstraint.h"
-
-#if ORTHANC_BUILDING_SERVER_LIBRARY == 1
-#  include "../../../OrthancFramework/Sources/OrthancException.h"
-#else
-#  include <OrthancException.h>
-#endif
-
-#include <cassert>
-
-
-namespace Orthanc
-{
-  namespace Plugins
-  {
-#if ORTHANC_ENABLE_PLUGINS == 1
-    OrthancPluginResourceType Convert(ResourceType type)
-    {
-      switch (type)
-      {
-        case ResourceType_Patient:
-          return OrthancPluginResourceType_Patient;
-
-        case ResourceType_Study:
-          return OrthancPluginResourceType_Study;
-
-        case ResourceType_Series:
-          return OrthancPluginResourceType_Series;
-
-        case ResourceType_Instance:
-          return OrthancPluginResourceType_Instance;
-
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-#endif
-
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-    ResourceType Convert(OrthancPluginResourceType type)
-    {
-      switch (type)
-      {
-        case OrthancPluginResourceType_Patient:
-          return ResourceType_Patient;
-
-        case OrthancPluginResourceType_Study:
-          return ResourceType_Study;
-
-        case OrthancPluginResourceType_Series:
-          return ResourceType_Series;
-
-        case OrthancPluginResourceType_Instance:
-          return ResourceType_Instance;
-
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-#endif
-
-
-#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
-    OrthancPluginConstraintType Convert(ConstraintType constraint)
-    {
-      switch (constraint)
-      {
-        case ConstraintType_Equal:
-          return OrthancPluginConstraintType_Equal;
-
-        case ConstraintType_GreaterOrEqual:
-          return OrthancPluginConstraintType_GreaterOrEqual;
-
-        case ConstraintType_SmallerOrEqual:
-          return OrthancPluginConstraintType_SmallerOrEqual;
-
-        case ConstraintType_Wildcard:
-          return OrthancPluginConstraintType_Wildcard;
-
-        case ConstraintType_List:
-          return OrthancPluginConstraintType_List;
-
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-#endif    
-
-    
-#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
-    ConstraintType Convert(OrthancPluginConstraintType constraint)
-    {
-      switch (constraint)
-      {
-        case OrthancPluginConstraintType_Equal:
-          return ConstraintType_Equal;
-
-        case OrthancPluginConstraintType_GreaterOrEqual:
-          return ConstraintType_GreaterOrEqual;
-
-        case OrthancPluginConstraintType_SmallerOrEqual:
-          return ConstraintType_SmallerOrEqual;
-
-        case OrthancPluginConstraintType_Wildcard:
-          return ConstraintType_Wildcard;
-
-        case OrthancPluginConstraintType_List:
-          return ConstraintType_List;
-
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-#endif
-  }
-
-  DatabaseConstraint::DatabaseConstraint(ResourceType level,
-                                         const DicomTag& tag,
-                                         bool isIdentifier,
-                                         ConstraintType type,
-                                         const std::vector<std::string>& values,
-                                         bool caseSensitive,
-                                         bool mandatory) :
-    level_(level),
-    tag_(tag),
-    isIdentifier_(isIdentifier),
-    constraintType_(type),
-    values_(values),
-    caseSensitive_(caseSensitive),
-    mandatory_(mandatory)
-  {
-    if (type != ConstraintType_List &&
-        values_.size() != 1)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }      
-
-    
-#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
-  DatabaseConstraint::DatabaseConstraint(const OrthancPluginDatabaseConstraint& constraint) :
-    level_(Plugins::Convert(constraint.level)),
-    tag_(constraint.tagGroup, constraint.tagElement),
-    isIdentifier_(constraint.isIdentifierTag),
-    constraintType_(Plugins::Convert(constraint.type)),
-    caseSensitive_(constraint.isCaseSensitive),
-    mandatory_(constraint.isMandatory)
-  {
-    if (constraintType_ != ConstraintType_List &&
-        constraint.valuesCount != 1)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    values_.resize(constraint.valuesCount);
-
-    for (uint32_t i = 0; i < constraint.valuesCount; i++)
-    {
-      assert(constraint.values[i] != NULL);
-      values_[i].assign(constraint.values[i]);
-    }
-  }
-#endif
-    
-
-  const std::string& DatabaseConstraint::GetValue(size_t index) const
-  {
-    if (index >= values_.size())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-    else
-    {
-      return values_[index];
-    }
-  }
-
-
-  const std::string& DatabaseConstraint::GetSingleValue() const
-  {
-    if (values_.size() != 1)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      return values_[0];
-    }
-  }
-
-
-#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
-  void DatabaseConstraint::EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint,
-                                            std::vector<const char*>& tmpValues) const
-  {
-    memset(&constraint, 0, sizeof(constraint));
-    
-    tmpValues.resize(values_.size());
-
-    for (size_t i = 0; i < values_.size(); i++)
-    {
-      tmpValues[i] = values_[i].c_str();
-    }
-
-    constraint.level = Plugins::Convert(level_);
-    constraint.tagGroup = tag_.GetGroup();
-    constraint.tagElement = tag_.GetElement();
-    constraint.isIdentifierTag = isIdentifier_;
-    constraint.isCaseSensitive = caseSensitive_;
-    constraint.isMandatory = mandatory_;
-    constraint.type = Plugins::Convert(constraintType_);
-    constraint.valuesCount = values_.size();
-    constraint.values = (tmpValues.empty() ? NULL : &tmpValues[0]);
-  }
-#endif    
-}
--- a/OrthancServer/Sources/Search/DatabaseConstraint.h	Fri Sep 20 16:07:08 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,151 +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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#if !defined(ORTHANC_BUILDING_SERVER_LIBRARY)
-#  error Macro ORTHANC_BUILDING_SERVER_LIBRARY must be defined
-#endif
-
-#if ORTHANC_BUILDING_SERVER_LIBRARY == 1
-#  include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h"
-#else
-// This is for the "orthanc-databases" project to reuse this file
-#  include <DicomFormat/DicomMap.h>
-#endif
-
-#define ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT 0
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-#  include <orthanc/OrthancCDatabasePlugin.h>
-#  if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)      // Macro introduced in 1.3.1
-#    if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 2)
-#      undef  ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT
-#      define ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT 1
-#    endif
-#  endif
-#endif
-
-namespace Orthanc
-{
-  enum ConstraintType
-  {
-    ConstraintType_Equal,
-    ConstraintType_SmallerOrEqual,
-    ConstraintType_GreaterOrEqual,
-    ConstraintType_Wildcard,
-    ConstraintType_List
-  };
-
-  namespace Plugins
-  {
-#if ORTHANC_ENABLE_PLUGINS == 1
-    OrthancPluginResourceType Convert(ResourceType type);
-#endif
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-    ResourceType Convert(OrthancPluginResourceType type);
-#endif
-
-#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
-    OrthancPluginConstraintType Convert(ConstraintType constraint);
-#endif
-
-#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
-    ConstraintType Convert(OrthancPluginConstraintType constraint);
-#endif
-  }
-
-
-  // This class is also used by the "orthanc-databases" project
-  class DatabaseConstraint
-  {
-  private:
-    ResourceType              level_;
-    DicomTag                  tag_;
-    bool                      isIdentifier_;
-    ConstraintType            constraintType_;
-    std::vector<std::string>  values_;
-    bool                      caseSensitive_;
-    bool                      mandatory_;
-
-  public:
-    DatabaseConstraint(ResourceType level,
-                       const DicomTag& tag,
-                       bool isIdentifier,
-                       ConstraintType type,
-                       const std::vector<std::string>& values,
-                       bool caseSensitive,
-                       bool mandatory);
-
-#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
-    explicit DatabaseConstraint(const OrthancPluginDatabaseConstraint& constraint);
-#endif
-    
-    ResourceType GetLevel() const
-    {
-      return level_;
-    }
-
-    const DicomTag& GetTag() const
-    {
-      return tag_;
-    }
-
-    bool IsIdentifier() const
-    {
-      return isIdentifier_;
-    }
-
-    ConstraintType GetConstraintType() const
-    {
-      return constraintType_;
-    }
-
-    size_t GetValuesCount() const
-    {
-      return values_.size();
-    }
-
-    const std::string& GetValue(size_t index) const;
-
-    const std::string& GetSingleValue() const;
-
-    bool IsCaseSensitive() const
-    {
-      return caseSensitive_;
-    }
-
-    bool IsMandatory() const
-    {
-      return mandatory_;
-    }
-
-    bool IsMatch(const DicomMap& dicom) const;
-
-#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
-    void EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint,
-                          std::vector<const char*>& tmpValues) const;
-#endif    
-  };
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/DatabaseDicomTagConstraint.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,112 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "DatabaseDicomTagConstraint.h"
+
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+#  include "../../Plugins/Engine/PluginsEnumerations.h"
+#endif
+
+#include <boost/lexical_cast.hpp>
+#include <cassert>
+
+
+namespace Orthanc
+{
+  DatabaseDicomTagConstraint::DatabaseDicomTagConstraint(ResourceType level,
+                                                         const DicomTag& tag,
+                                                         bool isIdentifier,
+                                                         ConstraintType type,
+                                                         const std::vector<std::string>& values,
+                                                         bool caseSensitive,
+                                                         bool mandatory) :
+    level_(level),
+    tag_(tag),
+    isIdentifier_(isIdentifier),
+    constraintType_(type),
+    values_(values),
+    caseSensitive_(caseSensitive),
+    mandatory_(mandatory)
+  {
+    if (type != ConstraintType_List &&
+        values_.size() != 1)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }      
+
+    
+  const std::string& DatabaseDicomTagConstraint::GetValue(size_t index) const
+  {
+    if (index >= values_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return values_[index];
+    }
+  }
+
+
+  const std::string& DatabaseDicomTagConstraint::GetSingleValue() const
+  {
+    if (values_.size() != 1)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return values_[0];
+    }
+  }
+
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+  void DatabaseDicomTagConstraint::EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint,
+                                                    std::vector<const char*>& tmpValues) const
+  {
+    memset(&constraint, 0, sizeof(constraint));
+    
+    tmpValues.resize(values_.size());
+
+    for (size_t i = 0; i < values_.size(); i++)
+    {
+      tmpValues[i] = values_[i].c_str();
+    }
+
+    constraint.level = Plugins::Convert(level_);
+    constraint.tagGroup = tag_.GetGroup();
+    constraint.tagElement = tag_.GetElement();
+    constraint.isIdentifierTag = isIdentifier_;
+    constraint.isCaseSensitive = caseSensitive_;
+    constraint.isMandatory = mandatory_;
+    constraint.type = Plugins::Convert(constraintType_);
+    constraint.valuesCount = values_.size();
+    constraint.values = (tmpValues.empty() ? NULL : &tmpValues[0]);
+  }
+#endif    
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/DatabaseDicomTagConstraint.h	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,101 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h"
+#include "../ServerEnumerations.h"
+#include "IDatabaseConstraint.h"
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+#  include "../../Plugins/Include/orthanc/OrthancCDatabasePlugin.h"
+#endif
+
+namespace Orthanc
+{
+  class DatabaseDicomTagConstraint : public IDatabaseConstraint
+  {
+  private:
+    ResourceType              level_;
+    DicomTag                  tag_;
+    bool                      isIdentifier_;
+    ConstraintType            constraintType_;
+    std::vector<std::string>  values_;
+    bool                      caseSensitive_;
+    bool                      mandatory_;
+
+  public:
+    DatabaseDicomTagConstraint(ResourceType level,
+                               const DicomTag& tag,
+                               bool isIdentifier,
+                               ConstraintType type,
+                               const std::vector<std::string>& values,
+                               bool caseSensitive,
+                               bool mandatory);
+    
+    ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    const DicomTag& GetTag() const
+    {
+      return tag_;
+    }
+
+    bool IsIdentifier() const
+    {
+      return isIdentifier_;
+    }
+
+    virtual ConstraintType GetConstraintType() const ORTHANC_OVERRIDE
+    {
+      return constraintType_;
+    }
+
+    virtual size_t GetValuesCount() const ORTHANC_OVERRIDE
+    {
+      return values_.size();
+    }
+
+    virtual const std::string& GetValue(size_t index) const ORTHANC_OVERRIDE;
+
+    virtual const std::string& GetSingleValue() const ORTHANC_OVERRIDE;
+
+    virtual bool IsCaseSensitive() const ORTHANC_OVERRIDE
+    {
+      return caseSensitive_;
+    }
+
+    virtual bool IsMandatory() const ORTHANC_OVERRIDE
+    {
+      return mandatory_;
+    }
+
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+    void EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint,
+                          std::vector<const char*>& tmpValues) const;
+#endif    
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/DatabaseDicomTagConstraints.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,132 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "DatabaseDicomTagConstraints.h"
+
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+
+#include <boost/lexical_cast.hpp>
+#include <cassert>
+
+
+namespace Orthanc
+{
+  void DatabaseDicomTagConstraints::Clear()
+  {
+    for (size_t i = 0; i < constraints_.size(); i++)
+    {
+      assert(constraints_[i] != NULL);
+      delete constraints_[i];
+    }
+
+    constraints_.clear();
+  }
+
+
+  void DatabaseDicomTagConstraints::AddConstraint(DatabaseDicomTagConstraint* constraint)
+  {
+    if (constraint == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+      constraints_.push_back(constraint);
+    }
+  }
+
+
+  const DatabaseDicomTagConstraint& DatabaseDicomTagConstraints::GetConstraint(size_t index) const
+  {
+    if (index >= constraints_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(constraints_[index] != NULL);
+      return *constraints_[index];
+    }
+  }
+
+
+  std::string DatabaseDicomTagConstraints::Format() const
+  {
+    std::string s;
+
+    for (size_t i = 0; i < constraints_.size(); i++)
+    {
+      assert(constraints_[i] != NULL);
+      const DatabaseDicomTagConstraint& constraint = *constraints_[i];
+      s += "Constraint " + boost::lexical_cast<std::string>(i) + " at " + EnumerationToString(constraint.GetLevel()) +
+        ": " + constraint.GetTag().Format();
+
+      switch (constraint.GetConstraintType())
+      {
+        case ConstraintType_Equal:
+          s += " == " + constraint.GetSingleValue();
+          break;
+
+        case ConstraintType_SmallerOrEqual:
+          s += " <= " + constraint.GetSingleValue();
+          break;
+
+        case ConstraintType_GreaterOrEqual:
+          s += " >= " + constraint.GetSingleValue();
+          break;
+
+        case ConstraintType_Wildcard:
+          s += " ~~ " + constraint.GetSingleValue();
+          break;
+
+        case ConstraintType_List:
+        {
+          s += " in [ ";
+          bool first = true;
+          for (size_t j = 0; j < constraint.GetValuesCount(); j++)
+          {
+            if (first)
+            {
+              first = false;
+            }
+            else
+            {
+              s += ", ";
+            }
+            s += constraint.GetValue(j);
+          }
+          s += "]";
+          break;
+        }
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      s += "\n";
+    }
+
+    return s;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/DatabaseDicomTagConstraints.h	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,61 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "DatabaseDicomTagConstraint.h"
+
+#include <deque>
+
+namespace Orthanc
+{
+  class DatabaseDicomTagConstraints : public boost::noncopyable
+  {
+  private:
+    std::deque<DatabaseDicomTagConstraint*>  constraints_;
+
+  public:
+    ~DatabaseDicomTagConstraints()
+    {
+      Clear();
+    }
+
+    void Clear();
+
+    void AddConstraint(DatabaseDicomTagConstraint* constraint);  // Takes ownership
+
+    bool IsEmpty() const
+    {
+      return constraints_.empty();
+    }
+
+    size_t GetSize() const
+    {
+      return constraints_.size();
+    }
+
+    const DatabaseDicomTagConstraint& GetConstraint(size_t index) const;
+
+    std::string Format() const;
+  };
+}
--- a/OrthancServer/Sources/Search/DatabaseLookup.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Search/DatabaseLookup.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -282,6 +282,14 @@
 
   bool DatabaseLookup::HasOnlyMainDicomTags() const
   {
+    std::set<DicomTag> notUsed;
+    
+    return HasOnlyMainDicomTags(notUsed);
+  }
+
+
+  bool DatabaseLookup::HasOnlyMainDicomTags(std::set<DicomTag>& /* out*/ nonMainDicomTags) const
+  {
     std::set<DicomTag> allMainTags;
     DicomMap::GetAllMainDicomTags(allMainTags);
 
@@ -292,11 +300,11 @@
       if (allMainTags.find(constraints_[i]->GetTag()) == allMainTags.end())
       {
         // This is not a main DICOM tag
-        return false;
+        nonMainDicomTags.insert(constraints_[i]->GetTag());
       }
     }
 
-    return true;
+    return nonMainDicomTags.size() == 0;
   }
 
 
@@ -369,14 +377,4 @@
 
     return clone.release();
   }
-
-
-  void DatabaseLookup::AddLabel(const std::string& label)
-  {
-    if (!label.empty())
-    {
-      ServerToolbox::CheckValidLabel(label);
-      labels_.insert(label);
-    }
-  }
 }
--- a/OrthancServer/Sources/Search/DatabaseLookup.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Search/DatabaseLookup.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -23,7 +23,6 @@
 
 #pragma once
 
-#include "../Search/ISqlLookupFormatter.h"
 #include "DicomTagConstraint.h"
 
 class DcmItem;
@@ -34,8 +33,6 @@
   {
   private:
     std::vector<DicomTagConstraint*>  constraints_;
-    std::set<std::string>             labels_;
-    LabelsConstraint                  labelsConstraint_;
 
     void AddDicomConstraintInternal(const DicomTag& tag,
                                     ValueRepresentation vr,
@@ -46,11 +43,6 @@
     void AddConstraintInternal(DicomTagConstraint* constraint);  // Takes ownership
 
   public:
-    DatabaseLookup() :
-      labelsConstraint_(LabelsConstraint_All)
-    {
-    }
-
     ~DatabaseLookup();
 
     DatabaseLookup* Clone() const;
@@ -92,27 +84,12 @@
 
     bool HasOnlyMainDicomTags() const;
 
+    bool HasOnlyMainDicomTags(std::set<DicomTag>& /* out*/ nonMainDicomTags) const;
+
     std::string Format() const;
 
     bool HasTag(const DicomTag& tag) const;
 
     void RemoveConstraint(const DicomTag& tag);
-
-    void AddLabel(const std::string& label);
-
-    void SetLabelsConstraint(LabelsConstraint constraint)
-    {
-      labelsConstraint_ = constraint;
-    }
-
-    const std::set<std::string>& GetLabels() const
-    {
-      return labels_;
-    }
-
-    LabelsConstraint GetLabelsConstraint() const
-    {
-      return labelsConstraint_;
-    }
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/DatabaseMetadataConstraint.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,93 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "DatabaseMetadataConstraint.h"
+
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+
+#include <boost/lexical_cast.hpp>
+#include <cassert>
+
+
+namespace Orthanc
+{
+  DatabaseMetadataConstraint::DatabaseMetadataConstraint(MetadataType metadata,
+                                                         ConstraintType type,
+                                                         const std::vector<std::string>& values,
+                                                         bool caseSensitive) :
+    metadata_(metadata),
+    constraintType_(type),
+    values_(values),
+    caseSensitive_(caseSensitive)
+  {
+    if (type != ConstraintType_List &&
+        values_.size() != 1)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }      
+
+
+  DatabaseMetadataConstraint::DatabaseMetadataConstraint(MetadataType metadata,
+                                                         ConstraintType type,
+                                                         const std::string& value,
+                                                         bool caseSensitive) :
+    metadata_(metadata),
+    constraintType_(type),
+    caseSensitive_(caseSensitive)
+  {
+    if (type == ConstraintType_List)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    values_.push_back(value);
+  }      
+
+  const std::string& DatabaseMetadataConstraint::GetValue(size_t index) const
+  {
+    if (index >= values_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return values_[index];
+    }
+  }
+
+
+  const std::string& DatabaseMetadataConstraint::GetSingleValue() const
+  {
+    if (values_.size() != 1)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return values_[0];
+    }
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/DatabaseMetadataConstraint.h	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,81 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h"
+#include "../ServerEnumerations.h"
+#include "IDatabaseConstraint.h"
+
+namespace Orthanc
+{
+  class DatabaseMetadataConstraint : public IDatabaseConstraint
+  {
+  private:
+    MetadataType              metadata_;
+    ConstraintType            constraintType_;
+    std::vector<std::string>  values_;
+    bool                      caseSensitive_;
+
+  public:
+    DatabaseMetadataConstraint(MetadataType metadata,
+                               ConstraintType type,
+                               const std::string& value,
+                               bool caseSensitive);
+
+    DatabaseMetadataConstraint(MetadataType metadata,
+                               ConstraintType type,
+                               const std::vector<std::string>& values,
+                               bool caseSensitive);
+
+    const MetadataType& GetMetadata() const
+    {
+      return metadata_;
+    }
+
+    virtual ConstraintType GetConstraintType() const ORTHANC_OVERRIDE
+    {
+      return constraintType_;
+    }
+
+    virtual size_t GetValuesCount() const ORTHANC_OVERRIDE
+    {
+      return values_.size();
+    }
+
+    virtual const std::string& GetValue(size_t index) const ORTHANC_OVERRIDE;
+
+    virtual const std::string& GetSingleValue() const ORTHANC_OVERRIDE;
+
+    virtual bool IsCaseSensitive() const ORTHANC_OVERRIDE
+    {
+      return caseSensitive_;
+    }
+
+    virtual bool IsMandatory() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+  };
+}
--- a/OrthancServer/Sources/Search/DicomTagConstraint.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Search/DicomTagConstraint.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -30,7 +30,7 @@
 
 #include "../../../OrthancFramework/Sources/OrthancException.h"
 #include "../../../OrthancFramework/Sources/Toolbox.h"
-#include "DatabaseConstraint.h"
+#include "DatabaseDicomTagConstraint.h"
 
 #include <boost/regex.hpp>
 
@@ -154,7 +154,7 @@
   }
     
 
-  DicomTagConstraint::DicomTagConstraint(const DatabaseConstraint& constraint) :
+  DicomTagConstraint::DicomTagConstraint(const DatabaseDicomTagConstraint& constraint) :
     tag_(constraint.GetTag()),
     constraintType_(constraint.GetConstraintType()),
     caseSensitive_(constraint.IsCaseSensitive()),
@@ -215,6 +215,24 @@
   }
 
 
+  static bool HasIntersection(const std::set<std::string>& expected,
+                              const std::string& values)
+  {
+    std::vector<std::string> tokens;
+    Toolbox::TokenizeString(tokens, values, '\\');
+
+    for (size_t i = 0; i < tokens.size(); i++)
+    {
+      if (expected.find(tokens[i]) != expected.end())
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+
   bool DicomTagConstraint::IsMatch(const std::string& value) const
   {
     NormalizedString source(value, caseSensitive_);
@@ -224,7 +242,17 @@
       case ConstraintType_Equal:
       {
         NormalizedString reference(GetValue(), caseSensitive_);
-        return source.GetValue() == reference.GetValue();
+
+        if (GetTag() == DICOM_TAG_MODALITIES_IN_STUDY)
+        {
+          std::set<std::string> expected;
+          expected.insert(reference.GetValue());
+          return HasIntersection(expected, source.GetValue());
+        }
+        else
+        {
+          return source.GetValue() == reference.GetValue();
+        }
       }
 
       case ConstraintType_SmallerOrEqual:
@@ -251,17 +279,16 @@
 
       case ConstraintType_List:
       {
+        std::set<std::string> references;
+
         for (std::set<std::string>::const_iterator
                it = values_.begin(); it != values_.end(); ++it)
         {
           NormalizedString reference(*it, caseSensitive_);
-          if (source.GetValue() == reference.GetValue())
-          {
-            return true;
-          }
+          references.insert(reference.GetValue());
         }
 
-        return false;
+        return HasIntersection(references, source.GetValue());
       }
 
       default:
@@ -342,8 +369,9 @@
   }
 
 
-  DatabaseConstraint DicomTagConstraint::ConvertToDatabaseConstraint(ResourceType level,
-                                                                     DicomTagType tagType) const
+  DatabaseDicomTagConstraint* DicomTagConstraint::ConvertToDatabaseConstraint(bool& isIdentical,
+                                                                              ResourceType level,
+                                                                              DicomTagType tagType) const
   {
     bool isIdentifier, caseSensitive;
     
@@ -365,13 +393,21 @@
 
     std::vector<std::string> values;
     values.reserve(values_.size());
-      
+
+    isIdentical = true;
+
     for (std::set<std::string>::const_iterator
            it = values_.begin(); it != values_.end(); ++it)
     {
       if (isIdentifier)
       {
-        values.push_back(ServerToolbox::NormalizeIdentifier(*it));
+        std::string normalized = ServerToolbox::NormalizeIdentifier(*it);
+        values.push_back(normalized);
+
+        if (normalized != *it)
+        {
+          isIdentical = false;
+        }
       }
       else
       {
@@ -379,7 +415,7 @@
       }
     }
 
-    return DatabaseConstraint(level, tag_, isIdentifier, constraintType_,
-                              values, caseSensitive, mandatory_);
+    return new DatabaseDicomTagConstraint(level, tag_, isIdentifier, constraintType_,
+                                          values, caseSensitive, mandatory_);
   }  
 }
--- a/OrthancServer/Sources/Search/DicomTagConstraint.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Search/DicomTagConstraint.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -25,7 +25,7 @@
 
 #include "../ServerEnumerations.h"
 #include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h"
-#include "DatabaseConstraint.h"
+#include "DatabaseDicomTagConstraint.h"
 
 #include <boost/shared_ptr.hpp>
 
@@ -62,7 +62,7 @@
 
     explicit DicomTagConstraint(const DicomTagConstraint& other);
     
-    explicit DicomTagConstraint(const DatabaseConstraint& constraint);
+    explicit DicomTagConstraint(const DatabaseDicomTagConstraint& constraint);
 
     const DicomTag& GetTag() const
     {
@@ -109,7 +109,8 @@
 
     std::string Format() const;
 
-    DatabaseConstraint ConvertToDatabaseConstraint(ResourceType level,
-                                                   DicomTagType tagType) const;
+    DatabaseDicomTagConstraint* ConvertToDatabaseConstraint(bool& isIdentical /* out */,
+                                                            ResourceType level,
+                                                            DicomTagType tagType) const;
   };
 }
--- a/OrthancServer/Sources/Search/HierarchicalMatcher.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Search/HierarchicalMatcher.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/Search/HierarchicalMatcher.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Search/HierarchicalMatcher.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/IDatabaseConstraint.h	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,50 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../ServerEnumerations.h"
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class IDatabaseConstraint : public boost::noncopyable
+  {
+  public:
+    virtual ~IDatabaseConstraint()
+    {
+    }
+    
+    virtual ConstraintType GetConstraintType() const = 0;
+
+    virtual size_t GetValuesCount() const = 0;
+
+    virtual const std::string& GetValue(size_t index) const = 0;
+
+    virtual const std::string& GetSingleValue() const = 0;
+
+    virtual bool IsCaseSensitive() const  = 0;
+
+    virtual bool IsMandatory() const  = 0;
+  };
+}
--- a/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -21,25 +21,14 @@
  **/
 
 
-#if !defined(ORTHANC_BUILDING_SERVER_LIBRARY)
-#  error Macro ORTHANC_BUILDING_SERVER_LIBRARY must be defined
-#endif
-
-#if ORTHANC_BUILDING_SERVER_LIBRARY == 1
-#  include "../PrecompiledHeadersServer.h"
-#endif
-
+#include "../PrecompiledHeadersServer.h"
 #include "ISqlLookupFormatter.h"
 
-#if ORTHANC_BUILDING_SERVER_LIBRARY == 1
-#  include "../../../OrthancFramework/Sources/OrthancException.h"
-#  include "../../../OrthancFramework/Sources/Toolbox.h"
-#else
-#  include <OrthancException.h>
-#  include <Toolbox.h>
-#endif
-
-#include "DatabaseConstraint.h"
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+#include "../../../OrthancFramework/Sources/Toolbox.h"
+#include "../Database/FindRequest.h"
+#include "DatabaseDicomTagConstraint.h"
+#include "../Database/MainDicomTagsRegistry.h"
 
 #include <cassert>
 #include <boost/lexical_cast.hpp>
@@ -68,11 +57,32 @@
         throw OrthancException(ErrorCode_InternalError);
     }
   }      
-  
+
+  static std::string FormatLevel(const char* prefix, ResourceType level)
+  {
+    switch (level)
+    {
+      case ResourceType_Patient:
+        return std::string(prefix) + "patients";
+        
+      case ResourceType_Study:
+        return std::string(prefix) + "studies";
+        
+      case ResourceType_Series:
+        return std::string(prefix) + "series";
+        
+      case ResourceType_Instance:
+        return std::string(prefix) + "instances";
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }      
+
 
   static bool FormatComparison(std::string& target,
                                ISqlLookupFormatter& formatter,
-                               const DatabaseConstraint& constraint,
+                               const IDatabaseConstraint& constraint,
                                size_t index,
                                bool escapeBrackets)
   {
@@ -244,7 +254,7 @@
 
 
   static void FormatJoin(std::string& target,
-                         const DatabaseConstraint& constraint,
+                         const DatabaseDicomTagConstraint& constraint,
                          size_t index)
   {
     std::string tag = "t" + boost::lexical_cast<std::string>(index);
@@ -274,6 +284,99 @@
                boost::lexical_cast<std::string>(constraint.GetTag().GetElement()));
   }
 
+  static void FormatJoin(std::string& target,
+                         const DatabaseMetadataConstraint& constraint,
+                         ResourceType level,
+                         size_t index)
+  {
+    std::string tag = "t" + boost::lexical_cast<std::string>(index);
+
+    if (constraint.IsMandatory())
+    {
+      target = " INNER JOIN ";
+    }
+    else
+    {
+      target = " LEFT JOIN ";
+    }
+
+    target += "Metadata ";
+
+    target += tag + " ON " + tag + ".id = " + FormatLevel(level) +
+               ".internalId AND " + tag + ".type = " +
+               boost::lexical_cast<std::string>(constraint.GetMetadata());
+  }
+
+
+  static void FormatJoinForOrdering(std::string& target,
+                                    const DicomTag& tag,
+                                    size_t index,
+                                    ResourceType requestLevel)
+  {
+    std::string orderArg = "order" + boost::lexical_cast<std::string>(index);
+
+    target.clear();
+
+    ResourceType tagLevel;
+    DicomTagType tagType;
+    MainDicomTagsRegistry registry;
+
+    registry.LookupTag(tagLevel, tagType, tag);
+
+    if (tagLevel == ResourceType_Patient && requestLevel == ResourceType_Study)
+    { // Patient tags are copied at study level
+      tagLevel = ResourceType_Study;
+    }
+
+    std::string tagTable;
+    if (tagType == DicomTagType_Identifier)
+    {
+      tagTable = "DicomIdentifiers ";
+    }
+    else
+    {
+      tagTable = "MainDicomTags ";
+    }
+
+    std::string tagFilter = orderArg + ".tagGroup = " + boost::lexical_cast<std::string>(tag.GetGroup()) + " AND " + orderArg + ".tagElement = " + boost::lexical_cast<std::string>(tag.GetElement());
+
+    if (tagLevel == requestLevel)
+    {
+      target = " LEFT JOIN " + tagTable + " " + orderArg + " ON " + orderArg + ".id = " + FormatLevel(requestLevel) +
+                ".internalId AND " + tagFilter;
+    }
+    else if (static_cast<int32_t>(requestLevel) - static_cast<int32_t>(tagLevel) == 1)
+    {
+      target = " INNER JOIN Resources " + orderArg + "parent ON " + orderArg + "parent.internalId = " + FormatLevel(requestLevel) + ".parentId "
+               " LEFT JOIN " + tagTable + " " + orderArg + " ON " + orderArg + ".id = " + orderArg + "parent.internalId AND " + tagFilter;
+    }
+    else if (static_cast<int32_t>(requestLevel) - static_cast<int32_t>(tagLevel) == 2)
+    {
+      target = " INNER JOIN Resources " + orderArg + "parent ON " + orderArg + "parent.internalId = " + FormatLevel(requestLevel) + ".parentId "
+               " INNER JOIN Resources " + orderArg + "grandparent ON " + orderArg + "grandparent.internalId = " + orderArg + "parent.parentId "
+               " LEFT JOIN " + tagTable + " " + orderArg + " ON " + orderArg + ".id = " + orderArg + "grandparent.internalId AND " + tagFilter;
+    }
+    else if (static_cast<int32_t>(requestLevel) - static_cast<int32_t>(tagLevel) == 3)
+    {
+      target = " INNER JOIN Resources " + orderArg + "parent ON " + orderArg + "parent.internalId = " + FormatLevel(requestLevel) + ".parentId "
+               " INNER JOIN Resources " + orderArg + "grandparent ON " + orderArg + "grandparent.internalId = " + orderArg + "parent.parentId "
+               " INNER JOIN Resources " + orderArg + "grandgrandparent ON " + orderArg + "grandgrandparent.internalId = " + orderArg + "grandparent.parentId "
+               " LEFT JOIN " + tagTable + " " + orderArg + " ON " + orderArg + ".id = " + orderArg + "grandgrandparent.internalId AND " + tagFilter;
+    }
+  }
+
+  static void FormatJoinForOrdering(std::string& target,
+                                    const MetadataType& metadata,
+                                    size_t index,
+                                    ResourceType requestLevel)
+  {
+    std::string arg = "order" + boost::lexical_cast<std::string>(index);
+
+
+    target = " LEFT JOIN Metadata " + arg + " ON " + arg + ".id = " + FormatLevel(requestLevel) +
+             ".internalId AND " + arg + ".type = " +
+             boost::lexical_cast<std::string>(metadata);
+  }
 
   static std::string Join(const std::list<std::string>& values,
                           const std::string& prefix,
@@ -308,7 +411,7 @@
 
   static bool FormatComparison2(std::string& target,
                                 ISqlLookupFormatter& formatter,
-                                const DatabaseConstraint& constraint,
+                                const DatabaseDicomTagConstraint& constraint,
                                 bool escapeBrackets)
   {
     std::string comparison;
@@ -475,7 +578,10 @@
   }
 
 
-  void ISqlLookupFormatter::GetLookupLevels(ResourceType& lowerLevel, ResourceType& upperLevel, const ResourceType& queryLevel, const std::vector<DatabaseConstraint>& lookup)
+  void ISqlLookupFormatter::GetLookupLevels(ResourceType& lowerLevel,
+                                            ResourceType& upperLevel,
+                                            const ResourceType& queryLevel,
+                                            const DatabaseDicomTagConstraints& lookup)
   {
     assert(ResourceType_Patient < ResourceType_Study &&
            ResourceType_Study < ResourceType_Series &&
@@ -484,9 +590,9 @@
     lowerLevel = queryLevel;
     upperLevel = queryLevel;
 
-    for (size_t i = 0; i < lookup.size(); i++)
+    for (size_t i = 0; i < lookup.GetSize(); i++)
     {
-      ResourceType level = lookup[i].GetLevel();
+      ResourceType level = lookup.GetConstraint(i).GetLevel();
 
       if (level < upperLevel)
       {
@@ -503,12 +609,13 @@
 
   void ISqlLookupFormatter::Apply(std::string& sql,
                                   ISqlLookupFormatter& formatter,
-                                  const std::vector<DatabaseConstraint>& lookup,
+                                  const DatabaseDicomTagConstraints& lookup,
                                   ResourceType queryLevel,
                                   const std::set<std::string>& labels,
                                   LabelsConstraint labelsConstraint,
                                   size_t limit)
   {
+    // get the limit levels of the DICOM Tags lookup
     ResourceType lowerLevel, upperLevel;
     GetLookupLevels(lowerLevel, upperLevel, queryLevel, lookup);
 
@@ -521,14 +628,16 @@
 
     size_t count = 0;
     
-    for (size_t i = 0; i < lookup.size(); i++)
+    for (size_t i = 0; i < lookup.GetSize(); i++)
     {
+      const DatabaseDicomTagConstraint& constraint = lookup.GetConstraint(i);
+
       std::string comparison;
       
-      if (FormatComparison(comparison, formatter, lookup[i], count, escapeBrackets))
+      if (FormatComparison(comparison, formatter, constraint, count, escapeBrackets))
       {
         std::string join;
-        FormatJoin(join, lookup[i], count);
+        FormatJoin(join, constraint, count);
         joins += join;
 
         if (!comparison.empty())
@@ -612,9 +721,256 @@
   }
 
 
+  void ISqlLookupFormatter::Apply(std::string& sql,
+                                  ISqlLookupFormatter& formatter,
+                                  const FindRequest& request)
+  {
+    const bool escapeBrackets = formatter.IsEscapeBrackets();
+    ResourceType queryLevel = request.GetLevel();
+    const std::string& strQueryLevel = FormatLevel(queryLevel);
+
+    ResourceType lowerLevel, upperLevel;
+    GetLookupLevels(lowerLevel, upperLevel, queryLevel, request.GetDicomTagConstraints());
+
+    assert(upperLevel <= queryLevel &&
+           queryLevel <= lowerLevel);
+
+    std::string ordering;
+    std::string orderingJoins;
+
+    if (request.GetOrdering().size() > 0)
+    {
+      int counter = 0;
+      std::vector<std::string> orderByFields;
+      for (std::deque<FindRequest::Ordering*>::const_iterator it = request.GetOrdering().begin(); it != request.GetOrdering().end(); ++it)
+      {
+        std::string orderingJoin;
+
+        switch ((*it)->GetKeyType())
+        {
+          case FindRequest::KeyType_DicomTag:
+            FormatJoinForOrdering(orderingJoin, (*it)->GetDicomTag(), counter, request.GetLevel());
+            break;
+          case FindRequest::KeyType_Metadata:
+            FormatJoinForOrdering(orderingJoin, (*it)->GetMetadataType(), counter, request.GetLevel());
+            break;
+          default:
+            throw OrthancException(ErrorCode_InternalError);
+        }
+        orderingJoins += orderingJoin;
+        
+        std::string orderByField;
+
+#if ORTHANC_SQLITE_VERSION < 3030001
+        // this is a way to push NULL values at the end before "NULLS LAST" was introduced:
+        // first filter by 0/1 and then by the column value itself
+        orderByField += "order" + boost::lexical_cast<std::string>(counter) + ".value IS NULL, ";
+#endif
+        switch ((*it)->GetCast())
+        {
+          case FindRequest::OrderingCast_Int:
+            orderByField += "CAST(order" + boost::lexical_cast<std::string>(counter) + ".value AS INTEGER)";
+            break;
+          case FindRequest::OrderingCast_Float:
+            orderByField += "CAST(order" + boost::lexical_cast<std::string>(counter) + ".value AS REAL)";
+            break;
+          default:
+            orderByField += "order" + boost::lexical_cast<std::string>(counter) + ".value";
+        }
+
+        if ((*it)->GetDirection() == FindRequest::OrderingDirection_Ascending)
+        {
+          orderByField += " ASC";
+        }
+        else
+        {
+          orderByField += " DESC";
+        }
+        orderByFields.push_back(orderByField);
+        ++counter;
+      }
+
+      std::string orderByFieldsString;
+      Toolbox::JoinStrings(orderByFieldsString, orderByFields, ", ");
+
+      ordering = "ROW_NUMBER() OVER (ORDER BY " + orderByFieldsString;
+#if ORTHANC_SQLITE_VERSION >= 3030001
+      ordering += " NULLS LAST";
+#endif
+      ordering += ") AS rowNumber";
+    }
+    else
+    {
+      ordering = "ROW_NUMBER() OVER (ORDER BY " + strQueryLevel + ".publicId) AS rowNumber";  // we need a default ordering in order to make default queries repeatable when using since&limit
+    }
+
+    sql = ("SELECT " +
+           strQueryLevel + ".publicId, " +
+           strQueryLevel + ".internalId, " +
+           ordering + 
+           " FROM Resources AS " + strQueryLevel);
+
+
+    std::string joins, comparisons;
+
+    // handle parent constraints
+    if (request.GetOrthancIdentifiers().IsDefined() && request.GetOrthancIdentifiers().DetectLevel() <= queryLevel)
+    {
+      ResourceType topParentLevel = request.GetOrthancIdentifiers().DetectLevel();
+
+      if (topParentLevel == queryLevel)
+      {
+        comparisons += " AND " + FormatLevel(topParentLevel) + ".publicId = " + formatter.GenerateParameter(request.GetOrthancIdentifiers().GetLevel(topParentLevel));
+      }
+      else
+      {
+        comparisons += " AND " + FormatLevel("parent", topParentLevel) + ".publicId = " + formatter.GenerateParameter(request.GetOrthancIdentifiers().GetLevel(topParentLevel));
+
+        for (int level = queryLevel; level > topParentLevel; level--)
+        {
+          joins += " INNER JOIN Resources " +
+                  FormatLevel("parent", static_cast<ResourceType>(level - 1)) + " ON " +
+                  FormatLevel("parent", static_cast<ResourceType>(level - 1)) + ".internalId = ";
+          if (level == queryLevel)
+          {
+            joins += FormatLevel(static_cast<ResourceType>(level)) + ".parentId";
+          }
+          else
+          {
+            joins += FormatLevel("parent", static_cast<ResourceType>(level)) + ".parentId";
+          }
+        }
+      }
+    }
+
+    size_t count = 0;
+    
+    const DatabaseDicomTagConstraints& dicomTagsConstraints = request.GetDicomTagConstraints();
+    for (size_t i = 0; i < dicomTagsConstraints.GetSize(); i++)
+    {
+      const DatabaseDicomTagConstraint& constraint = dicomTagsConstraints.GetConstraint(i);
+
+      std::string comparison;
+      
+      if (FormatComparison(comparison, formatter, constraint, count, escapeBrackets))
+      {
+        std::string join;
+        FormatJoin(join, constraint, count);
+
+        if (constraint.GetLevel() <= queryLevel)
+        {
+          joins += join;
+        }
+        else if (constraint.GetLevel() == queryLevel + 1 && !comparison.empty())
+        {
+          // new in v 1.12.6, the constraints on child tags are actually looking for one child with this value
+          comparison = " EXISTS (SELECT 1 FROM Resources AS " + FormatLevel(static_cast<ResourceType>(queryLevel + 1)) + 
+                       join + 
+                       " WHERE " + comparison + " AND " + 
+                       FormatLevel(static_cast<ResourceType>(queryLevel + 1)) + ".parentId = " + FormatLevel(static_cast<ResourceType>(queryLevel)) + ".internalId) ";
+        }
+
+        if (!comparison.empty())
+        {
+          comparisons += " AND " + comparison;
+        }
+
+        count ++;
+      }
+    }
+
+    for (std::deque<DatabaseMetadataConstraint*>::const_iterator it = request.GetMetadataConstraint().begin(); it != request.GetMetadataConstraint().end(); ++it)
+    {
+      std::string comparison;
+      
+      if (FormatComparison(comparison, formatter, *(*it), count, escapeBrackets))
+      {
+        std::string join;
+        FormatJoin(join, *(*it), request.GetLevel(), count);
+        joins += join;
+
+        if (!comparison.empty())
+        {
+          comparisons += " AND " + comparison;
+        }
+        
+        count ++;
+      }
+    }
+
+    for (int level = queryLevel - 1; level >= upperLevel; level--)
+    {
+      sql += (" INNER JOIN Resources " +
+              FormatLevel(static_cast<ResourceType>(level)) + " ON " +
+              FormatLevel(static_cast<ResourceType>(level)) + ".internalId=" +
+              FormatLevel(static_cast<ResourceType>(level + 1)) + ".parentId");
+    }
+      
+    // disabled in v 1.12.6 now that the child levels are considered as "is there at least one child that meets this constraint"
+    // for (int level = queryLevel + 1; level <= lowerLevel; level++)
+    // {
+    //   sql += (" INNER JOIN Resources " +
+    //           FormatLevel(static_cast<ResourceType>(level)) + " ON " +
+    //           FormatLevel(static_cast<ResourceType>(level - 1)) + ".internalId=" +
+    //           FormatLevel(static_cast<ResourceType>(level)) + ".parentId");
+    // }
+
+    std::list<std::string> where;
+    where.push_back(strQueryLevel + ".resourceType = " +
+                    formatter.FormatResourceType(queryLevel) + comparisons);
+
+
+    if (!request.GetLabels().empty())
+    {
+      /**
+       * "In SQL Server, NOT EXISTS and NOT IN predicates are the best
+       * way to search for missing values, as long as both columns in
+       * question are NOT NULL."
+       * https://explainextended.com/2009/09/15/not-in-vs-not-exists-vs-left-join-is-null-sql-server/
+       **/
+
+      const std::set<std::string>& labels = request.GetLabels();
+      std::list<std::string> formattedLabels;
+      for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
+      {
+        formattedLabels.push_back(formatter.GenerateParameter(*it));
+      }
+
+      std::string condition;
+      switch (request.GetLabelsConstraint())
+      {
+        case LabelsConstraint_Any:
+          condition = "> 0";
+          break;
+          
+        case LabelsConstraint_All:
+          condition = "= " + boost::lexical_cast<std::string>(labels.size());
+          break;
+          
+        case LabelsConstraint_None:
+          condition = "= 0";
+          break;
+          
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+      
+      where.push_back("(SELECT COUNT(1) FROM Labels AS selectedLabels WHERE selectedLabels.id = " + strQueryLevel +
+                      ".internalId AND selectedLabels.label IN (" + Join(formattedLabels, "", ", ") + ")) " + condition);
+    }
+
+    sql += joins + orderingJoins + Join(where, " WHERE ", " AND ");
+
+    if (request.HasLimits())
+    {
+      sql += formatter.FormatLimits(request.GetLimitsSince(), request.GetLimitsCount());
+    }
+  }
+
+
   void ISqlLookupFormatter::ApplySingleLevel(std::string& sql,
                                              ISqlLookupFormatter& formatter,
-                                             const std::vector<DatabaseConstraint>& lookup,
+                                             const DatabaseDicomTagConstraints& lookup,
                                              ResourceType queryLevel,
                                              const std::set<std::string>& labels,
                                              LabelsConstraint labelsConstraint,
@@ -631,15 +987,17 @@
     
     std::vector<std::string> mainDicomTagsComparisons, dicomIdentifiersComparisons;
 
-    for (size_t i = 0; i < lookup.size(); i++)
+    for (size_t i = 0; i < lookup.GetSize(); i++)
     {
+      const DatabaseDicomTagConstraint& constraint = lookup.GetConstraint(i);
+
       std::string comparison;
       
-      if (FormatComparison2(comparison, formatter, lookup[i], escapeBrackets))
+      if (FormatComparison2(comparison, formatter, constraint, escapeBrackets))
       {
         if (!comparison.empty())
         {
-          if (lookup[i].IsIdentifier())
+          if (constraint.IsIdentifier())
           {
             dicomIdentifiersComparisons.push_back(comparison);
           }
@@ -723,5 +1081,4 @@
       sql += " LIMIT " + boost::lexical_cast<std::string>(limit);
     }
   }
-
 }
--- a/OrthancServer/Sources/Search/ISqlLookupFormatter.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -23,19 +23,17 @@
 
 #pragma once
 
-#if ORTHANC_BUILDING_SERVER_LIBRARY == 1
-#  include "../../../OrthancFramework/Sources/Enumerations.h"
-#else
-#  include <Enumerations.h>
-#endif
+#include "../../../OrthancFramework/Sources/Enumerations.h"
 
 #include <boost/noncopyable.hpp>
+#include <stdint.h>
 #include <vector>
 
 namespace Orthanc
 {
-  class DatabaseConstraint;
-  
+  class DatabaseDicomTagConstraints;
+  class FindRequest;
+
   enum LabelsConstraint
   {
     LabelsConstraint_All,
@@ -43,7 +41,6 @@
     LabelsConstraint_None
   };
 
-  // This class is also used by the "orthanc-databases" project
   class ISqlLookupFormatter : public boost::noncopyable
   {
   public:
@@ -57,6 +54,8 @@
 
     virtual std::string FormatWildcardEscape() = 0;
 
+    virtual std::string FormatLimits(uint64_t since, uint64_t count) = 0;
+
     /**
      * Whether to escape '[' and ']', which is only needed for
      * MSSQL. New in Orthanc 1.10.0, from the following changeset:
@@ -64,11 +63,14 @@
      **/
     virtual bool IsEscapeBrackets() const = 0;
 
-    static void GetLookupLevels(ResourceType& lowerLevel, ResourceType& upperLevel, const ResourceType& queryLevel, const std::vector<DatabaseConstraint>& lookup);
+    static void GetLookupLevels(ResourceType& lowerLevel,
+                                ResourceType& upperLevel,
+                                const ResourceType& queryLevel,
+                                const DatabaseDicomTagConstraints& lookup);
 
     static void Apply(std::string& sql,
                       ISqlLookupFormatter& formatter,
-                      const std::vector<DatabaseConstraint>& lookup,
+                      const DatabaseDicomTagConstraints& lookup,
                       ResourceType queryLevel,
                       const std::set<std::string>& labels,  // New in Orthanc 1.12.0
                       LabelsConstraint labelsConstraint,    // New in Orthanc 1.12.0
@@ -76,10 +78,14 @@
 
     static void ApplySingleLevel(std::string& sql,
                                  ISqlLookupFormatter& formatter,
-                                 const std::vector<DatabaseConstraint>& lookup,
+                                 const DatabaseDicomTagConstraints& lookup,
                                  ResourceType queryLevel,
                                  const std::set<std::string>& labels,  // New in Orthanc 1.12.0
                                  LabelsConstraint labelsConstraint,    // New in Orthanc 1.12.0
                                  size_t limit);
+
+    static void Apply(std::string& sql,
+                      ISqlLookupFormatter& formatter,
+                      const FindRequest& request);
   };
 }
--- a/OrthancServer/Sources/ServerContext.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -42,6 +42,7 @@
 
 #include "OrthancConfiguration.h"
 #include "OrthancRestApi/OrthancRestApi.h"
+#include "ResourceFinder.h"
 #include "Search/DatabaseLookup.h"
 #include "ServerJobs/OrthancJobUnserializer.h"
 #include "ServerToolbox.h"
@@ -49,6 +50,9 @@
 
 #include <dcmtk/dcmdata/dcfilefo.h>
 #include <dcmtk/dcmnet/dimse.h>
+#include <dcmtk/dcmdata/dcuid.h>        /* for variable dcmAllStorageSOPClassUIDs */
+
+#include <boost/regex.hpp>
 
 #if HAVE_MALLOC_TRIM == 1
 #  include <malloc.h>
@@ -68,12 +72,6 @@
 
 namespace Orthanc
 {
-  static void ComputeStudyTags(ExpandedResource& resource,
-                               ServerContext& context,
-                               const std::string& studyPublicId,
-                               const std::set<DicomTag>& requestedTags);
-
-
   static bool IsUncompressedTransferSyntax(DicomTransferSyntax transferSyntax)
   {
     return (transferSyntax == DicomTransferSyntax_LittleEndianImplicit ||
@@ -360,8 +358,10 @@
   ServerContext::ServerContext(IDatabaseWrapper& database,
                                IStorageArea& area,
                                bool unitTesting,
-                               size_t maxCompletedJobs) :
-    index_(*this, database, (unitTesting ? 20 : 500)),
+                               size_t maxCompletedJobs,
+                               bool readOnly,
+                               unsigned int maxConcurrentDcmtkTranscoder) :
+    index_(*this, database, (unitTesting ? 20 : 500), readOnly),
     area_(area),
     compressionEnabled_(false),
     storeMD5_(true),
@@ -382,18 +382,17 @@
     isExecuteLuaEnabled_(false),
     isRestApiWriteToFileSystemEnabled_(false),
     overwriteInstances_(false),
-    dcmtkTranscoder_(new DcmtkTranscoder),
+    dcmtkTranscoder_(new DcmtkTranscoder(maxConcurrentDcmtkTranscoder)),
     isIngestTranscoding_(false),
     ingestTranscodingOfUncompressed_(true),
     ingestTranscodingOfCompressed_(true),
     preferredTransferSyntax_(DicomTransferSyntax_LittleEndianExplicit),
+    readOnly_(readOnly),
     deidentifyLogs_(false),
     serverStartTimeUtc_(boost::posix_time::second_clock::universal_time())
   {
     try
     {
-      unsigned int lossyQuality;
-
       {
         OrthancConfiguration::ReaderLock lock;
 
@@ -403,7 +402,14 @@
           new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("MediaArchiveSize", 1)));
         defaultLocalAet_ = lock.GetConfiguration().GetOrthancAET();
         jobsEngine_.SetWorkersCount(lock.GetConfiguration().GetUnsignedIntegerParameter("ConcurrentJobs", 2));
+
         saveJobs_ = lock.GetConfiguration().GetBooleanParameter("SaveJobs", true);
+        if (readOnly_ && saveJobs_)
+        {
+          LOG(WARNING) << "READ-ONLY SYSTEM: SaveJobs = true is incompatible with a ReadOnly system, ignoring this configuration";
+          saveJobs_ = false;
+        }
+
         metricsRegistry_->SetEnabled(lock.GetConfiguration().GetBooleanParameter("MetricsEnabled", true));
 
         // New configuration options in Orthanc 1.5.1
@@ -417,7 +423,6 @@
         // New options in Orthanc 1.7.0
         transcodeDicomProtocol_ = lock.GetConfiguration().GetBooleanParameter("TranscodeDicomProtocol", true);
         builtinDecoderTranscoderOrder_ = StringToBuiltinDecoderTranscoderOrder(lock.GetConfiguration().GetStringParameter("BuiltinDecoderTranscoderOrder", "After"));
-        lossyQuality = lock.GetConfiguration().GetUnsignedIntegerParameter("DicomLossyTranscodingQuality", 90);
 
         std::string s;
         if (lock.GetConfiguration().LookupStringParameter(s, "IngestTranscoding"))
@@ -485,6 +490,17 @@
         lock.GetConfiguration().GetAcceptedTransferSyntaxes(acceptedTransferSyntaxes_);
 
         isUnknownSopClassAccepted_ = lock.GetConfiguration().GetBooleanParameter("UnknownSopClassAccepted", false);
+
+        // New options in Orthanc 1.12.6
+        std::list<std::string> acceptedSopClasses;
+        std::set<std::string> rejectedSopClasses;
+        lock.GetConfiguration().GetListOfStringsParameter(acceptedSopClasses, "AcceptedSopClasses");
+        lock.GetConfiguration().GetSetOfStringsParameter(rejectedSopClasses, "RejectSopClasses");
+        SetAcceptedSopClasses(acceptedSopClasses, rejectedSopClasses);
+
+        defaultDicomRetrieveMethod_ = StringToRetrieveMethod(lock.GetConfiguration().GetStringParameter("DicomDefaultRetrieveMethod", "C-MOVE"));
+
+        dynamic_cast<DcmtkTranscoder&>(*dcmtkTranscoder_).SetDefaultLossyQuality(lock.GetConfiguration().GetDicomLossyTranscodingQuality());
       }
 
       jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200);
@@ -499,8 +515,6 @@
 #else
       LOG(INFO) << "Your platform does not support malloc_trim(), not starting the memory trimming thread";
 #endif
-      
-      dynamic_cast<DcmtkTranscoder&>(*dcmtkTranscoder_).SetLossyQuality(lossyQuality);
     }
     catch (OrthancException&)
     {
@@ -934,10 +948,20 @@
         source.SetExternalBuffer(dicom->GetBufferData(), dicom->GetBufferSize());
         
         IDicomTranscoder::DicomImage transcoded;
+        
         if (Transcode(transcoded, source, syntaxes, true /* allow new SOP instance UID */))
         {
           std::unique_ptr<ParsedDicomFile> tmp(transcoded.ReleaseAsParsedDicomFile());
 
+          if (isReconstruct)
+          {
+            // when reconstructing, we always want to keep the DICOM IDs untouched even if the transcoding has generated a new SOPInstanceUID.
+            const Orthanc::ParsedDicomFile& sourceDicom = dicom->GetParsedDicomFile();
+            std::string sopInstanceUid;
+            sourceDicom.GetTagValue(sopInstanceUid, DICOM_TAG_SOP_INSTANCE_UID);
+            tmp->ReplacePlainString(DICOM_TAG_SOP_INSTANCE_UID, sopInstanceUid);
+          }
+
           std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromParsedDicomFile(*tmp));
           toStore->SetOrigin(dicom->GetOrigin());
           toStore->CopyMetadata(dicom->GetMetadata());
@@ -958,24 +982,16 @@
 
   
   void ServerContext::AnswerAttachment(RestApiOutput& output,
-                                       const std::string& resourceId,
-                                       FileContentType content)
+                                       const FileInfo& attachment,
+                                       const std::string& filename)
   {
-    FileInfo attachment;
-    int64_t revision;
-    if (!index_.LookupAttachment(attachment, revision, resourceId, content))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-    else
-    {
-      StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
-      accessor.AnswerFile(output, attachment, GetFileContentMime(content));
-    }
+    StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
+    accessor.AnswerFile(output, attachment, GetFileContentMime(attachment.GetContentType()), filename);
   }
 
 
-  void ServerContext::ChangeAttachmentCompression(const std::string& resourceId,
+  void ServerContext::ChangeAttachmentCompression(ResourceType level,
+                                                  const std::string& resourceId,
                                                   FileContentType attachmentType,
                                                   CompressionType compression)
   {
@@ -986,7 +1002,7 @@
 
     FileInfo attachment;
     int64_t revision;
-    if (!index_.LookupAttachment(attachment, revision, resourceId, attachmentType))
+    if (!index_.LookupAttachment(attachment, revision, level, resourceId, attachmentType))
     {
       throw OrthancException(ErrorCode_UnknownResource);
     }
@@ -1035,11 +1051,91 @@
     dicomAsJson["7fe0,0010"] = pixelData;
   }
 
+
+  static bool LookupMetadata(std::string& value,
+                             MetadataType key,
+                             const std::map<MetadataType, std::string>& instanceMetadata)
+  {
+    std::map<MetadataType, std::string>::const_iterator found = instanceMetadata.find(key);
+
+    if (found == instanceMetadata.end())
+    {
+      return false;
+    }
+    else
+    {
+      value = found->second;
+      return true;
+    }
+  }
+
+
+  static bool LookupAttachment(FileInfo& target,
+                               FileContentType type,
+                               const std::map<FileContentType, FileInfo>& attachments)
+  {
+    std::map<FileContentType, FileInfo>::const_iterator found = attachments.find(type);
+
+    if (found == attachments.end())
+    {
+      return false;
+    }
+    else if (found->second.GetContentType() == type)
+    {
+      target = found->second;
+      return true;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin);
+    }
+  }
+
   
   void ServerContext::ReadDicomAsJson(Json::Value& result,
                                       const std::string& instancePublicId,
                                       const std::set<DicomTag>& ignoreTagLength)
   {
+    // TODO-FIND: This is a compatibility method, should be removed
+
+    std::map<MetadataType, std::string> metadata;
+    std::map<FileContentType, FileInfo> attachments;
+
+    FileInfo attachment;
+    int64_t revision;  // Ignored
+
+    if (index_.LookupAttachment(attachment, revision, ResourceType_Instance, instancePublicId, FileContentType_Dicom))
+    {
+      attachments[FileContentType_Dicom] = attachment;
+    }
+
+    if (index_.LookupAttachment(attachment, revision, ResourceType_Instance, instancePublicId, FileContentType_DicomUntilPixelData))
+    {
+      attachments[FileContentType_DicomUntilPixelData] = attachment;
+    }
+
+    if (index_.LookupAttachment(attachment, revision, ResourceType_Instance, instancePublicId, FileContentType_DicomAsJson))
+    {
+      attachments[FileContentType_DicomAsJson] = attachment;
+    }
+
+    std::string s;
+    if (index_.LookupMetadata(s, instancePublicId, ResourceType_Instance,
+                              MetadataType_Instance_PixelDataOffset))
+    {
+      metadata[MetadataType_Instance_PixelDataOffset] = s;
+    }
+
+    ReadDicomAsJson(result, instancePublicId, metadata, attachments, ignoreTagLength);
+  }
+
+
+  void ServerContext::ReadDicomAsJson(Json::Value& result,
+                                      const std::string& instancePublicId,
+                                      const std::map<MetadataType, std::string>& instanceMetadata,
+                                      const std::map<FileContentType, FileInfo>& instanceAttachments,
+                                      const std::set<DicomTag>& ignoreTagLength)
+  {
     /**
      * CASE 1: The DICOM file, truncated at pixel data, is available
      * as an attachment (it was created either because the storage
@@ -1048,9 +1144,8 @@
      **/
     
     FileInfo attachment;
-    int64_t revision;  // Ignored
 
-    if (index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_DicomUntilPixelData))
+    if (LookupAttachment(attachment, FileContentType_DicomUntilPixelData, instanceAttachments))
     {
       std::string dicom;
 
@@ -1076,8 +1171,7 @@
 
       {
         std::string s;
-        if (index_.LookupMetadata(s, revision, instancePublicId, ResourceType_Instance,
-                                  MetadataType_Instance_PixelDataOffset))
+        if (LookupMetadata(s, MetadataType_Instance_PixelDataOffset, instanceMetadata))
         {
           hasPixelDataOffset = false;
 
@@ -1108,7 +1202,7 @@
 
       if (hasPixelDataOffset &&
           area_.HasReadRange() &&
-          index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_Dicom) &&
+          LookupAttachment(attachment, FileContentType_Dicom, instanceAttachments) &&
           attachment.GetCompressionType() == CompressionType_None)
       {
         /**
@@ -1131,7 +1225,7 @@
         InjectEmptyPixelData(result);
       }
       else if (ignoreTagLength.empty() &&
-               index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_DicomAsJson))
+               LookupAttachment(attachment, FileContentType_DicomAsJson, instanceAttachments))
       {
         /**
          * CASE 3: This instance was created using Orthanc <=
@@ -1212,8 +1306,20 @@
                                 std::string& attachmentId,
                                 const std::string& instancePublicId)
   {
+    FileInfo attachment;
     int64_t revision;
-    ReadAttachment(dicom, revision, attachmentId, instancePublicId, FileContentType_Dicom, true /* uncompress */);
+
+    if (!index_.LookupAttachment(attachment, revision, ResourceType_Instance, instancePublicId, FileContentType_Dicom))
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Unable to read attachment " + EnumerationToString(FileContentType_Dicom) +
+                             " of instance " + instancePublicId);
+    }
+
+    assert(attachment.GetContentType() == FileContentType_Dicom);
+    attachmentId = attachment.GetUuid();
+
+    ReadAttachment(dicom, attachment, true /* uncompress */);
   }
 
 
@@ -1238,7 +1344,7 @@
   {
     FileInfo attachment;
     int64_t revision;  // Ignored
-    if (index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_DicomUntilPixelData))
+    if (index_.LookupAttachment(attachment, revision, ResourceType_Instance, instancePublicId, FileContentType_DicomUntilPixelData))
     {
       StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
 
@@ -1253,7 +1359,7 @@
       return false;
     }
     
-    if (!index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_Dicom))
+    if (!index_.LookupAttachment(attachment, revision, ResourceType_Instance, instancePublicId, FileContentType_Dicom))
     {
       throw OrthancException(ErrorCode_InternalError,
                              "Unable to read the DICOM file of instance " + instancePublicId);
@@ -1262,7 +1368,7 @@
     std::string s;
 
     if (attachment.GetCompressionType() == CompressionType_None &&
-        index_.LookupMetadata(s, revision, instancePublicId, ResourceType_Instance,
+        index_.LookupMetadata(s, instancePublicId, ResourceType_Instance,
                               MetadataType_Instance_PixelDataOffset) &&
         !s.empty())
     {
@@ -1288,47 +1394,40 @@
   
 
   void ServerContext::ReadAttachment(std::string& result,
-                                     int64_t& revision,
-                                     std::string& attachmentId,
-                                     const std::string& instancePublicId,
-                                     FileContentType content,
+                                     const FileInfo& attachment,
                                      bool uncompressIfNeeded,
                                      bool skipCache)
   {
-    FileInfo attachment;
-    if (!index_.LookupAttachment(attachment, revision, instancePublicId, content))
+    std::unique_ptr<StorageAccessor> accessor;
+      
+    if (skipCache)
     {
-      throw OrthancException(ErrorCode_InternalError,
-                             "Unable to read attachment " + EnumerationToString(content) +
-                             " of instance " + instancePublicId);
+      accessor.reset(new StorageAccessor(area_, GetMetricsRegistry()));
+    }
+    else
+    {
+      accessor.reset(new StorageAccessor(area_, storageCache_, GetMetricsRegistry()));
     }
 
-    assert(attachment.GetContentType() == content);
-    attachmentId = attachment.GetUuid();
-    
+    if (uncompressIfNeeded)
+    {
+      accessor->Read(result, attachment);
+    }
+    else
     {
-      std::unique_ptr<StorageAccessor> accessor;
-      
-      if (skipCache)
-      {
-        accessor.reset(new StorageAccessor(area_, GetMetricsRegistry()));
-      }
-      else
-      {
-        accessor.reset(new StorageAccessor(area_, storageCache_, GetMetricsRegistry()));
-      }
+      // Do not uncompress the content of the storage area, return the
+      // raw data
+      accessor->ReadRaw(result, attachment);
+    }
+  }
 
-      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 StorageAccessor::Range &range,
+                                          bool uncompressIfNeeded)
+  {
+    StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
+    accessor.ReadRange(result, attachment, range, uncompressIfNeeded);
   }
 
 
@@ -1346,18 +1445,17 @@
       // Throttle to avoid loading several large DICOM files simultaneously
       largeDicomLocker_.reset(new Semaphore::Locker(context.largeDicomThrottler_));
       
-      std::string content;
-      context_.ReadDicom(content, instancePublicId);
+      context_.ReadDicom(buffer_, instancePublicId_);
 
       // Release the throttle if loading "small" DICOM files (under
       // 50MB, which is an arbitrary value)
-      if (content.size() < 50 * 1024 * 1024)
+      if (buffer_.size() < 50 * 1024 * 1024)
       {
         largeDicomLocker_.reset(NULL);
       }
       
-      dicom_.reset(new ParsedDicomFile(content));
-      dicomSize_ = content.size();
+      dicom_.reset(new ParsedDicomFile(buffer_));
+      dicomSize_ = buffer_.size();
     }
 
     assert(accessor_.get() != NULL ||
@@ -1393,6 +1491,18 @@
     }
   }
 
+  const std::string& ServerContext::DicomCacheLocker::GetBuffer()
+  {
+    if (buffer_.size() > 0)
+    {
+      return buffer_;
+    }
+    else
+    {
+      context_.ReadDicom(buffer_, instancePublicId_);
+      return buffer_;
+    }
+  }
   
   void ServerContext::SetStoreMD5ForAttachments(bool storeMD5)
   {
@@ -1530,190 +1640,6 @@
   }
 
 
-  void ServerContext::Apply(ILookupVisitor& visitor,
-                            const DatabaseLookup& lookup,
-                            ResourceType queryLevel,
-                            size_t since,
-                            size_t limit)
-  {    
-    unsigned int databaseLimit = (queryLevel == ResourceType_Instance ?
-                                  limitFindInstances_ : limitFindResults_);
-      
-    std::vector<std::string> resources, instances;
-    const DicomTagConstraint* dicomModalitiesConstraint = NULL;
-
-    bool hasModalitiesInStudyLookup = (queryLevel == ResourceType_Study &&
-          lookup.GetConstraint(dicomModalitiesConstraint, DICOM_TAG_MODALITIES_IN_STUDY) &&
-          ((dicomModalitiesConstraint->GetConstraintType() == ConstraintType_Equal && !dicomModalitiesConstraint->GetValue().empty()) ||
-          (dicomModalitiesConstraint->GetConstraintType() == ConstraintType_List && !dicomModalitiesConstraint->GetValues().empty())));
-
-    std::unique_ptr<DatabaseLookup> fastLookup(lookup.Clone());
-    
-    if (hasModalitiesInStudyLookup)
-    {
-      fastLookup->RemoveConstraint(DICOM_TAG_MODALITIES_IN_STUDY);
-    }
-
-    {
-      const size_t lookupLimit = (databaseLimit == 0 ? 0 : databaseLimit + 1);
-      GetIndex().ApplyLookupResources(resources, &instances, *fastLookup, queryLevel,
-                                      lookup.GetLabels(), lookup.GetLabelsConstraint(), lookupLimit);
-    }
-
-    bool complete = (databaseLimit == 0 ||
-                     resources.size() <= databaseLimit);
-
-    LOG(INFO) << "Number of candidate resources after fast DB filtering on main DICOM tags: " << resources.size();
-
-    /**
-     * "resources" contains the Orthanc ID of the resource at level
-     * "queryLevel", "instances" contains one the Orthanc ID of one
-     * sample instance from this resource.
-     **/
-    assert(resources.size() == instances.size());
-
-    size_t countResults = 0;
-    size_t skipped = 0;
-
-    const bool isDicomAsJsonNeeded = visitor.IsDicomAsJsonNeeded();
-    
-    for (size_t i = 0; i < instances.size(); i++)
-    {
-      // Optimization in Orthanc 1.5.1 - Don't read the full JSON from
-      // the disk if only "main DICOM tags" are to be returned
-
-      boost::shared_ptr<Json::Value> dicomAsJson;
-
-      bool hasOnlyMainDicomTags;
-      DicomMap dicom;
-      DicomMap allMainDicomTagsFromDB;
-      
-      if (!IsStorageAccessAllowedForAnswers(findStorageAccessMode_) 
-          || fastLookup->HasOnlyMainDicomTags())
-      {
-        // Case (1): The main DICOM tags, as stored in the database,
-        // are sufficient to look for match
-
-        if (!GetIndex().GetAllMainDicomTags(allMainDicomTagsFromDB, instances[i]))
-        {
-          // The instance has been removed during the execution of the
-          // lookup, ignore it
-          continue;
-        }
-
-        // New in Orthanc 1.6.0: Only keep the main DICOM tags at the
-        // level of interest for the query
-        switch (queryLevel)
-        {
-          // WARNING: Don't reorder cases below, and don't add "break"
-          case ResourceType_Instance:
-            dicom.MergeMainDicomTags(allMainDicomTagsFromDB, ResourceType_Instance);
-
-          case ResourceType_Series:
-            dicom.MergeMainDicomTags(allMainDicomTagsFromDB, ResourceType_Series);
-
-          case ResourceType_Study:
-            dicom.MergeMainDicomTags(allMainDicomTagsFromDB, ResourceType_Study);
-            
-          case ResourceType_Patient:
-            dicom.MergeMainDicomTags(allMainDicomTagsFromDB, ResourceType_Patient);
-            break;
-
-          default:
-            throw OrthancException(ErrorCode_InternalError);
-        }
-        
-        hasOnlyMainDicomTags = true;
-      }
-      else
-      {
-        // Case (2): Need to read the "DICOM-as-JSON" attachment from
-        // the storage area
-        dicomAsJson.reset(new Json::Value);
-        ReadDicomAsJson(*dicomAsJson, instances[i]);
-
-        dicom.FromDicomAsJson(*dicomAsJson);
-
-        // This map contains the entire JSON, i.e. more than the main DICOM tags
-        hasOnlyMainDicomTags = false;   
-      }
-      
-      if (fastLookup->IsMatch(dicom))
-      {
-        bool isMatch = true;
-
-        if (hasModalitiesInStudyLookup)
-        {
-          std::set<DicomTag> requestedTags;
-          requestedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY);
-          ExpandedResource resource;
-          ComputeStudyTags(resource, *this, resources[i], requestedTags);
-
-          std::vector<std::string> modalities;
-          Toolbox::TokenizeString(modalities, resource.GetMainDicomTags().GetValue(DICOM_TAG_MODALITIES_IN_STUDY).GetContent(), '\\');
-          bool hasAtLeastOneModalityMatching = false;
-          for (size_t m = 0; m < modalities.size(); m++)
-          {
-            hasAtLeastOneModalityMatching |= dicomModalitiesConstraint->IsMatch(modalities[m]);
-          }
-
-          isMatch = isMatch && hasAtLeastOneModalityMatching;
-          // copy the value of ModalitiesInStudy such that it can be reused to build the answer
-          allMainDicomTagsFromDB.SetValue(DICOM_TAG_MODALITIES_IN_STUDY, resource.GetMainDicomTags().GetValue(DICOM_TAG_MODALITIES_IN_STUDY));
-        }
-
-        if (isMatch)
-        {
-          if (skipped < since)
-          {
-            skipped++;
-          }
-          else if (limit != 0 &&
-                  countResults >= limit)
-          {
-            // Too many results, don't mark as complete
-            complete = false;
-            break;
-          }
-          else
-          {
-            if (IsStorageAccessAllowedForAnswers(findStorageAccessMode_) &&
-                dicomAsJson.get() == NULL &&
-                isDicomAsJsonNeeded)
-            {
-              dicomAsJson.reset(new Json::Value);
-              ReadDicomAsJson(*dicomAsJson, instances[i]);
-            }
-
-            if (hasOnlyMainDicomTags)
-            {
-              // This is Case (1): The variable "dicom" only contains the main DICOM tags
-              visitor.Visit(resources[i], instances[i], allMainDicomTagsFromDB, dicomAsJson.get());
-            }
-            else
-            {
-              // Remove the non-main DICOM tags from "dicom" if Case (2)
-              // was used, for consistency with Case (1)
-
-              DicomMap mainDicomTags;
-              mainDicomTags.ExtractMainDicomTags(dicom);
-              visitor.Visit(resources[i], instances[i], mainDicomTags, dicomAsJson.get());            
-            }
-              
-            countResults ++;
-          }
-        }
-      }
-    }
-
-    if (complete)
-    {
-      visitor.MarkAsComplete();
-    }
-
-    LOG(INFO) << "Number of matching resources: " << countResults;
-  }
-
   bool ServerContext::LookupOrReconstructMetadata(std::string& target,
                                                   const std::string& publicId,
                                                   ResourceType level,
@@ -1726,8 +1652,7 @@
     if (metadata == MetadataType_Instance_SopClassUid ||
         metadata == MetadataType_Instance_TransferSyntax)
     {
-      int64_t revision;  // Ignored
-      if (index_.LookupMetadata(target, revision, publicId, level, metadata))
+      if (index_.LookupMetadata(target, publicId, level, metadata))
       {
         return true;
       }
@@ -1782,8 +1707,7 @@
     else
     {
       // No backward
-      int64_t revision;  // Ignored
-      return index_.LookupMetadata(target, revision, publicId, level, metadata);
+      return index_.LookupMetadata(target, publicId, level, metadata);
     }
   }
 
@@ -1846,27 +1770,50 @@
   }
 
 
+
+
+
   ImageAccessor* ServerContext::DecodeDicomFrame(const std::string& publicId,
                                                  unsigned int frameIndex)
   {
+    ServerContext::DicomCacheLocker locker(*this, publicId);
+    std::unique_ptr<ImageAccessor> decoded(DecodeDicomFrame(locker.GetDicom(), locker.GetBuffer().c_str(), locker.GetBuffer().size(), 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;
+      }
+      return NULL;
+    }
+
+    return decoded.release();
+  }
+
+
+  ImageAccessor* ServerContext::DecodeDicomFrame(const ParsedDicomFile& parsedDicom,
+                                                 const void* buffer,  // actually the buffer that is the source of the ParsedDicomFile
+                                                 size_t size,
+                                                 unsigned int frameIndex)
+  {
+    std::unique_ptr<ImageAccessor> decoded;
+
     if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before)
     {
-      // Use Orthanc's built-in decoder, using the cache to speed-up
-      // things on multi-frame images
+      // Use Orthanc's built-in decoder
 
-      std::unique_ptr<ImageAccessor> decoded;
       try
       {
-        ServerContext::DicomCacheLocker locker(*this, publicId);
-        decoded.reset(locker.GetDicom().DecodeFrame(frameIndex));
+        decoded.reset(parsedDicom.DecodeFrame(frameIndex));
+        if (decoded.get() != NULL)
+        {
+          return decoded.release();
+        }
       }
       catch (OrthancException& e)
-      {
-      }
-      
-      if (decoded.get() != NULL)
-      {
-        return decoded.release();
+      { // ignore, we'll try other alternatives
       }
     }
 
@@ -1874,14 +1821,9 @@
     if (HasPlugins() &&
         GetPlugins().HasCustomImageDecoder())
     {
-      // TODO: Store the raw buffer in the DicomCacheLocker
-      std::string dicomContent;
-      ReadDicom(dicomContent, publicId);
-      
-      std::unique_ptr<ImageAccessor> decoded;
       try
       {
-        decoded.reset(GetPlugins().Decode(dicomContent.c_str(), dicomContent.size(), frameIndex));
+        decoded.reset(GetPlugins().Decode(buffer, size, frameIndex));
       }
       catch (OrthancException& e)
       {
@@ -1901,69 +1843,52 @@
 
     if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
     {
-      ServerContext::DicomCacheLocker locker(*this, publicId);        
-      return locker.GetDicom().DecodeFrame(frameIndex);
+      try
+      { 
+        decoded.reset(parsedDicom.DecodeFrame(frameIndex));
+        if (decoded.get() != NULL)
+        {
+          return decoded.release();
+        }
+      }
+      catch (OrthancException& e)
+      {
+        LOG(INFO) << e.GetDetails();
+      }
     }
-    else
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+    if (HasPlugins() && GetPlugins().HasCustomTranscoder())
     {
-      return NULL;  // Built-in decoder is disabled
+      LOG(INFO) << "The plugins and built-in image decoders failed to decode a frame, "
+                << "trying to transcode the file to LittleEndianExplicit using the plugins.";
+      DicomImage explicitTemporaryImage;
+      DicomImage source;
+      std::set<DicomTransferSyntax> allowedSyntaxes;
+
+      source.SetExternalBuffer(buffer, size);
+      allowedSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
+
+      if (Transcode(explicitTemporaryImage, source, allowedSyntaxes, true))
+      {
+        std::unique_ptr<ParsedDicomFile> file(explicitTemporaryImage.ReleaseAsParsedDicomFile());
+        return file->DecodeFrame(frameIndex);
+      }
     }
+#endif
+
+    return NULL;
   }
 
 
   ImageAccessor* ServerContext::DecodeDicomFrame(const DicomInstanceToStore& dicom,
                                                  unsigned int frameIndex)
   {
-    if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before)
-    {
-      std::unique_ptr<ImageAccessor> decoded;
-      try
-      {
-        decoded.reset(dicom.DecodeFrame(frameIndex));
-      }
-      catch (OrthancException& e)
-      {
-      }
-        
-      if (decoded.get() != NULL)
-      {
-        return decoded.release();
-      }
-    }
+    return DecodeDicomFrame(dicom.GetParsedDicomFile(),
+                            dicom.GetBufferData(),
+                            dicom.GetBufferSize(),
+                            frameIndex);
 
-#if ORTHANC_ENABLE_PLUGINS == 1
-    if (HasPlugins() &&
-        GetPlugins().HasCustomImageDecoder())
-    {
-      std::unique_ptr<ImageAccessor> decoded;
-      try
-      {
-        decoded.reset(GetPlugins().Decode(dicom.GetBufferData(), dicom.GetBufferSize(), frameIndex));
-      }
-      catch (OrthancException& e)
-      {
-      }
-    
-      if (decoded.get() != NULL)
-      {
-        return decoded.release();
-      }
-      else if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
-      {
-        LOG(INFO) << "The installed image decoding plugins cannot handle an image, "
-                  << "fallback to the built-in DCMTK decoder";
-      }
-    }
-#endif
-
-    if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
-    {
-      return dicom.DecodeFrame(frameIndex);
-    }
-    else
-    {
-      return NULL;
-    }
   }
 
 
@@ -1971,8 +1896,8 @@
                                                  size_t size,
                                                  unsigned int frameIndex)
   {
-    std::unique_ptr<DicomInstanceToStore> instance(DicomInstanceToStore::CreateFromBuffer(dicom, size));
-    return DecodeDicomFrame(*instance, frameIndex);
+    std::unique_ptr<ParsedDicomFile> instance(new ParsedDicomFile(dicom, size));
+    return DecodeDicomFrame(*instance, dicom, size, frameIndex);
   }
   
 
@@ -2031,15 +1956,31 @@
     return true;
   }
 
-
   bool ServerContext::Transcode(DicomImage& target,
                                 DicomImage& source /* in, "GetParsed()" possibly modified */,
                                 const std::set<DicomTransferSyntax>& allowedSyntaxes,
                                 bool allowNewSopInstanceUid)
   {
+    unsigned int lossyQuality;
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      lossyQuality = lock.GetConfiguration().GetDicomLossyTranscodingQuality();
+    }
+
+    return Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid, lossyQuality);
+  }
+
+
+  bool ServerContext::Transcode(DicomImage& target,
+                                DicomImage& source /* in, "GetParsed()" possibly modified */,
+                                const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                                bool allowNewSopInstanceUid,
+                                unsigned int lossyQuality)
+  {
     if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before)
     {
-      if (dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid))
+      if (dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid, lossyQuality))
       {
         return true;
       }
@@ -2049,7 +1990,7 @@
     if (HasPlugins() &&
         GetPlugins().HasCustomTranscoder())
     {
-      if (GetPlugins().Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid))
+      if (GetPlugins().Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid))  // TODO: pass lossyQuality to plugins -> needs a new plugin interface
       {
         return true;
       }
@@ -2063,7 +2004,7 @@
 
     if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
     {
-      return dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid);
+      return dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid, lossyQuality);
     }
     else
     {
@@ -2094,8 +2035,118 @@
     }
   }
 
+  void ServerContext::SetAcceptedSopClasses(const std::list<std::string>& acceptedSopClasses,
+                                            const std::set<std::string>& rejectedSopClasses)
+  {
+    boost::mutex::scoped_lock lock(dynamicOptionsMutex_);
+    acceptedSopClasses_.clear();
 
-  void ServerContext::GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax>& syntaxes)
+    size_t count = 0;
+    std::set<std::string> allDcmtkSopClassUids;
+    std::set<std::string> shortDcmtkSopClassUids;
+
+    // we actually take a list of default 120 most common storage SOP classes defined in DCMTK
+    while (dcmLongSCUStorageSOPClassUIDs[count] != NULL)
+    {
+      shortDcmtkSopClassUids.insert(dcmLongSCUStorageSOPClassUIDs[count++]);
+    }
+
+    count = 0;
+    while (dcmAllStorageSOPClassUIDs[count] != NULL)
+    {
+      allDcmtkSopClassUids.insert(dcmAllStorageSOPClassUIDs[count++]);
+    }
+
+    if (acceptedSopClasses.size() == 0)
+    {
+      // by default, include the short list first and then all the others
+      for (std::set<std::string>::const_iterator it = shortDcmtkSopClassUids.begin(); it != shortDcmtkSopClassUids.end(); ++it)
+      {
+        acceptedSopClasses_.push_back(*it);
+      }
+
+      for (std::set<std::string>::const_iterator it = allDcmtkSopClassUids.begin(); it != allDcmtkSopClassUids.end(); ++it)
+      {
+        if (shortDcmtkSopClassUids.find(*it) == shortDcmtkSopClassUids.end()) // don't add the classes that we have already added
+        {
+          acceptedSopClasses_.push_back(*it);
+        }
+      }
+    }
+    else
+    {
+      std::set<std::string> addedSopClasses;
+
+      for (std::list<std::string>::const_iterator it = acceptedSopClasses.begin(); it != acceptedSopClasses.end(); ++it)
+      {
+        if (it->find('*') != std::string::npos || it->find('?') != std::string::npos)
+        {
+          // if it contains wildcard, add all the matching SOP classes known by DCMTK
+          boost::regex pattern(Toolbox::WildcardToRegularExpression(*it));
+
+          for (std::set<std::string>::const_iterator itall = allDcmtkSopClassUids.begin(); itall != allDcmtkSopClassUids.end(); ++itall)
+          {
+            if (regex_match(*itall, pattern) && addedSopClasses.find(*itall) == addedSopClasses.end())
+            {
+              acceptedSopClasses_.push_back(*itall);
+              addedSopClasses.insert(*itall);
+            }
+          }
+        }
+        else
+        {
+          // if it is a SOP Class UID, add it without checking if it is known by DCMTK
+          acceptedSopClasses_.push_back(*it);
+          addedSopClasses.insert(*it);
+        }
+      }
+    }
+    
+    // now remove all rejected syntaxes
+    if (rejectedSopClasses.size() > 0)
+    {
+      for (std::set<std::string>::const_iterator it = rejectedSopClasses.begin(); it != rejectedSopClasses.end(); ++it)
+      {
+        if (it->find('*') != std::string::npos || it->find('?') != std::string::npos)
+        {
+          // if it contains wildcard, get all the matching SOP classes known by DCMTK
+          boost::regex pattern(Toolbox::WildcardToRegularExpression(*it));
+
+          for (std::set<std::string>::const_iterator itall = allDcmtkSopClassUids.begin(); itall != allDcmtkSopClassUids.end(); ++itall)
+          {
+            if (regex_match(*itall, pattern))
+            {
+              acceptedSopClasses_.remove(*itall);
+            }
+          }
+        }
+        else
+        {
+          // if it is a SOP Class UID, remove it without checking if it is known by DCMTK
+          acceptedSopClasses_.remove(*it);
+        }
+      }
+    }
+  }
+
+  void ServerContext::GetAcceptedSopClasses(std::set<std::string>& sopClasses, size_t maxCount) const
+  {
+    sopClasses.clear();
+
+    boost::mutex::scoped_lock lock(dynamicOptionsMutex_);
+
+    size_t count = 0;
+    std::list<std::string>::const_iterator it = acceptedSopClasses_.begin();
+
+    while (it != acceptedSopClasses_.end() && (maxCount == 0 || count < maxCount))
+    {
+      sopClasses.insert(*it);
+      count++;
+      ++it;
+    }
+  }
+
+  void ServerContext::GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax>& syntaxes) const
   {
     boost::mutex::scoped_lock lock(dynamicOptionsMutex_);
     syntaxes = acceptedTransferSyntaxes_;
@@ -2109,7 +2160,25 @@
   }
 
 
-  bool ServerContext::IsUnknownSopClassAccepted()
+  void ServerContext::GetProposedStorageTransferSyntaxes(std::list<DicomTransferSyntax>& syntaxes) const
+  {
+    boost::mutex::scoped_lock lock(dynamicOptionsMutex_);
+    
+    // // TODO: investigate: actually, neither Orthanc 1.12.4 nor DCM4CHEE will accept to send a LittleEndianExplicit file
+    // //                    while e.g., Jpeg-LS has been presented (and accepted) as the preferred TS for the C-Store SCP.
+    // // if we have defined IngestTranscoding, let's propose this TS first to avoid any unnecessary transcoding
+    // if (isIngestTranscoding_)
+    // {
+    //   syntaxes.push_back(ingestTransferSyntax_);
+    // }
+    
+    // then, propose the default ones
+    syntaxes.push_back(DicomTransferSyntax_LittleEndianExplicit);
+    syntaxes.push_back(DicomTransferSyntax_LittleEndianImplicit);
+  }
+  
+
+  bool ServerContext::IsUnknownSopClassAccepted() const
   {
     boost::mutex::scoped_lock lock(dynamicOptionsMutex_);
     return isUnknownSopClassAccepted_;
@@ -2122,564 +2191,6 @@
     isUnknownSopClassAccepted_ = accepted;
   }
 
-
-  static void SerializeExpandedResource(Json::Value& target,
-                                        const ExpandedResource& resource,
-                                        DicomToJsonFormat format,
-                                        const std::set<DicomTag>& requestedTags)
-  {
-    target = Json::objectValue;
-
-    target["Type"] = GetResourceTypeText(resource.GetLevel(), false, true);
-    target["ID"] = resource.GetPublicId();
-
-    switch (resource.GetLevel())
-    {
-      case ResourceType_Patient:
-        break;
-
-      case ResourceType_Study:
-        target["ParentPatient"] = resource.parentId_;
-        break;
-
-      case ResourceType_Series:
-        target["ParentStudy"] = resource.parentId_;
-        break;
-
-      case ResourceType_Instance:
-        target["ParentSeries"] = resource.parentId_;
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    switch (resource.GetLevel())
-    {
-      case ResourceType_Patient:
-      case ResourceType_Study:
-      case ResourceType_Series:
-      {
-        Json::Value c = Json::arrayValue;
-
-        for (std::list<std::string>::const_iterator
-                it = resource.childrenIds_.begin(); it != resource.childrenIds_.end(); ++it)
-        {
-          c.append(*it);
-        }
-
-        if (resource.GetLevel() == ResourceType_Patient)
-        {
-          target["Studies"] = c;
-        }
-        else if (resource.GetLevel() == ResourceType_Study)
-        {
-          target["Series"] = c;
-        }
-        else
-        {
-          target["Instances"] = c;
-        }
-        break;
-      }
-
-      case ResourceType_Instance:
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    switch (resource.GetLevel())
-    {
-      case ResourceType_Patient:
-      case ResourceType_Study:
-        break;
-
-      case ResourceType_Series:
-        if (resource.expectedNumberOfInstances_ < 0)
-        {
-          target["ExpectedNumberOfInstances"] = Json::nullValue;
-        }
-        else
-        {
-          target["ExpectedNumberOfInstances"] = resource.expectedNumberOfInstances_;
-        }
-        target["Status"] = resource.status_;
-        break;
-
-      case ResourceType_Instance:
-      {
-        target["FileSize"] = static_cast<unsigned int>(resource.fileSize_);
-        target["FileUuid"] = resource.fileUuid_;
-
-        if (resource.indexInSeries_ < 0)
-        {
-          target["IndexInSeries"] = Json::nullValue;
-        }
-        else
-        {
-          target["IndexInSeries"] = resource.indexInSeries_;
-        }
-
-        break;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    if (!resource.anonymizedFrom_.empty())
-    {
-      target["AnonymizedFrom"] = resource.anonymizedFrom_;
-    }
-    
-    if (!resource.modifiedFrom_.empty())
-    {
-      target["ModifiedFrom"] = resource.modifiedFrom_;
-    }
-
-    if (resource.GetLevel() == ResourceType_Patient ||
-        resource.GetLevel() == ResourceType_Study ||
-        resource.GetLevel() == ResourceType_Series)
-    {
-      target["IsStable"] = resource.isStable_;
-
-      if (!resource.lastUpdate_.empty())
-      {
-        target["LastUpdate"] = resource.lastUpdate_;
-      }
-    }
-
-    // serialize tags
-
-    static const char* const MAIN_DICOM_TAGS = "MainDicomTags";
-    static const char* const PATIENT_MAIN_DICOM_TAGS = "PatientMainDicomTags";
-
-    DicomMap mainDicomTags;
-    resource.GetMainDicomTags().ExtractResourceInformation(mainDicomTags, resource.GetLevel());
-
-    target[MAIN_DICOM_TAGS] = Json::objectValue;
-    FromDcmtkBridge::ToJson(target[MAIN_DICOM_TAGS], mainDicomTags, format);
-    
-    if (resource.GetLevel() == ResourceType_Study)
-    {
-      DicomMap patientMainDicomTags;
-      resource.GetMainDicomTags().ExtractPatientInformation(patientMainDicomTags);
-
-      target[PATIENT_MAIN_DICOM_TAGS] = Json::objectValue;
-      FromDcmtkBridge::ToJson(target[PATIENT_MAIN_DICOM_TAGS], patientMainDicomTags, format);
-    }
-
-    if (requestedTags.size() > 0)
-    {
-      static const char* const REQUESTED_TAGS = "RequestedTags";
-
-      DicomMap tags;
-      resource.GetMainDicomTags().ExtractTags(tags, requestedTags);
-
-      target[REQUESTED_TAGS] = Json::objectValue;
-      FromDcmtkBridge::ToJson(target[REQUESTED_TAGS], tags, format);
-
-    }
-
-    {
-      Json::Value labels = Json::arrayValue;
-
-      for (std::set<std::string>::const_iterator it = resource.labels_.begin(); it != resource.labels_.end(); ++it)
-      {
-        labels.append(*it);
-      }
-
-      target["Labels"] = labels;
-    }
-  }
-
-
-  static void ComputeInstanceTags(ExpandedResource& resource,
-                                  ServerContext& context,
-                                  const std::string& instancePublicId,
-                                  const std::set<DicomTag>& requestedTags)
-  {
-    if (requestedTags.count(DICOM_TAG_INSTANCE_AVAILABILITY) > 0)
-    {
-      resource.GetMainDicomTags().SetValue(DICOM_TAG_INSTANCE_AVAILABILITY, "ONLINE", false);
-      resource.missingRequestedTags_.erase(DICOM_TAG_INSTANCE_AVAILABILITY);
-    }
-  }
-
-
-  static void ComputeSeriesTags(ExpandedResource& resource,
-                                ServerContext& context,
-                                const std::string& seriesPublicId,
-                                const std::set<DicomTag>& requestedTags)
-  {
-    if (requestedTags.count(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES) > 0)
-    {
-      ServerIndex& index = context.GetIndex();
-      std::list<std::string> instances;
-
-      index.GetChildren(instances, seriesPublicId);
-
-      resource.GetMainDicomTags().SetValue(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES,
-                              boost::lexical_cast<std::string>(instances.size()), false);
-      resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES);
-    }
-  }
-
-  static void ComputeStudyTags(ExpandedResource& resource,
-                               ServerContext& context,
-                               const std::string& studyPublicId,
-                               const std::set<DicomTag>& requestedTags)
-  {
-    ServerIndex& index = context.GetIndex();
-    std::list<std::string> series;
-    std::list<std::string> instances;
-
-    bool hasNbRelatedSeries = requestedTags.count(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES) > 0;
-    bool hasNbRelatedInstances = requestedTags.count(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) > 0;
-    bool hasModalitiesInStudy = requestedTags.count(DICOM_TAG_MODALITIES_IN_STUDY) > 0;
-    bool hasSopClassesInStudy = requestedTags.count(DICOM_TAG_SOP_CLASSES_IN_STUDY) > 0;
-
-    index.GetChildren(series, studyPublicId);
-
-    if (hasModalitiesInStudy)
-    {
-      std::set<std::string> values;
-
-      for (std::list<std::string>::const_iterator
-            it = series.begin(); it != series.end(); ++it)
-      {
-        DicomMap tags;
-        index.GetMainDicomTags(tags, *it, ResourceType_Series, ResourceType_Series);
-
-        const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY);
-
-        if (value != NULL &&
-            !value->IsNull() &&
-            !value->IsBinary())
-        {
-          values.insert(value->GetContent());
-        }
-      }
-
-      std::string modalities;
-      Toolbox::JoinStrings(modalities, values, "\\");
-
-      resource.GetMainDicomTags().SetValue(DICOM_TAG_MODALITIES_IN_STUDY, modalities, false);
-      resource.missingRequestedTags_.erase(DICOM_TAG_MODALITIES_IN_STUDY);
-    }
-
-    if (hasNbRelatedSeries)
-    {
-      resource.GetMainDicomTags().SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES,
-                              boost::lexical_cast<std::string>(series.size()), false);
-      resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES);
-    }
-
-    if (hasNbRelatedInstances || hasSopClassesInStudy)
-    {
-      for (std::list<std::string>::const_iterator
-            it = series.begin(); it != series.end(); ++it)
-      {
-        std::list<std::string> seriesInstancesIds;
-        index.GetChildren(seriesInstancesIds, *it);
-
-        instances.splice(instances.end(), seriesInstancesIds);
-      }
-
-      if (hasNbRelatedInstances)
-      {
-        resource.GetMainDicomTags().SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES,
-                                boost::lexical_cast<std::string>(instances.size()), false);      
-        resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES);
-      }
-
-      if (hasSopClassesInStudy)
-      {
-        std::set<std::string> values;
-
-        for (std::list<std::string>::const_iterator
-              it = instances.begin(); it != instances.end(); ++it)
-        {
-          std::string value;
-
-          if (context.LookupOrReconstructMetadata(value, *it, ResourceType_Instance, MetadataType_Instance_SopClassUid))
-          {
-            values.insert(value);
-          }
-        }
-
-        if (values.size() > 0)
-        {
-          std::string sopClassUids;
-          Toolbox::JoinStrings(sopClassUids, values, "\\");
-          resource.GetMainDicomTags().SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, sopClassUids, false);
-        }
-
-        resource.missingRequestedTags_.erase(DICOM_TAG_SOP_CLASSES_IN_STUDY);
-      }
-    }
-  }
-
-  static void ComputePatientTags(ExpandedResource& resource,
-                                 ServerContext& context,
-                                 const std::string& patientPublicId,
-                                 const std::set<DicomTag>& requestedTags)
-  {
-    ServerIndex& index = context.GetIndex();
-
-    std::list<std::string> studies;
-    std::list<std::string> series;
-    std::list<std::string> instances;
-
-    bool hasNbRelatedStudies = requestedTags.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES) > 0;
-    bool hasNbRelatedSeries = requestedTags.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) > 0;
-    bool hasNbRelatedInstances = requestedTags.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES) > 0;
-
-    index.GetChildren(studies, patientPublicId);
-
-    if (hasNbRelatedStudies)
-    {
-      resource.GetMainDicomTags().SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES,
-                              boost::lexical_cast<std::string>(studies.size()), false);
-      resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES);
-    }
-
-    if (hasNbRelatedSeries || hasNbRelatedInstances)
-    {
-      for (std::list<std::string>::const_iterator
-            it = studies.begin(); it != studies.end(); ++it)
-      {
-        std::list<std::string> thisSeriesIds;
-        index.GetChildren(thisSeriesIds, *it);
-        series.splice(series.end(), thisSeriesIds);
-      }
-
-      if (hasNbRelatedSeries)
-      {
-        resource.GetMainDicomTags().SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES,
-                                boost::lexical_cast<std::string>(series.size()), false);
-        resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES);
-      }
-    }
-
-    if (hasNbRelatedInstances)
-    {
-      for (std::list<std::string>::const_iterator
-            it = series.begin(); it != series.end(); ++it)
-      {
-        std::list<std::string> thisInstancesIds;
-        index.GetChildren(thisInstancesIds, *it);
-        instances.splice(instances.end(), thisInstancesIds);
-      }
-
-      resource.GetMainDicomTags().SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES,
-                              boost::lexical_cast<std::string>(instances.size()), false);
-      resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES);
-    }
-  }
-
-
-  static void ComputeTags(ExpandedResource& resource,
-                          ServerContext& context,
-                          const std::string& resourceId,
-                          ResourceType level,
-                          const std::set<DicomTag>& requestedTags)
-  {
-    if (level == ResourceType_Patient 
-        && DicomMap::HasComputedTags(resource.missingRequestedTags_, ResourceType_Patient))
-    {
-      ComputePatientTags(resource, context, resourceId, requestedTags);
-    }
-
-    if (level == ResourceType_Study 
-        && DicomMap::HasComputedTags(resource.missingRequestedTags_, ResourceType_Study))
-    {
-      ComputeStudyTags(resource, context, resourceId, requestedTags);
-    }
-
-    if (level == ResourceType_Series 
-        && DicomMap::HasComputedTags(resource.missingRequestedTags_, ResourceType_Series))
-    {
-      ComputeSeriesTags(resource, context, resourceId, requestedTags);
-    }
-
-    if (level == ResourceType_Instance 
-        && DicomMap::HasComputedTags(resource.missingRequestedTags_, ResourceType_Instance))
-    {
-      ComputeInstanceTags(resource, context, resourceId, requestedTags);
-    }
-  }
-
-  bool ServerContext::ExpandResource(Json::Value& target,
-                                     const std::string& publicId,
-                                     ResourceType level,
-                                     DicomToJsonFormat format,
-                                     const std::set<DicomTag>& requestedTags,
-                                     bool allowStorageAccess)
-  {
-    std::string unusedInstanceId;
-    Json::Value* unusedDicomAsJson = NULL;
-    DicomMap unusedMainDicomTags;
-
-    return ExpandResource(target, publicId, unusedMainDicomTags, unusedInstanceId, unusedDicomAsJson, level, format, requestedTags, allowStorageAccess);
-  }
-
-  bool ServerContext::ExpandResource(Json::Value& target,
-                                     const std::string& publicId,
-                                     const DicomMap& mainDicomTags,    // optional: the main dicom tags for the resource (if already available)
-                                     const std::string& instanceId,    // optional: the id of an instance for the resource (if already available)
-                                     const Json::Value* dicomAsJson,   // optional: the dicom-as-json for the resource (if already available)
-                                     ResourceType level,
-                                     DicomToJsonFormat format,
-                                     const std::set<DicomTag>& requestedTags,
-                                     bool allowStorageAccess)
-  {
-    ExpandedResource resource;
-
-    if (ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, level, requestedTags, ExpandResourceFlags_Default, allowStorageAccess))
-    {
-      SerializeExpandedResource(target, resource, format, requestedTags);
-      return true;
-    }
-
-    return false;
-  }
-  
-  bool ServerContext::ExpandResource(ExpandedResource& resource,
-                                     const std::string& publicId,
-                                     const DicomMap& mainDicomTags,    // optional: the main dicom tags for the resource (if already available)
-                                     const std::string& instanceId,    // optional: the id of an instance for the resource (if already available)
-                                     const Json::Value* dicomAsJson,   // optional: the dicom-as-json for the resource (if already available)
-                                     ResourceType level,
-                                     const std::set<DicomTag>& requestedTags,
-                                     ExpandResourceFlags expandFlags,
-                                     bool allowStorageAccess)
-  {
-    // first try to get the tags from what is already available
-    
-    if ((expandFlags & ExpandResourceFlags_IncludeMainDicomTags) &&
-        mainDicomTags.GetSize() > 0 &&
-        dicomAsJson != NULL)
-    {
-      
-      resource.GetMainDicomTags().Merge(mainDicomTags);
-
-      if (dicomAsJson->isObject())
-      {
-        resource.GetMainDicomTags().FromDicomAsJson(*dicomAsJson);
-      }
-
-      std::set<DicomTag> retrievedTags;
-      std::set<DicomTag> missingTags;
-      resource.GetMainDicomTags().GetTags(retrievedTags);
-
-      Toolbox::GetMissingsFromSet(missingTags, requestedTags, retrievedTags);
-
-      // if all possible tags have been read, no need to get them from DB anymore
-      if (missingTags.size() == 0 || DicomMap::HasOnlyComputedTags(missingTags))
-      {
-        expandFlags = static_cast<ExpandResourceFlags>(expandFlags & ~ExpandResourceFlags_IncludeMainDicomTags);
-      }
-
-      if (missingTags.size() == 0 && expandFlags == ExpandResourceFlags_None)  // we have already retrieved anything we need
-      {
-        return true;
-      }
-    }
-
-    if (expandFlags != ExpandResourceFlags_None &&
-        GetIndex().ExpandResource(resource, publicId, level, requestedTags,
-                                  static_cast<ExpandResourceFlags>(expandFlags | ExpandResourceFlags_IncludeMetadata)))  // we always need the metadata to get the mainDicomTagsSignature
-    {
-      // check the main dicom tags list has not changed since the resource was stored
-      if (resource.mainDicomTagsSignature_ != DicomMap::GetMainDicomTagsSignature(resource.GetLevel()))
-      {
-        OrthancConfiguration::ReaderLock lock;
-        if (lock.GetConfiguration().IsWarningEnabled(Warnings_002_InconsistentDicomTagsInDb))
-        {
-          LOG(WARNING) << "W002: " << Orthanc::GetResourceTypeText(resource.GetLevel(), false , false)
-                       << " has been stored with another version of Main Dicom Tags list, you should POST to /"
-                       << Orthanc::GetResourceTypeText(resource.GetLevel(), true, false)
-                       << "/" << resource.GetPublicId()
-                       << "/reconstruct to update the list of tags saved in DB.  Some MainDicomTags might be missing from this answer.";
-        }
-      }
-
-      // possibly merge missing requested tags from dicom-as-json
-      if (allowStorageAccess &&
-          !resource.missingRequestedTags_.empty() &&
-          !DicomMap::HasOnlyComputedTags(resource.missingRequestedTags_))
-      {
-        OrthancConfiguration::ReaderLock lock;
-        if (lock.GetConfiguration().IsWarningEnabled(Warnings_001_TagsBeingReadFromStorage))
-        {
-          std::set<DicomTag> missingTags;
-          Toolbox::AppendSets(missingTags, resource.missingRequestedTags_);
-          for (std::set<DicomTag>::const_iterator it = resource.missingRequestedTags_.begin(); it != resource.missingRequestedTags_.end(); ++it)
-          {
-            if (DicomMap::IsComputedTag(*it))
-            {
-              missingTags.erase(*it);
-            }
-          }
-
-          std::string missings;
-          FromDcmtkBridge::FormatListOfTags(missings, missingTags);
-
-          LOG(WARNING) << "W001: Accessing Dicom tags from storage when accessing "
-                       << Orthanc::GetResourceTypeText(resource.GetLevel(), false, false)
-                       << " : " << missings;
-        }
-
-
-        std::string instanceId_ = instanceId;
-        DicomMap tagsFromJson;
-
-        if (dicomAsJson == NULL)
-        {
-          if (instanceId_.empty())
-          {
-            if (level == ResourceType_Instance)
-            {
-              instanceId_ = publicId;
-            }
-            else
-            {
-              std::list<std::string> instancesIds;
-              GetIndex().GetChildInstances(instancesIds, publicId);
-              if (instancesIds.size() < 1)
-              {
-                throw OrthancException(ErrorCode_InternalError, "ExpandResource: no instances found");
-              }
-              instanceId_ = instancesIds.front();
-            }
-          }
-  
-          Json::Value tmpDicomAsJson;
-          ReadDicomAsJson(tmpDicomAsJson, instanceId_, resource.missingRequestedTags_ /* ignoreTagLength */);  // read all tags from DICOM and avoid cropping requested tags
-          tagsFromJson.FromDicomAsJson(tmpDicomAsJson, false /* append */, true /* parseSequences*/);
-        }
-        else
-        {
-          tagsFromJson.FromDicomAsJson(*dicomAsJson, false /* append */, true /* parseSequences*/);
-        }
-
-        resource.GetMainDicomTags().Merge(tagsFromJson);
-      }
-
-      // compute the requested tags
-      ComputeTags(resource, *this, publicId, level, requestedTags);
-    }
-    else
-    {
-      return false;
-    }
-
-    return true;
-  }
-
   int64_t ServerContext::GetServerUpTime() const
   {
     boost::posix_time::ptime nowUtc = boost::posix_time::second_clock::universal_time();
@@ -2687,5 +2198,4 @@
 
     return elapsed.total_seconds();
   }
-
 }
--- a/OrthancServer/Sources/ServerContext.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -38,6 +38,8 @@
 
 #include <boost/date_time/posix_time/posix_time.hpp>
 
+#include "../../OrthancFramework/Sources/FileStorage/StorageAccessor.h"
+
 namespace Orthanc
 {
   class DicomInstanceToStore;
@@ -66,25 +68,6 @@
     friend class ServerIndex;  // To access "RemoveFile()"
     
   public:
-    class ILookupVisitor : public boost::noncopyable
-    {
-    public:
-      virtual ~ILookupVisitor()
-      {
-      }
-
-      virtual bool IsDicomAsJsonNeeded() const = 0;
-      
-      virtual void MarkAsComplete() = 0;
-
-      // NB: "dicomAsJson" must *not* be deleted, and can be NULL if
-      // "!IsDicomAsJsonNeeded()"
-      virtual void Visit(const std::string& publicId,
-                         const std::string& instanceId,
-                         const DicomMap& mainDicomTags,
-                         const Json::Value* dicomAsJson) = 0;
-    };
-    
     struct StoreResult
     {
     private:
@@ -250,6 +233,7 @@
         
     std::unique_ptr<SharedArchive>  queryRetrieveArchive_;
     std::string defaultLocalAet_;
+    RetrieveMethod defaultDicomRetrieveMethod_;
     OrthancHttpHandler  httpHandler_;
     bool saveJobs_;
     FindStorageAccessMode findStorageAccessMode_;
@@ -274,9 +258,11 @@
 
     // New in Orthanc 1.9.0
     DicomTransferSyntax preferredTransferSyntax_;
-    boost::mutex dynamicOptionsMutex_;
+    mutable boost::mutex dynamicOptionsMutex_;
     bool isUnknownSopClassAccepted_;
     std::set<DicomTransferSyntax>  acceptedTransferSyntaxes_;
+    std::list<std::string>         acceptedSopClasses_;  // ordered; the most 120 common ones first
+    bool readOnly_;
 
     StoreResult StoreAfterTranscoding(std::string& resultPublicId,
                                       DicomInstanceToStore& dicom,
@@ -305,6 +291,7 @@
       std::unique_ptr<ParsedDicomFile>             dicom_;
       size_t                                       dicomSize_;
       std::unique_ptr<Semaphore::Locker>           largeDicomLocker_;
+      std::string                                  buffer_;
 
     public:
       DicomCacheLocker(ServerContext& context,
@@ -313,12 +300,16 @@
       ~DicomCacheLocker();
 
       ParsedDicomFile& GetDicom() const;
+
+      const std::string& GetBuffer();
     };
 
     ServerContext(IDatabaseWrapper& database,
                   IStorageArea& area,
                   bool unitTesting,
-                  size_t maxCompletedJobs);
+                  size_t maxCompletedJobs,
+                  bool readOnly,
+                  unsigned int maxConcurrentDcmtkTranscoder);
 
     ~ServerContext();
 
@@ -341,6 +332,15 @@
     {
       return compressionEnabled_;
     }
+    bool IsReadOnly() const
+    {
+      return readOnly_;
+    }
+
+    bool IsSaveJobs() const
+    {
+      return saveJobs_;
+    }
 
     bool AddAttachment(int64_t& newRevision,
                        const std::string& resourceId,
@@ -361,19 +361,26 @@
                                   bool isReconstruct = false);
 
     void AnswerAttachment(RestApiOutput& output,
-                          const std::string& resourceId,
-                          FileContentType content);
+                          const FileInfo& fileInfo,
+                          const std::string& filename);
 
-    void ChangeAttachmentCompression(const std::string& resourceId,
+    void ChangeAttachmentCompression(ResourceType level,
+                                     const std::string& resourceId,
                                      FileContentType attachmentType,
                                      CompressionType compression);
 
     void ReadDicomAsJson(Json::Value& result,
                          const std::string& instancePublicId,
+                         const std::map<MetadataType, std::string>& instanceMetadata,
+                         const std::map<FileContentType, FileInfo>& instanceAttachments,
                          const std::set<DicomTag>& ignoreTagLength);
 
     void ReadDicomAsJson(Json::Value& result,
-                         const std::string& instancePublicId);
+                         const std::string& instancePublicId,
+                         const std::set<DicomTag>& ignoreTagLength);  // TODO-FIND: Can this be removed?
+
+    void ReadDicomAsJson(Json::Value& result,
+                         const std::string& instancePublicId);  // TODO-FIND: Can this be removed?
 
     void ReadDicom(std::string& dicom,
                    const std::string& instancePublicId);
@@ -390,13 +397,15 @@
 
     // This method is for low-level operations on "/instances/.../attachments/..."
     void ReadAttachment(std::string& result,
-                        int64_t& revision,
-                        std::string& attachmentId,
-                        const std::string& instancePublicId,
-                        FileContentType content,
+                        const FileInfo& attachment,
                         bool uncompressIfNeeded,
                         bool skipCache = false);
 
+    void ReadAttachmentRange(std::string& result,
+                             const FileInfo& attachment,
+                             const StorageAccessor::Range& range,
+                             bool uncompressIfNeeded);
+
     void SetStoreMD5ForAttachments(bool storeMD5);
 
     bool IsStoreMD5ForAttachments() const
@@ -430,6 +439,11 @@
       return defaultLocalAet_;
     }
 
+    RetrieveMethod GetDefaultDicomRetrieveMethod() const
+    {
+      return defaultDicomRetrieveMethod_;
+    }
+
     LuaScripting& GetLuaScripting()
     {
       return mainLua_;
@@ -442,11 +456,10 @@
 
     void Stop();
 
-    void Apply(ILookupVisitor& visitor,
-               const DatabaseLookup& lookup,
-               ResourceType queryLevel,
-               size_t since,
-               size_t limit);
+    uint64_t GetDatabaseLimits(ResourceType level) const
+    {
+      return (level == ResourceType_Instance ? limitFindInstances_ : limitFindResults_);
+    }
 
     bool LookupOrReconstructMetadata(std::string& target,
                                      const std::string& publicId,
@@ -544,7 +557,12 @@
     ImageAccessor* DecodeDicomFrame(const void* dicom,
                                     size_t size,
                                     unsigned int frameIndex);
-    
+
+    ImageAccessor* DecodeDicomFrame(const ParsedDicomFile& parsedDicom,
+                                    const void* buffer,  // actually the buffer that is the source of the ParsedDicomFile
+                                    size_t size,
+                                    unsigned int frameIndex);
+
     void StoreWithTranscoding(std::string& sopClassUid,
                               std::string& sopInstanceUid,
                               DicomStoreUserConnection& connection,
@@ -559,6 +577,12 @@
                            DicomImage& source /* in, "GetParsed()" possibly modified */,
                            const std::set<DicomTransferSyntax>& allowedSyntaxes,
                            bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
+    
+    virtual bool Transcode(DicomImage& target,
+                           DicomImage& source /* in, "GetParsed()" possibly modified */,
+                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                           bool allowNewSopInstanceUid,
+                           unsigned int lossyQuality) ORTHANC_OVERRIDE;
 
     virtual bool TranscodeWithCache(std::string& target,
                                     const std::string& source,
@@ -573,41 +597,21 @@
 
     const std::string& GetDeidentifiedContent(const DicomElement& element) const;
 
-    void GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax>& syntaxes);
+    void GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax>& syntaxes) const;
 
     void SetAcceptedTransferSyntaxes(const std::set<DicomTransferSyntax>& syntaxes);
 
-    bool IsUnknownSopClassAccepted();
+    void GetProposedStorageTransferSyntaxes(std::list<DicomTransferSyntax>& syntaxes) const;
+
+    void SetAcceptedSopClasses(const std::list<std::string>& acceptedSopClasses,
+                               const std::set<std::string>& rejectedSopClasses);
+
+    void GetAcceptedSopClasses(std::set<std::string>& sopClasses, size_t maxCount) const;
+
+    bool IsUnknownSopClassAccepted() const;
 
     void SetUnknownSopClassAccepted(bool accepted);
 
-    bool ExpandResource(Json::Value& target,
-                        const std::string& publicId,
-                        ResourceType level,
-                        DicomToJsonFormat format,
-                        const std::set<DicomTag>& requestedTags,
-                        bool allowStorageAccess);
-
-    bool ExpandResource(Json::Value& target,
-                        const std::string& publicId,
-                        const DicomMap& mainDicomTags,    // optional: the main dicom tags for the resource (if already available)
-                        const std::string& instanceId,    // optional: the id of an instance for the resource
-                        const Json::Value* dicomAsJson,   // optional: the dicom-as-json for the resource
-                        ResourceType level,
-                        DicomToJsonFormat format,
-                        const std::set<DicomTag>& requestedTags,
-                        bool allowStorageAccess);
-
-    bool ExpandResource(ExpandedResource& target,
-                        const std::string& publicId,
-                        const DicomMap& mainDicomTags,    // optional: the main dicom tags for the resource (if already available)
-                        const std::string& instanceId,    // optional: the id of an instance for the resource
-                        const Json::Value* dicomAsJson,   // optional: the dicom-as-json for the resource
-                        ResourceType level,
-                        const std::set<DicomTag>& requestedTags,
-                        ExpandResourceFlags expandFlags,
-                        bool allowStorageAccess);
-
     FindStorageAccessMode GetFindStorageAccessMode() const
     {
       return findStorageAccessMode_;
--- a/OrthancServer/Sources/ServerEnumerations.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerEnumerations.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -431,6 +431,86 @@
     }
   }
 
+  ChangeType StringToChangeType(const std::string& value)
+  {
+    if (value == "CompletedSeries")
+    {
+      return ChangeType_CompletedSeries;
+    }
+    else if (value == "NewInstance")
+    {
+      return ChangeType_NewInstance;
+    }
+    else if (value == "NewPatient")
+    {
+      return ChangeType_NewPatient;
+    }
+    else if (value == "NewSeries")
+    {
+      return ChangeType_NewSeries;
+    }
+    else if (value == "NewStudy")
+    {
+      return ChangeType_NewStudy;
+    }
+    else if (value == "AnonymizedStudy")
+    {
+      return ChangeType_AnonymizedStudy;
+    }
+    else if (value == "AnonymizedSeries")
+    {
+      return ChangeType_AnonymizedSeries;
+    }
+    else if (value == "ModifiedStudy")
+    {
+      return ChangeType_ModifiedStudy;
+    }
+    else if (value == "ModifiedSeries")
+    {
+      return ChangeType_ModifiedSeries;
+    }
+    else if (value == "AnonymizedPatient")
+    {
+      return ChangeType_AnonymizedPatient;
+    }
+    else if (value == "ModifiedPatient")
+    {
+      return ChangeType_ModifiedPatient;
+    }
+    else if (value == "StablePatient")
+    {
+      return ChangeType_StablePatient;
+    }
+    else if (value == "StableStudy")
+    {
+      return ChangeType_StableStudy;
+    }
+    else if (value == "StableSeries")
+    {
+      return ChangeType_StableSeries;
+    }
+    else if (value == "Deleted")
+    {
+      return ChangeType_Deleted;
+    }
+    else if (value == "NewChildInstance")
+    {
+      return ChangeType_NewChildInstance;
+    }
+    else if (value == "UpdatedAttachment")
+    {
+      return ChangeType_UpdatedAttachment;
+    }
+    else if (value == "UpdatedMetadata")
+    {
+      return ChangeType_UpdatedMetadata;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange, "Invalid value for a change: " + value);
+    }
+  }
+
 
   const char* EnumerationToString(Verbosity verbosity)
   {
@@ -536,4 +616,49 @@
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
   }
-}
+
+  ResponseContentFlags StringToResponseContent(const std::string& value)
+  {
+    if (value == "MainDicomTags")
+    {
+      return ResponseContentFlags_MainDicomTags;
+    }
+    else if (value == "RequestedTags")
+    {
+      return ResponseContentFlags_RequestedTags;
+    }
+    else if (value == "Metadata")
+    {
+      return ResponseContentFlags_Metadata;
+    }
+    else if (value == "Status")
+    {
+      return ResponseContentFlags_Status;
+    }
+    else if (value == "Parent")
+    {
+      return ResponseContentFlags_Parent;
+    }
+    else if (value == "Children")
+    {
+      return ResponseContentFlags_Children;
+    }
+    else if (value == "Labels")
+    {
+      return ResponseContentFlags_Labels;
+    }
+    else if (value == "Attachments")
+    {
+      return ResponseContentFlags_Attachments;
+    }
+    else if (value == "IsStable")
+    {
+      return ResponseContentFlags_IsStable;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Unrecognized value for \"ResponseContent\": " + value);
+    }    
+  }
+}
\ No newline at end of file
--- a/OrthancServer/Sources/ServerEnumerations.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerEnumerations.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -112,6 +112,51 @@
     TransactionType_ReadWrite
   };
 
+  enum ConstraintType
+  {
+    ConstraintType_Equal,
+    ConstraintType_SmallerOrEqual,
+    ConstraintType_GreaterOrEqual,
+    ConstraintType_Wildcard,
+    ConstraintType_List
+  };
+
+  enum ResponseContentFlags
+  {
+    ResponseContentFlags_ID                   = (1 << 0),
+    ResponseContentFlags_Type                 = (1 << 1),
+    ResponseContentFlags_RequestedTags        = (1 << 2),
+    ResponseContentFlags_MainDicomTags        = (1 << 3),
+    ResponseContentFlags_MetadataLegacy       = (1 << 4),    // when "Expand": true -> all metadata are included at root level
+    ResponseContentFlags_AttachmentsLegacy    = (1 << 5),    // when "Expand": true -> include attachments info at instance level
+    ResponseContentFlags_Metadata             = (1 << 6),    // all metadata are listed in a "Metadata" field
+    ResponseContentFlags_Attachments          = (1 << 7),    // all attachments are listed in a "Attachments" field
+    ResponseContentFlags_Status               = (1 << 8),
+    ResponseContentFlags_Parent               = (1 << 9),
+    ResponseContentFlags_Children             = (1 << 10),
+    ResponseContentFlags_Labels               = (1 << 11),
+    ResponseContentFlags_IsStable             = (1 << 12),
+
+    ResponseContentFlags_INTERNAL_CountResources = (1 << 30),
+    
+    // Some predefined combinations
+    ResponseContentFlags_ExpandTrue  = (ResponseContentFlags_ID |
+                                        ResponseContentFlags_Type |
+                                        ResponseContentFlags_RequestedTags |
+                                        ResponseContentFlags_MainDicomTags |
+                                        ResponseContentFlags_MetadataLegacy |
+                                        ResponseContentFlags_AttachmentsLegacy | 
+                                        ResponseContentFlags_Status | 
+                                        ResponseContentFlags_Parent | 
+                                        ResponseContentFlags_Children | 
+                                        ResponseContentFlags_Labels |
+                                        ResponseContentFlags_IsStable),  // equivalent to "Expand": true
+    
+    ResponseContentFlags_Default = (ResponseContentFlags_ID |
+                                    ResponseContentFlags_Type |
+                                    ResponseContentFlags_RequestedTags) // minimal content as soon as you have a "ResponseContent"
+    
+  };
 
   /**
    * WARNING: Do not change the explicit values in the enumerations
@@ -206,6 +251,11 @@
     Warnings_None,
     Warnings_001_TagsBeingReadFromStorage,
     Warnings_002_InconsistentDicomTagsInDb,
+    Warnings_003_DecoderFailure,                           // new in Orthanc 1.12.5
+    Warnings_004_NoMainDicomTagsSignature,                 // new in Orthanc 1.12.5
+    Warnings_005_RequestingTagFromLowerResourceLevel,      // new in Orthanc 1.12.5
+    Warnings_006_RequestingTagFromMetaHeader,              // new in Orthanc 1.12.5
+    Warnings_007_MissingRequestedTagsNotReadFromDisk       // new in Orthanc 1.12.5
   };
 
 
@@ -238,6 +288,8 @@
 
   Verbosity StringToVerbosity(const std::string& str);
 
+  ResponseContentFlags StringToResponseContent(const std::string& str);
+
   std::string EnumerationToString(FileContentType type);
 
   std::string GetFileContentMime(FileContentType type);
@@ -250,6 +302,8 @@
   const char* EnumerationToString(StoreStatus status);
 
   const char* EnumerationToString(ChangeType type);
+  
+  ChangeType StringToChangeType(const std::string& value);
 
   const char* EnumerationToString(Verbosity verbosity);
 
--- a/OrthancServer/Sources/ServerIndex.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerIndex.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -266,7 +266,6 @@
     }
   };
 
-
   void ServerIndex::FlushThread(ServerIndex* that,
                                 unsigned int threadSleepGranularityMilliseconds)
   {
@@ -313,26 +312,46 @@
 
   ServerIndex::ServerIndex(ServerContext& context,
                            IDatabaseWrapper& db,
-                           unsigned int threadSleepGranularityMilliseconds) :
-    StatelessDatabaseOperations(db),
+                           unsigned int threadSleepGranularityMilliseconds,
+                           bool readOnly) :
+    StatelessDatabaseOperations(db, readOnly),
     done_(false),
     maximumStorageMode_(MaxStorageMode_Recycle),
     maximumStorageSize_(0),
-    maximumPatients_(0)
+    maximumPatients_(0),
+    readOnly_(readOnly)
   {
     SetTransactionContextFactory(new TransactionContextFactory(context));
 
     // Initial recycling if the parameters have changed since the last
     // execution of Orthanc
-    StandaloneRecycling(maximumStorageMode_, maximumStorageSize_, maximumPatients_);
+    if (!readOnly)
+    {
+      StandaloneRecycling(maximumStorageMode_, maximumStorageSize_, maximumPatients_);
+    }
 
+    // For some DB engines (like SQLite), make sure we flush the DB to disk at regular interval
     if (GetDatabaseCapabilities().HasFlushToDisk())
     {
-      flushThread_ = boost::thread(FlushThread, this, threadSleepGranularityMilliseconds);
+      if (readOnly)
+      {
+        LOG(WARNING) << "READ-ONLY SYSTEM: not starting the flush disk thread";
+      }
+      else
+      {
+        flushThread_ = boost::thread(FlushThread, this, threadSleepGranularityMilliseconds);
+      }
     }
 
-    unstableResourcesMonitorThread_ = boost::thread
-      (UnstableResourcesMonitorThread, this, threadSleepGranularityMilliseconds);
+    if (readOnly)
+    {
+      LOG(WARNING) << "READ-ONLY SYSTEM: not starting the unstable resources monitor thread";
+    }
+    else
+    {
+      unstableResourcesMonitorThread_ = boost::thread
+        (UnstableResourcesMonitorThread, this, threadSleepGranularityMilliseconds);
+    }
   }
 
 
--- a/OrthancServer/Sources/ServerIndex.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerIndex.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -49,6 +49,7 @@
     MaxStorageMode  maximumStorageMode_;
     uint64_t        maximumStorageSize_;
     unsigned int    maximumPatients_;
+    bool            readOnly_;
 
     static void FlushThread(ServerIndex* that,
                             unsigned int threadSleep);
@@ -60,13 +61,11 @@
                         int64_t id,
                         const std::string& publicId);
 
-    bool IsUnstableResource(ResourceType type,
-                            int64_t id);
-
   public:
     ServerIndex(ServerContext& context,
                 IDatabaseWrapper& database,
-                unsigned int threadSleepGranularityMilliseconds);
+                unsigned int threadSleepGranularityMilliseconds,
+                bool readOnly);
 
     ~ServerIndex();
 
@@ -99,5 +98,8 @@
                               bool hasOldRevision,
                               int64_t oldRevision,
                               const std::string& oldMD5);
+
+    bool IsUnstableResource(ResourceType type,
+                            int64_t id);
   };
 }
--- a/OrthancServer/Sources/ServerIndexChange.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerIndexChange.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -31,8 +31,10 @@
 #include "../../../OrthancFramework/Sources/Logging.h"
 #include "../../../OrthancFramework/Sources/OrthancException.h"
 #include "../../../OrthancFramework/Sources/MultiThreading/Semaphore.h"
+#include "../../../OrthancFramework/Sources/SerializationToolbox.h"
 #include "../OrthancConfiguration.h"
 #include "../ServerContext.h"
+#include "../SimpleInstanceOrdering.h"
 
 #include <stdio.h>
 #include <boost/range/algorithm/count.hpp>
@@ -86,11 +88,13 @@
     ServerContext&                        context_;
     bool                                  transcode_;
     DicomTransferSyntax                   transferSyntax_;
+    unsigned int                          lossyQuality_;
   public:
-    explicit InstanceLoader(ServerContext& context, bool transcode, DicomTransferSyntax transferSyntax)
+    explicit InstanceLoader(ServerContext& context, bool transcode, DicomTransferSyntax transferSyntax, unsigned int lossyQuality)
     : context_(context),
       transcode_(transcode),
-      transferSyntax_(transferSyntax)
+      transferSyntax_(transferSyntax),
+      lossyQuality_(lossyQuality)
     {
     }
 
@@ -98,7 +102,7 @@
     {
     }
 
-    virtual void PrepareDicom(const std::string& instanceId)
+    virtual void PrepareDicom(const std::string& instanceId, const FileInfo& fileInfo)
     {
     }
 
@@ -112,7 +116,7 @@
         IDicomTranscoder::DicomImage source, transcoded;
         source.SetExternalBuffer(sourceBuffer);
 
-        if (context_.Transcode(transcoded, source, syntaxes, true /* allow new SOP instance UID */))
+        if (context_.Transcode(transcoded, source, syntaxes, true /* allow new SOP instance UID */, lossyQuality_))
         {
           transcodedBuffer.assign(reinterpret_cast<const char*>(transcoded.GetBufferData()), transcoded.GetBufferSize());
           return true;
@@ -127,7 +131,7 @@
       return false;
     }
 
-    virtual void GetDicom(std::string& dicom, const std::string& instanceId) = 0;
+    virtual void GetDicom(std::string& dicom, const std::string& instanceId, const FileInfo& fileInfo) = 0;
 
     virtual void Clear()
     {
@@ -137,14 +141,14 @@
   class ArchiveJob::SynchronousInstanceLoader : public ArchiveJob::InstanceLoader
   {
   public:
-    explicit SynchronousInstanceLoader(ServerContext& context, bool transcode, DicomTransferSyntax transferSyntax)
-    : InstanceLoader(context, transcode, transferSyntax)
+    explicit SynchronousInstanceLoader(ServerContext& context, bool transcode, DicomTransferSyntax transferSyntax, unsigned int lossyQuality)
+    : InstanceLoader(context, transcode, transferSyntax, lossyQuality)
     {
     }
 
-    virtual void GetDicom(std::string& dicom, const std::string& instanceId) ORTHANC_OVERRIDE
+    virtual void GetDicom(std::string& dicom, const std::string& instanceId, const FileInfo& fileInfo) ORTHANC_OVERRIDE
     {
-      context_.ReadDicom(dicom, instanceId);
+      context_.ReadAttachment(dicom, fileInfo, true);
 
       if (transcode_)
       {
@@ -158,21 +162,25 @@
     }
   };
 
-  class InstanceId : public Orthanc::IDynamicObject
+  class InstanceToPreload : public Orthanc::IDynamicObject
   {
   private:
     std::string id_;
+    FileInfo    fileInfo_;
 
   public:
-    explicit InstanceId(const std::string& id) : id_(id)
+    explicit InstanceToPreload(const std::string& id, const FileInfo& fileInfo) : 
+      id_(id),
+      fileInfo_(fileInfo)
     {
     }
 
-    virtual ~InstanceId() ORTHANC_OVERRIDE
+    virtual ~InstanceToPreload() ORTHANC_OVERRIDE
     {
     }
 
-    std::string GetId() const {return id_;};
+    const std::string& GetId() const {return id_;};
+    const FileInfo& GetFileInfo() const {return fileInfo_;};
   };
 
   class ArchiveJob::ThreadedInstanceLoader : public ArchiveJob::InstanceLoader
@@ -186,8 +194,8 @@
 
 
   public:
-    ThreadedInstanceLoader(ServerContext& context, size_t threadCount, bool transcode, DicomTransferSyntax transferSyntax)
-    : InstanceLoader(context, transcode, transferSyntax),
+    ThreadedInstanceLoader(ServerContext& context, size_t threadCount, bool transcode, DicomTransferSyntax transferSyntax, unsigned int lossyQuality)
+    : InstanceLoader(context, transcode, transferSyntax, lossyQuality),
       availableInstancesSemaphore_(0),
       bufferedInstancesSemaphore_(3*threadCount)
     {
@@ -229,8 +237,8 @@
 
       while (true)
       {
-        std::unique_ptr<InstanceId> instanceId(dynamic_cast<InstanceId*>(that->instancesToPreload_.Dequeue(0)));
-        if (instanceId.get() == NULL)  // that's the signal to exit the thread
+        std::unique_ptr<InstanceToPreload> instanceToPreload(dynamic_cast<InstanceToPreload*>(that->instancesToPreload_.Dequeue(0)));
+        if (instanceToPreload.get() == NULL)  // that's the signal to exit the thread
         {
           return;
         }
@@ -241,12 +249,12 @@
         try
         {
           boost::shared_ptr<std::string> dicomContent(new std::string());
-          that->context_.ReadDicom(*dicomContent, instanceId->GetId());
+          that->context_.ReadAttachment(*dicomContent, instanceToPreload->GetFileInfo(), true);
 
           if (that->transcode_)
           {
             boost::shared_ptr<std::string> transcodedDicom(new std::string());
-            if (that->TranscodeDicom(*transcodedDicom, *dicomContent, instanceId->GetId()))
+            if (that->TranscodeDicom(*transcodedDicom, *dicomContent, instanceToPreload->GetId()))
             {
               dicomContent = transcodedDicom;
             }
@@ -254,7 +262,7 @@
 
           {
             boost::mutex::scoped_lock lock(that->availableInstancesMutex_);
-            that->availableInstances_[instanceId->GetId()] = dicomContent;
+            that->availableInstances_[instanceToPreload->GetId()] = dicomContent;
           }
 
           that->availableInstancesSemaphore_.Release();
@@ -263,18 +271,18 @@
         {
           boost::mutex::scoped_lock lock(that->availableInstancesMutex_);
           // store a NULL result to notify that we could not read the instance
-          that->availableInstances_[instanceId->GetId()] = boost::shared_ptr<std::string>(); 
+          that->availableInstances_[instanceToPreload->GetId()] = boost::shared_ptr<std::string>(); 
           that->availableInstancesSemaphore_.Release();
         }
       }
     }
 
-    virtual void PrepareDicom(const std::string& instanceId) ORTHANC_OVERRIDE
+    virtual void PrepareDicom(const std::string& instanceId, const FileInfo& fileInfo) ORTHANC_OVERRIDE
     {
-      instancesToPreload_.Enqueue(new InstanceId(instanceId));
+      instancesToPreload_.Enqueue(new InstanceToPreload(instanceId, fileInfo));
     }
 
-    virtual void GetDicom(std::string& dicom, const std::string& instanceId) ORTHANC_OVERRIDE
+    virtual void GetDicom(std::string& dicom, const std::string& instanceId, const FileInfo& fileInfo) ORTHANC_OVERRIDE
     {
       while (true)
       {
@@ -284,6 +292,8 @@
 
         boost::shared_ptr<std::string> dicomContent;
         {
+          boost::mutex::scoped_lock lock(availableInstancesMutex_);
+
           if (availableInstances_.find(instanceId) != availableInstances_.end())
           {
             // this is the instance we were waiting for
@@ -366,7 +376,7 @@
     {
       case ResourceType_Patient:
         return ArchiveResourceType_Patient;
-      case ArchiveResourceType_Study:
+      case ResourceType_Study:
        return ArchiveResourceType_PatientInfoFromStudy;
       case ResourceType_Series:
         return ArchiveResourceType_Series;
@@ -506,7 +516,8 @@
     virtual void Close() = 0;
 
     virtual void AddInstance(const std::string& instanceId,
-                             uint64_t uncompressedSize) = 0;
+                             uint32_t index,
+                             const FileInfo& fileInfo) = 0;
   };
 
 
@@ -516,12 +527,15 @@
     struct Instance
     {
       std::string  id_;
-      uint64_t     uncompressedSize_;
+      uint32_t     index_;
+      FileInfo     fileInfo_;
 
       Instance(const std::string& id,
-               uint64_t uncompressedSize) : 
+               uint32_t index,
+               const FileInfo& fileInfo) :
         id_(id),
-        uncompressedSize_(uncompressedSize)
+        index_(index),
+        fileInfo_(fileInfo)
       {
       }
     };
@@ -534,22 +548,18 @@
     std::list<Instance>  instances_;   // Only at instance level
 
 
-    void AddResourceToExpand(ServerIndex& index,
-                             const std::string& id)
+    void AddResourceToExpand(const std::string& id)
     {
-      if (level_ == ArchiveResourceType_Instance)
-      {
-        FileInfo tmp;
-        int64_t revision;  // ignored
-        if (index.LookupAttachment(tmp, revision, id, FileContentType_Dicom))
-        {
-          instances_.push_back(Instance(id, tmp.GetUncompressedSize()));
-        }
-      }
-      else
-      {
-        resources_[id] = NULL;
-      }
+      assert(level_ != ArchiveResourceType_Instance);
+      resources_[id] = NULL;
+    }
+
+    void AddInstance(const std::string& id,
+                     uint32_t indexInSeries,
+                     const FileInfo& fileInfo)
+    {
+      assert(level_ == ArchiveResourceType_Instance);
+      instances_.push_back(Instance(id, indexInSeries, fileInfo));
     }
 
 
@@ -577,7 +587,20 @@
 
       if (level_ == ArchiveResourceType_Instance)
       {
-        AddResourceToExpand(index, id);
+        std::string strIndexInSeries;
+        uint32_t indexInSeries = 0;
+        FileInfo fileInfo;
+        int64_t revisionNotUsed;
+        
+        if (index.LookupMetadata(strIndexInSeries, id, ResourceType_Instance, MetadataType_Instance_IndexInSeries))
+        {
+          SerializationToolbox::ParseUnsignedInteger32(indexInSeries, strIndexInSeries);
+        }
+
+        if (index.LookupAttachment(fileInfo, revisionNotUsed, ResourceType_Instance, id, FileContentType_Dicom))
+        {
+          AddInstance(id, indexInSeries, fileInfo);
+        }
       }
       else if (resource.GetLevel() == GetResourceLevel(level_))
       {
@@ -620,16 +643,31 @@
       {
         if (it->second == NULL)
         {
-          // This is resource is marked for expansion
-          std::list<std::string> children;
-          index.GetChildren(children, it->first);
-
+          // This resource is marked for expansion
           std::unique_ptr<ArchiveIndex> child(new ArchiveIndex(GetChildResourceType(level_)));
 
-          for (std::list<std::string>::const_iterator 
-                 it2 = children.begin(); it2 != children.end(); ++it2)
+          if (level_ == ArchiveResourceType_Series)
           {
-            child->AddResourceToExpand(index, *it2);
+            // Instances ordering is important !  
+            // From 1.12.6, when possible, the id in the filename will match the index in series.
+            // Only if there are duplicate index in series, we'll use a simple counter
+            SimpleInstanceOrdering orderedInstances(index, it->first);
+
+            for (size_t i = 0; i < orderedInstances.GetInstancesCount(); ++i)
+            {
+              child->AddInstance(orderedInstances.GetInstanceId(i), orderedInstances.GetInstanceIndexInSeries(i), orderedInstances.GetInstanceFileInfo(i));
+            }
+          }
+          else
+          {
+            std::list<std::string> children;
+            index.GetChildren(children, GetResourceLevel(level_), it->first);
+
+            for (std::list<std::string>::const_iterator 
+                  it2 = children.begin(); it2 != children.end(); ++it2)
+            {
+              child->AddResourceToExpand(*it2);
+            }
           }
 
           it->second = child.release();
@@ -648,7 +686,7 @@
         for (std::list<Instance>::const_iterator 
                it = instances_.begin(); it != instances_.end(); ++it)
         {
-          visitor.AddInstance(it->id_, it->uncompressedSize_);
+          visitor.AddInstance(it->id_,  it->index_, it->fileInfo_);
         }          
       }
       else
@@ -683,6 +721,7 @@
       Type          type_;
       std::string   filename_;
       std::string   instanceId_;
+      FileInfo      fileInfo_;
 
     public:
       explicit Command(Type type) :
@@ -701,10 +740,12 @@
         
       Command(Type type,
               const std::string& filename,
-              const std::string& instanceId) :
+              const std::string& instanceId,
+              const FileInfo& fileInfo) :
         type_(type),
         filename_(filename),
-        instanceId_(instanceId)
+        instanceId_(instanceId),
+        fileInfo_(fileInfo)
       {
         assert(type_ == Type_WriteInstance);
       }
@@ -733,7 +774,7 @@
 
             try
             {
-              instanceLoader.GetDicom(content, instanceId_);
+              instanceLoader.GetDicom(content, instanceId_, fileInfo_);
             }
             catch (OrthancException& e)
             {
@@ -858,12 +899,12 @@
 
     void AddWriteInstance(const std::string& filename,
                           const std::string& instanceId,
-                          uint64_t uncompressedSize)
+                          const FileInfo& fileInfo)
     {
-      instanceLoader_.PrepareDicom(instanceId);
-      commands_.push_back(new Command(Type_WriteInstance, filename, instanceId));
+      instanceLoader_.PrepareDicom(instanceId, fileInfo);
+      commands_.push_back(new Command(Type_WriteInstance, filename, instanceId, fileInfo));
       instancesCount_ ++;
-      uncompressedSize_ += uncompressedSize;
+      uncompressedSize_ += fileInfo.GetUncompressedSize();
     }
 
     bool IsZip64() const
@@ -982,13 +1023,13 @@
     }
 
     virtual void AddInstance(const std::string& instanceId,
-                             uint64_t uncompressedSize) ORTHANC_OVERRIDE
+                             uint32_t index,
+                             const FileInfo& fileInfo) ORTHANC_OVERRIDE
     {
       char filename[24];
-      snprintf(filename, sizeof(filename) - 1, instanceFormat_, counter_);
-      counter_ ++;
+      snprintf(filename, sizeof(filename) - 1, instanceFormat_, index);
 
-      commands_.AddWriteInstance(filename, instanceId, uncompressedSize);
+      commands_.AddWriteInstance(filename, instanceId, fileInfo);
     }
   };
 
@@ -1016,13 +1057,14 @@
     }
 
     virtual void AddInstance(const std::string& instanceId,
-                             uint64_t uncompressedSize) ORTHANC_OVERRIDE
+                             uint32_t indexNotUsed,
+                             const FileInfo& fileInfo) ORTHANC_OVERRIDE
     {
       // "DICOM restricts the filenames on DICOM media to 8
       // characters (some systems wrongly use 8.3, but this does not
       // conform to the standard)."
       std::string filename = "IM" + boost::lexical_cast<std::string>(counter_);
-      commands_.AddWriteInstance(filename, instanceId, uncompressedSize);
+      commands_.AddWriteInstance(filename, instanceId, fileInfo);
 
       counter_ ++;
     }
@@ -1203,12 +1245,14 @@
     archive_(new ArchiveIndex(GetArchiveResourceType(jobLevel))),  // get patient Info from this level
     isMedia_(isMedia),
     enableExtendedSopClass_(enableExtendedSopClass),
+    filename_("archive.zip"),
     currentStep_(0),
     instancesCount_(0),
     uncompressedSize_(0),
     archiveSize_(0),
     transcode_(false),
     transferSyntax_(DicomTransferSyntax_LittleEndianImplicit),
+    lossyQuality_(100),
     loaderThreads_(0)
   {
   }
@@ -1256,7 +1300,18 @@
     }
   }
 
-  
+  void ArchiveJob::SetFilename(const std::string& filename)
+  {
+    if (writer_.get() != NULL)   // Already started
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      filename_ = filename;
+    }
+  }
+
   void ArchiveJob::AddResource(const std::string& publicId,
                                bool mustExist,
                                ResourceType expectedType)
@@ -1297,7 +1352,20 @@
     }
   }
 
-  
+
+  void ArchiveJob::SetLossyQuality(unsigned int lossyQuality)
+  {
+    if (writer_.get() != NULL)   // Already started
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      lossyQuality_ = lossyQuality;
+    }
+  }
+
+
   void ArchiveJob::SetLoaderThreads(unsigned int loaderThreads)
   {
     if (writer_.get() != NULL)   // Already started
@@ -1323,11 +1391,11 @@
     if (loaderThreads_ == 0)
     {
       // default behaviour before loaderThreads was introducted in 1.10.0
-      instanceLoader_.reset(new SynchronousInstanceLoader(context_, transcode_, transferSyntax_));
+      instanceLoader_.reset(new SynchronousInstanceLoader(context_, transcode_, transferSyntax_, lossyQuality_));
     }
     else
     {
-      instanceLoader_.reset(new ThreadedInstanceLoader(context_, loaderThreads_, transcode_, transferSyntax_));
+      instanceLoader_.reset(new ThreadedInstanceLoader(context_, loaderThreads_, transcode_, transferSyntax_, lossyQuality_));
     }
 
     if (writer_.get() != NULL)
@@ -1478,7 +1546,7 @@
   }
 
 
-  float ArchiveJob::GetProgress()
+  float ArchiveJob::GetProgress() const
   {
     if (writer_.get() == NULL ||
         writer_->GetStepsCount() == 0)
@@ -1493,7 +1561,7 @@
   }
 
     
-  void ArchiveJob::GetJobType(std::string& target)
+  void ArchiveJob::GetJobType(std::string& target) const
   {
     if (isMedia_)
     {
@@ -1506,7 +1574,7 @@
   }
 
 
-  void ArchiveJob::GetPublicContent(Json::Value& value)
+  void ArchiveJob::GetPublicContent(Json::Value& value) const
   {
     value = Json::objectValue;
     value[KEY_DESCRIPTION] = description_;
@@ -1542,7 +1610,7 @@
         const DynamicTemporaryFile& f = dynamic_cast<DynamicTemporaryFile&>(accessor.GetItem());
         f.GetFile().Read(output);
         mime = MimeType_Zip;
-        filename = "archive.zip";
+        filename = filename_;
         return true;
       }
       else
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -57,6 +57,7 @@
     bool                                  isMedia_;
     bool                                  enableExtendedSopClass_;
     std::string                           description_;
+    std::string                           filename_;
 
     boost::shared_ptr<ZipWriterIterator>  writer_;
     size_t                                currentStep_;
@@ -68,6 +69,7 @@
     // New in Orthanc 1.7.0
     bool                 transcode_;
     DicomTransferSyntax  transferSyntax_;
+    unsigned int         lossyQuality_;
 
     // New in Orthanc 1.10.0
     unsigned int         loaderThreads_;
@@ -91,12 +93,21 @@
       return description_;
     }
 
+    void SetFilename(const std::string& filename);
+
+    const std::string& GetFilename() const
+    {
+      return filename_;
+    }
+
     void AddResource(const std::string& publicId,
                      bool mustExist,
                      ResourceType expectedType);
 
     void SetTranscode(DicomTransferSyntax transferSyntax);
 
+    void SetLossyQuality(unsigned int lossyQuality);
+
     void SetLoaderThreads(unsigned int loaderThreads);
 
     virtual void Reset() ORTHANC_OVERRIDE;
@@ -107,13 +118,13 @@
 
     virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
 
-    virtual float GetProgress() ORTHANC_OVERRIDE;
+    virtual float GetProgress() const ORTHANC_OVERRIDE;
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE;
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE;
     
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE
+    virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE
     {
       return false;  // Cannot serialize this kind of job
     }
--- a/OrthancServer/Sources/ServerJobs/CleaningInstancesJob.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/CleaningInstancesJob.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -83,7 +83,7 @@
   }
 
   
-  bool CleaningInstancesJob::Serialize(Json::Value& target)
+  bool CleaningInstancesJob::Serialize(Json::Value& target) const
   {
     if (!SetOfInstancesJob::Serialize(target))
     {
--- a/OrthancServer/Sources/ServerJobs/CleaningInstancesJob.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/CleaningInstancesJob.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -62,7 +62,7 @@
     
     void SetKeepSource(bool keep);
 
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
 
     virtual void Start() ORTHANC_OVERRIDE;
   };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/DicomGetScuJob.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,121 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DicomGetScuJob.h"
+
+#include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
+#include "../../../OrthancFramework/Sources/SerializationToolbox.h"
+#include "../ServerContext.h"
+#include <dcmtk/dcmnet/dimse.h>
+#include <algorithm>
+
+static const char* const LOCAL_AET = "LocalAet";
+static const char* const QUERY = "Query";
+static const char* const QUERY_FORMAT = "QueryFormat";  // New in 1.9.5
+static const char* const REMOTE = "Remote";
+static const char* const TIMEOUT = "Timeout";
+
+namespace Orthanc
+{
+
+
+  static uint16_t InstanceReceivedHandler(void* callbackContext,
+                                          DcmDataset& dataset,
+                                          const std::string& remoteAet,
+                                          const std::string& remoteIp,
+                                          const std::string& calledAet)
+  {
+    // this code is equivalent to OrthancStoreRequestHandler
+    ServerContext* context = reinterpret_cast<ServerContext*>(callbackContext);
+
+    std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromDcmDataset(dataset));
+    
+    if (toStore->GetBufferSize() > 0)
+    {
+      toStore->SetOrigin(DicomInstanceOrigin::FromDicomProtocol
+                         (remoteIp.c_str(), remoteAet.c_str(), calledAet.c_str()));
+
+      std::string id;
+      ServerContext::StoreResult result = context->Store(id, *toStore, StoreInstanceMode_Default);
+      return result.GetCStoreStatusCode();
+    }
+
+    return STATUS_STORE_Error_CannotUnderstand;
+  }
+
+  void DicomGetScuJob::Retrieve(const DicomMap& findAnswer)
+  {
+    if (connection_.get() == NULL)
+    {
+      std::set<std::string> sopClassesToPropose;
+      std::set<std::string> acceptedSopClasses;
+      std::list<DicomTransferSyntax> proposedTransferSyntaxes;
+
+      if (sopClassesFromResourcesToRetrieve_.size() > 0)
+      {
+        context_.GetAcceptedSopClasses(acceptedSopClasses, 0); 
+
+        // keep the sop classes from the resources to retrieve only if they are accepted by Orthanc
+        Toolbox::GetIntersection(sopClassesToPropose, sopClassesFromResourcesToRetrieve_, acceptedSopClasses);
+      }
+      else
+      {
+        // when we don't know what SOP Classes to use, we include the 120 most common SOP Classes because 
+        // there are only 128 presentation contexts available
+        context_.GetAcceptedSopClasses(sopClassesToPropose, 120); 
+      }
+
+      if (sopClassesToPropose.size() == 0)
+      {
+        throw OrthancException(ErrorCode_NoPresentationContext, "Cannot perform C-Get, no SOPClassUID have been accepted by Orthanc.");        
+      }
+
+      context_.GetProposedStorageTransferSyntaxes(proposedTransferSyntaxes);
+
+      connection_.reset(new DicomControlUserConnection(parameters_, 
+                                                       ScuOperationFlags_Get, 
+                                                       sopClassesToPropose,
+                                                       proposedTransferSyntaxes));
+    }
+
+    connection_->SetProgressListener(this);
+    connection_->Get(findAnswer, InstanceReceivedHandler, &context_);
+  }
+
+  void DicomGetScuJob::AddFindAnswer(const DicomMap& answer)
+  {
+    DicomRetrieveScuBaseJob::AddFindAnswer(answer);
+
+    std::set<std::string> sopClassesInStudy;
+    if (answer.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY) 
+        && answer.LookupStringValues(sopClassesInStudy, DICOM_TAG_SOP_CLASSES_IN_STUDY, false))
+    {
+      for (std::set<std::string>::const_iterator it = sopClassesInStudy.begin(); it != sopClassesInStudy.end(); ++it)
+      {
+        sopClassesFromResourcesToRetrieve_.insert(*it);
+      }
+    }
+  }
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/DicomGetScuJob.h	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,60 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../../OrthancFramework/Sources/Compatibility.h"
+#include "../../../OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h"
+#include "DicomRetrieveScuBaseJob.h"
+#include "../QueryRetrieveHandler.h"
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  class DicomGetScuJob : public DicomRetrieveScuBaseJob
+  {
+  private:
+    std::set<std::string> sopClassesFromResourcesToRetrieve_;
+
+    virtual void Retrieve(const DicomMap& findAnswer) ORTHANC_OVERRIDE;
+    
+  public:
+    explicit DicomGetScuJob(ServerContext& context) :
+      DicomRetrieveScuBaseJob(context)
+    {
+    }
+
+    DicomGetScuJob(ServerContext& context,
+                   const Json::Value& serialized);
+
+
+    virtual void AddFindAnswer(const DicomMap &answer) ORTHANC_OVERRIDE;
+
+
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
+    {
+      target = "DicomGetScu";
+    }
+  };
+}
--- a/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -242,7 +242,7 @@
   }
   
 
-  void DicomModalityStoreJob::GetPublicContent(Json::Value& value)
+  void DicomModalityStoreJob::GetPublicContent(Json::Value& value) const
   {
     SetOfInstancesJob::GetPublicContent(value);
     
@@ -281,7 +281,7 @@
   }
 
 
-  bool DicomModalityStoreJob::Serialize(Json::Value& target)
+  bool DicomModalityStoreJob::Serialize(Json::Value& target) const
   {
     if (!SetOfInstancesJob::Serialize(target))
     {
--- a/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -88,14 +88,14 @@
 
     virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
     {
       target = "DicomModalityStore";
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
 
     virtual void Reset() ORTHANC_OVERRIDE;
 
--- a/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -36,123 +36,21 @@
 
 namespace Orthanc
 {
-  class DicomMoveScuJob::Command : public SetOfCommandsJob::ICommand
-  {
-  private:
-    DicomMoveScuJob&           that_;
-    std::unique_ptr<DicomMap>  findAnswer_;
 
-  public:
-    Command(DicomMoveScuJob& that,
-            const DicomMap&  findAnswer) :
-      that_(that),
-      findAnswer_(findAnswer.Clone())
-    {
-    }
-
-    virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE
-    {
-      that_.Retrieve(*findAnswer_);
-      return true;
-    }
-
-    virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE
-    {
-      findAnswer_->Serialize(target);
-    }
-  };
-
-
-  class DicomMoveScuJob::Unserializer :
-    public SetOfCommandsJob::ICommandUnserializer
-  {
-  private:
-    DicomMoveScuJob&   that_;
-
-  public:
-    explicit Unserializer(DicomMoveScuJob&  that) :
-      that_(that)
-    {
-    }
-
-    virtual ICommand* Unserialize(const Json::Value& source) const ORTHANC_OVERRIDE
-    {
-      DicomMap findAnswer;
-      findAnswer.Unserialize(source);
-      return new Command(that_, findAnswer);
-    }
-  };
 
 
   void DicomMoveScuJob::Retrieve(const DicomMap& findAnswer)
   {
     if (connection_.get() == NULL)
     {
-      connection_.reset(new DicomControlUserConnection(parameters_));
+      connection_.reset(new DicomControlUserConnection(parameters_, ScuOperationFlags_Move));
     }
     
+    connection_->SetProgressListener(this);
     connection_->Move(targetAet_, findAnswer);
   }
 
 
-  static void AddToQuery(DicomFindAnswers& query,
-                         const DicomMap& item)
-  {
-    query.Add(item);
-
-    /**
-     * Compatibility with Orthanc <= 1.9.4: Remove the
-     * "SpecificCharacterSet" (0008,0005) tag that is automatically
-     * added if creating a ParsedDicomFile object from a DicomMap.
-     **/
-    query.GetAnswer(query.GetSize() - 1).Remove(DICOM_TAG_SPECIFIC_CHARACTER_SET);
-  }
-
-  // this method is used to implement the retrieve part of a Q&R 
-  // it keeps only the main dicom tags from the C-Find answer
-  void DicomMoveScuJob::AddFindAnswer(const DicomMap& answer)
-  {
-    DicomMap item;
-    item.CopyTagIfExists(answer, DICOM_TAG_QUERY_RETRIEVE_LEVEL);
-    item.CopyTagIfExists(answer, DICOM_TAG_PATIENT_ID);
-    item.CopyTagIfExists(answer, DICOM_TAG_STUDY_INSTANCE_UID);
-    item.CopyTagIfExists(answer, DICOM_TAG_SERIES_INSTANCE_UID);
-    item.CopyTagIfExists(answer, DICOM_TAG_SOP_INSTANCE_UID);
-    item.CopyTagIfExists(answer, DICOM_TAG_ACCESSION_NUMBER);
-    AddToQuery(query_, item);
-    
-    AddCommand(new Command(*this, answer));
-  }
-
-  // this method is used to implement a C-Move
-  // it keeps all tags from the C-Move query
-  void DicomMoveScuJob::AddQuery(const DicomMap& query)
-  {
-    AddToQuery(query_, query);
-    AddCommand(new Command(*this, query));
-  }
-  
-  void DicomMoveScuJob::AddFindAnswer(QueryRetrieveHandler& query,
-                                      size_t i)
-  {
-    DicomMap answer;
-    query.GetAnswer(answer, i);
-    AddFindAnswer(answer);
-  }    
-
-
-  void DicomMoveScuJob::SetLocalAet(const std::string& aet)
-  {
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      parameters_.SetLocalApplicationEntityTitle(aet);
-    }
-  }
-
   
   void DicomMoveScuJob::SetTargetAet(const std::string& aet)
   {
@@ -167,109 +65,33 @@
   }
 
   
-  void DicomMoveScuJob::SetRemoteModality(const RemoteModalityParameters& remote)
-  {
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      parameters_.SetRemoteModality(remote);
-    }
-  }
 
 
-  void DicomMoveScuJob::SetTimeout(uint32_t seconds)
-  {
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      parameters_.SetTimeout(seconds);
-    }
-  }
-
-  
-  void DicomMoveScuJob::Stop(JobStopReason reason)
-  {
-    connection_.reset();
-  }
-  
-
-  void DicomMoveScuJob::SetQueryFormat(DicomToJsonFormat format)
+  void DicomMoveScuJob::GetPublicContent(Json::Value& value) const
   {
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      queryFormat_ = format;
-    }
-  }
+    DicomRetrieveScuBaseJob::GetPublicContent(value);
 
-
-  void DicomMoveScuJob::GetPublicContent(Json::Value& value)
-  {
-    SetOfCommandsJob::GetPublicContent(value);
-
-    value[LOCAL_AET] = parameters_.GetLocalApplicationEntityTitle();
-    value["RemoteAet"] = parameters_.GetRemoteModality().GetApplicationEntityTitle();
-
-    value[QUERY] = Json::objectValue;
-    query_.ToJson(value[QUERY], queryFormat_);
+    value[TARGET_AET] = targetAet_;
   }
 
 
   DicomMoveScuJob::DicomMoveScuJob(ServerContext& context,
                                    const Json::Value& serialized) :
-    SetOfCommandsJob(new Unserializer(*this), serialized),
-    context_(context),
-    parameters_(DicomAssociationParameters::UnserializeJob(serialized)),
-    targetAet_(SerializationToolbox::ReadString(serialized, TARGET_AET)),
-    query_(true),
-    queryFormat_(DicomToJsonFormat_Short)
+    DicomRetrieveScuBaseJob(context, serialized),
+    targetAet_(SerializationToolbox::ReadString(serialized, TARGET_AET))
   {
-    if (serialized.isMember(QUERY))
-    {
-      const Json::Value& query = serialized[QUERY];
-      if (query.type() == Json::arrayValue)
-      {
-        for (Json::Value::ArrayIndex i = 0; i < query.size(); i++)
-        {
-          DicomMap item;
-          FromDcmtkBridge::FromJson(item, query[i]);
-          AddToQuery(query_, item);
-        }
-      }
-    }
-
-    if (serialized.isMember(QUERY_FORMAT))
-    {
-      queryFormat_ = StringToDicomToJsonFormat(SerializationToolbox::ReadString(serialized, QUERY_FORMAT));
-    }
   }
 
   
-  bool DicomMoveScuJob::Serialize(Json::Value& target)
+  bool DicomMoveScuJob::Serialize(Json::Value& target) const
   {
-    if (!SetOfCommandsJob::Serialize(target))
+    if (!DicomRetrieveScuBaseJob::Serialize(target))
     {
       return false;
     }
     else
     {
-      parameters_.SerializeJob(target);
       target[TARGET_AET] = targetAet_;
-
-      // "Short" is for compatibility with Orthanc <= 1.9.4
-      target[QUERY] = Json::objectValue;
-      query_.ToJson(target[QUERY], DicomToJsonFormat_Short);
-
-      target[QUERY_FORMAT] = EnumerationToString(queryFormat_);
       
       return true;
     }
--- a/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -25,7 +25,8 @@
 
 #include "../../../OrthancFramework/Sources/Compatibility.h"
 #include "../../../OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h"
-#include "../../../OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h"
+// #include "../../../OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h"
+#include "DicomRetrieveScuBaseJob.h"
 
 #include "../QueryRetrieveHandler.h"
 
@@ -33,51 +34,22 @@
 {
   class ServerContext;
   
-  class DicomMoveScuJob : public SetOfCommandsJob
+  class DicomMoveScuJob : public DicomRetrieveScuBaseJob
   {
   private:
-    class Command;
-    class Unserializer;
+    std::string                 targetAet_;
     
-    ServerContext&              context_;
-    DicomAssociationParameters  parameters_;
-    std::string                 targetAet_;
-    DicomFindAnswers            query_;
-    DicomToJsonFormat           queryFormat_;  // New in 1.9.5
-
-    std::unique_ptr<DicomControlUserConnection>  connection_;
-    
-    void Retrieve(const DicomMap& findAnswer);
+    virtual void Retrieve(const DicomMap& findAnswer) ORTHANC_OVERRIDE;
     
   public:
     explicit DicomMoveScuJob(ServerContext& context) :
-      context_(context),
-      query_(false  /* this is not for worklists */),
-      queryFormat_(DicomToJsonFormat_Short)
+      DicomRetrieveScuBaseJob(context)
     {
     }
 
     DicomMoveScuJob(ServerContext& context,
                     const Json::Value& serialized);
 
-    void AddFindAnswer(const DicomMap& answer);
-    
-    void AddQuery(const DicomMap& query);
-
-    void AddFindAnswer(QueryRetrieveHandler& query,
-                       size_t i);
-
-    const DicomAssociationParameters& GetParameters() const
-    {
-      return parameters_;
-    }
-    
-    void SetLocalAet(const std::string& aet);
-
-    void SetRemoteModality(const RemoteModalityParameters& remote);
-
-    void SetTimeout(uint32_t timeout);
-
     const std::string& GetTargetAet() const
     {
       return targetAet_;
@@ -85,22 +57,13 @@
     
     void SetTargetAet(const std::string& aet);
 
-    void SetQueryFormat(DicomToJsonFormat format);
-
-    DicomToJsonFormat GetQueryFormat() const
-    {
-      return queryFormat_;
-    }
-
-    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
-
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
     {
       target = "DicomMoveScu";
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/DicomRetrieveScuBaseJob.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,248 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DicomGetScuJob.h"
+
+#include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
+#include "../../../OrthancFramework/Sources/SerializationToolbox.h"
+#include "../ServerContext.h"
+#include <dcmtk/dcmnet/dimse.h>
+#include <algorithm>
+#include "../../../OrthancFramework/Sources/Logging.h"
+
+static const char* const LOCAL_AET = "LocalAet";
+static const char* const QUERY = "Query";
+static const char* const QUERY_FORMAT = "QueryFormat";  // New in 1.9.5
+static const char* const REMOTE = "Remote";
+static const char* const TIMEOUT = "Timeout";
+
+namespace Orthanc
+{
+
+  static void AddToQuery(DicomFindAnswers& query,
+                         const DicomMap& item)
+  {
+    query.Add(item);
+
+    /**
+     * Compatibility with Orthanc <= 1.9.4: Remove the
+     * "SpecificCharacterSet" (0008,0005) tag that is automatically
+     * added if creating a ParsedDicomFile object from a DicomMap.
+     **/
+    query.GetAnswer(query.GetSize() - 1).Remove(DICOM_TAG_SPECIFIC_CHARACTER_SET);
+  }
+
+  // this method is used to implement the retrieve part of a Q&R 
+  // it keeps only the main dicom tags from the C-Find answer
+  void DicomRetrieveScuBaseJob::AddFindAnswer(const DicomMap& answer)
+  {
+    DicomMap item;
+    item.CopyTagIfExists(answer, DICOM_TAG_QUERY_RETRIEVE_LEVEL);
+    item.CopyTagIfExists(answer, DICOM_TAG_PATIENT_ID);
+    item.CopyTagIfExists(answer, DICOM_TAG_STUDY_INSTANCE_UID);
+    item.CopyTagIfExists(answer, DICOM_TAG_SERIES_INSTANCE_UID);
+    item.CopyTagIfExists(answer, DICOM_TAG_SOP_INSTANCE_UID);
+    item.CopyTagIfExists(answer, DICOM_TAG_ACCESSION_NUMBER);
+    AddToQuery(query_, item);
+    
+    AddCommand(new Command(*this, answer));
+  }
+
+  void DicomRetrieveScuBaseJob::AddFindAnswer(QueryRetrieveHandler& query,
+                                              size_t i)
+  {
+    DicomMap answer;
+    query.GetAnswer(answer, i);
+    AddFindAnswer(answer);
+  }    
+
+  // this method is used to implement a C-Move
+  // it keeps all tags from the C-Move query
+  void DicomRetrieveScuBaseJob::AddQuery(const DicomMap& query)
+  {
+    AddToQuery(query_, query);
+    AddCommand(new Command(*this, query));
+  }
+ 
+
+  void DicomRetrieveScuBaseJob::SetLocalAet(const std::string& aet)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      parameters_.SetLocalApplicationEntityTitle(aet);
+    }
+  }
+
+  
+  void DicomRetrieveScuBaseJob::SetRemoteModality(const RemoteModalityParameters& remote)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      parameters_.SetRemoteModality(remote);
+    }
+  }
+
+
+  void DicomRetrieveScuBaseJob::SetTimeout(uint32_t seconds)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      parameters_.SetTimeout(seconds);
+    }
+  }
+
+  
+  void DicomRetrieveScuBaseJob::Stop(JobStopReason reason)
+  {
+    connection_.reset();
+  }
+  
+
+  void DicomRetrieveScuBaseJob::SetQueryFormat(DicomToJsonFormat format)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      queryFormat_ = format;
+    }
+  }
+
+
+  void DicomRetrieveScuBaseJob::GetPublicContent(Json::Value& value) const
+  {
+    SetOfCommandsJob::GetPublicContent(value);
+
+    value[LOCAL_AET] = parameters_.GetLocalApplicationEntityTitle();
+    value["RemoteAet"] = parameters_.GetRemoteModality().GetApplicationEntityTitle();
+
+    value[QUERY] = Json::objectValue;
+    query_.ToJson(value[QUERY], queryFormat_);
+  }
+
+
+  DicomRetrieveScuBaseJob::DicomRetrieveScuBaseJob(ServerContext &context) :
+    context_(context),
+    query_(false /* this is not for worklists */),
+    queryFormat_(DicomToJsonFormat_Short),
+    nbRemainingSubOperations_(0),
+    nbCompletedSubOperations_(0),
+    nbFailedSubOperations_(0),
+    nbWarningSubOperations_(0)
+  {
+  }
+
+
+  DicomRetrieveScuBaseJob::DicomRetrieveScuBaseJob(ServerContext& context,
+                                                   const Json::Value& serialized) :
+    SetOfCommandsJob(new Unserializer(*this), serialized),
+    context_(context),
+    parameters_(DicomAssociationParameters::UnserializeJob(serialized)),
+    query_(true),
+    queryFormat_(DicomToJsonFormat_Short),
+    nbRemainingSubOperations_(0),
+    nbCompletedSubOperations_(0),
+    nbFailedSubOperations_(0),
+    nbWarningSubOperations_(0)  
+  {
+    if (serialized.isMember(QUERY))
+    {
+      const Json::Value& query = serialized[QUERY];
+      if (query.type() == Json::arrayValue)
+      {
+        for (Json::Value::ArrayIndex i = 0; i < query.size(); i++)
+        {
+          DicomMap item;
+          FromDcmtkBridge::FromJson(item, query[i]);
+          AddToQuery(query_, item);
+        }
+      }
+    }
+
+    if (serialized.isMember(QUERY_FORMAT))
+    {
+      queryFormat_ = StringToDicomToJsonFormat(SerializationToolbox::ReadString(serialized, QUERY_FORMAT));
+    }
+  }
+
+  
+  bool DicomRetrieveScuBaseJob::Serialize(Json::Value& target) const
+  {
+    if (!SetOfCommandsJob::Serialize(target))
+    {
+      return false;
+    }
+    else
+    {
+      parameters_.SerializeJob(target);
+
+      // "Short" is for compatibility with Orthanc <= 1.9.4
+      target[QUERY] = Json::objectValue;
+      query_.ToJson(target[QUERY], DicomToJsonFormat_Short);
+
+      target[QUERY_FORMAT] = EnumerationToString(queryFormat_);
+      
+      return true;
+    }
+  }
+
+  void DicomRetrieveScuBaseJob::OnProgressUpdated(uint16_t nbRemainingSubOperations,
+                                                  uint16_t nbCompletedSubOperations,
+                                                  uint16_t nbFailedSubOperations,
+                                                  uint16_t nbWarningSubOperations)
+  {
+    boost::mutex::scoped_lock lock(progressMutex_);
+
+    nbRemainingSubOperations_ = nbRemainingSubOperations;
+    nbCompletedSubOperations_ = nbCompletedSubOperations;
+    nbFailedSubOperations_ = nbFailedSubOperations;
+    nbWarningSubOperations_ = nbWarningSubOperations;
+  }
+
+  float DicomRetrieveScuBaseJob::GetProgress() const
+  {
+    boost::mutex::scoped_lock lock(progressMutex_);
+    
+    uint32_t totalOperations = nbRemainingSubOperations_ + nbCompletedSubOperations_ + nbFailedSubOperations_ + nbWarningSubOperations_;
+    if (totalOperations == 0)
+    {
+      return 0.0f;
+    }
+
+    return float(nbCompletedSubOperations_ + nbFailedSubOperations_ + nbWarningSubOperations_) / float(totalOperations);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/DicomRetrieveScuBaseJob.h	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,149 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include "../../../OrthancFramework/Sources/Compatibility.h"
+#include "../../../OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h"
+#include "../../../OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h"
+#include <boost/thread/mutex.hpp>
+
+#include "../QueryRetrieveHandler.h"
+
+namespace Orthanc
+{
+  class ServerContext;
+
+  class DicomRetrieveScuBaseJob : public SetOfCommandsJob, public DicomControlUserConnection::IProgressListener
+  {
+  protected:
+    class Command : public SetOfCommandsJob::ICommand
+    {
+    private:
+      DicomRetrieveScuBaseJob &that_;
+      std::unique_ptr<DicomMap> findAnswer_;
+
+    public:
+      Command(DicomRetrieveScuBaseJob &that,
+              const DicomMap &findAnswer) :
+      that_(that),
+      findAnswer_(findAnswer.Clone())
+      {
+      }
+
+      virtual bool Execute(const std::string &jobId) ORTHANC_OVERRIDE
+      {
+        that_.Retrieve(*findAnswer_);
+        return true;
+      }
+
+      virtual void Serialize(Json::Value &target) const ORTHANC_OVERRIDE
+      {
+        findAnswer_->Serialize(target);
+      }
+    };
+
+    class Unserializer : public SetOfCommandsJob::ICommandUnserializer
+    {
+    protected:
+      DicomRetrieveScuBaseJob &that_;
+
+    public:
+      explicit Unserializer(DicomRetrieveScuBaseJob &that) : 
+      that_(that)
+      {
+      }
+
+      virtual ICommand *Unserialize(const Json::Value &source) const ORTHANC_OVERRIDE
+      {
+        DicomMap findAnswer;
+        findAnswer.Unserialize(source);
+        return new Command(that_, findAnswer);
+      }
+    };
+
+    ServerContext &context_;
+    DicomAssociationParameters parameters_;
+    DicomFindAnswers query_;
+    DicomToJsonFormat queryFormat_; // New in 1.9.5
+
+    std::unique_ptr<DicomControlUserConnection> connection_;
+
+    mutable boost::mutex progressMutex_;
+    uint16_t nbRemainingSubOperations_;
+    uint16_t nbCompletedSubOperations_;
+    uint16_t nbFailedSubOperations_;
+    uint16_t nbWarningSubOperations_;
+
+    virtual void Retrieve(const DicomMap &findAnswer) = 0;
+
+    explicit DicomRetrieveScuBaseJob(ServerContext &context);
+
+    DicomRetrieveScuBaseJob(ServerContext &context,
+                            const Json::Value &serialized);
+
+  public:
+    virtual void AddFindAnswer(const DicomMap &answer);
+
+    void AddQuery(const DicomMap& query);
+
+    void AddFindAnswer(QueryRetrieveHandler &query,
+                       size_t i);
+
+    const DicomAssociationParameters &GetParameters() const
+    {
+      return parameters_;
+    }
+
+    void SetLocalAet(const std::string &aet);
+
+    void SetRemoteModality(const RemoteModalityParameters &remote);
+
+    void SetTimeout(uint32_t timeout);
+
+    void SetQueryFormat(DicomToJsonFormat format);
+
+    DicomToJsonFormat GetQueryFormat() const
+    {
+      return queryFormat_;
+    }
+
+    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
+
+    virtual void GetPublicContent(Json::Value &value) const ORTHANC_OVERRIDE;
+
+    virtual bool Serialize(Json::Value &target) const ORTHANC_OVERRIDE;
+
+    virtual void OnProgressUpdated(uint16_t nbRemainingSubOperations,
+                                    uint16_t nbCompletedSubOperations,
+                                    uint16_t nbFailedSubOperations,
+                                    uint16_t nbWarningSubOperations) ORTHANC_OVERRIDE;
+
+    virtual float GetProgress() const ORTHANC_OVERRIDE;
+
+    virtual bool NeedsProgressUpdateBetweenSteps() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+  };
+}
--- a/OrthancServer/Sources/ServerJobs/IStorageCommitmentFactory.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/IStorageCommitmentFactory.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/ServerJobs/LuaJobManager.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/LuaJobManager.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/ServerJobs/LuaJobManager.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/LuaJobManager.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -49,7 +49,7 @@
 
     // Add all the instances of the series as to be processed
     std::list<std::string> instances;
-    GetContext().GetIndex().GetChildren(instances, series);
+    GetContext().GetIndex().GetChildren(instances, ResourceType_Series, series);
 
     for (std::list<std::string>::const_iterator
            it = instances.begin(); it != instances.end(); ++it)
@@ -69,7 +69,7 @@
     else
     {
       std::list<std::string> series;
-      GetContext().GetIndex().GetChildren(series, study);
+      GetContext().GetIndex().GetChildren(series, ResourceType_Study, study);
 
       for (std::list<std::string>::const_iterator
              it = series.begin(); it != series.end(); ++it)
@@ -190,7 +190,7 @@
     DicomTag::AddTagsForModule(removals_, DicomModule_Study);
     
     std::list<std::string> instances;
-    GetContext().GetIndex().GetChildInstances(instances, targetStudy);
+    GetContext().GetIndex().GetChildInstances(instances, targetStudy, ResourceType_Study);
     
     if (instances.empty())
     {
@@ -353,7 +353,7 @@
   }
   
 
-  void MergeStudyJob::GetPublicContent(Json::Value& value)
+  void MergeStudyJob::GetPublicContent(Json::Value& value) const
   {
     CleaningInstancesJob::GetPublicContent(value);
     value["TargetStudy"] = targetStudy_;
@@ -386,7 +386,7 @@
   }
 
   
-  bool MergeStudyJob::Serialize(Json::Value& target)
+  bool MergeStudyJob::Serialize(Json::Value& target) const
   {
     if (!CleaningInstancesJob::Serialize(target))
     {
--- a/OrthancServer/Sources/ServerJobs/MergeStudyJob.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/MergeStudyJob.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -89,13 +89,13 @@
     {
     }
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
     {
       target = "MergeStudy";
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancServer/Sources/ServerJobs/Operations/DeleteResourceOperation.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/DeleteResourceOperation.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/ServerJobs/Operations/DeleteResourceOperation.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/DeleteResourceOperation.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/ServerJobs/Operations/DicomInstanceOperationValue.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/DicomInstanceOperationValue.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/ServerJobs/Operations/DicomInstanceOperationValue.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/DicomInstanceOperationValue.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/ServerJobs/Operations/ModifyInstanceOperation.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/ModifyInstanceOperation.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/ServerJobs/Operations/ModifyInstanceOperation.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/ModifyInstanceOperation.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/ServerJobs/Operations/StorePeerOperation.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/StorePeerOperation.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/ServerJobs/Operations/StorePeerOperation.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/StorePeerOperation.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/ServerJobs/Operations/StoreScuOperation.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/StoreScuOperation.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/ServerJobs/Operations/StoreScuOperation.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/StoreScuOperation.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/ServerJobs/Operations/SystemCallOperation.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/SystemCallOperation.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/ServerJobs/Operations/SystemCallOperation.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/SystemCallOperation.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/ServerJobs/OrthancJobUnserializer.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/OrthancJobUnserializer.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/ServerJobs/OrthancJobUnserializer.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/OrthancJobUnserializer.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -220,7 +220,7 @@
   }
 
 
-  void OrthancPeerStoreJob::GetPublicContent(Json::Value& value)
+  void OrthancPeerStoreJob::GetPublicContent(Json::Value& value) const
   {
     SetOfInstancesJob::GetPublicContent(value);
     
@@ -285,7 +285,7 @@
   }
 
 
-  bool OrthancPeerStoreJob::Serialize(Json::Value& target)
+  bool OrthancPeerStoreJob::Serialize(Json::Value& target) const
   {
     if (!SetOfInstancesJob::Serialize(target))
     {
--- a/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -92,13 +92,13 @@
 
     virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;   // For pausing jobs
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
     {
       target = "OrthancPeerStore";
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -542,7 +542,7 @@
   }
 
 
-  void ResourceModificationJob::GetPublicContent(Json::Value& value)
+  void ResourceModificationJob::GetPublicContent(Json::Value& value) const
   {
     boost::recursive_mutex::scoped_lock lock(outputMutex_);
 
@@ -636,7 +636,7 @@
     }
   }
   
-  bool ResourceModificationJob::Serialize(Json::Value& value)
+  bool ResourceModificationJob::Serialize(Json::Value& value) const
   {
     if (modification_.get() == NULL)
     {
@@ -744,10 +744,18 @@
         }
         else
         {
-          ExpandedResource originalStudy;
-          if (GetContext().GetIndex().ExpandResource(originalStudy, *studyId, ResourceType_Study, emptyRequestedTags, ExpandResourceFlags_IncludeMainDicomTags))
+          FindRequest request(ResourceType_Study);
+          request.SetOrthancStudyId(*studyId);
+          request.SetRetrieveMainDicomTags(true);
+
+          FindResponse response;
+          GetContext().GetIndex().ExecuteFind(response, request);
+
+          if (response.GetSize() == 1)
           {
-            targetPatientId = originalStudy.GetMainDicomTags().GetStringValue(DICOM_TAG_PATIENT_ID, "", false);
+            DicomMap tags;
+            response.GetResourceByIndex(0).GetMainDicomTags(tags, ResourceType_Study);
+            targetPatientId = tags.GetStringValue(DICOM_TAG_PATIENT_ID, "", false);
           }
           else
           {
@@ -762,22 +770,34 @@
         // if the patient exists, check how many child studies it has.
         if (lookupPatientResult.size() >= 1)
         {
-          ExpandedResource targetPatient;
-          
-          if (GetContext().GetIndex().ExpandResource(targetPatient, lookupPatientResult[0], ResourceType_Patient, emptyRequestedTags, static_cast<ExpandResourceFlags>(ExpandResourceFlags_IncludeMainDicomTags | ExpandResourceFlags_IncludeChildren)))
+          FindRequest request(ResourceType_Patient);
+          request.SetOrthancPatientId(lookupPatientResult[0]);
+          request.SetRetrieveMainDicomTags(true);
+          request.GetChildrenSpecification(ResourceType_Study).SetRetrieveIdentifiers(true);
+
+          FindResponse response;
+          GetContext().GetIndex().ExecuteFind(response, request);
+
+          if (response.GetSize() == 1)
           {
-            const std::list<std::string> childrenIds = targetPatient.childrenIds_;
+            const FindResponse::Resource& targetPatient = response.GetResourceByIndex(0);
+
+            const std::set<std::string>& childrenIds = targetPatient.GetChildrenIdentifiers(ResourceType_Study);
+
             bool targetPatientHasOtherStudies = childrenIds.size() > 1;
             if (childrenIds.size() == 1)
             {
-              targetPatientHasOtherStudies = std::find(childrenIds.begin(), childrenIds.end(), *studyId) == childrenIds.end();  // if the patient has one study that is not the one being modified
+              targetPatientHasOtherStudies = (childrenIds.find(*studyId) == childrenIds.end());  // if the patient has one study that is not the one being modified
             }
 
             if (targetPatientHasOtherStudies)
             {
+              DicomMap mainDicomTags;
+              targetPatient.GetMainDicomTags(mainDicomTags, ResourceType_Patient);
+
               // this is allowed if all patient replacedTags do match the target patient tags
               DicomMap targetPatientTags;
-              targetPatient.GetMainDicomTags().ExtractPatientInformation(targetPatientTags);
+              mainDicomTags.ExtractPatientInformation(targetPatientTags);
 
               std::set<DicomTag> mainPatientTags;
               DicomMap::GetMainDicomTags(mainPatientTags, ResourceType_Patient);
--- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -116,14 +116,14 @@
     // Only possible if "IsSingleResourceModification()"
     ResourceType GetOutputLevel() const;
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
     {
       target = "ResourceModification";
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
     
-    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE;
 
     virtual void Reset() ORTHANC_OVERRIDE;
 
--- a/OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -213,7 +213,7 @@
 
       // Add all the instances of the series as to be processed
       std::list<std::string> instances;
-      GetContext().GetIndex().GetChildren(instances, series);
+      GetContext().GetIndex().GetChildren(instances, ResourceType_Series, series);
 
       for (std::list<std::string>::const_iterator
              it = instances.begin(); it != instances.end(); ++it)
@@ -306,7 +306,7 @@
   }
   
     
-  void SplitStudyJob::GetPublicContent(Json::Value& value)
+  void SplitStudyJob::GetPublicContent(Json::Value& value) const
   {
     CleaningInstancesJob::GetPublicContent(value);
 
@@ -351,7 +351,7 @@
   }
 
   
-  bool SplitStudyJob::Serialize(Json::Value& target)
+  bool SplitStudyJob::Serialize(Json::Value& target) const
   {
     if (!CleaningInstancesJob::Serialize(target))
     {
--- a/OrthancServer/Sources/ServerJobs/SplitStudyJob.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/SplitStudyJob.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -113,13 +113,13 @@
     {
     }
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
     {
       target = "SplitStudy";
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -404,7 +404,7 @@
   }
 
 
-  void StorageCommitmentScpJob::GetPublicContent(Json::Value& value)
+  void StorageCommitmentScpJob::GetPublicContent(Json::Value& value) const
   {
     SetOfCommandsJob::GetPublicContent(value);
       
@@ -434,7 +434,7 @@
   }
   
 
-  bool StorageCommitmentScpJob::Serialize(Json::Value& target)
+  bool StorageCommitmentScpJob::Serialize(Json::Value& target) const
   {
     if (!SetOfCommandsJob::Serialize(target))
     {
--- a/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -89,13 +89,13 @@
     {
     }
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
     {
       target = "StorageCommitmentScp";
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -376,7 +376,7 @@
   static const char* KEY_WORKERS_COUNT = "WorkersCount";
 
 
-  void ThreadedSetOfInstancesJob::GetPublicContent(Json::Value& target)
+  void ThreadedSetOfInstancesJob::GetPublicContent(Json::Value& target) const
   {
     boost::recursive_mutex::scoped_lock lock(mutex_);
 
@@ -391,7 +391,7 @@
   }
 
 
-  bool ThreadedSetOfInstancesJob::Serialize(Json::Value& target)
+  bool ThreadedSetOfInstancesJob::Serialize(Json::Value& target) const
   {
     boost::recursive_mutex::scoped_lock lock(mutex_);
 
@@ -478,7 +478,7 @@
     return keepSource_;
   }
 
-  float ThreadedSetOfInstancesJob::GetProgress()
+  float ThreadedSetOfInstancesJob::GetProgress() const
   {
     boost::recursive_mutex::scoped_lock lock(mutex_);
 
--- a/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -140,15 +140,15 @@
     
     virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
 
-    virtual float GetProgress() ORTHANC_OVERRIDE;
+    virtual float GetProgress() const ORTHANC_OVERRIDE;
 
     bool IsStarted() const;
 
     virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE;
     
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
     
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
 
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
--- a/OrthancServer/Sources/ServerToolbox.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerToolbox.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/ServerToolbox.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/ServerToolbox.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/SimpleInstanceOrdering.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PrecompiledHeadersServer.h"
+#include "SimpleInstanceOrdering.h"
+
+#include "../../OrthancFramework/Sources/Logging.h"
+#include "../../OrthancFramework/Sources/Toolbox.h"
+#include "../../OrthancFramework/Sources/SerializationToolbox.h"
+#include "ServerEnumerations.h"
+#include "ServerIndex.h"
+#include "ResourceFinder.h"
+
+#include <algorithm>
+#include <boost/lexical_cast.hpp>
+#include <boost/noncopyable.hpp>
+
+
+namespace Orthanc
+{
+  struct SimpleInstanceOrdering::Instance : public boost::noncopyable
+  {
+  private:
+    std::string   instanceId_;
+    uint32_t      indexInSeries_;
+    FileInfo      fileInfo_;
+
+
+  public:
+    Instance(const std::string& instanceId,
+             uint32_t indexInSeries,
+             const FileInfo& fileInfo) :
+      instanceId_(instanceId),
+      indexInSeries_(indexInSeries),
+      fileInfo_(fileInfo)
+    {
+    }
+
+    const std::string& GetIdentifier() const
+    {
+      return instanceId_;
+    }
+
+    uint32_t GetIndexInSeries() const
+    {
+      return indexInSeries_;
+    }
+ 
+    const FileInfo& GetFileInfo() const
+    {
+      return fileInfo_;
+    }
+  };
+
+  bool SimpleInstanceOrdering::IndexInSeriesComparator(const SimpleInstanceOrdering::Instance* a,
+                                                       const SimpleInstanceOrdering::Instance* b)
+  {
+    return a->GetIndexInSeries() < b->GetIndexInSeries();
+  }  
+
+
+  SimpleInstanceOrdering::SimpleInstanceOrdering(ServerIndex& index,
+                                                 const std::string& seriesId) :
+    hasDuplicateIndexInSeries_(false)
+  {
+    FindRequest request(ResourceType_Instance);
+    request.SetOrthancSeriesId(seriesId);
+    request.SetRetrieveMetadata(true);
+    request.SetRetrieveAttachments(true);
+
+    FindResponse response;
+    index.ExecuteFind(response, request);
+
+    std::set<uint32_t> allIndexInSeries;
+
+    for (size_t i = 0; i < response.GetSize(); ++i)
+    {
+      const FindResponse::Resource& resource = response.GetResourceByIndex(i);
+      std::string instanceId = resource.GetIdentifier();
+
+      std::string strIndexInSeries;
+      uint32_t indexInSeries = 0;
+      FileInfo fileInfo;
+      int64_t revisionNotUsed;
+			
+      if (resource.LookupMetadata(strIndexInSeries, ResourceType_Instance, MetadataType_Instance_IndexInSeries))
+      {
+        SerializationToolbox::ParseUnsignedInteger32(indexInSeries, strIndexInSeries);
+      }
+
+      if (resource.LookupAttachment(fileInfo, revisionNotUsed, FileContentType_Dicom))
+      {
+        allIndexInSeries.insert(indexInSeries);
+        instances_.push_back(new SimpleInstanceOrdering::Instance(instanceId, indexInSeries, fileInfo));
+      }
+    }
+
+    // if there are duplicates, the set will be smaller than the vector
+    hasDuplicateIndexInSeries_ = allIndexInSeries.size() != instances_.size(); 
+
+    std::sort(instances_.begin(), instances_.end(), IndexInSeriesComparator);
+  }
+
+  SimpleInstanceOrdering::~SimpleInstanceOrdering()
+  {
+    for (std::vector<Instance*>::iterator
+           it = instances_.begin(); it != instances_.end(); ++it)
+    {
+      if (*it != NULL)
+      {
+        delete *it;
+      }
+    }
+  }
+
+  const std::string& SimpleInstanceOrdering::GetInstanceId(size_t index) const
+  {
+    return instances_[index]->GetIdentifier();
+  }
+
+  uint32_t SimpleInstanceOrdering::GetInstanceIndexInSeries(size_t index) const
+  {
+    if (hasDuplicateIndexInSeries_)
+    {
+      // if there are duplicates, we count the instances from 0
+      return index; 
+    }
+    else
+    {
+      // if there are no duplicates, we return the real index in series (that may not start at 0 but that is more human friendly)
+      return instances_[index]->GetIndexInSeries();
+    }
+  }
+
+  const FileInfo& SimpleInstanceOrdering::GetInstanceFileInfo(size_t index) const
+  {
+    return instances_[index]->GetFileInfo();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/SimpleInstanceOrdering.h	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,61 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../OrthancFramework/Sources/DicomFormat/DicomMap.h"
+#include "../../OrthancFramework/Sources/FileStorage/FileInfo.h"
+
+namespace Orthanc
+{
+  class ServerIndex;
+
+  class SimpleInstanceOrdering
+  {
+  private:
+    struct Instance;
+
+    std::vector<Instance*>   instances_;
+    bool                     hasDuplicateIndexInSeries_;
+
+  public:
+    SimpleInstanceOrdering(ServerIndex& index,
+                           const std::string& seriesId);
+    ~SimpleInstanceOrdering();
+
+    size_t GetInstancesCount() const
+    {
+      return instances_.size();
+    }
+
+    const std::string& GetInstanceId(size_t index) const;
+
+    uint32_t GetInstanceIndexInSeries(size_t index) const;
+
+    const FileInfo& GetInstanceFileInfo(size_t index) const;
+
+  private:
+    static bool IndexInSeriesComparator(const SimpleInstanceOrdering::Instance* a,
+                                        const SimpleInstanceOrdering::Instance* b);
+  };
+}
--- a/OrthancServer/Sources/SliceOrdering.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/SliceOrdering.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -198,8 +198,7 @@
 
       try
       {
-        int64_t revision;  // Ignored
-        if (index.LookupMetadata(s, revision, instanceId, ResourceType_Instance, MetadataType_Instance_IndexInSeries))
+        if (index.LookupMetadata(s, instanceId, ResourceType_Instance, MetadataType_Instance_IndexInSeries))
         {
           indexInSeries_ = boost::lexical_cast<size_t>(Toolbox::StripSpaces(s));
           hasIndexInSeries_ = true;
@@ -297,7 +296,7 @@
   void SliceOrdering::CreateInstances()
   {
     std::list<std::string> instancesId;
-    index_.GetChildren(instancesId, seriesId_);
+    index_.GetChildren(instancesId, ResourceType_Series, seriesId_);
 
     instances_.reserve(instancesId.size());
     for (std::list<std::string>::const_iterator
--- a/OrthancServer/Sources/SliceOrdering.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/SliceOrdering.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/StorageCommitmentReports.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/StorageCommitmentReports.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/StorageCommitmentReports.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/StorageCommitmentReports.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/Sources/main.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/Sources/main.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -63,6 +63,8 @@
 static const char* const KEY_DICOM_TLS_MINIMUM_PROTOCOL_VERSION = "DicomTlsMinimumProtocolVersion";
 static const char* const KEY_DICOM_TLS_ACCEPTED_CIPHERS = "DicomTlsCiphersAccepted";
 static const char* const KEY_MAXIMUM_PDU_LENGTH = "MaximumPduLength";
+static const char* const KEY_READ_ONLY = "ReadOnly";
+static const char* const KEY_MAXIMUM_CONCURRENT_DCMTK_TRANSCODERS = "MaximumConcurrentDcmtkTranscoders";
 
 
 class OrthancStoreRequestHandler : public IStoreRequestHandler
@@ -494,6 +496,13 @@
     context_.GetAcceptedTransferSyntaxes(target);
   }
 
+  virtual void GetProposedStorageTransferSyntaxes(std::list<DicomTransferSyntax>& target,
+                                                  const std::string& remoteIp,
+                                                  const std::string& remoteAet,
+                                                  const std::string& calledAet) ORTHANC_OVERRIDE
+  {
+    context_.GetProposedStorageTransferSyntaxes(target);
+  }
   
   virtual bool IsUnknownSopClassAccepted(const std::string& remoteIp,
                                          const std::string& remoteAet,
@@ -501,6 +510,12 @@
   {
     return context_.IsUnknownSopClassAccepted();
   }
+
+  virtual void GetAcceptedSopClasses(std::set<std::string>& sopClasses,
+                                     size_t maxCount) ORTHANC_OVERRIDE
+  {
+    context_.GetAcceptedSopClasses(sopClasses, maxCount);
+  }
 };
 
 
@@ -746,8 +761,8 @@
     << path << " " << ORTHANC_VERSION << std::endl
     << "Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics Department, University Hospital of Liege (Belgium)" << std::endl
     << "Copyright (C) 2017-2023 Osimis S.A. (Belgium)" << std::endl
-    << "Copyright (C) 2024-2024 Orthanc Team SRL (Belgium)" << std::endl
-    << "Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain (Belgium)" << std::endl
+    << "Copyright (C) 2024-2025 Orthanc Team SRL (Belgium)" << std::endl
+    << "Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain (Belgium)" << std::endl
     << "Licensing GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>." << std::endl
     << "This is free software: you are free to change and redistribute it." << std::endl
     << "There is NO WARRANTY, to the extent permitted by law." << std::endl
@@ -825,6 +840,7 @@
     PrintErrorCode(ErrorCode_MainDicomTagsMultiplyDefined, "A main DICOM Tag has been defined multiple times for the same resource level");
     PrintErrorCode(ErrorCode_ForbiddenAccess, "Access to a resource is forbidden");
     PrintErrorCode(ErrorCode_DuplicateResource, "Duplicate resource");
+    PrintErrorCode(ErrorCode_IncompatibleConfigurations, "Your configuration file contains configuration that are mutually incompatible");
     PrintErrorCode(ErrorCode_SQLiteNotOpened, "SQLite: The database is not opened");
     PrintErrorCode(ErrorCode_SQLiteAlreadyOpened, "SQLite: Connection is already open");
     PrintErrorCode(ErrorCode_SQLiteCannotOpen, "SQLite: Unable to open the database");
@@ -836,7 +852,7 @@
     PrintErrorCode(ErrorCode_SQLiteFlush, "SQLite: Unable to flush the database");
     PrintErrorCode(ErrorCode_SQLiteCannotRun, "SQLite: Cannot run a cached statement");
     PrintErrorCode(ErrorCode_SQLiteCannotStep, "SQLite: Cannot step over a cached statement");
-    PrintErrorCode(ErrorCode_SQLiteBindOutOfRange, "SQLite: Bing a value while out of range (serious error)");
+    PrintErrorCode(ErrorCode_SQLiteBindOutOfRange, "SQLite: Bind a value while out of range (serious error)");
     PrintErrorCode(ErrorCode_SQLitePrepareStatement, "SQLite: Cannot prepare a cached statement");
     PrintErrorCode(ErrorCode_SQLiteTransactionAlreadyStarted, "SQLite: Beginning the same transaction twice");
     PrintErrorCode(ErrorCode_SQLiteTransactionCommit, "SQLite: Failure when committing the transaction");
@@ -886,6 +902,7 @@
     PrintErrorCode(ErrorCode_AlreadyExistingTag, "Cannot override the value of a tag that already exists");
     PrintErrorCode(ErrorCode_NoStorageCommitmentHandler, "No request handler factory for DICOM N-ACTION SCP (storage commitment)");
     PrintErrorCode(ErrorCode_NoCGetHandler, "No request handler factory for DICOM C-GET SCP");
+    PrintErrorCode(ErrorCode_DicomGetUnavailable, "DicomUserConnection: The C-GET command is not supported by the remote SCP");
     PrintErrorCode(ErrorCode_UnsupportedMediaType, "Unsupported media type");
   }
 
@@ -1187,7 +1204,7 @@
       else
       {
         context.SetRestApiWriteToFileSystemEnabled(false);
-        LOG(WARNING) << "REST API cannot write to the file system bacause the \"RestApiWriteToFileSystemEnabled\" configuration is set to false.  The URI /instances/../export is disabled.  This is the most secure configuration.";
+        LOG(WARNING) << "REST API cannot write to the file system because the \"RestApiWriteToFileSystemEnabled\" configuration is set to false.  The URI /instances/../export is disabled.  This is the most secure configuration.";
       }
 
       if (lock.GetConfiguration().GetBooleanParameter("WebDavEnabled", true))
@@ -1517,16 +1534,24 @@
                                    bool loadJobsFromDatabase)
 {
   size_t maxCompletedJobs;
-  
+  bool readOnly;
+  unsigned int maxDcmtkConcurrentTranscoders;
+
   {
     OrthancConfiguration::ReaderLock lock;
 
     // These configuration options must be set before creating the
     // ServerContext, otherwise the possible Lua scripts will not be
     // able to properly issue HTTP/HTTPS queries
+
+    std::string httpsCaCertificates = lock.GetConfiguration().GetStringParameter("HttpsCACertificates", "");
+    if (!httpsCaCertificates.empty())
+    {
+      httpsCaCertificates = lock.GetConfiguration().InterpretStringParameterAsPath(httpsCaCertificates);
+    }
+
     HttpClient::ConfigureSsl(lock.GetConfiguration().GetBooleanParameter("HttpsVerifyPeers", true),
-                             lock.GetConfiguration().InterpretStringParameterAsPath
-                             (lock.GetConfiguration().GetStringParameter("HttpsCACertificates", "")));
+                             httpsCaCertificates);
     HttpClient::SetDefaultVerbose(lock.GetConfiguration().GetBooleanParameter("HttpVerbose", false));
 
     // The value "0" below makes the class HttpClient use its default
@@ -1544,6 +1569,16 @@
       LOG(WARNING) << "Setting option \"JobsHistorySize\" to zero is not recommended";
     }
 
+    // New option in Orthanc 1.12.5
+    readOnly = lock.GetConfiguration().GetBooleanParameter(KEY_READ_ONLY, false);
+    
+    // New option in Orthanc 1.12.6
+    maxDcmtkConcurrentTranscoders = lock.GetConfiguration().GetUnsignedIntegerParameter(KEY_MAXIMUM_CONCURRENT_DCMTK_TRANSCODERS, 0);
+    if (maxDcmtkConcurrentTranscoders == 0)
+    {
+      maxDcmtkConcurrentTranscoders = static_cast<unsigned int>(boost::thread::hardware_concurrency());
+    }
+
     // Configuration of DICOM TLS for Orthanc SCU (since Orthanc 1.9.0)
     DicomAssociationParameters::SetDefaultOwnCertificatePath(
       lock.GetConfiguration().GetStringParameter(KEY_DICOM_TLS_PRIVATE_KEY, ""),
@@ -1558,46 +1593,54 @@
       lock.GetConfiguration().GetBooleanParameter(KEY_DICOM_TLS_REMOTE_CERTIFICATE_REQUIRED, true));
   }
   
-  ServerContext context(database, storageArea, false /* not running unit tests */, maxCompletedJobs);
+  ServerContext context(database, storageArea, false /* not running unit tests */, maxCompletedJobs, readOnly, maxDcmtkConcurrentTranscoders);
 
   {
     OrthancConfiguration::ReaderLock lock;
 
-    context.SetCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("StorageCompression", false));
-    context.SetStoreMD5ForAttachments(lock.GetConfiguration().GetBooleanParameter("StoreMD5ForAttachments", true));
+    if (context.IsReadOnly())
+    {
+      LOG(WARNING) << "READ-ONLY SYSTEM: ignoring these configurations: StorageCompression, StoreMD5ForAttachments, OverwriteInstances, MaximumPatientCount, MaximumStorageSize, MaximumStorageMode, SaveJobs"; 
+    }
+    else
+    {
+      context.SetCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("StorageCompression", false));
+      context.SetStoreMD5ForAttachments(lock.GetConfiguration().GetBooleanParameter("StoreMD5ForAttachments", true));
 
-    // New option in Orthanc 1.4.2
-    context.SetOverwriteInstances(lock.GetConfiguration().GetBooleanParameter("OverwriteInstances", false));
+      // New option in Orthanc 1.4.2
+      context.SetOverwriteInstances(lock.GetConfiguration().GetBooleanParameter("OverwriteInstances", false));
 
-    try
-    {
-      context.GetIndex().SetMaximumPatientCount(lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumPatientCount", 0));
-    }
-    catch (...)
-    {
-      context.GetIndex().SetMaximumPatientCount(0);
+      try
+      {
+        context.GetIndex().SetMaximumPatientCount(lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumPatientCount", 0));
+      }
+      catch (...)
+      {
+        context.GetIndex().SetMaximumPatientCount(0);
+      }
+
+      try
+      {
+        uint64_t size = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumStorageSize", 0);
+        context.GetIndex().SetMaximumStorageSize(size * 1024 * 1024);
+      }
+      catch (...)
+      {
+        context.GetIndex().SetMaximumStorageSize(0);
+      }
+
+      try
+      {
+        std::string mode = lock.GetConfiguration().GetStringParameter("MaximumStorageMode", "Recycle");
+        context.GetIndex().SetMaximumStorageMode(StringToMaxStorageMode(mode));
+      }
+      catch (...)
+      {
+        context.GetIndex().SetMaximumStorageMode(MaxStorageMode_Recycle);
+      }
     }
 
-    try
-    {
-      uint64_t size = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumStorageSize", 0);
-      context.GetIndex().SetMaximumStorageSize(size * 1024 * 1024);
-    }
-    catch (...)
-    {
-      context.GetIndex().SetMaximumStorageSize(0);
-    }
-
-    try
-    {
-      std::string mode = lock.GetConfiguration().GetStringParameter("MaximumStorageMode", "Recycle");
-      context.GetIndex().SetMaximumStorageMode(StringToMaxStorageMode(mode));
-    }
-    catch (...)
-    {
-      context.GetIndex().SetMaximumStorageMode(MaxStorageMode_Recycle);
-    }
-
+    // note: this config is valid in ReadOnlyMode
     try
     {
       uint64_t size = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumStorageCacheSize", 128);
@@ -1955,7 +1998,7 @@
           SQLiteDatabaseWrapper inMemoryDatabase;
           inMemoryDatabase.Open();
           MemoryStorageArea inMemoryStorage;
-          ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */);
+          ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */, false /* readonly */, 1 /* DCMTK concurrent transcoders */);
           OrthancRestApi restApi(context, false /* no Orthanc Explorer */);
           restApi.GenerateOpenApiDocumentation(openapi);
           context.Stop();
@@ -2006,7 +2049,7 @@
           SQLiteDatabaseWrapper inMemoryDatabase;
           inMemoryDatabase.Open();
           MemoryStorageArea inMemoryStorage;
-          ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */);
+          ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */, false /* readonly */, 1 /* DCMTK concurrent transcoders */);
           OrthancRestApi restApi(context, false /* no Orthanc Explorer */);
           restApi.GenerateReStructuredTextCheatSheet(cheatsheet, "https://orthanc.uclouvain.be/api/index.html");
           context.Stop();
--- a/OrthancServer/UnitTestsSources/DatabaseLookupTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/UnitTestsSources/DatabaseLookupTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/UnitTestsSources/LuaServerTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/UnitTestsSources/LuaServerTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -138,38 +138,43 @@
 TEST(Lua, Http)
 {
   Orthanc::LuaContext lua;
-
-#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1
-  // The "http://www.orthanc-server.com/downloads/third-party/" does
-  // not automatically redirect to HTTPS, so we use it even if the
-  // OpenSSL/HTTPS support is disabled in curl
-  const std::string BASE = "http://www.orthanc-server.com/downloads/third-party/";
-
-#if LUA_VERSION_NUM >= 502
-  // Since Lua >= 5.2.0, the function "loadstring" has been replaced by "load"
-  lua.Execute("JSON = load(HttpGet('" + BASE + "JSON.lua')) ()");
-#else
-  lua.Execute("JSON = loadstring(HttpGet('" + BASE + "JSON.lua')) ()");
-#endif
-
-  const std::string url(BASE + "Product.json");
-#endif
+  ASSERT_TRUE(lua.IsHttpsVerifyPeers());
+  lua.SetHttpsVerifyPeers(false);
+  ASSERT_FALSE(lua.IsHttpsVerifyPeers());
 
   std::string s;
   lua.Execute(s, "print(HttpGet({}))");
   ASSERT_EQ("nil", Orthanc::Toolbox::StripSpaces(s));
 
-#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1  
-  lua.Execute(s, "print(string.len(HttpGet(\"" + url + "\")))");
-  ASSERT_LE(100, boost::lexical_cast<int>(Orthanc::Toolbox::StripSpaces(s)));
+#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1
+  // The "http://httpbin.org/get" URL does not automatically redirect
+  // to HTTPS, so we can use it even if the OpenSSL/HTTPS support is
+  // disabled in curl
 
-  // Parse a JSON file
-  lua.Execute(s, "print(JSON:decode(HttpGet(\"" + url + "\")) ['Product'])");
-  ASSERT_EQ("OrthancClient", Orthanc::Toolbox::StripSpaces(s));
+  const std::string URL = "http://httpbin.org/get";
+  lua.Execute(s, "print(HttpGet(\"" + URL + "\"))");
+
+  Json::Value json;
+  Orthanc::Toolbox::ReadJson(json, s);
+
+  ASSERT_TRUE(json.type() == Json::objectValue);
+  ASSERT_TRUE(json.isMember("url"));
+  ASSERT_EQ(URL, json["url"].asString());
 
 #if 0
   // This part of the test can only be executed if one instance of
-  // Orthanc is running on the localhost
+  // Orthanc is running on the localhost, with configuration option
+  // "ExecuteLuaEnabled" equals to "true"
+
+  const std::string JSON_LUA_TOOLBOX = "https://regex.info/code/JSON.lua";
+  lua.Execute("HttpGet('" + JSON_LUA_TOOLBOX + "')");
+
+#if LUA_VERSION_NUM >= 502
+  // Since Lua >= 5.2.0, the function "loadstring" has been replaced by "load"
+  lua.Execute("JSON = load(HttpGet('" + JSON_LUA_TOOLBOX + "')) ()");
+#else
+  lua.Execute("JSON = loadstring(HttpGet('" + JSON_LUA_TOOLBOX + "')) ()");
+#endif
 
   lua.Execute("modality = {}");
   lua.Execute("table.insert(modality, 'ORTHANC')");
--- a/OrthancServer/UnitTestsSources/PluginsTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/UnitTestsSources/PluginsTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/UnitTestsSources/PrecompiledHeadersUnitTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/UnitTestsSources/PrecompiledHeadersUnitTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/UnitTestsSources/PrecompiledHeadersUnitTests.h	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/UnitTestsSources/PrecompiledHeadersUnitTests.h	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/UnitTestsSources/ServerConfigTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -0,0 +1,176 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PrecompiledHeadersUnitTests.h"
+#include <gtest/gtest.h>
+
+#include "../../OrthancFramework/Sources/Compatibility.h"
+#include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h"
+#include "../../OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h"
+#include "../../OrthancFramework/Sources/Logging.h"
+#include "../../OrthancFramework/Sources/SerializationToolbox.h"
+
+#include "../Sources/Database/SQLiteDatabaseWrapper.h"
+#include "../Sources/ServerContext.h"
+
+using namespace Orthanc;
+
+TEST(ServerConfig, AcceptedSopClasses)
+{
+  const std::string path = "UnitTestsStorage";
+
+  MemoryStorageArea storage;
+  SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
+  db.Open();
+  ServerContext context(db, storage, true /* running unit tests */, 10, false, 1);
+
+  { // default config -> all SOP Classes should be accepted
+    std::set<std::string> s;
+
+    context.GetAcceptedSopClasses(s, 0);
+    ASSERT_LE(100u, s.size());
+
+    ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") != s.end());
+    ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") != s.end());
+
+    context.GetAcceptedSopClasses(s, 1);
+    ASSERT_EQ(1u, s.size());
+  }
+
+  {
+    std::list<std::string> acceptedStorageClasses;
+    std::set<std::string> rejectedStorageClasses;
+
+    std::set<std::string> s;
+    context.GetAcceptedSopClasses(s, 0);
+    size_t allSize = s.size();
+
+    { // default config but reject one class
+      acceptedStorageClasses.clear();
+      rejectedStorageClasses.clear();
+      rejectedStorageClasses.insert("1.2.840.10008.5.1.4.1.1.4");
+
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_EQ(allSize - 1, s.size());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") == s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") != s.end());
+
+      context.GetAcceptedSopClasses(s, 1);
+      ASSERT_EQ(1u, s.size());
+    }
+
+    { // default config but reject one regex
+      acceptedStorageClasses.clear();
+      rejectedStorageClasses.clear();
+      rejectedStorageClasses.insert("1.2.840.10008.5.1.4.1.*");
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") == s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") == s.end());
+    }
+
+    { // accept a single - no rejection
+      acceptedStorageClasses.clear();
+      acceptedStorageClasses.push_back("1.2.840.10008.5.1.4.1.1.4");
+      rejectedStorageClasses.clear();
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_EQ(1, s.size());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") != s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") == s.end());
+    }
+
+    { // accept from regex - reject one
+      acceptedStorageClasses.clear();
+      acceptedStorageClasses.push_back("1.2.840.10008.5.1.4.1.*");
+      rejectedStorageClasses.clear();
+      rejectedStorageClasses.insert("1.2.840.10008.5.1.4.1.1.12.1.1");
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_LE(10, s.size());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") != s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") == s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.2.1") != s.end());
+    }
+
+    { // accept from regex - reject from regex
+      acceptedStorageClasses.clear();
+      acceptedStorageClasses.push_back("1.2.840.10008.5.1.4.1.*");
+      rejectedStorageClasses.clear();
+      rejectedStorageClasses.insert("1.2.840.10008.5.1.4.1.1.12.1.*");
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_LE(10, s.size());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") != s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") == s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.2.1") != s.end());
+    }
+
+    { // accept one that is unknown form DCMTK
+      acceptedStorageClasses.clear();
+      acceptedStorageClasses.push_back("1.2.3.4");
+      rejectedStorageClasses.clear();
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_EQ(1, s.size());
+      ASSERT_TRUE(s.find("1.2.3.4") != s.end());
+    }
+
+    { // accept the default ones + a custom one
+      acceptedStorageClasses.clear();
+      acceptedStorageClasses.push_back("1.2.840.*");
+      acceptedStorageClasses.push_back("1.2.3.4");
+      rejectedStorageClasses.clear();
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_LE(100u, s.size());
+      ASSERT_TRUE(s.find("1.2.3.4") != s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.2.1") != s.end());
+    }
+
+    { // test the ? in regex to replace a single char
+      acceptedStorageClasses.clear();
+      acceptedStorageClasses.push_back("1.2.840.10008.5.1.4.1.1.12.2.?");
+      acceptedStorageClasses.push_back("1.2.3.4");
+      rejectedStorageClasses.clear();
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_EQ(2u, s.size());
+      ASSERT_TRUE(s.find("1.2.3.4") != s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.2.1") != s.end());
+    }
+
+  }
+
+  context.Stop();
+  db.Close();
+}
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -166,8 +166,9 @@
       
       DicomTagConstraint c(tag, type, value, true, true);
       
-      std::vector<DatabaseConstraint> lookup;
-      lookup.push_back(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
+      DatabaseDicomTagConstraints lookup;
+      bool isEquivalent;  // unused
+      lookup.AddConstraint(c.ConvertToDatabaseConstraint(isEquivalent, level, DicomTagType_Identifier));
 
       std::set<std::string> noLabel;
       transaction_->ApplyLookupResources(result, NULL, lookup, level, noLabel, LabelsConstraint_All, 0 /* no limit */);
@@ -185,10 +186,11 @@
       
       DicomTagConstraint c1(tag, type1, value1, true, true);
       DicomTagConstraint c2(tag, type2, value2, true, true);
-      
-      std::vector<DatabaseConstraint> lookup;
-      lookup.push_back(c1.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
-      lookup.push_back(c2.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
+
+      DatabaseDicomTagConstraints lookup;
+      bool isEquivalent;  // unused
+      lookup.AddConstraint(c1.ConvertToDatabaseConstraint(isEquivalent, level, DicomTagType_Identifier));
+      lookup.AddConstraint(c2.ConvertToDatabaseConstraint(isEquivalent, level, DicomTagType_Identifier));
       
       std::set<std::string> noLabel;
       transaction_->ApplyLookupResources(result, NULL, lookup, level, noLabel, LabelsConstraint_All, 0 /* no limit */);
@@ -619,7 +621,7 @@
   FilesystemStorage storage(path);
   SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
   db.Open();
-  ServerContext context(db, storage, true /* running unit tests */, 10);
+  ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */);
   context.SetupJobsEngine(true, false);
 
   ServerIndex& index = context.GetIndex();
@@ -701,7 +703,7 @@
   FilesystemStorage storage(path);
   SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
   db.Open();
-  ServerContext context(db, storage, true /* running unit tests */, 10);
+  ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */);
   context.SetupJobsEngine(true, false);
   ServerIndex& index = context.GetIndex();
 
@@ -818,7 +820,7 @@
     MemoryStorageArea storage;
     SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
     db.Open();
-    ServerContext context(db, storage, true /* running unit tests */, 10);
+    ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */);
     context.SetupJobsEngine(true, false);
     context.SetCompressionEnabled(true);
 
@@ -863,15 +865,15 @@
     {
       FileInfo nope;
       int64_t revision;
-      ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, revision, id, FileContentType_DicomAsJson));
+      ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, revision, ResourceType_Instance, id, FileContentType_DicomAsJson));
     }
 
     FileInfo dicom1, pixelData1;
-    int64_t revision;
-    ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom1, revision, id, FileContentType_Dicom));
+    int64_t revision = -1;
+    ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom1, revision, ResourceType_Instance, id, FileContentType_Dicom));
     ASSERT_EQ(0, revision);
     revision = -1;
-    ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData1, revision, id, FileContentType_DicomUntilPixelData));
+    ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData1, revision, ResourceType_Instance, id, FileContentType_DicomUntilPixelData));
     ASSERT_EQ(0, revision);
 
     context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
@@ -913,14 +915,14 @@
     {
       FileInfo nope;
       int64_t revision;
-      ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, revision, id, FileContentType_DicomAsJson));
+      ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, revision, ResourceType_Instance, id, FileContentType_DicomAsJson));
     }
 
     FileInfo dicom2, pixelData2;
-    ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom2, revision, id, FileContentType_Dicom));
+    ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom2, revision, ResourceType_Instance, id, FileContentType_Dicom));
     ASSERT_EQ(0, revision);
     revision = -1;
-    ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData2, revision, id, FileContentType_DicomUntilPixelData));
+    ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData2, revision, ResourceType_Instance, id, FileContentType_DicomUntilPixelData));
     ASSERT_EQ(0, revision);
 
     context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
@@ -983,7 +985,7 @@
     MemoryStorageArea storage;
     SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
     db.Open();
-    ServerContext context(db, storage, true /* running unit tests */, 10);
+    ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */);
     context.SetupJobsEngine(true, false);
     context.SetCompressionEnabled(compression);
 
--- a/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -106,24 +106,24 @@
     {
     }
 
-    virtual float GetProgress() ORTHANC_OVERRIDE
+    virtual float GetProgress() const ORTHANC_OVERRIDE
     {
       return static_cast<float>(count_) / static_cast<float>(steps_ - 1);
     }
 
-    virtual void GetJobType(std::string& type) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& type) const ORTHANC_OVERRIDE
     {
       type = "DummyJob";
     }
 
-    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE
+    virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE
     {
       value = Json::objectValue;
       value["Type"] = "DummyJob";
       return true;
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE
     {
       value["hello"] = "world";
     }
@@ -202,7 +202,7 @@
     {
     }
 
-    virtual void GetJobType(std::string& s) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& s) const ORTHANC_OVERRIDE
     {
       s = "DummyInstancesJob";
     }
@@ -536,7 +536,7 @@
     OrthancJobsSerialization()
     {
       db_.Open();
-      context_.reset(new ServerContext(db_, storage_, true /* running unit tests */, 10));
+      context_.reset(new ServerContext(db_, storage_, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */));
       context_->SetupJobsEngine(true, false);
     }
 
--- a/OrthancServer/UnitTestsSources/SizeOfTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/UnitTestsSources/SizeOfTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/UnitTestsSources/UnitTestsMain.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/UnitTestsSources/UnitTestsMain.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
--- a/OrthancServer/UnitTestsSources/VersionsTests.cpp	Fri Sep 20 16:07:08 2024 +0200
+++ b/OrthancServer/UnitTestsSources/VersionsTests.cpp	Tue Mar 18 13:37:18 2025 +0100
@@ -3,8 +3,8 @@
  * 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-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 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
@@ -113,14 +113,14 @@
 
 TEST(Versions, BoostStatic)
 {
-  ASSERT_TRUE(std::string(BOOST_LIB_VERSION) == "1_85" ||
+  ASSERT_TRUE(std::string(BOOST_LIB_VERSION) == "1_86" ||
               std::string(BOOST_LIB_VERSION) == "1_69" /* if USE_LEGACY_BOOST */);
 }
 
 TEST(Versions, CurlStatic)
 {
   curl_version_info_data* v = curl_version_info(CURLVERSION_NOW);
-  ASSERT_STREQ("8.5.0", v->version);
+  ASSERT_STREQ("8.9.0", v->version);
 }
 
 TEST(Versions, PngStatic)
--- a/README	Fri Sep 20 16:07:08 2024 +0200
+++ b/README	Tue Mar 18 13:37:18 2025 +0100
@@ -49,9 +49,7 @@
 Licensing
 ---------
 
-Orthanc is licensed under the GPLv3 license, with the OpenSSL
-exception:
-http://people.gnome.org/~markmc/openssl-and-the-gpl.html
+Orthanc is licensed under the GPLv3 license.
 
 Full information about the licensing of the Orthanc ecosystem is
 available in the Orthanc Book:
--- a/TODO	Fri Sep 20 16:07:08 2024 +0200
+++ b/TODO	Tue Mar 18 13:37:18 2025 +0100
@@ -1,3 +1,11 @@
+current work on C-Get SCU:
+- for the negotiation, limit SOPClassUID to the ones listed in a C-Find response or to a list provided in the Rest API ?
+- SetupPresentationContexts
+- handle progress
+- handle cancellation when the job is cancelled ?
+
+
+
 =======================
 === Orthanc Roadmap ===
 =======================
@@ -28,19 +36,20 @@
   For a DICOM Transfer, that would be nice to include the modality in the context + a study identifier or a job id.
 * (1) Accept extra DICOM tags dictionaries in the DCMTK format '.dic' (easier to use than declare
   them in the Orthanc configuration file).  Even the standard dictionaries could be 
-  overriden by these custom dictionaries.
-* Provide more flexibility wrt Dicom TLS ciphers and TLS version:
-  https://groups.google.com/g/orthanc-users/c/X4IhmXCSr7I/m/EirawAFcBwAJ
-  Can maybe be achieved by adding a configuration to select the TLS Security Profile:
-  https://github.com/DCMTK/dcmtk/blob/master/dcmtls/include/dcmtk/dcmtls/tlsciphr.h#L83 
-  (e.g: TSP_Profile_BCP195_ND instead of TSP_Profile_BCP195)
+  overridden by these custom dictionaries.
 * Add configurations to enable/disable warnings:
   - Modifying an instance while keeping its original SOPInstanceUID: This should be avoided!
   - Modifying a study while keeping its original StudyInstanceUID: This should be avoided!
+  In order to be able to disable/enable warnings in both the server and the framework, we should add a map of
+  enabled warnings in the logging classes directly and have something like:
+  LOG_WARNING_IF_ENABLED("Warning_ID") << ...
+  ENABLE_WARNING("Warning_ID", true)
+  Warnings from Framework should have a separate range like W999_ or WF001_ ...
 * Store the job registry in a dedicatd table in DB ?
   https://discourse.orthanc-server.org/t/performance-issue-when-adding-a-lot-of-jobs-in-the-queue/3915/2
   Note: that might also be the right time to have a central jobs registry when working
   with multiple Orthanc instances on the same DB.
+  Note: the json serialization of a job "content" can be very large -> compress it before saving it to DB ?
 * Right now, some Stable events never occurs (e.g. when Orthanc is restarted before the event is triggered).
   Since these events are used to e.g. generate dicom-web cache (or update it !), we should try
   to make sure these events always happen.
@@ -48,6 +57,8 @@
   - Also consider the use case of an Orthanc cluster that is being scaled-down just after one Orthanc instance
     has received a few instances -> we can not only check for missing stable events at startup since no Orthanc will start.  
     We would need to maintain the list of "unstable" resources in DB instead of memory only.
+  - Also check the PG plugin and its new table InvalidChildCounts, with a timestamp there, we can detect for
+    how long a study has not been modified !
 * In prometheus metrics, implement Histograms or Exponential Histograms to measure durations.  Right now, we only provide
   "average" durations that are not very relevant
   (https://opentelemetry.io/docs/specs/otel/metrics/data-model/#histogram)
@@ -71,7 +82,7 @@
 * Write a getting started guide (step by step) for each platform to replace
   https://orthanc.uclouvain.be/book/users/cookbook.html :
   - Ubuntu/Debian
-  - Windows
+  - Windows (done)
   - OSX
   - Docker on Linux
   Each step by step guide should contain:
@@ -153,7 +164,7 @@
   https://groups.google.com/g/orthanc-users/c/hsZ1jng5rIg/m/8xZL2C1VBgAJ
 * add an "AutoDeleteIfSuccessful": false option when creating jobs 
   https://discourse.orthanc-server.org/t/job-history-combined-with-auto-forwarding/3729/10
-* Allow skiping automatic conversion of color-space in transcoding/decoding.
+* Allow skipping automatic conversion of color-space in transcoding/decoding.
   The patch that was initialy provided was breaking the IngestTranscoding.
   This might require a DCMTK decoding plugin ?
   https://discourse.orthanc-server.org/t/orthanc-convert-ybr-to-rgb-but-does-not-change-metadata/3533/9
@@ -177,7 +188,12 @@
 Mid-term
 --------
 
-* Support C-GET SCU (note that C-GET SCP was introduced in Orthanc 1.7.0)
+* Check how Orthanc shall behave wrt to AcceptedSopClasses in these situations (consider Orthanc
+  accepts CT but not PT)
+  - What shall we log/warn if an external modality tries to send a PT/CT
+  - What shall we log/warn if we try to C-GET a PT/CT
+  Should the rejected files be logged as Failed, Warning, Refused, ...
+  Note: some tentative work has been initiated in the get-scu-test branch.
 * Support "Retrieve AE Title" (0008,0054) in C-FIND:
   - On SCP side: done by https://orthanc.uclouvain.be/hg/orthanc/rev/1ec3e1e18f50
   - On SCU side:
@@ -187,10 +203,13 @@
 * Strict hierarchical C-FIND:
   https://groups.google.com/d/msg/orthanc-users/VBHpeGVSNKM/tkaVvjWFBwAJ
 * report DIMSE error codes in Rest API and job status for /store /query /move /retrieve
-* report progress report of C-Move operation in jop progress.  There are 
-  progress callbacks available in DIMSE_moveUser
-  https://groups.google.com/g/orthanc-users/c/c8cGnA7FzsE/m/BSs66D8wBwAJ
 * Log outgoing C-Find queries
+* Support other Transfer Syntaxes in the Worklist plugin:
+  https://discourse.orthanc-server.org/t/could-you-please-create-an-option-to-set-the-transfer-syntax-in-the-worklist-plugin-currently-little-endian-explicit-is-fixed/4871
+* Deidentify Trace level logs: https://discourse.orthanc-server.org/t/dicom-log-deidentification-at-trace-log-level/5563
+  We should implement something like GetDeidentifiedContent(const DcmDataSet& dataset) that would 
+  reproduce DcmDataSet::print but hide individual elements that contain PHI.
+
 
 ---------
 Long-term
@@ -205,6 +224,8 @@
   We should first filter in SQL by StudyDate only, combine it with StudyTime into a single 
   DateTime string and filter again in C++.
   https://discourse.orthanc-server.org/t/performin-find-within-orthanc-for-time-frames/4704
+* Worklist plugin: support MPPS
+  https://github.com/orthanc-server/orthanc-setup-samples/blob/master/python-samples/worklist-with-mpps.py
 
 --------------------
 Internationalization
@@ -222,16 +243,8 @@
 Performance
 ===========
 
-* (3) ServerContext::DicomCacheLocker => give access to the raw buffer,
-  useful in ServerContext::DecodeDicomInstance()
 * (2) DicomMap: create a cache to the main DICOM tags index
 * (3) Check out rapidjson: https://github.com/miloyip/nativejson-benchmark
-* For C-Find results: we could store the computed tags
-    in metadata on some events like NewSeries + DeletedSeries (same for other computer tags).
-    OtherTags that could be saved in Metadata as well:
-    - ModalitiesInStudy
-    - all computed counters at series/study/patient level
-    - RequestAttributesSequence (sequence that must be included in all DicomWeb QIDO-RS for series)
 
 * Long-shot & not sure it is even feasible at all: try to reduce memory usage by implementing streaming
   when receiving DICOM instances from the Rest API or from DICOM and store files directly to disk as they
@@ -248,6 +261,11 @@
     to Orthanc and/or the Storage plugin (instead of passing a memory buffer) -> the object-storage plugin could 
     "stream" the file to the storage.  The HTTP server could also "stream" its response from file handles.
     Transcoding should be "file based" too.
+  Alternative option 4: Catch out-of-memory exceptions at quite high level in HTTPHandlers and DICOM receivers
+    and implement retries.  After a few retries, fail for good and return "out-of-resources".
+    Would be interesting to log these errors and count them in the prometheus metrics.
+  Note: I'm (maybe naively) thinking that you only need the beginning of the file to get the DICOM tags to be stored 
+  in the database, so maybe the rest of the file could be "streamed" directly to disk and not kept in memory?
 * To investigate: usage of mapped_file (not only in the indexer plugin): 
   https://discourse.orthanc-server.org/t/patch-for-orthanc-indexer-plugin-crashing-on-big-non-dicom-files/3849/7
 
@@ -284,8 +302,22 @@
   https://groups.google.com/g/orthanc-users/c/ymtaAmgSs6Q/m/PqVBactQAQAJ
 * Add an index on the UUID column in the DelayedDeletion plugin:
   https://discourse.orthanc-server.org/t/delayeddeletion-improvement-unique-index-on-pending-uuid-column/4032
+* Orthanc shall refuse to start if one registers 2 storage plugins.
+  Right now, this is not possible because OrthancPluginRegisterStorageArea2 does not return any value
+  and it can not throw an Exception because that's a core function called from a plugin -> the Exception
+  can not cross the C/C++ frontier safely -> we need a OrthancPluginRegisterStorageArea3 with a return value.
+  Ex: install DelayedDeletion + S3 storage.  Right now, the second plugin to load is just ignored with an error
+  message in the logs.
 
 
+-----------
+Housekeeper
+-----------
+
+* The Housekeeper should just refuse to start if you are using a lossy transfer syntax because that would generate 
+  new orthanc ids as well and there is a risk of messing up things.  tools/reconstruct shall return a 400 as well.
+  https://discourse.orthanc-server.org/t/crashes-on-housekeeper-transcode-storing-in-s3/5330/2
+
 ----------------
 Ideas of plugins
 ----------------