changeset 5807:8279eaab0d1d attach-custom-data

merged default -> attach-custom-data
author Alain Mazy <am@orthanc.team>
date Tue, 24 Sep 2024 11:39:52 +0200
parents 79f98ee4f04b (current diff) 16df20505710 (diff)
children 63c025cf6958
files NEWS OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake OrthancFramework/Sources/FileStorage/FileInfo.cpp OrthancFramework/Sources/FileStorage/FileInfo.h OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp OrthancFramework/Sources/FileStorage/FilesystemStorage.h OrthancFramework/Sources/FileStorage/IStorageArea.h OrthancFramework/Sources/FileStorage/MemoryStorageArea.h OrthancFramework/Sources/FileStorage/StorageAccessor.cpp OrthancFramework/Sources/FileStorage/StorageAccessor.h OrthancFramework/UnitTestsSources/FileStorageTests.cpp OrthancServer/CMakeLists.txt OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabase.h OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h OrthancServer/Plugins/Engine/OrthancPlugins.cpp OrthancServer/Plugins/Engine/OrthancPlugins.h OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto OrthancServer/Plugins/Samples/AdvancedStorage/Plugin.cpp OrthancServer/Resources/Configuration.json OrthancServer/Sources/Database/IDatabaseWrapper.h OrthancServer/Sources/Database/PrepareDatabase.sql OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h OrthancServer/Sources/OrthancInitialization.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h OrthancServer/Sources/ServerEnumerations.h OrthancServer/Sources/ServerIndex.cpp OrthancServer/UnitTestsSources/ServerIndexTests.cpp TODO
diffstat 720 files changed, 23871 insertions(+), 7374 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Thu Sep 15 18:13:17 2022 +0200
+++ b/.hgignore	Tue Sep 24 11:39:52 2024 +0200
@@ -6,6 +6,7 @@
 .vs/
 .vscode/
 *~
+*.cmake.orig
 
 # when opening Orthanc in VSCode, it might find a java project and create files we wan't to ignore:
 .settings/
--- a/AUTHORS	Thu Sep 15 18:13:17 2022 +0200
+++ b/AUTHORS	Tue Sep 24 11:39:52 2024 +0200
@@ -14,13 +14,19 @@
   4000 Liege
   Belgium
 
-* Osimis S.A. <info@osimis.io>
+* Osimis S.A.
   Quai Banning 6
   4000 Liege
   Belgium
-  http://www.osimis.io/
+
+* Orthanc Team SRL <info@orthanc.team>
+  Rue Joseph Marchal 14
+  4910 Theux
+  Belgium
+  https://orthanc.team/
 
 * ICTEAM, UCLouvain
   Place de l'Universite 1
   1348 Ottignies-Louvain-la-Neuve
   Belgium
+  https://uclouvain.be/icteam
--- a/CITATION.cff	Thu Sep 15 18:13:17 2022 +0200
+++ b/CITATION.cff	Tue Sep 24 11:39:52 2024 +0200
@@ -9,6 +9,6 @@
     given-names: "Sébastien"
 doi: "10.1007/s10278-018-0082-y"
 license: "GPL-3.0-or-later"
-repository-code: "https://hg.orthanc-server.com/orthanc/"
-version: 1.10.1
-date-released: 2022-03-23
+repository-code: "https://orthanc.uclouvain.be/hg/orthanc/"
+version: 1.12.4
+date-released: 2024-06-05
--- a/LinuxCompilation.txt	Thu Sep 15 18:13:17 2022 +0200
+++ b/LinuxCompilation.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -161,7 +161,7 @@
        	       	       uuid-dev libcurl4-openssl-dev liblua5.3-dev \
        	       	       libgtest-dev libpng-dev libsqlite3-dev libssl-dev libjpeg-dev \
 		       zlib1g-dev libdcmtk-dev libboost-all-dev libwrap0-dev \
-                       libcharls-dev libjsoncpp-dev libpugixml-dev locales
+                       libcharls-dev libjsoncpp-dev libpugixml-dev locales protobuf-compiler
 
 # cd ./Build
 # cmake -DALLOW_DOWNLOADS=ON \
@@ -235,7 +235,7 @@
 
 You can find build instructions for Orthanc up to 0.7.0 on the
 following Wiki page:
-https://book.orthanc-server.com/faq/compiling-old.html
+https://orthanc.uclouvain.be/book/faq/compiling-old.html
 
 These instructions will not work as such beyond Orthanc 0.7.0, but
 they might give indications.
--- a/NEWS	Thu Sep 15 18:13:17 2022 +0200
+++ b/NEWS	Tue Sep 24 11:39:52 2024 +0200
@@ -1,6 +1,7 @@
 Pending changes in the mainline
 ===============================
 
+<<<<<<< working copy
 General
 -------
 
@@ -18,6 +19,387 @@
 
 
 version 1.11.2 (2022-08-30)
+=======
+REST API
+-----------
+
+* Improved parsing of multiple numerical values in DICOM tags.
+  https://discourse.orthanc-server.org/t/qido-includefield-with-sequences/4746/6
+
+
+Maintenance
+-----------
+
+* DICOM TLS: "DicomTlsTrustedCertificates" is not required anymore when issuing
+  an outgoing SCU connexion when "DicomTlsRemoteCertificateRequired" is set to false.
+* Introduced a new thread to update the statistics at regular interval for the
+  DB plugins that are implementing the UpdateAndGetStatistics function (currently only
+  PostgreSQL).  This avoids very long update times in case you don't call /statistics
+  for a long period.
+* Fix C-Find queries not returning computed tags like ModalitiesInStudy, NumberOfStudyRelatedSeries, ...
+  in very specific use-cases.
+* Fix extremely rare error when 2 threads are trying to create the same folder in the File Storage 
+  at the same time.
+* Metrics:
+  - fix a few metrics that were not published
+  - added 2 metrics: orthanc_storage_cache_miss_count & orthanc_storage_cache_hit_count 
+* Upgraded dependencies for static builds:
+  - curl 8.9.0
+* 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 a new warning that can be disabled in the configuration: W003_DecoderFailure
+
+
+
+Version 1.12.4 (2024-06-05)
+===========================
+
+REST API
+--------
+
+* API version upgraded to 24
+* Added "MaximumPatientCount" in /system
+* Added a new "LimitToThisLevelMainDicomTags" field in the payload of 
+  /patients|studies|series/instances/../reconstruct to speed up the reconstruction
+  in case you just want to update the MainDicomTags of that resource level only 
+  (e.g., after you have updated the "ExtraMainDicomTags" for this level)
+* The "requestedTags" GET argument is deprecated in favor of "requested-tags"
+* Added "?whole" option to "/instances/{id}/tags" to access tags stored after pixel data
+
+Plugins
+-------
+
+* Multitenant DICOM plugin: added support for locales.
+* 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.
+  - Fixed broken /instances/../tags route after running the Housekeeper
+    after having changed the "IngestTranscoding".
+* SDK: added OrthancPluginLogMessage() as a new primitive for plugins
+  to log messages. This new primitive will display the plugin name,
+  the plugin file name, and the plugin line number in the logs. If
+  they are not using the LOG() facilities provided by the
+  OrthancFramework, plugins should now use ORTHANC_PLUGINS_LOG_INFO(),
+  ORTHANC_PLUGINS_LOG_WARNING(), and ORTHANC_PLUGINS_LOG_ERROR().
+
+Maintenance
+-----------
+
+* C-Find queries:
+  - In C-Find queries including "GenericGroupLength" tags, Orthanc was still
+    extracting these tags from the storage although they were already ignored
+    and not returned in the response.
+    They are now removed from the query earlier to avoid this disk access that
+    could slow down the response time. Note that this seems to happen mainly
+    when the query originates from some GE devices (AWS).
+  - "TimezoneOffsetFromUTC" is now ignored for matching.
+* The 0x0111 DIMSE Status is now considered as a warning instead of an error
+  when received as a response to a C-Store.
+  See https://discourse.orthanc-server.org/t/ignore-dimse-status-0x0111-when-sending-partial-duplicate-studies/4555/3
+* Removed potential PHI from the logs when Orthanc encounters an error while
+  creating a ZIP archive.
+* Monitoring of stable resources now also takes into consideration the
+  resource type, not only the resource identifier identifier.
+* DICOM TLS:
+  - In prior versions, when "DicomTlsRemoteCertificateRequired" was set to false, Orthanc
+    was still sending a client certificate request during the TLS handshake but was not
+    triggering and error if the client certificate was not trusted (equivalent to the
+    "--verify-peer-cert" DCMTK option). Starting with Orthanc 1.12.4, if this option is
+    set to "false", Orthanc will not send a client certificate request during the TLS
+    handshake anymore (equivalent to the "--ignore-peer-cert" DCMTK option).
+  - When working with "DicomTlsEnabled": true and "DicomTlsRemoteCertificateRequired": false,
+    Orthanc was refusing to start if no "DicomTlsTrustedCertificates" was provided.
+  - New configuration options:
+    - "DicomTlsMinimumProtocolVersion" to select the minimum TLS protocol version
+    - "DicomTlsCiphersAccepted" to fine tune the list of accepted ciphers
+* Fixed broken /instances/../tags route after calling of
+  /studies/../reconstruct after having changed the "IngestTranscoding".
+* Upgraded dependencies for static builds:
+  - boost 1.85.0
+
+
+Version 1.12.3 (2024-01-31)
+===========================
+
+General
+-------
+
+* Performance of databases:
+  - At startup, if using a database plugin, displays the latency to access the DB.
+  - Added support for new DB primitives to enable the "READ COMMITTED"
+    transaction mode in the PostgreSQL plugin.
+
+REST API
+--------
+
+* API version upgraded to 23
+* Added a 'KeepLabels' option in /modify routes (default = false)
+
+Maintenance
+-----------
+
+* Upgraded dependencies for static builds:
+  - boost 1.84.0
+  - curl 8.5.0
+  - dcmtk 3.6.8
+  - jsoncpp 1.9.5
+  - libjpeg 9f
+  - libpng 1.6.40
+  - openssl 3.1.4
+  - pugixml 1.14
+  - zlib 1.3.1
+
+
+Version 1.12.2 (2023-12-19)
+===========================
+
+General
+-------
+
+* Performance:
+  - Allow multiple plugins to use the plugin SDK at the same time.  In previous versions,
+    functions like instance transcoding or instance reading where mutually exclusive.
+    This can bring some significant improvements, especially in viewers.
+  - Optimized the StorageCache to prevent loading the same file multiple times if
+    multiple users request the same file at the same time.
+  - The StorageCache is now also storing transcoded instances that have been requested by /file?transcode=...
+    that is now used by the DICOMweb plugin.  This speeds up retrieval of transcoded frames through WADO-RS.
+  - Now displaying timings when reading from/writing to disk in the verbose logs.
+* HTTP compression:
+  - The default value of the "HttpCompressionEnabled" is now false by default.  This reduces
+    the Orthanc overall CPU usage and latency.  This is suitable for setups with large  
+    bandwidth network like LAN.
+  - When "HttpCompressionEnabled" is true, only the content that is clearly identified as
+    compressible is compressed (JSON, XML, HTML, text, ...).  DICOM files are never
+    compressed over HTTP.  In prior versions, all content types were compressed.
+    This notably greatly improves loading time of large DICOM 
+    files through WADO-RS e.g in StoneViewer when working on large bandwidth networks.
+  - When "HttpCompressionEnabled" is true, content < 2KB are never compressed.
+* Logs:
+  - Each line of log now contains the name of the thread that is logging the message.
+    A new "--logs-no-thread" command line option can be used to get back to the previous behavior to
+    keep backward compatibility.
+
+REST API
+--------
+
+* API version upgraded to 22
+* Added a route to delete completed jobs from history: DELETE /jobs/{id}
+* Added a "transcode" option to the /file route:
+  e.g: /instances/../file?transcode=1.2.840.10008.1.2.4.80
+* now accepting GET requests on these 3 routes to create archive/media:
+  /tools/create-archive?resources=..,..2&transcode=1.2.840.10008.1.2.4.80
+  /tools/create-media?resources=..,..2&transcode=1.2.840.10008.1.2.4.80
+  /tools/create-media-extended?resources=..,..2&transcode=1.2.840.10008.1.2.4.80
+* All "expand" GET arguments now accepts "expand=true" and "expand=false" values.
+  The /studies/../instances and sibling routes are the only whose expand is true if not specified.
+  These routes now accepts "expand=false" to simply list the child resources ids.
+* In /tools/metrics-prometheus:
+  - "orthanc_dicom_cache_size" renamed as "orthanc_dicom_cache_size_mb"
+  - added "orthanc_storage_cache_count" and "orthanc_storage_cache_size_mb"
+
+Plugins
+-------
+
+* Housekeeper plugin:
+  - Update to rebuild the cache of the DICOMweb plugin when updating to DICOMweb 1.15.
+  - New trigger configuration: "DicomWebCacheChange"
+  - Fixed reading the triggers configuration.
+  - Introduced a "sleep" to lower CPU usage when idle.
+* Plugins are now allowed to modify/delete private metadata/attachments
+  (i.e. whose identifiers are < 1024)
+* Added "OrthancPluginSetCurrentThreadName()" in the plugin SDK.
+
+Maintenance
+-----------
+
+* Fix unit test PngWriter.Color16Pattern on big-endian architectures,
+  as suggested by Etienne Mollier: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1041813
+* Prevent the leak of the full path of the source files in the binaries
+* Fix loading of DCMTK dictionary in the MultitenantDicom plugin when built dynamically:
+  https://discourse.orthanc-server.org/t/dimse-failure-using-multitenant-plugin/3665
+* Support multiple values in SpecificCharacterSet in C-Find answers:
+  https://discourse.orthanc-server.org/t/c-find-fails-on-unknown-specific-character-set-iso-2022-ir-6-iso-2022-ir-100/3947
+* When exporting a study archive, make sure to use the PatientName from the study and not from the patient
+  in case of PatientID collision.
+* DICOM C-Store:
+  - Avoid some unnecessary renegotiation of DICOM association.
+  - Force renegotiation in case no presentation context were accepted in previous association (we have
+    observed PACS that were not consistent in the accepted presentation contexts)
+  - Improved logging
+* Solved a deadlock related to the Job Engine events and plugins.  Job events are now pushed
+  into a queue to be handled asynchronously by plugins.
+* ZIP of studies whose PatientName and PatientID did not contain any ASCII character are now valid.
+* Upgraded minizip library to stay away from CVE-2023-45853 although Orthanc is likely not affected since ZIP
+  filenames are based on DICOM Tag values whose length is limited in size.
+  Great thanks to James Addison for notifying us about the vulnerability and patch to apply !
+* Fix XSS in Orthanc error reporting (as reported by Sébastien Doria, Vumetric Cybersecurity) by:
+  - always including a "Content-Type" header in HTTP responses with a body.
+  - always including "X-Content-Type-Options: nosniff"
+* Upgraded dependencies for static builds:
+  - boost 1.83.0
+
+
+Version 1.12.1 (2023-07-04)
+===========================
+
+General
+-------
+
+* Orthanc now anonymizes according to Basic Profile of PS 3.15-2023b Table E.1-1
+* Added metrics:
+  - "orthanc_storage_read_bytes" 
+  - "orthanc_storage_written_bytes"
+  - "orthanc_memory_trimming_duration_ms"
+
+REST API
+--------
+
+* API version upgraded to 21
+* "/tools/create-dicom" can now be used to create Encapsulated 3D
+  Manufacturing Model IODs (MTL, OBJ, or STL)
+* Added a route to delete the output of an asynchronous job (right now
+  only for archive jobs): e.g. DELETE /jobs/../archive
+
+Plugins
+-------
+
+* Added "OrthancPluginLoadDicomInstance()" to load DICOM instances from the database
+* Added "OrthancPluginSetMetricsIntegerValue()" to track metrics with integer values
+
+Maintenance
+-----------
+
+* Fix decoding of YBR_FULL RLE images for which the "Planar Configuration" 
+  tag (0028,0006) equals 1
+* Made Orthanc more resilient to common spelling errors in SpecificCharacterSet
+* Modality worklists plugin: Allow searching on private tags (exact match only)
+* Fix orphan files remaining in storage when working with MaximumStorageSize
+  (https://discourse.orthanc-server.org/t/issue-with-deleting-incoming-dicoms-when-maximumstoragesize-is-reached/3510)
+* When deleting a resource, the "LastUpdate" metadata of its parents are now updated
+* Reduced the memory usage when downloading archives when "ZipLoaderThreads" > 0
+* Metrics can be stored either as floating-point numbers, or as integers
+* Reduce the frequency of memory trimming from 100ms to 30s to avoid high idle
+  CPU load (https://discourse.orthanc-server.org/t/onchange-callbacks-and-cpu-loads/3534).
+* Upgraded dependencies for static builds:
+  - boost 1.82.0
+
+
+Version 1.12.0 (2023-04-14)
+===========================
+
+General
+-------
+
+* Support for labels associated with patients, studies, series, and instances
+* Added a sample plugin bringing multitenant DICOM support through labels
+
+REST API
+--------
+
+* API version upgraded to 20
+* New URIs "/.../{id}/labels/{label}" to test/set/remove labels
+* "/patients/{id}", "/studies/{id}", "/series/{id}" and "/instances/{id}"
+  contain the "Labels" field
+* "/tools/find" now accepts the "Labels" and "LabelsConstraint" arguments
+* "/tools/labels" lists all the labels that are associated with any resource
+* "/system": added "UserMetadata" and "HasLabels"
+* Added option "?numeric" if listing metadata
+
+Plugins
+-------
+
+* Added "OrthancPluginRegisterDatabaseBackendV4()" to communicate using Google
+  Protocol Buffers between the Orthanc core and database plugins
+
+Orthanc Explorer
+----------------
+
+* Added support for labels
+* Added buttons to copy the URL of ZIP archives and DICOM files to the clipboard
+
+Maintenance
+-----------
+
+* Enforce the existence of the patient/study/instance while creating its archive
+* Security: New configuration option "RestApiWriteToFileSystemEnabled"
+  to allow "/instances/../export" (the latter is now disabled by default)
+* Fix issue 214: VOILUTSequence is not returned in Wado-RS
+* Fix /tools/reset crashing when ExtraMainDicomTags were defined
+* Fix Housekeeper plugin infinite loop if Orthanc is empty.
+* Fix a crash in /tools/reconstruct triggered by the Housekeeper plugin 
+  when only changing the StorageCompression.
+* Avoid the use of "externalproject_add()" to build the sample plugins
+* Upgraded dependencies for static builds:
+  - openssl 3.1.0
+
+
+Version 1.11.3 (2023-02-03)
+===========================
+
+General
+-------
+
+* C-Store SCU now gives priority to the preferred TransferSyntax
+  proposed by the receiving SCP instead of Orthanc own
+  AcceptedTransferSyntaxes.
+* Made the default SQLite DB more robust wrt future updates like
+  adding new columns in DB.
+* Made the HTTP Client errors more verbose by including the URL in the logs.
+* Optimization: now using multiple threads to transcode files for
+  asynchronous download of studies archive.
+* New configuration "KeepAliveTimeout" with a default value of 1 second.
+* ResourceModification jobs (/modify + /anonymize) can now use multiple threads to speed up processing
+  - New configuration "JobsEngineThreadsCount.ResourceModification" to configure the number of threads.
+* For systems using glibc > 2.8 (most of Linux systems except LSB
+  binaries): Introduced a new thread for to trim memory in Orthanc (different
+  from the Housekeeper sample plugin). This thread regularly try to
+  give back memory that Orthanc no longer uses to the system. This
+  reduces the overall memory consumption. More information in
+  OrthancServer/Resources/ImplementationNotes/memory_consumption.txt.
+
+REST API
+--------
+
+* API version upgraded to 19
+* Loosen the sanity checks for DICOM modifications, if "Force" option is given:
+  - allow modification of PatientID at study level
+  - allow modification of PatientID, StudyInstanceUID at series level
+  - allow modification of PatientID, StudyInstanceUID, SeriesInstanceUID at instance level
+  - allow modification of a patient without changing her PatientID
+  Added sanity checks for modifications to make sure the user preserves the DICOM model when modifying high level tags.
+    E.g. if you modify the PatientID at study level, also make sure to modify all other Patient related
+    tags (PatientName, PatientBirthDate, ...)
+* Automatically reconstruct the modified resources at the end of the DICOM modifications job to ensure
+  improved consistency of the DICOM model.
+* If specifying 'Transcode' option to /modify or /anonymize, this value will take over the 'IngestTranscoding'
+  global configuration
+* Allow the HTTP server to return responses > 2GB (fixes asynchronous download of zip studies > 2GB)
+* /modalities/.../store now accepts "CalledAet", "Host", "Port" to override the modality configuration 
+  from the configuration file for a specific operation.
+* /tools/metrics-prometheus: added orthanc_last_change and orthanc_up_time_s
+* Tolerance for "image/jpg" MIME type instead of "image/jpeg" in /tools/create-dicom
+* /system: added MaximumStorageMode and MaximumStorageSize
+
+Plugins
+-------
+
+* Added a "header" argument to all OrthancPeers::DoPost, DoPut, ... in the "OrthancPluginCppWrapper"
+* Added "OrthancPluginCreateJob2()" in the plugin SDK to avoid
+  possible crashes when "OrthancPluginJobGetContent()" or
+  "OrthancPluginJobGetSerialized()" get called
+
+Maintenance
+-----------
+
+* Fix decoding of RLE images for which the "Planar Configuration" tag (0028,0006) equals 1
+* Fix issue #212 (Anonymization process transcodes data and loses resource link).
+
+
+Version 1.11.2 (2022-08-30)
+>>>>>>> merge rev
 ===========================
 
 General
@@ -77,7 +459,7 @@
 * Housekeeper plugin: Fix resume of previous processing
 * Added missing MOVEPatientRootQueryRetrieveInformationModel in 
   DicomControlUserConnection::SetupPresentationContexts()
-* Improved HttpClient error logging (add method + url)
+* Improved HttpClient error logging (add method + URL)
 
 REST API
 --------
--- a/OrthancFramework/Resources/CMake/AutoGeneratedCode.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/AutoGeneratedCode.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancFramework/Resources/CMake/BoostConfiguration.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/BoostConfiguration.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -54,7 +55,7 @@
   
   # Patch by xnox to fix issue #166 (CMake find_boost version is now
   # broken with newer boost/cmake)
-  # https://bugs.orthanc-server.com/show_bug.cgi?id=166
+  # https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=166
   if (POLICY CMP0093)
     set(BOOST144 1.44)
   else()
@@ -90,11 +91,11 @@
   ## Parameters for static compilation of Boost 
   ##
   
-  set(BOOST_NAME boost_1_80_0)
-  set(BOOST_VERSION 1.80.0)
-  set(BOOST_BCP_SUFFIX bcpdigest-1.11.2)
-  set(BOOST_MD5 "7734e19f9a39a4411b807a9913e4a5ff")
-  set(BOOST_URL "http://orthanc.osimis.io/ThirdPartyDownloads/${BOOST_NAME}_${BOOST_BCP_SUFFIX}.tar.gz")
+  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_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})
 
   if (IS_DIRECTORY "${BOOST_SOURCES_DIR}")
@@ -107,6 +108,25 @@
 
 
   ##
+  ## Apply the patches to remove threads from boost::locale (required
+  ## since around Emscripten 3.x)
+  ##
+
+  if (FirstRun)
+    execute_process(
+      COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
+      ${CMAKE_CURRENT_LIST_DIR}/../Patches/boost-1.85.0-emscripten.patch
+      WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+      RESULT_VARIABLE Failure
+      )
+
+    if (Failure)
+      message(FATAL_ERROR "Error while patching a file")
+    endif()
+  endif()
+
+
+  ##
   ## Generic configuration of Boost
   ## 
 
@@ -300,6 +320,7 @@
       ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/icu/conversion.cpp
       ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/icu/date_time.cpp
       ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/icu/formatter.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/icu/formatters_cache.cpp
       ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/icu/icu_backend.cpp
       ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/icu/numeric.cpp
       ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/icu/time_zone.cpp
@@ -310,12 +331,14 @@
       ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/shared/date_time.cpp
       ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/shared/formatting.cpp
       ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/shared/generator.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/shared/iconv_codecvt.cpp
       ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/shared/ids.cpp
       ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/shared/localization_backend.cpp
       ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/shared/message.cpp
       ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/shared/mo_lambda.cpp
       ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/util/codecvt_converter.cpp
       ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/util/default_locale.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/util/encoding.cpp
       ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/util/gregorian.cpp
       ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/util/info.cpp
       ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/util/locale_data.cpp
--- a/OrthancFramework/Resources/CMake/BoostConfiguration.sh	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/BoostConfiguration.sh	Tue Sep 24 11:39:52 2024 +0200
@@ -22,10 +22,14 @@
 ##   - Orthanc between 1.4.0 and 1.4.2: Boost 1.67.0
 ##   - Orthanc between 1.5.0 and 1.5.4: Boost 1.68.0
 ##   - Orthanc between 1.5.5 and 1.11.1: Boost 1.69.0
-##   - Orthanc >= 1.11.2: Boost 1.80.0
+##   - Orthanc between 1.11.2 and 1.12.0: Boost 1.80.0
+##   - Orthanc 1.12.1: Boost 1.82.0
+##   - Orthanc 1.12.2: Boost 1.83.0
+##   - Orthanc 1.12.3: Boost 1.84.0
+##   - Orthanc > 1.12.3: Boost 1.85.0
 
-BOOST_VERSION=1_80_0
-ORTHANC_VERSION=1.11.2
+BOOST_VERSION=1_85_0
+ORTHANC_VERSION=1.12.4
 
 rm -rf /tmp/boost_${BOOST_VERSION}
 rm -rf /tmp/bcp/boost_${BOOST_VERSION}
--- a/OrthancFramework/Resources/CMake/BoostConfigurationStatic-1.69.0.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/BoostConfigurationStatic-1.69.0.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -28,7 +29,7 @@
   set(BOOST_VERSION 1.69.0)
   set(BOOST_BCP_SUFFIX bcpdigest-1.5.6)
   set(BOOST_MD5 "579bccc0ea4d1a261c1d0c5e27446c3d")
-  set(BOOST_URL "http://orthanc.osimis.io/ThirdPartyDownloads/${BOOST_NAME}_${BOOST_BCP_SUFFIX}.tar.gz")
+  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})
 
   if (IS_DIRECTORY "${BOOST_SOURCES_DIR}")
--- a/OrthancFramework/Resources/CMake/CivetwebConfiguration.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/CivetwebConfiguration.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -27,14 +28,14 @@
   ## "civetweb-1.14-fixed.tar.gz" as follows:
   ##
   ##  $ cd /tmp
-  ##  $ wget http://orthanc.osimis.io/ThirdPartyDownloads/civetweb-1.14.tar.gz
+  ##  $ wget https://orthanc.uclouvain.be/downloads/third-party-downloads/civetweb-1.14.tar.gz
   ##  $ tar xvf civetweb-1.14.tar.gz
   ##  $ rm -rf civetweb-1.14/src/third_party/ civetweb-1.14/test/
   ##  $ tar cvfz civetweb-1.14-fixed.tar.gz civetweb-1.14
   ##
   
   set(CIVETWEB_SOURCES_DIR ${CMAKE_BINARY_DIR}/civetweb-1.14)
-  set(CIVETWEB_URL "http://orthanc.osimis.io/ThirdPartyDownloads/civetweb-1.14-fixed.tar.gz")
+  set(CIVETWEB_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/civetweb-1.14-fixed.tar.gz")
   set(CIVETWEB_MD5 "1f25d516b7a4e65d8b270d1cc399e0a9")
 
   if (IS_DIRECTORY "${CIVETWEB_SOURCES_DIR}")
@@ -140,9 +141,3 @@
 
   unset(CMAKE_REQUIRED_LIBRARIES)  # This reset must be after "CHECK_LIBRARY_EXISTS"
 endif()
-
-
-# New in Orthanc 1.8.1
-add_definitions(
-  -DCIVETWEB_KEEP_ALIVE_TIMEOUT_SECONDS=1
-  )
--- a/OrthancFramework/Resources/CMake/Compiler.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/Compiler.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -43,6 +44,13 @@
   # use by "ExternalProject" in CMake
   SET(CMAKE_LSB_CC $ENV{LSB_CC} CACHE STRING "")
   SET(CMAKE_LSB_CXX $ENV{LSB_CXX} CACHE STRING "")
+
+  # This is necessary to build "Orthanc mainline - Framework LSB
+  # Release" on "buildbot-worker-debian11"
+  set(LSB_PTHREAD_NONSHARED "${LSB_PATH}/lib64-${LSB_TARGET_VERSION}/libpthread_nonshared.a")
+  if (EXISTS ${LSB_PTHREAD_NONSHARED})
+    set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${LSB_PTHREAD_NONSHARED}")
+  endif()
 endif()
 
 
@@ -124,12 +132,17 @@
     ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
     ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
 
-  if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" AND
+  if (# NOT ${CMAKE_SYSTEM_VERSION} STREQUAL "LinuxStandardBase" AND
+      NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" AND
       NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
     # The "--no-undefined" linker flag makes the shared libraries
     # (plugins ModalityWorklists and ServeFolders) fail to compile on
     # OpenBSD, and make the PostgreSQL plugin complain about missing
-    # "environ" global variable in FreeBSD
+    # "environ" global variable in FreeBSD.
+    #
+    # TODO - Furthermore, on Linux Standard Base running on Debian 12,
+    # the "-Wl,--no-undefined" seems to break the compilation (added
+    # after Orthanc 1.12.2). This is disabled for now.
     set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--no-undefined")
     set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined")
   endif()
@@ -219,6 +232,10 @@
   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
     )
@@ -237,7 +254,8 @@
 
 
 if (DEFINED ENABLE_PROFILING AND ENABLE_PROFILING)
-  if (CMAKE_COMPILER_IS_GNUCXX)
+  if (CMAKE_COMPILER_IS_GNUCXX OR
+      CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
     set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pg")
     set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pg")
     set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pg")
@@ -262,3 +280,24 @@
   # preceding batches. https://cmake.org/Bug/view.php?id=14874
   set(CMAKE_CXX_ARCHIVE_APPEND "<CMAKE_AR> <LINK_FLAGS> q <TARGET> <OBJECTS>")
 endif()
+
+
+# This function defines macro "__ORTHANC_FILE__" as a replacement to
+# macro "__FILE__", as the latter leaks the full path of the source
+# files in the binaries
+# https://stackoverflow.com/questions/8487986/file-macro-shows-full-path
+# https://twitter.com/wget42/status/1676877802375634944?s=20
+function(DefineSourceBasenameForTarget targetname)
+  # Microsoft Visual Studio is extremely slow if using
+  # "set_property()", we only enable this feature for gcc and clang
+  if (CMAKE_COMPILER_IS_GNUCXX OR
+      CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
+    get_target_property(source_files "${targetname}" SOURCES)
+    foreach(sourcefile ${source_files})
+      get_filename_component(basename "${sourcefile}" NAME)
+      set_property(
+        SOURCE "${sourcefile}" APPEND
+        PROPERTY COMPILE_DEFINITIONS "__ORTHANC_FILE__=\"${basename}\"")
+    endforeach()
+  endif()
+endfunction()
--- a/OrthancFramework/Resources/CMake/DcmtkConfiguration.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/DcmtkConfiguration.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -36,6 +37,8 @@
     include(${CMAKE_CURRENT_LIST_DIR}/DcmtkConfigurationStatic-3.6.6.cmake)
   elseif (DCMTK_STATIC_VERSION STREQUAL "3.6.7")
     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)
   else()
     message(FATAL_ERROR "Unsupported version of DCMTK: ${DCMTK_STATIC_VERSION}")
   endif()
@@ -280,6 +283,7 @@
   if (DCMTK_DICTIONARY_DIR STREQUAL "")
     find_path(DCMTK_DICTIONARY_DIR_AUTO dicom.dic
       /usr/share/dcmtk
+      /usr/share/dcmtk-3.6.8
       /usr/share/libdcmtk1
       /usr/share/libdcmtk2
       /usr/share/libdcmtk3
@@ -301,6 +305,7 @@
       /usr/share/libdcmtk19
       /usr/share/libdcmtk20
       /usr/local/share/dcmtk
+      /usr/local/share/dcmtk-3.6.8
       )
 
     if (${DCMTK_DICTIONARY_DIR_AUTO} MATCHES "DCMTK_DICTIONARY_DIR_AUTO-NOTFOUND")
--- a/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.0.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.0.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -22,7 +23,7 @@
 SET(DCMTK_VERSION_NUMBER 360)
 SET(DCMTK_PACKAGE_VERSION "3.6.0")
 SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.0)
-SET(DCMTK_URL "http://orthanc.osimis.io/ThirdPartyDownloads/dcmtk-3.6.0.zip")
+SET(DCMTK_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/dcmtk-3.6.0.zip")
 SET(DCMTK_MD5 "219ad631b82031806147e4abbfba4fa4")
 
 if (IS_DIRECTORY "${DCMTK_SOURCES_DIR}")
--- a/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.2.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.2.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -22,7 +23,7 @@
 SET(DCMTK_VERSION_NUMBER 362)
 SET(DCMTK_PACKAGE_VERSION "3.6.2")
 SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.2)
-SET(DCMTK_URL "http://orthanc.osimis.io/ThirdPartyDownloads/dcmtk-3.6.2.tar.gz")
+SET(DCMTK_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/dcmtk-3.6.2.tar.gz")
 SET(DCMTK_MD5 "d219a4152772985191c9b89d75302d12")
 
 macro(DCMTK_UNSET)
--- a/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.4.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.4.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -22,7 +23,7 @@
 SET(DCMTK_VERSION_NUMBER 364)
 SET(DCMTK_PACKAGE_VERSION "3.6.4")
 SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.4)
-SET(DCMTK_URL "http://orthanc.osimis.io/ThirdPartyDownloads/dcmtk-3.6.4.tar.gz")
+SET(DCMTK_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/dcmtk-3.6.4.tar.gz")
 SET(DCMTK_MD5 "97597439a2ae7a39086066318db5f3bc")
 
 macro(DCMTK_UNSET)
--- a/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.5.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.5.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -22,7 +23,7 @@
 SET(DCMTK_VERSION_NUMBER 365)
 SET(DCMTK_PACKAGE_VERSION "3.6.5")
 SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.5)
-SET(DCMTK_URL "http://orthanc.osimis.io/ThirdPartyDownloads/dcmtk-3.6.5.tar.gz")
+SET(DCMTK_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/dcmtk-3.6.5.tar.gz")
 SET(DCMTK_MD5 "e19707f64ee5695c496b9c1e48e39d07")
 
 macro(DCMTK_UNSET)
--- a/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.6.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.6.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -22,7 +23,7 @@
 SET(DCMTK_VERSION_NUMBER 366)
 SET(DCMTK_PACKAGE_VERSION "3.6.6")
 SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.6)
-SET(DCMTK_URL "http://orthanc.osimis.io/ThirdPartyDownloads/dcmtk-3.6.6.tar.gz")
+SET(DCMTK_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/dcmtk-3.6.6.tar.gz")
 SET(DCMTK_MD5 "f815879d315b916366a9da71339c7575")
 
 macro(DCMTK_UNSET)
--- a/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.7.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.7.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -22,7 +23,7 @@
 SET(DCMTK_VERSION_NUMBER 367)
 SET(DCMTK_PACKAGE_VERSION "3.6.7")
 SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.7)
-SET(DCMTK_URL "http://orthanc.osimis.io/ThirdPartyDownloads/dcmtk-3.6.7.tar.gz")
+SET(DCMTK_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/dcmtk-3.6.7.tar.gz")
 SET(DCMTK_MD5 "e4d519bb315ec3944f3f1d61df465cbd")
 
 macro(DCMTK_UNSET)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.8.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,285 @@
+# 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/>.
+
+
+SET(DCMTK_VERSION_NUMBER 368)
+SET(DCMTK_PACKAGE_VERSION "3.6.8")
+SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-DCMTK-3.6.8)
+SET(DCMTK_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/dcmtk-3.6.8.tar.gz")
+SET(DCMTK_MD5 "ce3e878c05165f1a3322c29e67f2426f")
+
+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.8.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.8-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(${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 BOOST_LOCALE_BACKEND OR   # This is the case if locale support is disabled (e.g. in Stone)
+    BOOST_LOCALE_BACKEND STREQUAL "gcc")
+  set(DCMTK_ENABLE_CHARSET_CONVERSION "DCMTK_CHARSET_CONVERSION_STDLIBC_ICONV" CACHE STRING "" FORCE)
+elseif (BOOST_LOCALE_BACKEND STREQUAL "libiconv")
+  set(DCMTK_ENABLE_CHARSET_CONVERSION "DCMTK_CHARSET_CONVERSION_ICONV" CACHE STRING "" FORCE)
+elseif (BOOST_LOCALE_BACKEND STREQUAL "icu")
+  set(DCMTK_ENABLE_CHARSET_CONVERSION "DCMTK_CHARSET_CONVERSION_ICU" CACHE STRING "" FORCE)
+else()
+  message(FATAL_ERROR "Invalid value for BOOST_LOCALE_BACKEND: ${BOOST_LOCALE_BACKEND}")
+endif()
+
+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"
+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()
+endif()
--- a/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -150,6 +151,20 @@
         set(ORTHANC_FRAMEWORK_MD5 "962c4a4a706a2ef28b390d8515dd7091")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.11.1")
         set(ORTHANC_FRAMEWORK_MD5 "a39661c406adf22cf574fde290cf4bbf")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.11.2")
+        set(ORTHANC_FRAMEWORK_MD5 "ede3de356493a8868545f8cb4b8bc8b5")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.11.3")
+        set(ORTHANC_FRAMEWORK_MD5 "e48fc0cb09c4856803791a1be28c07dc")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.0")
+        set(ORTHANC_FRAMEWORK_MD5 "d32a0cde03b6eb603d8dd2b33d38bf1b")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.1")
+        set(ORTHANC_FRAMEWORK_MD5 "8a435140efc8ff4a01d8242f092f21de")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.2")
+        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")
 
       # Below this point are development snapshots that were used to
       # release some plugin, before an official release of the Orthanc
@@ -160,19 +175,28 @@
       #
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "ae0e3fd609df")
         # DICOMweb 1.1 (framework pre-1.6.0)
+        set(ORTHANC_FRAMEWORK_PRE_RELEASE ON)
         set(ORTHANC_FRAMEWORK_MD5 "7e09e9b530a2f527854f0b782d7e0645")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "82652c5fc04f")
         # Stone Web viewer 1.0 (framework pre-1.8.1)
+        set(ORTHANC_FRAMEWORK_PRE_RELEASE ON)
         set(ORTHANC_FRAMEWORK_MD5 "d77331d68917e66a3f4f9b807bbdab7f")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "4a3ba4bf4ba7")
         # PostgreSQL 3.3 (framework pre-1.8.2)
+        set(ORTHANC_FRAMEWORK_PRE_RELEASE ON)
         set(ORTHANC_FRAMEWORK_MD5 "2d82bddf06f9cfe82095495cb3b8abde")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "23ad1b9c7800")
         # For "Toolbox::ReadJson()" and "Toolbox::Write{...}Json()" (pre-1.9.0)
+        set(ORTHANC_FRAMEWORK_PRE_RELEASE ON)
         set(ORTHANC_FRAMEWORK_MD5 "9af92080e57c60dd288eba46ce606c00")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "b2e08d83e21d")
         # WSI 1.1 (framework pre-1.10.0), to remove "-std=c++11"
+        set(ORTHANC_FRAMEWORK_PRE_RELEASE ON)
         set(ORTHANC_FRAMEWORK_MD5 "2eaa073cbb4b44ffba199ad93393b2b1")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "daf4807631c5")
+        # DICOMweb 1.15 (framework pre-1.12.2)
+        set(ORTHANC_FRAMEWORK_PRE_RELEASE ON)
+        set(ORTHANC_FRAMEWORK_MD5 "ebe8bdf388319f1c9536b2b680451848")
       endif()
     endif()
   endif()
@@ -260,7 +284,7 @@
   else()
     message("Forking the Orthanc source repository using Mercurial")
     execute_process(
-      COMMAND ${ORTHANC_FRAMEWORK_HG} clone "https://hg.orthanc-server.com/orthanc/"
+      COMMAND ${ORTHANC_FRAMEWORK_HG} clone "https://orthanc.uclouvain.be/hg/orthanc/"
       WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
       RESULT_VARIABLE Failure
       )    
@@ -309,7 +333,11 @@
   else()
     # Default case: Download from the official Web site
     set(ORTHANC_FRAMEMORK_FILENAME Orthanc-${ORTHANC_FRAMEWORK_VERSION}.tar.gz)
-    set(ORTHANC_FRAMEWORK_URL "http://orthanc.osimis.io/ThirdPartyDownloads/orthanc-framework/${ORTHANC_FRAMEMORK_FILENAME}")
+    if (ORTHANC_FRAMEWORK_PRE_RELEASE)
+      set(ORTHANC_FRAMEWORK_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/orthanc-framework/${ORTHANC_FRAMEMORK_FILENAME}")
+    else()
+      set(ORTHANC_FRAMEWORK_URL "https://orthanc.uclouvain.be/downloads/sources/orthanc/${ORTHANC_FRAMEMORK_FILENAME}")
+    endif()
   endif()
 
   set(ORTHANC_FRAMEWORK_ARCHIVE "${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/${ORTHANC_FRAMEMORK_FILENAME}")
--- a/OrthancFramework/Resources/CMake/DownloadPackage.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/DownloadPackage.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -101,19 +102,26 @@
       message(FATAL_ERROR "CMake is not allowed to download from Internet. Please set the ALLOW_DOWNLOADS option to ON")
     endif()
 
-    if ("${MD5}" STREQUAL "no-check")
-      message(WARNING "Not checking the MD5 of: ${Url}")
-      file(DOWNLOAD "${Url}" "${TMP_PATH}"
-        SHOW_PROGRESS TIMEOUT 300 INACTIVITY_TIMEOUT 60
-        STATUS Failure)
-    else()
-      file(DOWNLOAD "${Url}" "${TMP_PATH}"
-        SHOW_PROGRESS TIMEOUT 300 INACTIVITY_TIMEOUT 60
-        EXPECTED_MD5 "${MD5}" STATUS Failure)
-    endif()
+    foreach (retry RANGE 1 5)   # Retries 5 times
+      if ("${MD5}" STREQUAL "no-check")
+        message(WARNING "Not checking the MD5 of: ${Url}")
+        file(DOWNLOAD "${Url}" "${TMP_PATH}"
+          SHOW_PROGRESS TIMEOUT 30 INACTIVITY_TIMEOUT 10
+          STATUS Failure)
+      else()
+        file(DOWNLOAD "${Url}" "${TMP_PATH}"
+          SHOW_PROGRESS TIMEOUT 30 INACTIVITY_TIMEOUT 10
+          EXPECTED_MD5 "${MD5}" STATUS Failure)
+      endif()
 
-    list(GET Failure 0 Status)
+      list(GET Failure 0 Status)
+      if (Status EQUAL 0)
+        break()  # Successful download
+      endif()
+    endforeach()
+
     if (NOT Status EQUAL 0)
+      file(REMOVE ${TMP_PATH})
       message(FATAL_ERROR "Cannot download file: ${Url}")
     endif()
     
--- a/OrthancFramework/Resources/CMake/EmscriptenParameters.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/EmscriptenParameters.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancFramework/Resources/CMake/GoogleTestConfiguration.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/GoogleTestConfiguration.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -50,7 +51,7 @@
 
 elseif (STATIC_BUILD OR NOT USE_SYSTEM_GOOGLE_TEST)
   set(GOOGLE_TEST_SOURCES_DIR ${CMAKE_BINARY_DIR}/googletest-release-1.8.1)
-  set(GOOGLE_TEST_URL "http://orthanc.osimis.io/ThirdPartyDownloads/gtest-1.8.1.tar.gz")
+  set(GOOGLE_TEST_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/gtest-1.8.1.tar.gz")
   set(GOOGLE_TEST_MD5 "2e6fbeb6a91310a16efe181886c59596")
 
   DownloadPackage(${GOOGLE_TEST_MD5} ${GOOGLE_TEST_URL} "${GOOGLE_TEST_SOURCES_DIR}")
--- a/OrthancFramework/Resources/CMake/JsonCppConfiguration.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/JsonCppConfiguration.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -24,13 +25,13 @@
 if (STATIC_BUILD OR NOT USE_SYSTEM_JSONCPP)
   if (USE_LEGACY_JSONCPP)
     set(JSONCPP_SOURCES_DIR ${CMAKE_BINARY_DIR}/jsoncpp-0.10.7)
-    set(JSONCPP_URL "http://orthanc.osimis.io/ThirdPartyDownloads/jsoncpp-0.10.7.tar.gz")
+    set(JSONCPP_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/jsoncpp-0.10.7.tar.gz")
     set(JSONCPP_MD5 "3a8072ca6a1fa9cbaf7715ae625f134f")
     add_definitions(-DORTHANC_LEGACY_JSONCPP=1)
   else()
-    set(JSONCPP_SOURCES_DIR ${CMAKE_BINARY_DIR}/jsoncpp-1.9.4)
-    set(JSONCPP_URL "http://orthanc.osimis.io/ThirdPartyDownloads/jsoncpp-1.9.4.tar.gz")
-    set(JSONCPP_MD5 "4757b26ec89798c5247fa638edfdc446")
+    set(JSONCPP_SOURCES_DIR ${CMAKE_BINARY_DIR}/jsoncpp-1.9.5)
+    set(JSONCPP_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/jsoncpp-1.9.5.tar.gz")
+    set(JSONCPP_MD5 "d6c8c609f2162eff373db62b90a051c7")
     add_definitions(-DORTHANC_LEGACY_JSONCPP=0)
     set(JSONCPP_CXX11 ON)
   endif()
--- a/OrthancFramework/Resources/CMake/LibCurlConfiguration.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/LibCurlConfiguration.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -20,9 +21,9 @@
 
 
 if (STATIC_BUILD OR NOT USE_SYSTEM_CURL)
-  SET(CURL_SOURCES_DIR ${CMAKE_BINARY_DIR}/curl-7.77.0)
-  SET(CURL_URL "http://orthanc.osimis.io/ThirdPartyDownloads/curl-7.77.0.tar.gz")
-  SET(CURL_MD5 "478e8b06801d9d030609c9e6cf859229")
+  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)
@@ -35,7 +36,7 @@
   if (FirstRun)
     execute_process(
       COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
-      ${CMAKE_CURRENT_LIST_DIR}/../Patches/curl-7.77.0.patch
+      ${CMAKE_CURRENT_LIST_DIR}/../Patches/curl-8.9.0.patch
       WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
       RESULT_VARIABLE Failure
       )
@@ -51,7 +52,9 @@
 
   AUX_SOURCE_DIRECTORY(${CURL_SOURCES_DIR}/lib CURL_SOURCES)
   AUX_SOURCE_DIRECTORY(${CURL_SOURCES_DIR}/lib/vauth CURL_SOURCES)
+  AUX_SOURCE_DIRECTORY(${CURL_SOURCES_DIR}/lib/vssh CURL_SOURCES)
   AUX_SOURCE_DIRECTORY(${CURL_SOURCES_DIR}/lib/vtls CURL_SOURCES)
+  AUX_SOURCE_DIRECTORY(${CURL_SOURCES_DIR}/lib/vquic CURL_SOURCES)
   source_group(ThirdParty\\LibCurl REGULAR_EXPRESSION ${CURL_SOURCES_DIR}/.*)
 
   add_definitions(
@@ -82,17 +85,18 @@
   endif()
 
   if (NOT EXISTS "${CURL_SOURCES_DIR}/lib/vauth/vauth/vauth.h")
-    #file(WRITE ${CURL_SOURCES_DIR}/lib/curl_config.h "")
-
-    file(WRITE ${CURL_SOURCES_DIR}/lib/vauth/vauth/vauth.h "#include \"../vauth.h\"\n")
     file(WRITE ${CURL_SOURCES_DIR}/lib/vauth/vauth/digest.h "#include \"../digest.h\"\n")
     file(WRITE ${CURL_SOURCES_DIR}/lib/vauth/vauth/ntlm.h "#include \"../ntlm.h\"\n")
+    file(WRITE ${CURL_SOURCES_DIR}/lib/vauth/vauth/vauth.h "#include \"../vauth.h\"\n")
     file(WRITE ${CURL_SOURCES_DIR}/lib/vauth/vtls/vtls.h "#include \"../../vtls/vtls.h\"\n")
+    file(WRITE ${CURL_SOURCES_DIR}/lib/vssh/curl_setup.h "#include \"../curl_setup.h\"\n")
+    file(WRITE ${CURL_SOURCES_DIR}/lib/vtls/vauth/vauth.h "#include \"../../vauth/vauth.h\"\n")
 
     file(GLOB CURL_LIBS_HEADERS ${CURL_SOURCES_DIR}/lib/*.h)
     foreach (header IN LISTS CURL_LIBS_HEADERS)
       get_filename_component(filename ${header} NAME)
       file(WRITE ${CURL_SOURCES_DIR}/lib/vauth/${filename} "#include \"../${filename}\"\n")
+      file(WRITE ${CURL_SOURCES_DIR}/lib/vquic/${filename} "#include \"../${filename}\"\n")
       file(WRITE ${CURL_SOURCES_DIR}/lib/vtls/${filename} "#include \"../${filename}\"\n")
     endforeach()
   endif()
@@ -109,7 +113,7 @@
     endif()
 
     set_property(
-      SOURCE ${CURL_SOURCES}
+      SOURCE ${CURL_SOURCES} APPEND
       PROPERTY COMPILE_DEFINITIONS "HAVE_CONFIG_H=1;OS=\"${TMP_OS}\""
       )
    
--- a/OrthancFramework/Resources/CMake/LibIconvConfiguration.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/LibIconvConfiguration.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -23,7 +24,7 @@
 
 if (STATIC_BUILD OR NOT USE_SYSTEM_LIBICONV)
   set(LIBICONV_SOURCES_DIR ${CMAKE_BINARY_DIR}/libiconv-1.15)
-  set(LIBICONV_URL "http://orthanc.osimis.io/ThirdPartyDownloads/libiconv-1.15.tar.gz")
+  set(LIBICONV_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/libiconv-1.15.tar.gz")
   set(LIBICONV_MD5 "ace8b5f2db42f7b3b3057585e80d9808")
 
   DownloadPackage(${LIBICONV_MD5} ${LIBICONV_URL} "${LIBICONV_SOURCES_DIR}")
--- a/OrthancFramework/Resources/CMake/LibIcuConfiguration.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/LibIcuConfiguration.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancFramework/Resources/CMake/LibJpegConfiguration.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/LibJpegConfiguration.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -20,10 +21,10 @@
 
 
 if (STATIC_BUILD OR NOT USE_SYSTEM_LIBJPEG)
-  set(LIBJPEG_SOURCES_DIR ${CMAKE_BINARY_DIR}/jpeg-9c)
+  set(LIBJPEG_SOURCES_DIR ${CMAKE_BINARY_DIR}/jpeg-9f)
   DownloadPackage(
-    "93c62597eeef81a84d988bccbda1e990"
-    "http://orthanc.osimis.io/ThirdPartyDownloads/jpegsrc.v9c.tar.gz"
+    "9ca58d68febb0fa9c1c087045b9a5483"
+    "https://orthanc.uclouvain.be/downloads/third-party-downloads/jpegsrc.v9f.tar.gz"
     "${LIBJPEG_SOURCES_DIR}")
 
   include_directories(
@@ -107,7 +108,7 @@
 else()
   include(FindJPEG)
 
-  if (NOT ${JPEG_FOUND})
+  if (NOT JPEG_FOUND)
     message(FATAL_ERROR "Unable to find libjpeg")
   endif()
 
--- a/OrthancFramework/Resources/CMake/LibP11Configuration.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/LibP11Configuration.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -25,7 +26,7 @@
   endif()
   
   SET(LIBP11_SOURCES_DIR ${CMAKE_BINARY_DIR}/libp11-0.4.0)
-  SET(LIBP11_URL "http://orthanc.osimis.io/ThirdPartyDownloads/libp11-0.4.0.tar.gz")
+  SET(LIBP11_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/libp11-0.4.0.tar.gz")
   SET(LIBP11_MD5 "00b3e41db5be840d822bda12f3ab2ca7")
  
   if (IS_DIRECTORY "${LIBP11_SOURCES_DIR}")
--- a/OrthancFramework/Resources/CMake/LibPngConfiguration.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/LibPngConfiguration.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -20,9 +21,9 @@
 
 
 if (STATIC_BUILD OR NOT USE_SYSTEM_LIBPNG)
-  SET(LIBPNG_SOURCES_DIR ${CMAKE_BINARY_DIR}/libpng-1.6.36)
-  SET(LIBPNG_URL "http://orthanc.osimis.io/ThirdPartyDownloads/libpng-1.6.36.tar.gz")
-  SET(LIBPNG_MD5 "65afdeaa05f5ec14e31d9276143012e9")
+  SET(LIBPNG_SOURCES_DIR ${CMAKE_BINARY_DIR}/libpng-1.6.40)
+  SET(LIBPNG_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/libpng-1.6.40.tar.gz")
+  SET(LIBPNG_MD5 "ec4b597c3a9b1f8d2826575f530367b7")
 
   DownloadPackage(${LIBPNG_MD5} ${LIBPNG_URL} "${LIBPNG_SOURCES_DIR}")
 
@@ -71,7 +72,7 @@
 else()
   include(FindPNG)
 
-  if (NOT ${PNG_FOUND})
+  if (NOT PNG_FOUND)
     message(FATAL_ERROR "Unable to find libpng")
   endif()
 
--- a/OrthancFramework/Resources/CMake/LuaConfiguration.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/LuaConfiguration.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -22,7 +23,7 @@
 if (STATIC_BUILD OR NOT USE_SYSTEM_LUA)
   SET(LUA_SOURCES_DIR ${CMAKE_BINARY_DIR}/lua-5.3.5)
   SET(LUA_MD5 "4f4b4f323fd3514a68e0ab3da8ce3455")
-  SET(LUA_URL "http://orthanc.osimis.io/ThirdPartyDownloads/lua-5.3.5.tar.gz")
+  SET(LUA_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/lua-5.3.5.tar.gz")
 
   DownloadPackage(${LUA_MD5} ${LUA_URL} "${LUA_SOURCES_DIR}")
 
--- a/OrthancFramework/Resources/CMake/MongooseConfiguration.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/MongooseConfiguration.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -32,7 +33,7 @@
     # Use Mongoose 3.1
     DownloadPackage(
       "e718fc287b4eb1bd523be3fa00942bb0"
-      "http://orthanc.osimis.io/ThirdPartyDownloads/mongoose-3.1.tgz"
+      "https://orthanc.uclouvain.be/downloads/third-party-downloads/mongoose-3.1.tgz"
       "${MONGOOSE_SOURCES_DIR}")
     
     add_definitions(-DMONGOOSE_USE_CALLBACKS=0)
@@ -42,7 +43,7 @@
     # Use Mongoose 3.8
     DownloadPackage(
       "7e3296295072792cdc3c633f9404e0c3"
-      "http://orthanc.osimis.io/ThirdPartyDownloads/mongoose-3.8.tgz"
+      "https://orthanc.uclouvain.be/downloads/third-party-downloads/mongoose-3.8.tgz"
       "${MONGOOSE_SOURCES_DIR}")
     
     add_definitions(-DMONGOOSE_USE_CALLBACKS=1)
--- a/OrthancFramework/Resources/CMake/OpenSslConfiguration.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/OpenSslConfiguration.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -59,7 +60,7 @@
 else()
   include(FindOpenSSL)
 
-  if (NOT ${OPENSSL_FOUND})
+  if (NOT OPENSSL_FOUND)
     message(FATAL_ERROR "Unable to find OpenSSL")
   endif()
 
--- a/OrthancFramework/Resources/CMake/OpenSslConfigurationStatic-1.1.1.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/OpenSslConfigurationStatic-1.1.1.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -20,7 +21,7 @@
 
 
 SET(OPENSSL_SOURCES_DIR ${CMAKE_BINARY_DIR}/openssl-1.1.1k)
-SET(OPENSSL_URL "http://orthanc.osimis.io/ThirdPartyDownloads/openssl-1.1.1k.tar.gz")
+SET(OPENSSL_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/openssl-1.1.1k.tar.gz")
 SET(OPENSSL_MD5 "c4e7d95f782b08116afa27b30393dd27")
 
 if (IS_DIRECTORY "${OPENSSL_SOURCES_DIR}")
@@ -288,7 +289,7 @@
     # crashes with segmentation fault in function
     # "build_SYS_str_reasons()", that is called from
     # "OPENSSL_init_ssl()"
-    # https://bugs.orthanc-server.com/show_bug.cgi?id=193
+    # https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=193
     -DOPENSSL_NO_ERR
     )
 
--- a/OrthancFramework/Resources/CMake/OpenSslConfigurationStatic-3.0.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/OpenSslConfigurationStatic-3.0.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2021 Osimis S.A., Belgium
-# Copyright (C) 2021-2021 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -20,13 +21,13 @@
 
 
 set(OPENSSL_VERSION_MAJOR 3)
-set(OPENSSL_VERSION_MINOR 0)
-set(OPENSSL_VERSION_PATCH 5)
+set(OPENSSL_VERSION_MINOR 1)
+set(OPENSSL_VERSION_PATCH 4)
 set(OPENSSL_VERSION_PRE_RELEASE "")
 set(OPENSSL_VERSION_FULL "${OPENSSL_VERSION_MAJOR}.${OPENSSL_VERSION_MINOR}.${OPENSSL_VERSION_PATCH}${OPENSSL_VERSION_PRE_RELEASE}")
 SET(OPENSSL_SOURCES_DIR ${CMAKE_BINARY_DIR}/openssl-${OPENSSL_VERSION_FULL})
-SET(OPENSSL_URL "http://orthanc.osimis.io/ThirdPartyDownloads/openssl-${OPENSSL_VERSION_FULL}.tar.gz")
-SET(OPENSSL_MD5 "163bb3e58c143793d1dc6a6ec7d185d5")
+SET(OPENSSL_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/openssl-${OPENSSL_VERSION_FULL}.tar.gz")
+SET(OPENSSL_MD5 "653ad58812c751b887e8ec37e02bba70")
 
 if (IS_DIRECTORY "${OPENSSL_SOURCES_DIR}")
   set(FirstRun OFF)
@@ -293,7 +294,6 @@
   ${OPENSSL_SOURCES_DIR}/crypto/LPdir_win32.c
   ${OPENSSL_SOURCES_DIR}/crypto/LPdir_wince.c
   ${OPENSSL_SOURCES_DIR}/crypto/aes/aes_x86core.c
-  ${OPENSSL_SOURCES_DIR}/crypto/armcap.c
   ${OPENSSL_SOURCES_DIR}/crypto/des/ncbc_enc.c
   ${OPENSSL_SOURCES_DIR}/crypto/ec/ecp_nistp224.c
   ${OPENSSL_SOURCES_DIR}/crypto/ec/ecp_nistp256.c
@@ -304,7 +304,6 @@
   ${OPENSSL_SOURCES_DIR}/crypto/ec/ecx_s390x.c
   ${OPENSSL_SOURCES_DIR}/crypto/poly1305/poly1305_base2_44.c
   ${OPENSSL_SOURCES_DIR}/crypto/rsa/rsa_acvp_test_params.c
-  ${OPENSSL_SOURCES_DIR}/crypto/s390xcap.c
   ${OPENSSL_SOURCES_DIR}/engines/e_devcrypto.c
   ${OPENSSL_SOURCES_DIR}/engines/e_loader_attic.c
   ${OPENSSL_SOURCES_DIR}/providers/common/securitycheck_fips.c
@@ -320,11 +319,17 @@
   ${OPENSSL_SOURCES_DIR}/crypto/chacha/chacha_ppc.c
   ${OPENSSL_SOURCES_DIR}/crypto/ec/ecp_ppc.c
   ${OPENSSL_SOURCES_DIR}/crypto/poly1305/poly1305_ppc.c
-  ${OPENSSL_SOURCES_DIR}/crypto/ppccap.c
   ${OPENSSL_SOURCES_DIR}/crypto/sha/sha_ppc.c
 
   # Disable SPARC sources
   ${OPENSSL_SOURCES_DIR}/crypto/bn/bn_sparc.c
+
+  # Disable CPUID for non-x86 platforms
+  ${OPENSSL_SOURCES_DIR}/crypto/armcap.c
+  ${OPENSSL_SOURCES_DIR}/crypto/loongarchcap.c
+  ${OPENSSL_SOURCES_DIR}/crypto/ppccap.c
+  ${OPENSSL_SOURCES_DIR}/crypto/riscvcap.c
+  ${OPENSSL_SOURCES_DIR}/crypto/s390xcap.c
   ${OPENSSL_SOURCES_DIR}/crypto/sparcv9cap.c
   )
 
@@ -389,7 +394,7 @@
     # crashes with segmentation fault in function
     # "build_SYS_str_reasons()", that is called from
     # "OPENSSL_init_ssl()"
-    # https://bugs.orthanc-server.com/show_bug.cgi?id=193
+    # https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=193
     -DOPENSSL_NO_ERR
     )
 
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -141,6 +142,11 @@
   unset(ENABLE_DCMTK_LOG CACHE)
 endif()
 
+if (NOT ENABLE_PROTOBUF)
+  unset(USE_SYSTEM_PROTOBUF CACHE)
+  add_definitions(-DORTHANC_ENABLE_PROTOBUF=0)
+endif()
+
 
 #####################################################################
 ## List of source files
@@ -149,7 +155,6 @@
 set(ORTHANC_CORE_SOURCES_INTERNAL
   ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Cache/MemoryCache.cpp
   ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Cache/MemoryObjectCache.cpp
-  ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Cache/MemoryStringCache.cpp
   ${CMAKE_CURRENT_LIST_DIR}/../../Sources/ChunkedBuffer.cpp
   ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/DicomTag.cpp
   ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/DicomPath.cpp
@@ -476,6 +481,16 @@
 endif()
 
 
+##
+## Google Protocol Buffers
+##
+
+if (ENABLE_PROTOBUF)
+  include(${CMAKE_CURRENT_LIST_DIR}/ProtobufConfiguration.cmake)
+  add_definitions(-DORTHANC_ENABLE_PROTOBUF=1)
+endif()
+
+
 
 #####################################################################
 ## Inclusion of mandatory third-party dependencies
@@ -624,6 +639,7 @@
     )
 
   list(APPEND ORTHANC_CORE_SOURCES_INTERNAL
+    ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Cache/MemoryStringCache.cpp
     ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Cache/SharedArchive.cpp
     ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileBuffer.cpp
     ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/FilesystemStorage.cpp
@@ -712,6 +728,7 @@
   ${LUA_SOURCES}
   ${MONGOOSE_SOURCES}
   ${OPENSSL_SOURCES}
+  ${PROTOBUF_LIBRARY_SOURCES}
   ${PUGIXML_SOURCES}
   ${SQLITE_SOURCES}
   ${UUID_SOURCES}
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -38,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 "18")
+set(ORTHANC_API_VERSION "24")
 
 
 #####################################################################
@@ -70,6 +71,7 @@
 set(USE_SYSTEM_LUA ON CACHE BOOL "Use the system version of Lua")
 set(USE_SYSTEM_MONGOOSE ON CACHE BOOL "Use the system version of Mongoose")
 set(USE_SYSTEM_OPENSSL ON CACHE BOOL "Use the system version of OpenSSL")
+set(USE_SYSTEM_PROTOBUF ON CACHE BOOL "Use the system version of Google Protocol Buffers")
 set(USE_SYSTEM_PUGIXML ON CACHE BOOL "Use the system version of Pugixml")
 set(USE_SYSTEM_SQLITE ON CACHE BOOL "Use the system version of SQLite")
 set(USE_SYSTEM_UUID ON CACHE BOOL "Use the system version of the uuid library from e2fsprogs")
@@ -77,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.7" 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\", or \"3.6.7\")")
+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(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")
@@ -123,6 +125,8 @@
 set(ENABLE_LOCALE OFF CACHE INTERNAL "Enable support for locales (notably in Boost)")
 set(ENABLE_LUA OFF CACHE INTERNAL "Enable support of Lua scripting")
 set(ENABLE_PNG OFF CACHE INTERNAL "Enable support of PNG")
+set(ENABLE_PROTOBUF OFF CACHE INTERNAL "Enable support for Google Protocol Buffers' library")
+set(ENABLE_PROTOBUF_COMPILER OFF CACHE INTERNAL "Enable support for Google Protocol Buffers' compiler")
 set(ENABLE_PUGIXML OFF CACHE INTERNAL "Enable support of XML through Pugixml")
 set(ENABLE_SQLITE OFF CACHE INTERNAL "Enable support of SQLite databases")
 set(ENABLE_ZLIB OFF CACHE INTERNAL "Enable support of zlib")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/ProtobufConfiguration.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,86 @@
+# 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/>.
+
+
+if (STATIC_BUILD OR NOT USE_SYSTEM_PROTOBUF)
+  if (ENABLE_PROTOBUF_COMPILER)
+    include(ExternalProject)
+    externalproject_add(ProtobufCompiler
+      SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../ProtocolBuffers"
+      BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/ProtobufCompiler-build"
+      # this helps triggering build when changing the external project
+      BUILD_ALWAYS 1
+      CMAKE_ARGS
+      -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
+      -DCMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_BINARY_DIR}
+      )
+
+    # The "protoc" compiler is built using "externalproject_add",
+    # which builds for the host platform, not for the target platform
+    if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
+      set(Suffix ".exe")
+    else()
+      set(Suffix "")
+    endif()
+
+    set(PROTOC_EXECUTABLE ${CMAKE_CURRENT_BINARY_DIR}/protoc${Suffix})
+  endif()
+
+  include(${CMAKE_CURRENT_LIST_DIR}/../ProtocolBuffers/ProtobufLibrary.cmake)  
+  source_group(ThirdParty\\Protobuf REGULAR_EXPRESSION ${PROTOBUF_SOURCE_DIR}/.*)
+
+else()
+  if (CMAKE_CROSSCOMPILING)
+    message(FATAL_ERROR "If cross-compiling, the static version of Protocol Buffers should be used to avoid version mismatch")
+  endif()
+  
+  if (ENABLE_PROTOBUF_COMPILER)
+    find_program(PROTOC_EXECUTABLE protoc)
+    if (${PROTOC_EXECUTABLE} MATCHES "PROTOC_EXECUTABLE-NOTFOUND")
+      message(FATAL_ERROR "Please install the 'protoc' compiler for Protocol Buffers (package 'protobuf-compiler' on Debian/Ubuntu)")
+    endif()
+    add_custom_target(ProtobufCompiler)
+  endif()
+  
+  check_include_file_cxx(google/protobuf/any.h HAVE_PROTOBUF_H)
+  if (NOT HAVE_PROTOBUF_H)
+    message(FATAL_ERROR "Please install the libprotobuf-dev package")
+  endif()
+
+  set(CMAKE_REQUIRED_LIBRARIES "protobuf")
+
+  include(CheckCXXSourceCompiles) 
+  check_cxx_source_compiles(
+    "
+#include <google/protobuf/descriptor.h>
+int main()
+{
+  google::protobuf::FieldDescriptor::TypeName(google::protobuf::FieldDescriptor::TYPE_FLOAT);
+}
+"  HAVE_PROTOBUF_LIB)
+  if (NOT HAVE_PROTOBUF_LIB)
+    message(FATAL_ERROR "Cannot find the protobuf library")
+  endif()
+  
+  unset(CMAKE_REQUIRED_LIBRARIES)
+
+  link_libraries(protobuf)
+endif()
--- a/OrthancFramework/Resources/CMake/PugixmlConfiguration.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/PugixmlConfiguration.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -20,9 +21,9 @@
 
 
 if (STATIC_BUILD OR NOT USE_SYSTEM_PUGIXML)
-  set(PUGIXML_SOURCES_DIR ${CMAKE_BINARY_DIR}/pugixml-1.9)
-  set(PUGIXML_MD5 "7286ee2ed11376b6b780ced19fae0b64")
-  set(PUGIXML_URL "http://orthanc.osimis.io/ThirdPartyDownloads/pugixml-1.9.tar.gz")
+  set(PUGIXML_SOURCES_DIR ${CMAKE_BINARY_DIR}/pugixml-1.14)
+  set(PUGIXML_MD5 "06e4242ee2352ee63c2b6627c6e3addb")
+  set(PUGIXML_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/pugixml-1.14.tar.gz")
 
   DownloadPackage(${PUGIXML_MD5} ${PUGIXML_URL} "${PUGIXML_SOURCES_DIR}")
 
--- a/OrthancFramework/Resources/CMake/SQLiteConfiguration.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/SQLiteConfiguration.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -38,7 +39,7 @@
 if (SQLITE_STATIC)
   SET(SQLITE_SOURCES_DIR ${CMAKE_BINARY_DIR}/sqlite-amalgamation-3270100)
   SET(SQLITE_MD5 "16717b26358ba81f0bfdac07addc77da")
-  SET(SQLITE_URL "http://orthanc.osimis.io/ThirdPartyDownloads/sqlite-amalgamation-3270100.zip")
+  SET(SQLITE_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/sqlite-amalgamation-3270100.zip")
 
   set(ORTHANC_SQLITE_VERSION 3027001)
 
--- a/OrthancFramework/Resources/CMake/UuidConfiguration.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/UuidConfiguration.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -23,7 +24,7 @@
 
   if (STATIC_BUILD OR NOT USE_SYSTEM_UUID)
     SET(E2FSPROGS_SOURCES_DIR ${CMAKE_BINARY_DIR}/e2fsprogs-1.44.5)
-    SET(E2FSPROGS_URL "http://orthanc.osimis.io/ThirdPartyDownloads/e2fsprogs-1.44.5.tar.gz")
+    SET(E2FSPROGS_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/e2fsprogs-1.44.5.tar.gz")
     SET(E2FSPROGS_MD5 "8d78b11d04d26c0b2dd149529441fa80")
 
     if (IS_DIRECTORY "${E2FSPROGS_SOURCES_DIR}")
--- a/OrthancFramework/Resources/CMake/VisualStudioPrecompiledHeaders.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/VisualStudioPrecompiledHeaders.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/CMakeLists.txt	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/CMakeLists.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -51,7 +52,7 @@
 include(${CMAKE_SOURCE_DIR}/../../DownloadPackage.cmake)
 
 set(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.2)
-set(DCMTK_URL "http://orthanc.osimis.io/ThirdPartyDownloads/dcmtk-3.6.2.tar.gz")
+set(DCMTK_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/dcmtk-3.6.2.tar.gz")
 set(DCMTK_MD5 "d219a4152772985191c9b89d75302d12")
 
 if (IS_DIRECTORY "${DCMTK_SOURCES_DIR}")
--- a/OrthancFramework/Resources/CMake/ZlibConfiguration.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CMake/ZlibConfiguration.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -20,9 +21,9 @@
 
 
 if (STATIC_BUILD OR NOT USE_SYSTEM_ZLIB)
-  SET(ZLIB_SOURCES_DIR ${CMAKE_BINARY_DIR}/zlib-1.2.11)
-  SET(ZLIB_URL "http://orthanc.osimis.io/ThirdPartyDownloads/zlib-1.2.11.tar.gz")
-  SET(ZLIB_MD5 "1c9f62f0778697a09d36121ead88e08e")
+  SET(ZLIB_SOURCES_DIR ${CMAKE_BINARY_DIR}/zlib-1.3.1)
+  SET(ZLIB_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/zlib-1.3.1.tar.gz")
+  SET(ZLIB_MD5 "9855b6d802d7fe5b7bd5b196a2271655")
 
   DownloadPackage(${ZLIB_MD5} ${ZLIB_URL} "${ZLIB_SOURCES_DIR}")
 
--- a/OrthancFramework/Resources/CheckOrthancFrameworkSymbols.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CheckOrthancFrameworkSymbols.py	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancFramework/Resources/CodeGeneration/CheckDcmtkTransferSyntaxes.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CodeGeneration/CheckDcmtkTransferSyntaxes.py	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json	Tue Sep 24 11:39:52 2024 +0200
@@ -250,6 +250,18 @@
     "Name": "MainDicomTagsMultiplyDefined",
     "Description": "A main DICOM Tag has been defined multiple times for the same resource level"
   }, 
+  {
+    "Code": 45, 
+    "HttpStatus": 403, 
+    "Name": "ForbiddenAccess", 
+    "Description": "Access to a resource is forbidden"
+  }, 
+  {
+    "Code": 46,
+    "HttpStatus": 409,
+    "Name": "DuplicateResource", 
+    "Description": "Duplicate resource"
+  }, 
 
 
 
--- a/OrthancFramework/Resources/CodeGeneration/GenerateErrorCodes.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CodeGeneration/GenerateErrorCodes.py	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -122,7 +123,7 @@
 with open(path, 'r') as f:
     a = f.read()
 
-e = filter(lambda x: 'SQLite' in x and x['SQLite'], ERRORS)
+e = list(filter(lambda x: 'SQLite' in x and x['SQLite'], ERRORS))
 s = ',\n'.join(map(lambda x: '      ErrorCode_%s' % x['Name'], e))
 a = re.sub('(enum ErrorCode\s*{)[^}]*?(\s*};)', r'\1\n%s\2' % s, a, re.DOTALL)
 
--- a/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxes.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxes.py	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxesDcmtk.mustache	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxesDcmtk.mustache	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxesEnumerations.mustache	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxesEnumerations.mustache	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Resources/DcmtkTools/CMakeLists.txt	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/DcmtkTools/CMakeLists.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancFramework/Resources/DcmtkTools/dummy.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/DcmtkTools/dummy.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Resources/EmbedResources.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/EmbedResources.py	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancFramework/Resources/Graveyard/FromDcmtkBridge.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/Graveyard/FromDcmtkBridge.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -29,7 +30,7 @@
     {
       // This raises BitBucket issue 140 (Modifying private tags with
       // REST API changes VR from LO to UN)
-      // https://bugs.orthanc-server.com/show_bug.cgi?id=140
+      // 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);
--- a/OrthancFramework/Resources/Graveyard/Multithreading/BagOfTasks.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/Graveyard/Multithreading/BagOfTasks.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Resources/Graveyard/Multithreading/BagOfTasksProcessor.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/Graveyard/Multithreading/BagOfTasksProcessor.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Resources/Graveyard/Multithreading/BagOfTasksProcessor.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/Graveyard/Multithreading/BagOfTasksProcessor.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Resources/Graveyard/Multithreading/ICommand.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/Graveyard/Multithreading/ICommand.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Resources/Graveyard/Multithreading/ILockable.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/Graveyard/Multithreading/ILockable.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Resources/Graveyard/Multithreading/Locker.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/Graveyard/Multithreading/Locker.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Resources/Graveyard/Multithreading/Mutex.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/Graveyard/Multithreading/Mutex.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Resources/Graveyard/Multithreading/Mutex.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/Graveyard/Multithreading/Mutex.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Resources/Graveyard/Multithreading/ReaderWriterLock.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/Graveyard/Multithreading/ReaderWriterLock.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Resources/Graveyard/Multithreading/ReaderWriterLock.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/Graveyard/Multithreading/ReaderWriterLock.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Resources/Graveyard/TestTranscoding.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/Graveyard/TestTranscoding.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Resources/Patches/OpenSSL-ConfigureHeaders.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/Patches/OpenSSL-ConfigureHeaders.py	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2021 Osimis S.A., Belgium
-# Copyright (C) 2021-2021 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancFramework/Resources/Patches/OpenSSL-ExtractProvidersOIDs.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/Patches/OpenSSL-ExtractProvidersOIDs.py	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2021 Osimis S.A., Belgium
-# Copyright (C) 2021-2021 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancFramework/Resources/Patches/boost-1.65.1-linux-standard-base.patch	Thu Sep 15 18:13:17 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-diff -urEb boost_1_65_1.orig/boost/move/adl_move_swap.hpp boost_1_65_1/boost/move/adl_move_swap.hpp
---- boost_1_65_1.orig/boost/move/adl_move_swap.hpp	2017-11-08 17:43:20.000000000 +0100
-+++ boost_1_65_1/boost/move/adl_move_swap.hpp	2018-01-02 15:34:48.829052917 +0100
-@@ -28,6 +28,8 @@
- //Try to avoid including <algorithm>, as it's quite big
- #if defined(_MSC_VER) && defined(BOOST_DINKUMWARE_STDLIB)
-    #include <utility>   //Dinkum libraries define std::swap in utility which is lighter than algorithm
-+#elif defined(__LSB_VERSION__)
-+#  include <utility>
- #elif defined(BOOST_GNU_STDLIB)
-    //For non-GCC compilers, where GNUC version is not very reliable, or old GCC versions
-    //use the good old stl_algobase header, which is quite lightweight
--- a/OrthancFramework/Resources/Patches/boost-1.66.0-linux-standard-base.patch	Thu Sep 15 18:13:17 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-diff -urEb boost_1_66_0.orig/boost/move/adl_move_swap.hpp boost_1_66_0/boost/move/adl_move_swap.hpp
---- boost_1_66_0.orig/boost/move/adl_move_swap.hpp	2018-04-11 11:56:16.761768726 +0200
-+++ boost_1_66_0/boost/move/adl_move_swap.hpp	2018-04-11 11:57:01.073881330 +0200
-@@ -28,6 +28,8 @@
- //Try to avoid including <algorithm>, as it's quite big
- #if defined(_MSC_VER) && defined(BOOST_DINKUMWARE_STDLIB)
-    #include <utility>   //Dinkum libraries define std::swap in utility which is lighter than algorithm
-+#elif defined(__LSB_VERSION__)
-+#  include <utility>
- #elif defined(BOOST_GNU_STDLIB)
-    //For non-GCC compilers, where GNUC version is not very reliable, or old GCC versions
-    //use the good old stl_algobase header, which is quite lightweight
-Only in boost_1_66_0/boost/move: adl_move_swap.hpp~
--- a/OrthancFramework/Resources/Patches/boost-1.67.0-linux-standard-base.patch	Thu Sep 15 18:13:17 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,74 +0,0 @@
-diff -urEb boost_1_67_0.orig/boost/move/adl_move_swap.hpp boost_1_67_0/boost/move/adl_move_swap.hpp
---- boost_1_67_0.orig/boost/move/adl_move_swap.hpp	2018-06-20 17:42:27.000000000 +0200
-+++ boost_1_67_0/boost/move/adl_move_swap.hpp	2018-10-12 14:27:41.368076902 +0200
-@@ -28,6 +28,8 @@
- //Try to avoid including <algorithm>, as it's quite big
- #if defined(_MSC_VER) && defined(BOOST_DINKUMWARE_STDLIB)
-    #include <utility>   //Dinkum libraries define std::swap in utility which is lighter than algorithm
-+#elif defined(__LSB_VERSION__)
-+#  include <utility>
- #elif defined(BOOST_GNU_STDLIB)
-    //For non-GCC compilers, where GNUC version is not very reliable, or old GCC versions
-    //use the good old stl_algobase header, which is quite lightweight
-diff -urEb boost_1_67_0.orig/boost/thread/detail/config.hpp boost_1_67_0/boost/thread/detail/config.hpp
---- boost_1_67_0.orig/boost/thread/detail/config.hpp	2018-06-20 17:42:27.000000000 +0200
-+++ boost_1_67_0/boost/thread/detail/config.hpp	2018-10-12 14:27:41.372076898 +0200
-@@ -417,6 +417,8 @@
-   #define BOOST_THREAD_INTERNAL_CLOCK_IS_MONO
- #elif defined(BOOST_THREAD_CHRONO_MAC_API)
-   #define BOOST_THREAD_HAS_MONO_CLOCK
-+#elif defined(__LSB_VERSION__) || defined(__ANDROID__)
-+  #define BOOST_THREAD_HAS_MONO_CLOCK
- #else
-   #include <time.h> // check for CLOCK_MONOTONIC
-   #if defined(CLOCK_MONOTONIC)
-diff -urEb boost_1_67_0.orig/boost/type_traits/detail/has_postfix_operator.hpp boost_1_67_0/boost/type_traits/detail/has_postfix_operator.hpp
---- boost_1_67_0.orig/boost/type_traits/detail/has_postfix_operator.hpp	2018-06-20 17:42:27.000000000 +0200
-+++ boost_1_67_0/boost/type_traits/detail/has_postfix_operator.hpp	2018-10-12 14:31:27.539874170 +0200
-@@ -32,8 +32,11 @@
- namespace boost {
- namespace detail {
- 
-+// https://stackoverflow.com/a/15474269
-+#ifndef Q_MOC_RUN
- // This namespace ensures that argument-dependent name lookup does not mess things up.
- namespace BOOST_JOIN(BOOST_TT_TRAIT_NAME,_impl) {
-+#endif
- 
- // 1. a function to have an instance of type T without requiring T to be default
- // constructible
-@@ -181,7 +184,9 @@
-    BOOST_STATIC_CONSTANT(bool, value = (trait_impl1 < Lhs_noref, Ret, BOOST_TT_FORBIDDEN_IF >::value));
- };
- 
-+#ifndef Q_MOC_RUN
- } // namespace impl
-+#endif
- } // namespace detail
- 
- // this is the accessible definition of the trait to end user
-diff -urEb boost_1_67_0.orig/boost/type_traits/detail/has_prefix_operator.hpp boost_1_67_0/boost/type_traits/detail/has_prefix_operator.hpp
---- boost_1_67_0.orig/boost/type_traits/detail/has_prefix_operator.hpp	2018-06-20 17:42:27.000000000 +0200
-+++ boost_1_67_0/boost/type_traits/detail/has_prefix_operator.hpp	2018-10-12 14:31:40.991862281 +0200
-@@ -45,8 +45,11 @@
- namespace boost {
- namespace detail {
- 
-+// https://stackoverflow.com/a/15474269
-+#ifndef Q_MOC_RUN
- // This namespace ensures that argument-dependent name lookup does not mess things up.
- namespace BOOST_JOIN(BOOST_TT_TRAIT_NAME,_impl) {
-+#endif
- 
- // 1. a function to have an instance of type T without requiring T to be default
- // constructible
-@@ -194,7 +197,9 @@
-    BOOST_STATIC_CONSTANT(bool, value = (trait_impl1 < Rhs_noref, Ret, BOOST_TT_FORBIDDEN_IF >::value));
- };
- 
-+#ifndef Q_MOC_RUN
- } // namespace impl
-+#endif
- } // namespace detail
- 
- // this is the accessible definition of the trait to end user
--- a/OrthancFramework/Resources/Patches/boost-1.68.0-linux-standard-base.patch	Thu Sep 15 18:13:17 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +0,0 @@
-diff -urEb boost_1_68_0.orig/boost/move/adl_move_swap.hpp boost_1_68_0/boost/move/adl_move_swap.hpp
---- boost_1_68_0.orig/boost/move/adl_move_swap.hpp	2018-11-13 16:08:32.214434915 +0100
-+++ boost_1_68_0/boost/move/adl_move_swap.hpp	2018-11-13 16:09:03.558399048 +0100
-@@ -28,6 +28,8 @@
- //Try to avoid including <algorithm>, as it's quite big
- #if defined(_MSC_VER) && defined(BOOST_DINKUMWARE_STDLIB)
-    #include <utility>   //Dinkum libraries define std::swap in utility which is lighter than algorithm
-+#elif defined(__LSB_VERSION__)
-+#  include <utility>
- #elif defined(BOOST_GNU_STDLIB)
-    //For non-GCC compilers, where GNUC version is not very reliable, or old GCC versions
-    //use the good old stl_algobase header, which is quite lightweight
-diff -urEb boost_1_68_0.orig/boost/thread/detail/config.hpp boost_1_68_0/boost/thread/detail/config.hpp
---- boost_1_68_0.orig/boost/thread/detail/config.hpp	2018-11-13 16:08:32.210434920 +0100
-+++ boost_1_68_0/boost/thread/detail/config.hpp	2018-11-13 16:10:03.386329911 +0100
-@@ -417,7 +417,7 @@
-   #define BOOST_THREAD_INTERNAL_CLOCK_IS_MONO
- #elif defined(BOOST_THREAD_CHRONO_MAC_API)
-   #define BOOST_THREAD_HAS_MONO_CLOCK
--#elif defined(__ANDROID__)
-+#elif defined(__LSB_VERSION__) || defined(__ANDROID__)
-   #define BOOST_THREAD_HAS_MONO_CLOCK
-   #if defined(__ANDROID_API__) && __ANDROID_API__ >= 21
-     #define BOOST_THREAD_INTERNAL_CLOCK_IS_MONO
-diff -urEb boost_1_68_0.orig/boost/type_traits/detail/has_postfix_operator.hpp boost_1_68_0/boost/type_traits/detail/has_postfix_operator.hpp
---- boost_1_68_0.orig/boost/type_traits/detail/has_postfix_operator.hpp	2018-11-13 16:08:32.206434924 +0100
-+++ boost_1_68_0/boost/type_traits/detail/has_postfix_operator.hpp	2018-11-13 16:11:08.374253901 +0100
-@@ -32,8 +32,11 @@
- namespace boost {
- namespace detail {
- 
-+// https://stackoverflow.com/a/15474269
-+#ifndef Q_MOC_RUN
- // This namespace ensures that argument-dependent name lookup does not mess things up.
- namespace BOOST_JOIN(BOOST_TT_TRAIT_NAME,_impl) {
-+#endif
- 
- // 1. a function to have an instance of type T without requiring T to be default
- // constructible
-@@ -181,7 +184,9 @@
-    BOOST_STATIC_CONSTANT(bool, value = (trait_impl1 < Lhs_noref, Ret, BOOST_TT_FORBIDDEN_IF >::value));
- };
- 
-+#ifndef Q_MOC_RUN
- } // namespace impl
-+#endif
- } // namespace detail
- 
- // this is the accessible definition of the trait to end user
-diff -urEb boost_1_68_0.orig/boost/type_traits/detail/has_prefix_operator.hpp boost_1_68_0/boost/type_traits/detail/has_prefix_operator.hpp
---- boost_1_68_0.orig/boost/type_traits/detail/has_prefix_operator.hpp	2018-11-13 16:08:32.206434924 +0100
-+++ boost_1_68_0/boost/type_traits/detail/has_prefix_operator.hpp	2018-11-13 16:14:30.278012856 +0100
-@@ -45,8 +45,11 @@
- namespace boost {
- namespace detail {
- 
-+// https://stackoverflow.com/a/15474269
-+#ifndef Q_MOC_RUN
- // This namespace ensures that argument-dependent name lookup does not mess things up.
- namespace BOOST_JOIN(BOOST_TT_TRAIT_NAME,_impl) {
-+#endif
- 
- // 1. a function to have an instance of type T without requiring T to be default
- // constructible
-@@ -194,7 +197,10 @@
-    BOOST_STATIC_CONSTANT(bool, value = (trait_impl1 < Rhs_noref, Ret, BOOST_TT_FORBIDDEN_IF >::value));
- };
- 
-+
-+#ifndef Q_MOC_RUN
- } // namespace impl
-+#endif
- } // namespace detail
- 
- // this is the accessible definition of the trait to end user
-Only in boost_1_68_0/boost/type_traits/detail: has_prefix_operator.hpp~
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/boost-1.85.0-emscripten.patch	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,125 @@
+diff -urEb boost_1_85_0.orig/libs/locale/src/boost/locale/shared/date_time.cpp boost_1_85_0/libs/locale/src/boost/locale/shared/date_time.cpp
+--- boost_1_85_0.orig/libs/locale/src/boost/locale/shared/date_time.cpp	2024-05-16 20:54:25.516816710 +0200
++++ boost_1_85_0/libs/locale/src/boost/locale/shared/date_time.cpp	2024-05-16 20:55:09.144319528 +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,6 +425,7 @@
+             return boost::exchange(tz_id(), new_id);
+         }
+     } // namespace time_zone
++#endif
+ 
+ }} // namespace boost::locale
+ 
+diff -urEb boost_1_85_0.orig/libs/locale/src/boost/locale/shared/generator.cpp boost_1_85_0/libs/locale/src/boost/locale/shared/generator.cpp
+--- boost_1_85_0.orig/libs/locale/src/boost/locale/shared/generator.cpp	2024-05-16 20:54:25.516816710 +0200
++++ boost_1_85_0/libs/locale/src/boost/locale/shared/generator.cpp	2024-05-16 20:56:20.231509636 +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,7 +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 +105,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 +132,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_85_0.orig/libs/locale/src/boost/locale/shared/localization_backend.cpp boost_1_85_0/libs/locale/src/boost/locale/shared/localization_backend.cpp
+--- boost_1_85_0.orig/libs/locale/src/boost/locale/shared/localization_backend.cpp	2024-05-16 20:54:25.516816710 +0200
++++ boost_1_85_0/libs/locale/src/boost/locale/shared/localization_backend.cpp	2024-05-16 20:56:58.823070064 +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/civetweb-1.11.patch	Thu Sep 15 18:13:17 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,98 +0,0 @@
-diff -urEb civetweb-1.11.orig/include/civetweb.h civetweb-1.11/include/civetweb.h
---- civetweb-1.11.orig/include/civetweb.h	2019-01-17 21:09:41.844888908 +0100
-+++ civetweb-1.11/include/civetweb.h	2019-01-21 12:05:08.138998659 +0100
-@@ -1507,6 +1507,10 @@
- #endif
- 
- 
-+// Added by SJ
-+CIVETWEB_API void mg_disable_keep_alive(struct mg_connection *conn);
-+
-+
- #ifdef __cplusplus
- }
- #endif /* __cplusplus */
-diff -urEb civetweb-1.11.orig/src/civetweb.c civetweb-1.11/src/civetweb.c
---- civetweb-1.11.orig/src/civetweb.c	2019-01-17 21:09:41.852888857 +0100
-+++ civetweb-1.11/src/civetweb.c	2019-01-21 12:06:35.826868284 +0100
-@@ -59,6 +59,9 @@
- #if defined(__linux__) && !defined(_XOPEN_SOURCE)
- #define _XOPEN_SOURCE 600 /* For flockfile() on Linux */
- #endif
-+#if defined(__LSB_VERSION__)
-+#define NEED_TIMEGM
-+#endif
- #if !defined(_LARGEFILE_SOURCE)
- #define _LARGEFILE_SOURCE /* For fseeko(), ftello() */
- #endif
-@@ -129,6 +132,12 @@
- 
- 
- /* Alternative queue is well tested and should be the new default */
-+#if defined(__LSB_VERSION__)
-+/* Function "eventfd()" is not available in Linux Standard Base, can't
-+ * use the alternative queue */
-+#define NO_ALTERNATIVE_QUEUE
-+#endif
-+
- #if defined(NO_ALTERNATIVE_QUEUE)
- #if defined(ALTERNATIVE_QUEUE)
- #error "Define ALTERNATIVE_QUEUE or NO_ALTERNATIVE_QUEUE or none, but not both"
-@@ -536,6 +545,10 @@
- #if !defined(EWOULDBLOCK)
- #define EWOULDBLOCK WSAEWOULDBLOCK
- #endif /* !EWOULDBLOCK */
-+#if !defined(ECONNRESET)
-+/* This macro is not defined e.g. in Visual Studio 2008 */
-+#define ECONNRESET WSAECONNRESET
-+#endif /* !ECONNRESET */
- #define _POSIX_
- #define INT64_FMT "I64d"
- #define UINT64_FMT "I64u"
-@@ -2939,6 +2952,13 @@
- #endif
- 
- 
-+#if defined(__LSB_VERSION__)
-+static void
-+mg_set_thread_name(const char *threadName)
-+{
-+  /* prctl() does not seem to be available in Linux Standard Base */
-+}
-+#else
- static void
- mg_set_thread_name(const char *name)
- {
-@@ -2980,6 +3000,7 @@
- 	(void)prctl(PR_SET_NAME, threadName, 0, 0, 0);
- #endif
- }
-+#endif
- #else /* !defined(NO_THREAD_NAME) */
- void
- mg_set_thread_name(const char *threadName)
-@@ -16919,6 +16940,10 @@
- 	/* Message is a valid request */
- 
- 	/* Is there a "host" ? */
-+        /* https://github.com/civetweb/civetweb/pull/675/commits/96e3e8c50acb4b8e0c946d02b5f880a3e62986e1 */
-+	if (conn->host!=NULL) {
-+		mg_free((void *)conn->host);
-+	}
- 	conn->host = alloc_get_host(conn);
- 	if (!conn->host) {
- 		mg_snprintf(conn,
-@@ -19857,4 +19882,13 @@
- }
- 
- 
-+// Added by SJ
-+void mg_disable_keep_alive(struct mg_connection *conn)
-+{
-+  if (conn != NULL) {
-+    conn->must_close = 1;
-+  }
-+}
-+
-+
- /* End of civetweb.c */
--- a/OrthancFramework/Resources/Patches/civetweb-1.12.patch	Thu Sep 15 18:13:17 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,41 +0,0 @@
-diff -urEb civetweb-1.12.orig/include/civetweb.h civetweb-1.12/include/civetweb.h
---- civetweb-1.12.orig/include/civetweb.h	2020-10-06 12:39:10.634902843 +0200
-+++ civetweb-1.12/include/civetweb.h	2020-10-06 12:39:30.630872089 +0200
-@@ -1614,6 +1614,9 @@
-                                   struct mg_error_data *error);
- #endif
- 
-+// Added by SJ
-+CIVETWEB_API void mg_disable_keep_alive(struct mg_connection *conn);
-+
- #ifdef __cplusplus
- }
- #endif /* __cplusplus */
-diff -urEb civetweb-1.12.orig/src/civetweb.c civetweb-1.12/src/civetweb.c
---- civetweb-1.12.orig/src/civetweb.c	2020-10-06 12:39:10.638902837 +0200
-+++ civetweb-1.12/src/civetweb.c	2020-10-06 12:41:40.110671929 +0200
-@@ -10525,6 +10525,11 @@
-     /* + MicroSoft extensions
-      * https://msdn.microsoft.com/en-us/library/aa142917.aspx */
- 
-+    /* Added by SJ, for write access to WebDAV on Windows >= 7 */
-+    {"LOCK", 1, 1, 0, 0, 0},
-+    {"UNLOCK", 1, 0, 0, 0, 0},
-+    {"PROPPATCH", 1, 1, 0, 0, 0},
-+    
-     /* REPORT method (RFC 3253) */
-     {"REPORT", 1, 1, 1, 1, 1},
-     /* REPORT method only allowed for CGI/Lua/LSP and callbacks. */
-@@ -20704,5 +20709,12 @@
- 	return 1;
- }
- 
-+// Added by SJ
-+void mg_disable_keep_alive(struct mg_connection *conn)
-+{
-+  if (conn != NULL) {
-+    conn->must_close = 1;
-+  }
-+}
- 
- /* End of civetweb.c */
--- a/OrthancFramework/Resources/Patches/civetweb-1.14.patch	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/Patches/civetweb-1.14.patch	Tue Sep 24 11:39:52 2024 +0200
@@ -1,6 +1,38 @@
 diff -urEb civetweb-1.14.orig/src/civetweb.c civetweb-1.14/src/civetweb.c
---- civetweb-1.14.orig/src/civetweb.c	2021-06-21 17:42:52.343136123 +0200
-+++ civetweb-1.14/src/civetweb.c	2021-06-21 17:43:11.623158128 +0200
+--- civetweb-1.14.orig/src/civetweb.c	2023-07-06 15:48:01.163703913 +0200
++++ civetweb-1.14/src/civetweb.c	2023-07-06 15:48:51.207843938 +0200
+@@ -567,7 +567,7 @@
+ #if (_MSC_VER < 1300)
+ #define STRX(x) #x
+ #define STR(x) STRX(x)
+-#define __func__ __FILE__ ":" STR(__LINE__)
++#define __func__ __ORTHANC_FILE__ ":" STR(__LINE__)
+ #define strtoull(x, y, z) ((unsigned __int64)_atoi64(x))
+ #define strtoll(x, y, z) (_atoi64(x))
+ #else
+@@ -1450,14 +1450,14 @@
+ }
+ 
+ 
+-#define mg_malloc(a) mg_malloc_ex(a, NULL, __FILE__, __LINE__)
+-#define mg_calloc(a, b) mg_calloc_ex(a, b, NULL, __FILE__, __LINE__)
+-#define mg_realloc(a, b) mg_realloc_ex(a, b, NULL, __FILE__, __LINE__)
+-#define mg_free(a) mg_free_ex(a, __FILE__, __LINE__)
+-
+-#define mg_malloc_ctx(a, c) mg_malloc_ex(a, c, __FILE__, __LINE__)
+-#define mg_calloc_ctx(a, b, c) mg_calloc_ex(a, b, c, __FILE__, __LINE__)
+-#define mg_realloc_ctx(a, b, c) mg_realloc_ex(a, b, c, __FILE__, __LINE__)
++#define mg_malloc(a) mg_malloc_ex(a, NULL, __ORTHANC_FILE__, __LINE__)
++#define mg_calloc(a, b) mg_calloc_ex(a, b, NULL, __ORTHANC_FILE__, __LINE__)
++#define mg_realloc(a, b) mg_realloc_ex(a, b, NULL, __ORTHANC_FILE__, __LINE__)
++#define mg_free(a) mg_free_ex(a, __ORTHANC_FILE__, __LINE__)
++
++#define mg_malloc_ctx(a, c) mg_malloc_ex(a, c, __ORTHANC_FILE__, __LINE__)
++#define mg_calloc_ctx(a, b, c) mg_calloc_ex(a, b, c, __ORTHANC_FILE__, __LINE__)
++#define mg_realloc_ctx(a, b, c) mg_realloc_ex(a, b, c, __ORTHANC_FILE__, __LINE__)
+ 
+ 
+ #else /* USE_SERVER_STATS */
 @@ -1774,6 +1774,7 @@
  #if !defined(OPENSSL_API_3_0)
  #define OPENSSL_API_3_0
--- a/OrthancFramework/Resources/Patches/curl-7.77.0.patch	Thu Sep 15 18:13:17 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,24 +0,0 @@
-diff -urEb curl-7.77.0.orig/CMake/Macros.cmake curl-7.77.0/CMake/Macros.cmake
---- curl-7.77.0.orig/CMake/Macros.cmake	2021-06-22 10:31:21.875004553 +0200
-+++ curl-7.77.0/CMake/Macros.cmake	2021-06-22 10:31:45.219024665 +0200
-@@ -59,7 +59,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)
-diff -urEb curl-7.77.0.orig/lib/vssh/ssh.h curl-7.77.0/lib/vssh/ssh.h
---- curl-7.77.0.orig/lib/vssh/ssh.h	2021-06-22 10:31:21.843004526 +0200
-+++ curl-7.77.0/lib/vssh/ssh.h	2021-06-22 10:36:34.271266232 +0200
-@@ -22,7 +22,7 @@
-  *
-  ***************************************************************************/
- 
--#include "curl_setup.h"
-+#include "../curl_setup.h"
- 
- #if defined(HAVE_LIBSSH2_H)
- #include <libssh2.h>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/curl-8.9.0.patch	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,12 @@
+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	2024-07-26 18:47:52.920588300 +0200
++++ curl-8.9.0/CMake/Macros.cmake	2024-07-26 18:48:08.345522100 +0200
+@@ -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)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/dcmtk-3.6.8-visual-studio.patch	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,995 @@
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jccoefct.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jccoefct.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jccoefct.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jccoefct.c	2024-01-09 17:48:28.974677157 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jcdiffct.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jcdiffct.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jcdiffct.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jcdiffct.c	2024-01-09 17:48:36.414609533 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jcpred.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jcpred.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jcpred.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jcpred.c	2024-01-09 17:48:49.766488124 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jctrans.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jctrans.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jctrans.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jctrans.c	2024-01-09 17:49:00.070394388 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jdmerge.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jdmerge.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jdmerge.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jdmerge.c	2024-01-09 17:49:11.910286634 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jdpostct.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jdpostct.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jdpostct.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jdpostct.c	2024-01-09 17:49:24.910168268 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jdpred.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jdpred.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jdpred.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jdpred.c	2024-01-09 17:50:00.513843814 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jdsample.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jdsample.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jdsample.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jdsample.c	2024-01-09 17:50:36.545515066 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jdscale.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jdscale.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jdscale.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jdscale.c	2024-01-09 17:50:42.833457657 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jquant1.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jquant1.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jquant1.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jquant1.c	2024-01-09 17:51:03.049273013 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jquant2.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jquant2.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jquant2.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jquant2.c	2024-01-09 17:51:16.685148405 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jccoefct.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jccoefct.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jccoefct.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jccoefct.c	2024-01-09 17:51:24.997072424 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jcdiffct.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jcdiffct.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jcdiffct.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jcdiffct.c	2024-01-09 17:51:31.549012520 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jcpred.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jcpred.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jcpred.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jcpred.c	2024-01-09 17:51:40.740928459 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jctrans.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jctrans.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jctrans.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jctrans.c	2024-01-09 17:51:49.244850672 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jdmerge.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jdmerge.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jdmerge.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jdmerge.c	2024-01-09 17:51:59.852753613 +0100
+@@ -171,7 +171,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 */
+@@ -221,8 +221,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-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jdpostct.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jdpostct.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jdpostct.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jdpostct.c	2024-01-09 17:52:12.796635145 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jdpred.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jdpred.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jdpred.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jdpred.c	2024-01-09 17:53:08.884121363 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jdsample.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jdsample.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jdsample.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jdsample.c	2024-01-09 17:53:28.779938946 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jdscale.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jdscale.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jdscale.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jdscale.c	2024-01-09 17:53:34.795883773 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jquant1.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jquant1.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jquant1.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jquant1.c	2024-01-09 17:53:53.891708593 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jquant2.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jquant2.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jquant2.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jquant2.c	2024-01-09 17:54:05.051606183 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jccoefct.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jccoefct.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jccoefct.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jccoefct.c	2024-01-09 17:54:11.635545753 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jcdiffct.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jcdiffct.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jcdiffct.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jcdiffct.c	2024-01-09 17:54:16.815498204 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jcpred.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jcpred.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jcpred.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jcpred.c	2024-01-09 17:54:25.827415468 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jctrans.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jctrans.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jctrans.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jctrans.c	2024-01-09 17:54:33.939340981 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jdmerge.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jdmerge.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jdmerge.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jdmerge.c	2024-01-09 17:54:41.019275962 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jdpostct.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jdpostct.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jdpostct.c	2024-01-09 17:13:10.349673411 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jdpostct.c	2024-01-09 17:54:48.891203659 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jdpred.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jdpred.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jdpred.c	2024-01-09 17:13:10.349673411 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jdpred.c	2024-01-09 17:55:02.179081586 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jdsample.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jdsample.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jdsample.c	2024-01-09 17:13:10.349673411 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jdsample.c	2024-01-09 17:55:13.234979994 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jdscale.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jdscale.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jdscale.c	2024-01-09 17:13:10.349673411 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jdscale.c	2024-01-09 17:55:21.722901985 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jquant1.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jquant1.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jquant1.c	2024-01-09 17:13:10.349673411 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jquant1.c	2024-01-09 17:48:22.270738074 +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-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jquant2.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jquant2.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jquant2.c	2024-01-09 17:13:10.349673411 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jquant2.c	2024-01-09 17:47:42.343100533 +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 */
+ }
+ 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/dcmtk-3.6.8.patch	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,139 @@
+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
+@@ -224,6 +224,8 @@
+ 
+ # 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("char" SIZEOF_CHAR)
+ CHECK_TYPE_SIZE("double" SIZEOF_DOUBLE)
+ CHECK_TYPE_SIZE("float" SIZEOF_FLOAT)
+@@ -231,6 +233,7 @@
+ 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-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
+@@ -162,6 +162,12 @@
+     /// returns an iterator to the end of the repeating tag 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-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
+@@ -914,3 +914,5 @@
+   wrlock().clear();
+   wrunlock();
+ }
++
++#include "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
+@@ -31,6 +31,9 @@
+ #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-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
+@@ -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-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
+@@ -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-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
+@@ -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-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
+@@ -570,7 +570,7 @@
+    */
+   void setlinebuf()
+   {
+-#if defined(_WIN32) || defined(__hpux)
++#if defined(_WIN32) || defined(__hpux) || defined(__LSB_VERSION__)
+     this->setvbuf(NULL, _IOLBF, 0);
+ #else
+     :: setlinebuf(file_);
--- a/OrthancFramework/Resources/Patches/openssl-3.0.5.patch	Thu Sep 15 18:13:17 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-diff -urEb openssl-3.0.5.orig/crypto/threads_win.c openssl-3.0.5/crypto/threads_win.c
---- openssl-3.0.5.orig/crypto/threads_win.c	2022-08-15 15:37:28.944910076 +0200
-+++ openssl-3.0.5/crypto/threads_win.c	2022-08-15 15:38:34.120081931 +0200
-@@ -207,13 +207,30 @@
- int CRYPTO_atomic_or(uint64_t *val, uint64_t op, uint64_t *ret,
-                      CRYPTO_RWLOCK *lock)
- {
-+#if defined(_WIN32) && !defined(_WIN64)
-+    /**
-+     * Prevents the following error, at least on Visual Studio 2008,
-+     * but most probably on any Window 32bit system:
-+     * "CoreLibrary.lib(threads_win.obj) : error LNK2019: unresolved
-+     * external symbol _InterlockedOr64 referenced in function
-+     * _CRYPTO_atomic_or". TODO - The lock should be locked!
-+     * https://developercommunity.visualstudio.com/t/-interlockedexchangeadd64-is-unresolved-on-x86/1227636
-+     **/
-+    *ret = (*val) | op;
-+#else
-     *ret = (uint64_t)InterlockedOr64((LONG64 volatile *)val, (LONG64)op) | op;
-+#endif
-     return 1;
- }
- 
- int CRYPTO_atomic_load(uint64_t *val, uint64_t *ret, CRYPTO_RWLOCK *lock)
- {
-+#if defined(_WIN32) && !defined(_WIN64)
-+    /* See comment above */
-+    *ret = *val;
-+#else
-     *ret = (uint64_t)InterlockedOr64((LONG64 volatile *)val, 0);
-+#endif
-     return 1;
- }
- 
-diff -urEb openssl-3.0.5.orig/providers/implementations/rands/seeding/rand_unix.c openssl-3.0.5/providers/implementations/rands/seeding/rand_unix.c
---- openssl-3.0.5.orig/providers/implementations/rands/seeding/rand_unix.c	2022-08-15 15:37:28.968909770 +0200
-+++ openssl-3.0.5/providers/implementations/rands/seeding/rand_unix.c	2022-08-15 15:39:00.963742658 +0200
-@@ -453,6 +453,7 @@
-              * system call and this should always succeed which renders
-              * this alternative but essentially identical source moot.
-              */
-+#if !defined(__LSB_VERSION__)  // "syscall()" is not available in LSB
-             if (uname(&un) == 0) {
-                 kernel[0] = atoi(un.release);
-                 p = strchr(un.release, '.');
-@@ -463,6 +464,7 @@
-                     return 0;
-                 }
-             }
-+#endif
-             /* Open /dev/random and wait for it to be readable */
-             if ((fd = open(DEVRANDOM_WAIT, O_RDONLY)) != -1) {
-                 if (DEVRANDM_WAIT_USE_SELECT && fd < FD_SETSIZE) {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/openssl-3.1.4.patch	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,47 @@
+diff -urEb openssl-3.1.4.orig/crypto/riscvcap.c openssl-3.1.4/crypto/riscvcap.c
+--- openssl-3.1.4.orig/crypto/riscvcap.c	2024-01-24 16:58:48.308108757 +0100
++++ openssl-3.1.4/crypto/riscvcap.c	2024-01-24 17:01:04.114914015 +0100
+@@ -37,7 +37,8 @@
+ 
+ static void strtoupper(char *str)
+ {
+-    for (char *x = str; *x; ++x)
++    char* x;
++    for (x = str; *x; ++x)
+         *x = toupper(*x);
+ }
+ 
+@@ -51,12 +52,13 @@
+ {
+     char envstrupper[BUFLEN];
+     char buf[BUFLEN];
++    size_t i;
+ 
+     /* Convert env str to all uppercase */
+     OPENSSL_strlcpy(envstrupper, envstr, sizeof(envstrupper));
+     strtoupper(envstrupper);
+ 
+-    for (size_t i = 0; i < kRISCVNumCaps; ++i) {
++    for (i = 0; i < kRISCVNumCaps; ++i) {
+         /* Prefix capability with underscore in preparation for search */
+         BIO_snprintf(buf, BUFLEN, "_%s", RISCV_capabilities[i].name);
+         if (strstr(envstrupper, buf) != NULL) {
+diff -urEb openssl-3.1.4.orig/providers/implementations/rands/seeding/rand_unix.c openssl-3.1.4/providers/implementations/rands/seeding/rand_unix.c
+--- openssl-3.1.4.orig/providers/implementations/rands/seeding/rand_unix.c	2024-01-24 16:58:48.332108547 +0100
++++ openssl-3.1.4/providers/implementations/rands/seeding/rand_unix.c	2024-01-24 17:01:30.182683539 +0100
+@@ -452,6 +452,7 @@
+              * system call and this should always succeed which renders
+              * this alternative but essentially identical source moot.
+              */
++#if !defined(__LSB_VERSION__)  // "syscall()" is not available in LSB
+             if (uname(&un) == 0) {
+                 kernel[0] = atoi(un.release);
+                 p = strchr(un.release, '.');
+@@ -462,6 +463,7 @@
+                     return 0;
+                 }
+             }
++#endif
+             /* Open /dev/random and wait for it to be readable */
+             if ((fd = open(DEVRANDOM_WAIT, O_RDONLY)) != -1) {
+                 if (DEVRANDM_WAIT_USE_SELECT && fd < FD_SETSIZE) {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/protobuf-3.5.1.patch	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,30 @@
+diff -urEb protobuf-3.5.1.orig/src/google/protobuf/stubs/io_win32.cc protobuf-3.5.1/src/google/protobuf/stubs/io_win32.cc
+--- 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;
++  /**
++   * "nullptr" is not known to Visual Studio 2008, because this is a
++   * C++11 construction, which shouldn't be present in protobuf 3.5.1
++   * that is supposed to comply with C++98.
++   **/
++  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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/ProtocolBuffers/CMakeLists.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,150 @@
+# 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/>.
+
+
+cmake_minimum_required(VERSION 2.8.3)
+
+project(ProtocolBuffers)
+
+set(ALLOW_DOWNLOADS ON)
+
+include(${CMAKE_SOURCE_DIR}/../CMake/DownloadPackage.cmake)
+include(${CMAKE_SOURCE_DIR}/../CMake/Compiler.cmake)
+
+include(${CMAKE_SOURCE_DIR}/ProtobufLibrary.cmake)
+
+set(PROTOBUF_COMPILER_SOURCES
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/code_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/command_line_interface.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_enum.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_enum_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_extension.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_file.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_helpers.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_map_field.cc  
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_message.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_message_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_padding_optimizer.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_primitive_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_service.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_string_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_doc_comment.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_enum.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_enum_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_field_base.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_helpers.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_map_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_message.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_message_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_primitive_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_reflection_class.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_repeated_enum_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_repeated_message_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_repeated_primitive_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_source_generator_base.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_wrapper_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/importer.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_context.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_doc_comment.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_enum.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_enum_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_enum_field_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_enum_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_extension.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_extension_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_file.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_generator_factory.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_helpers.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_lazy_message_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_lazy_message_field_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_map_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_map_field_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_message.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_message_builder.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_message_builder_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_message_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_message_field_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_message_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_name_resolver.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_primitive_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_primitive_field_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_service.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_shared_code_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_string_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_string_field_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_enum.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_enum_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_extension.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_file.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_helpers.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_map_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_message.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_message_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_primitive_field.cc
+  #${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/js/embed.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/js/js_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/js/well_known_types_embed.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/main.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_enum.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_enum_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_extension.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_file.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_helpers.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_map_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_message.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_message_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_oneof.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_primitive_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/parser.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/php/php_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/plugin.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/plugin.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/python/python_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/ruby/ruby_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/subprocess.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/zip_writer.cc
+  )
+
+if (NOT CMAKE_SYSTEM_NAME STREQUAL "Windows")
+  set_property(
+    SOURCE ${PROTOBUF_COMPILER_SOURCES} APPEND
+    PROPERTY COMPILE_DEFINITIONS "HAVE_PTHREAD=1"
+    )
+endif()
+
+add_executable(protoc
+  ${PROTOBUF_LIBRARY_SOURCES}
+  ${PROTOBUF_COMPILER_SOURCES}
+  )
+
+install(
+  TARGETS protoc
+  RUNTIME DESTINATION .
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/ProtocolBuffers/NOTES.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,29 @@
+
+Version
+=======
+
+We use Google's Protocol Buffers version 3.5.1, as this is the last
+release to be compatible with C++98, which is mandatory for Visual
+Studio 2008 and Linux Standard Base.
+
+References:
+https://github.com/protocolbuffers/protobuf/releases/tag/v3.5.1
+https://github.com/protocolbuffers/protobuf/issues/2780
+
+
+Linux Standard Base
+===================
+
+$ mkdir lsb
+$ cd lsb
+$ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -DCMAKE_BUILD_TYPE=Release -DALLOW_DOWNLOADS=ON -DCMAKE_TOOLCHAIN_FILE=../../Toolchains/LinuxStandardBaseToolchain.cmake -G Ninja
+$ ninja
+
+
+MinGW for 32bits
+================
+
+$ mkdir w32
+$ cd w32
+$ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -DCMAKE_BUILD_TYPE=Release -DALLOW_DOWNLOADS=ON -DCMAKE_TOOLCHAIN_FILE=../../Toolchains/MinGW-W64-Toolchain32.cmake -G Ninja
+$ ninja
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/ProtocolBuffers/ProtobufLibrary.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,145 @@
+# 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/>.
+
+
+set(PROTOBUF_SOURCE_DIR ${CMAKE_CURRENT_BINARY_DIR}/protobuf-3.5.1)
+
+if (IS_DIRECTORY "${PROTOBUF_SOURCE_DIR}")
+  set(FirstRun OFF)
+else()
+  set(FirstRun ON)
+endif()
+
+DownloadPackage(
+  "ca0d9b243e649d398a6b419acd35103a"
+  "https://orthanc.uclouvain.be/downloads/third-party-downloads/protobuf-cpp-3.5.1.tar.gz"
+  "${CMAKE_CURRENT_BINARY_DIR}/protobuf-3.5.1")
+
+if (FirstRun)
+  # Apply the patches
+  execute_process(
+    COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
+    ${CMAKE_CURRENT_LIST_DIR}/../Patches/protobuf-3.5.1.patch
+    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+    RESULT_VARIABLE Failure
+    )
+
+  if (Failure)
+    message(FATAL_ERROR "Error while patching a file")
+  endif()
+endif()
+
+include_directories(
+  ${PROTOBUF_SOURCE_DIR}/src
+  )
+  
+set(PROTOBUF_LIBRARY_SOURCES
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/any.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/any.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/api.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/arena.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/arenastring.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/descriptor.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/descriptor.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/descriptor_database.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/duration.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/dynamic_message.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/empty.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/extension_set.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/extension_set_heavy.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/field_mask.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/generated_message_reflection.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/generated_message_table_driven.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/generated_message_table_driven_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/generated_message_util.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/io/coded_stream.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/io/gzip_stream.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/io/printer.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/io/strtod.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/io/tokenizer.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/io/zero_copy_stream.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/io/zero_copy_stream_impl.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/io/zero_copy_stream_impl_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/map_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/message.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/message_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/reflection_ops.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/repeated_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/service.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/source_context.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/struct.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/atomicops_internals_arm64_gcc.h
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/atomicops_internals_arm_gcc.h
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/atomicops_internals_generic_gcc.h
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/atomicops_internals_mips_gcc.h
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/atomicops_internals_ppc_gcc.h
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/atomicops_internals_x86_gcc.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/atomicops_internals_x86_gcc.h
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/atomicops_internals_x86_msvc.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/common.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/int128.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/io_win32.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/mathlimits.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/once.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/status.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/statusor.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/stringpiece.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/stringprintf.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/structurally_valid.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/strutil.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/substitute.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/time.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/bytestream.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/text_format.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/timestamp.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/type.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/unknown_field_set.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/delimited_message_util.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/field_comparator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/field_mask_util.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/datapiece.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/default_value_objectwriter.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/error_listener.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/field_mask_utility.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/json_escaping.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/json_objectwriter.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/json_stream_parser.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/object_writer.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/proto_writer.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/protostream_objectsource.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/protostream_objectwriter.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/type_info.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/utility.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/json_util.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/message_differencer.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/time_util.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/type_resolver_util.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/wire_format.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/wire_format_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/wrappers.pb.cc
+  )
+
+if (NOT CMAKE_SYSTEM_NAME STREQUAL "Windows")
+  set_property(
+    SOURCE ${PROTOBUF_LIBRARY_SOURCES} APPEND
+    PROPERTY COMPILE_DEFINITIONS "HAVE_PTHREAD=1"
+    )
+endif()
--- a/OrthancFramework/Resources/RetrieveCACertificates.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/RetrieveCACertificates.py	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -31,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	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/Samples/MicroService/CMakeLists.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancFramework/Resources/Samples/MicroService/Sample.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/Samples/MicroService/Sample.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Resources/ThirdParty/icu/CMakeLists.txt	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/ThirdParty/icu/CMakeLists.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancFramework/Resources/ThirdParty/icu/Version.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/ThirdParty/icu/Version.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -32,7 +33,7 @@
   set(LIBICU_SUFFIX "l")
 endif()
 
-set(LIBICU_BASE_URL "http://orthanc.osimis.io/ThirdPartyDownloads")
+set(LIBICU_BASE_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads")
 
 if (USE_LEGACY_LIBICU)
   # This is the latest version of icu that compiles without C++11
--- a/OrthancFramework/Resources/ThirdParty/minizip/NOTES	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/ThirdParty/minizip/NOTES	Tue Sep 24 11:39:52 2024 +0200
@@ -1,1 +1,2 @@
-These files come from the "contrib/minizip" directory in zlib 1.2.11.
+These files come from the "contrib/minizip" directory in zlib 1.3+.
+It was last synced on this commit: https://github.com/madler/zlib/commit/73331a6a0481067628f065ffe87bb1d8f787d10c.
--- a/OrthancFramework/Resources/ThirdParty/minizip/crypt.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/ThirdParty/minizip/crypt.h	Tue Sep 24 11:39:52 2024 +0200
@@ -32,12 +32,12 @@
 /***********************************************************************
  * Return the next byte in the pseudo-random sequence
  */
-static int decrypt_byte(unsigned long* pkeys, const z_crc_t* pcrc_32_tab)
-{
+static int decrypt_byte(unsigned long* pkeys, const z_crc_t* pcrc_32_tab) {
     unsigned temp;  /* POTENTIAL BUG:  temp*(temp^1) may overflow in an
                      * unpredictable manner on 16-bit systems; not a problem
                      * with any known compiler so far, though */
 
+    (void)pcrc_32_tab;
     temp = ((unsigned)(*(pkeys+2)) & 0xffff) | 2;
     return (int)(((temp * (temp ^ 1)) >> 8) & 0xff);
 }
@@ -45,8 +45,7 @@
 /***********************************************************************
  * Update the encryption keys with the next byte of plain text
  */
-static int update_keys(unsigned long* pkeys,const z_crc_t* pcrc_32_tab,int c)
-{
+static int update_keys(unsigned long* pkeys, const z_crc_t* pcrc_32_tab, int c) {
     (*(pkeys+0)) = CRC32((*(pkeys+0)), c);
     (*(pkeys+1)) += (*(pkeys+0)) & 0xff;
     (*(pkeys+1)) = (*(pkeys+1)) * 134775813L + 1;
@@ -62,8 +61,7 @@
  * Initialize the encryption keys and the random header according to
  * the given password.
  */
-static void init_keys(const char* passwd,unsigned long* pkeys,const z_crc_t* pcrc_32_tab)
-{
+static void init_keys(const char* passwd, unsigned long* pkeys, const z_crc_t* pcrc_32_tab) {
     *(pkeys+0) = 305419896L;
     *(pkeys+1) = 591751049L;
     *(pkeys+2) = 878082192L;
@@ -77,24 +75,23 @@
     (update_keys(pkeys,pcrc_32_tab,c ^= decrypt_byte(pkeys,pcrc_32_tab)))
 
 #define zencode(pkeys,pcrc_32_tab,c,t) \
-    (t=decrypt_byte(pkeys,pcrc_32_tab), update_keys(pkeys,pcrc_32_tab,c), t^(c))
+    (t=decrypt_byte(pkeys,pcrc_32_tab), update_keys(pkeys,pcrc_32_tab,c), (Byte)t^(c))
 
 #ifdef INCLUDECRYPTINGCODE_IFCRYPTALLOWED
 
 #define RAND_HEAD_LEN  12
    /* "last resort" source for second part of crypt seed pattern */
 #  ifndef ZCR_SEED2
-#    define ZCR_SEED2 3141592654UL     /* use PI as default pattern */
+#    define ZCR_SEED2 3141592654UL      /* use PI as default pattern */
 #  endif
 
-static int crypthead(const char* passwd,      /* password string */
-                     unsigned char* buf,      /* where to write header */
-                     int bufSize,
-                     unsigned long* pkeys,
-                     const z_crc_t* pcrc_32_tab,
-                     unsigned long crcForCrypting)
-{
-    int n;                       /* index in random header */
+static unsigned crypthead(const char* passwd,       /* password string */
+                          unsigned char* buf,       /* where to write header */
+                          int bufSize,
+                          unsigned long* pkeys,
+                          const z_crc_t* pcrc_32_tab,
+                          unsigned long crcForCrypting) {
+    unsigned n;                  /* index in random header */
     int t;                       /* temporary */
     int c;                       /* random byte */
     unsigned char header[RAND_HEAD_LEN-2]; /* random header */
--- a/OrthancFramework/Resources/ThirdParty/minizip/ioapi.c	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/ThirdParty/minizip/ioapi.c	Tue Sep 24 11:39:52 2024 +0200
@@ -14,7 +14,7 @@
         #define _CRT_SECURE_NO_WARNINGS
 #endif
 
-#if defined(__APPLE__) || defined(IOAPI_NO_64)
+#if defined(__APPLE__) || defined(IOAPI_NO_64) || defined(__HAIKU__) || defined(MINIZIP_FOPEN_NO_64)
 // In darwin and perhaps other BSD variants off_t is a 64 bit value, hence no need for specific 64 bit functions
 #define FOPEN_FUNC(filename, mode) fopen(filename, mode)
 #define FTELLO_FUNC(stream) ftello(stream)
@@ -28,8 +28,7 @@
 
 #include "ioapi.h"
 
-voidpf call_zopen64 (const zlib_filefunc64_32_def* pfilefunc,const void*filename,int mode)
-{
+voidpf call_zopen64 (const zlib_filefunc64_32_def* pfilefunc, const void*filename, int mode) {
     if (pfilefunc->zfile_func64.zopen64_file != NULL)
         return (*(pfilefunc->zfile_func64.zopen64_file)) (pfilefunc->zfile_func64.opaque,filename,mode);
     else
@@ -38,8 +37,7 @@
     }
 }
 
-long call_zseek64 (const zlib_filefunc64_32_def* pfilefunc,voidpf filestream, ZPOS64_T offset, int origin)
-{
+long call_zseek64 (const zlib_filefunc64_32_def* pfilefunc,voidpf filestream, ZPOS64_T offset, int origin) {
     if (pfilefunc->zfile_func64.zseek64_file != NULL)
         return (*(pfilefunc->zfile_func64.zseek64_file)) (pfilefunc->zfile_func64.opaque,filestream,offset,origin);
     else
@@ -52,13 +50,12 @@
     }
 }
 
-ZPOS64_T call_ztell64 (const zlib_filefunc64_32_def* pfilefunc,voidpf filestream)
-{
+ZPOS64_T call_ztell64 (const zlib_filefunc64_32_def* pfilefunc, voidpf filestream) {
     if (pfilefunc->zfile_func64.zseek64_file != NULL)
         return (*(pfilefunc->zfile_func64.ztell64_file)) (pfilefunc->zfile_func64.opaque,filestream);
     else
     {
-        uLong tell_uLong = (*(pfilefunc->ztell32_file))(pfilefunc->zfile_func64.opaque,filestream);
+        uLong tell_uLong = (uLong)(*(pfilefunc->ztell32_file))(pfilefunc->zfile_func64.opaque,filestream);
         if ((tell_uLong) == MAXU32)
             return (ZPOS64_T)-1;
         else
@@ -66,11 +63,9 @@
     }
 }
 
-void fill_zlib_filefunc64_32_def_from_filefunc32(zlib_filefunc64_32_def* p_filefunc64_32,const zlib_filefunc_def* p_filefunc32)
-{
+void fill_zlib_filefunc64_32_def_from_filefunc32(zlib_filefunc64_32_def* p_filefunc64_32, const zlib_filefunc_def* p_filefunc32) {
     p_filefunc64_32->zfile_func64.zopen64_file = NULL;
     p_filefunc64_32->zopen32_file = p_filefunc32->zopen_file;
-    p_filefunc64_32->zfile_func64.zerror_file = p_filefunc32->zerror_file;
     p_filefunc64_32->zfile_func64.zread_file = p_filefunc32->zread_file;
     p_filefunc64_32->zfile_func64.zwrite_file = p_filefunc32->zwrite_file;
     p_filefunc64_32->zfile_func64.ztell64_file = NULL;
@@ -84,18 +79,10 @@
 
 
 
-static voidpf  ZCALLBACK fopen_file_func OF((voidpf opaque, const char* filename, int mode));
-static uLong   ZCALLBACK fread_file_func OF((voidpf opaque, voidpf stream, void* buf, uLong size));
-static uLong   ZCALLBACK fwrite_file_func OF((voidpf opaque, voidpf stream, const void* buf,uLong size));
-static ZPOS64_T ZCALLBACK ftell64_file_func OF((voidpf opaque, voidpf stream));
-static long    ZCALLBACK fseek64_file_func OF((voidpf opaque, voidpf stream, ZPOS64_T offset, int origin));
-static int     ZCALLBACK fclose_file_func OF((voidpf opaque, voidpf stream));
-static int     ZCALLBACK ferror_file_func OF((voidpf opaque, voidpf stream));
-
-static voidpf ZCALLBACK fopen_file_func (voidpf opaque, const char* filename, int mode)
-{
+static voidpf ZCALLBACK fopen_file_func(voidpf opaque, const char* filename, int mode) {
     FILE* file = NULL;
     const char* mode_fopen = NULL;
+    (void)opaque;
     if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER)==ZLIB_FILEFUNC_MODE_READ)
         mode_fopen = "rb";
     else
@@ -110,10 +97,10 @@
     return file;
 }
 
-static voidpf ZCALLBACK fopen64_file_func (voidpf opaque, const void* filename, int mode)
-{
+static voidpf ZCALLBACK fopen64_file_func(voidpf opaque, const void* filename, int mode) {
     FILE* file = NULL;
     const char* mode_fopen = NULL;
+    (void)opaque;
     if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER)==ZLIB_FILEFUNC_MODE_READ)
         mode_fopen = "rb";
     else
@@ -129,39 +116,39 @@
 }
 
 
-static uLong ZCALLBACK fread_file_func (voidpf opaque, voidpf stream, void* buf, uLong size)
-{
+static uLong ZCALLBACK fread_file_func(voidpf opaque, voidpf stream, void* buf, uLong size) {
     uLong ret;
+    (void)opaque;
     ret = (uLong)fread(buf, 1, (size_t)size, (FILE *)stream);
     return ret;
 }
 
-static uLong ZCALLBACK fwrite_file_func (voidpf opaque, voidpf stream, const void* buf, uLong size)
-{
+static uLong ZCALLBACK fwrite_file_func(voidpf opaque, voidpf stream, const void* buf, uLong size) {
     uLong ret;
+    (void)opaque;
     ret = (uLong)fwrite(buf, 1, (size_t)size, (FILE *)stream);
     return ret;
 }
 
-static long ZCALLBACK ftell_file_func (voidpf opaque, voidpf stream)
-{
+static long ZCALLBACK ftell_file_func(voidpf opaque, voidpf stream) {
     long ret;
+    (void)opaque;
     ret = ftell((FILE *)stream);
     return ret;
 }
 
 
-static ZPOS64_T ZCALLBACK ftell64_file_func (voidpf opaque, voidpf stream)
-{
+static ZPOS64_T ZCALLBACK ftell64_file_func(voidpf opaque, voidpf stream) {
     ZPOS64_T ret;
-    ret = FTELLO_FUNC((FILE *)stream);
+    (void)opaque;
+    ret = (ZPOS64_T)FTELLO_FUNC((FILE *)stream);
     return ret;
 }
 
-static long ZCALLBACK fseek_file_func (voidpf  opaque, voidpf stream, uLong offset, int origin)
-{
+static long ZCALLBACK fseek_file_func(voidpf opaque, voidpf stream, uLong offset, int origin) {
     int fseek_origin=0;
     long ret;
+    (void)opaque;
     switch (origin)
     {
     case ZLIB_FILEFUNC_SEEK_CUR :
@@ -176,15 +163,15 @@
     default: return -1;
     }
     ret = 0;
-    if (fseek((FILE *)stream, offset, fseek_origin) != 0)
+    if (fseek((FILE *)stream, (long)offset, fseek_origin) != 0)
         ret = -1;
     return ret;
 }
 
-static long ZCALLBACK fseek64_file_func (voidpf  opaque, voidpf stream, ZPOS64_T offset, int origin)
-{
+static long ZCALLBACK fseek64_file_func(voidpf opaque, voidpf stream, ZPOS64_T offset, int origin) {
     int fseek_origin=0;
     long ret;
+    (void)opaque;
     switch (origin)
     {
     case ZLIB_FILEFUNC_SEEK_CUR :
@@ -200,30 +187,28 @@
     }
     ret = 0;
 
-    if(FSEEKO_FUNC((FILE *)stream, offset, fseek_origin) != 0)
+    if(FSEEKO_FUNC((FILE *)stream, (z_off64_t)offset, fseek_origin) != 0)
                         ret = -1;
 
     return ret;
 }
 
 
-static int ZCALLBACK fclose_file_func (voidpf opaque, voidpf stream)
-{
+static int ZCALLBACK fclose_file_func(voidpf opaque, voidpf stream) {
     int ret;
+    (void)opaque;
     ret = fclose((FILE *)stream);
     return ret;
 }
 
-static int ZCALLBACK ferror_file_func (voidpf opaque, voidpf stream)
-{
+static int ZCALLBACK ferror_file_func(voidpf opaque, voidpf stream) {
     int ret;
+    (void)opaque;
     ret = ferror((FILE *)stream);
     return ret;
 }
 
-void fill_fopen_filefunc (pzlib_filefunc_def)
-  zlib_filefunc_def* pzlib_filefunc_def;
-{
+void fill_fopen_filefunc(zlib_filefunc_def* pzlib_filefunc_def) {
     pzlib_filefunc_def->zopen_file = fopen_file_func;
     pzlib_filefunc_def->zread_file = fread_file_func;
     pzlib_filefunc_def->zwrite_file = fwrite_file_func;
@@ -234,8 +219,7 @@
     pzlib_filefunc_def->opaque = NULL;
 }
 
-void fill_fopen64_filefunc (zlib_filefunc64_def*  pzlib_filefunc_def)
-{
+void fill_fopen64_filefunc(zlib_filefunc64_def* pzlib_filefunc_def) {
     pzlib_filefunc_def->zopen64_file = fopen64_file_func;
     pzlib_filefunc_def->zread_file = fread_file_func;
     pzlib_filefunc_def->zwrite_file = fwrite_file_func;
--- a/OrthancFramework/Resources/ThirdParty/minizip/ioapi.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/ThirdParty/minizip/ioapi.h	Tue Sep 24 11:39:52 2024 +0200
@@ -50,7 +50,7 @@
 #define ftello64 ftell
 #define fseeko64 fseek
 #else
-#ifdef __FreeBSD__
+#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__HAIKU__) || defined(MINIZIP_FOPEN_NO_64)
 #define fopen64 fopen
 #define ftello64 ftello
 #define fseeko64 fseeko
@@ -82,7 +82,7 @@
 #include "mz64conf.h"
 #endif
 
-/* a type choosen by DEFINE */
+/* a type chosen by DEFINE */
 #ifdef HAVE_64BIT_INT_CUSTOM
 typedef  64BIT_INT_CUSTOM_TYPE ZPOS64_T;
 #else
@@ -91,8 +91,7 @@
 typedef uint64_t ZPOS64_T;
 #else
 
-/* Maximum unsigned 32-bit value used as placeholder for zip64 */
-#define MAXU32 0xffffffff
+
 
 #if defined(_MSC_VER) || defined(__BORLANDC__)
 typedef unsigned __int64 ZPOS64_T;
@@ -102,7 +101,10 @@
 #endif
 #endif
 
-
+/* Maximum unsigned 32-bit value used as placeholder for zip64 */
+#ifndef MAXU32
+#define MAXU32 (0xffffffff)
+#endif
 
 #ifdef __cplusplus
 extern "C" {
@@ -132,17 +134,17 @@
 
 
 
-typedef voidpf   (ZCALLBACK *open_file_func)      OF((voidpf opaque, const char* filename, int mode));
-typedef uLong    (ZCALLBACK *read_file_func)      OF((voidpf opaque, voidpf stream, void* buf, uLong size));
-typedef uLong    (ZCALLBACK *write_file_func)     OF((voidpf opaque, voidpf stream, const void* buf, uLong size));
-typedef int      (ZCALLBACK *close_file_func)     OF((voidpf opaque, voidpf stream));
-typedef int      (ZCALLBACK *testerror_file_func) OF((voidpf opaque, voidpf stream));
+typedef voidpf   (ZCALLBACK *open_file_func)      (voidpf opaque, const char* filename, int mode);
+typedef uLong    (ZCALLBACK *read_file_func)      (voidpf opaque, voidpf stream, void* buf, uLong size);
+typedef uLong    (ZCALLBACK *write_file_func)     (voidpf opaque, voidpf stream, const void* buf, uLong size);
+typedef int      (ZCALLBACK *close_file_func)     (voidpf opaque, voidpf stream);
+typedef int      (ZCALLBACK *testerror_file_func) (voidpf opaque, voidpf stream);
 
-typedef long     (ZCALLBACK *tell_file_func)      OF((voidpf opaque, voidpf stream));
-typedef long     (ZCALLBACK *seek_file_func)      OF((voidpf opaque, voidpf stream, uLong offset, int origin));
+typedef long     (ZCALLBACK *tell_file_func)      (voidpf opaque, voidpf stream);
+typedef long     (ZCALLBACK *seek_file_func)      (voidpf opaque, voidpf stream, uLong offset, int origin);
 
 
-/* here is the "old" 32 bits structure structure */
+/* here is the "old" 32 bits structure */
 typedef struct zlib_filefunc_def_s
 {
     open_file_func      zopen_file;
@@ -155,9 +157,9 @@
     voidpf              opaque;
 } zlib_filefunc_def;
 
-typedef ZPOS64_T (ZCALLBACK *tell64_file_func)    OF((voidpf opaque, voidpf stream));
-typedef long     (ZCALLBACK *seek64_file_func)    OF((voidpf opaque, voidpf stream, ZPOS64_T offset, int origin));
-typedef voidpf   (ZCALLBACK *open64_file_func)    OF((voidpf opaque, const void* filename, int mode));
+typedef ZPOS64_T (ZCALLBACK *tell64_file_func)    (voidpf opaque, voidpf stream);
+typedef long     (ZCALLBACK *seek64_file_func)    (voidpf opaque, voidpf stream, ZPOS64_T offset, int origin);
+typedef voidpf   (ZCALLBACK *open64_file_func)    (voidpf opaque, const void* filename, int mode);
 
 typedef struct zlib_filefunc64_def_s
 {
@@ -171,8 +173,8 @@
     voidpf              opaque;
 } zlib_filefunc64_def;
 
-void fill_fopen64_filefunc OF((zlib_filefunc64_def* pzlib_filefunc_def));
-void fill_fopen_filefunc OF((zlib_filefunc_def* pzlib_filefunc_def));
+void fill_fopen64_filefunc(zlib_filefunc64_def* pzlib_filefunc_def);
+void fill_fopen_filefunc(zlib_filefunc_def* pzlib_filefunc_def);
 
 /* now internal definition, only for zip.c and unzip.h */
 typedef struct zlib_filefunc64_32_def_s
@@ -191,11 +193,11 @@
 #define ZCLOSE64(filefunc,filestream)             ((*((filefunc).zfile_func64.zclose_file))  ((filefunc).zfile_func64.opaque,filestream))
 #define ZERROR64(filefunc,filestream)             ((*((filefunc).zfile_func64.zerror_file))  ((filefunc).zfile_func64.opaque,filestream))
 
-voidpf call_zopen64 OF((const zlib_filefunc64_32_def* pfilefunc,const void*filename,int mode));
-long    call_zseek64 OF((const zlib_filefunc64_32_def* pfilefunc,voidpf filestream, ZPOS64_T offset, int origin));
-ZPOS64_T call_ztell64 OF((const zlib_filefunc64_32_def* pfilefunc,voidpf filestream));
+voidpf call_zopen64(const zlib_filefunc64_32_def* pfilefunc,const void*filename,int mode);
+long call_zseek64(const zlib_filefunc64_32_def* pfilefunc,voidpf filestream, ZPOS64_T offset, int origin);
+ZPOS64_T call_ztell64(const zlib_filefunc64_32_def* pfilefunc,voidpf filestream);
 
-void    fill_zlib_filefunc64_32_def_from_filefunc32(zlib_filefunc64_32_def* p_filefunc64_32,const zlib_filefunc_def* p_filefunc32);
+void fill_zlib_filefunc64_32_def_from_filefunc32(zlib_filefunc64_32_def* p_filefunc64_32,const zlib_filefunc_def* p_filefunc32);
 
 #define ZOPEN64(filefunc,filename,mode)         (call_zopen64((&(filefunc)),(filename),(mode)))
 #define ZTELL64(filefunc,filestream)            (call_ztell64((&(filefunc)),(filestream)))
--- a/OrthancFramework/Resources/ThirdParty/minizip/unzip.c	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/ThirdParty/minizip/unzip.c	Tue Sep 24 11:39:52 2024 +0200
@@ -49,12 +49,12 @@
         Copyright (C) 2007-2008 Even Rouault
 
 
-        Oct-2009 - Mathias Svensson - Removed cpl_* from symbol names (Even Rouault added them but since this is now moved to a new project (minizip64) I renamed them again).
+  Oct-2009 - Mathias Svensson - Removed cpl_* from symbol names (Even Rouault added them but since this is now moved to a new project (minizip64) I renamed them again).
   Oct-2009 - Mathias Svensson - Fixed problem if uncompressed size was > 4G and compressed size was <4G
                                 should only read the compressed/uncompressed size from the Zip64 format if
                                 the size from normal header was 0xFFFFFFFF
-  Oct-2009 - Mathias Svensson - Applied some bug fixes from paches recived from Gilles Vollant
-        Oct-2009 - Mathias Svensson - Applied support to unzip files with compression mathod BZIP2 (bzip2 lib is required)
+  Oct-2009 - Mathias Svensson - Applied some bug fixes from patches received from Gilles Vollant
+  Oct-2009 - Mathias Svensson - Applied support to unzip files with compression method BZIP2 (bzip2 lib is required)
                                 Patch created by Daniel Borca
 
   Jan-2010 - back to unzip and minizip 1.0 name scheme, with compatibility layer
@@ -77,8 +77,6 @@
 
 #ifdef STDC
 #  include <stddef.h>
-#  include <string.h>
-#  include <stdlib.h>
 #endif
 #ifdef NO_ERRNO_H
     extern int errno;
@@ -111,9 +109,6 @@
 #ifndef ALLOC
 # define ALLOC(size) (malloc(size))
 #endif
-#ifndef TRYFREE
-# define TRYFREE(p) {if (p) free(p);}
-#endif
 
 #define SIZECENTRALDIRITEM (0x2e)
 #define SIZEZIPLOCALHEADER (0x1e)
@@ -153,7 +148,7 @@
     ZPOS64_T rest_read_compressed; /* number of byte to be decompressed */
     ZPOS64_T rest_read_uncompressed;/*number of byte to be obtained after decomp*/
     zlib_filefunc64_32_def z_filefunc;
-    voidpf filestream;        /* io structore of the zipfile */
+    voidpf filestream;        /* io structure of the zipfile */
     uLong compression_method;   /* compression method (0==store) */
     ZPOS64_T byte_before_the_zipfile;/* byte before the zipfile, (>0 for sfx)*/
     int   raw;
@@ -166,7 +161,7 @@
 {
     zlib_filefunc64_32_def z_filefunc;
     int is64bitOpenFunction;
-    voidpf filestream;        /* io structore of the zipfile */
+    voidpf filestream;        /* io structure of the zipfile */
     unz_global_info64 gi;       /* public global information */
     ZPOS64_T byte_before_the_zipfile;/* byte before the zipfile, (>0 for sfx)*/
     ZPOS64_T num_file;             /* number of the current file in the zipfile*/
@@ -197,29 +192,44 @@
 #include "crypt.h"
 #endif
 
+
 /* ===========================================================================
-     Read a byte from a gz_stream; update next_in and avail_in. Return EOF
-   for end of file.
-   IN assertion: the stream s has been successfully opened for reading.
+   Reads a long in LSB order from the given gz_stream. Sets
 */
 
-
-local int unz64local_getByte OF((
-    const zlib_filefunc64_32_def* pzlib_filefunc_def,
-    voidpf filestream,
-    int *pi));
-
-local int unz64local_getByte(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, int *pi)
-{
-    unsigned char c;
-    int err = (int)ZREAD64(*pzlib_filefunc_def,filestream,&c,1);
-    if (err==1)
+local int unz64local_getShort(const zlib_filefunc64_32_def* pzlib_filefunc_def,
+                              voidpf filestream,
+                              uLong *pX) {
+    unsigned char c[2];
+    int err = (int)ZREAD64(*pzlib_filefunc_def,filestream,c,2);
+    if (err==2)
     {
-        *pi = (int)c;
+        *pX = c[0] | ((uLong)c[1] << 8);
         return UNZ_OK;
     }
     else
     {
+        *pX = 0;
+        if (ZERROR64(*pzlib_filefunc_def,filestream))
+            return UNZ_ERRNO;
+        else
+            return UNZ_EOF;
+    }
+}
+
+local int unz64local_getLong(const zlib_filefunc64_32_def* pzlib_filefunc_def,
+                             voidpf filestream,
+                             uLong *pX) {
+    unsigned char c[4];
+    int err = (int)ZREAD64(*pzlib_filefunc_def,filestream,c,4);
+    if (err==4)
+    {
+        *pX = c[0] | ((uLong)c[1] << 8) | ((uLong)c[2] << 16) | ((uLong)c[3] << 24);
+        return UNZ_OK;
+    }
+    else
+    {
+        *pX = 0;
         if (ZERROR64(*pzlib_filefunc_def,filestream))
             return UNZ_ERRNO;
         else
@@ -228,126 +238,29 @@
 }
 
 
-/* ===========================================================================
-   Reads a long in LSB order from the given gz_stream. Sets
-*/
-local int unz64local_getShort OF((
-    const zlib_filefunc64_32_def* pzlib_filefunc_def,
-    voidpf filestream,
-    uLong *pX));
-
-local int unz64local_getShort (const zlib_filefunc64_32_def* pzlib_filefunc_def,
-                             voidpf filestream,
-                             uLong *pX)
-{
-    uLong x ;
-    int i = 0;
-    int err;
-
-    err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
-    x = (uLong)i;
-
-    if (err==UNZ_OK)
-        err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
-    x |= ((uLong)i)<<8;
-
-    if (err==UNZ_OK)
-        *pX = x;
+local int unz64local_getLong64(const zlib_filefunc64_32_def* pzlib_filefunc_def,
+                               voidpf filestream,
+                               ZPOS64_T *pX) {
+    unsigned char c[8];
+    int err = (int)ZREAD64(*pzlib_filefunc_def,filestream,c,8);
+    if (err==8)
+    {
+        *pX = c[0] | ((ZPOS64_T)c[1] << 8) | ((ZPOS64_T)c[2] << 16) | ((ZPOS64_T)c[3] << 24)
+            | ((ZPOS64_T)c[4] << 32) | ((ZPOS64_T)c[5] << 40) | ((ZPOS64_T)c[6] << 48) | ((ZPOS64_T)c[7] << 56);
+        return UNZ_OK;
+    }
     else
+    {
         *pX = 0;
-    return err;
-}
-
-local int unz64local_getLong OF((
-    const zlib_filefunc64_32_def* pzlib_filefunc_def,
-    voidpf filestream,
-    uLong *pX));
-
-local int unz64local_getLong (const zlib_filefunc64_32_def* pzlib_filefunc_def,
-                            voidpf filestream,
-                            uLong *pX)
-{
-    uLong x ;
-    int i = 0;
-    int err;
-
-    err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
-    x = (uLong)i;
-
-    if (err==UNZ_OK)
-        err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
-    x |= ((uLong)i)<<8;
-
-    if (err==UNZ_OK)
-        err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
-    x |= ((uLong)i)<<16;
-
-    if (err==UNZ_OK)
-        err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
-    x += ((uLong)i)<<24;
-
-    if (err==UNZ_OK)
-        *pX = x;
-    else
-        *pX = 0;
-    return err;
-}
-
-local int unz64local_getLong64 OF((
-    const zlib_filefunc64_32_def* pzlib_filefunc_def,
-    voidpf filestream,
-    ZPOS64_T *pX));
-
-
-local int unz64local_getLong64 (const zlib_filefunc64_32_def* pzlib_filefunc_def,
-                            voidpf filestream,
-                            ZPOS64_T *pX)
-{
-    ZPOS64_T x ;
-    int i = 0;
-    int err;
-
-    err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
-    x = (ZPOS64_T)i;
-
-    if (err==UNZ_OK)
-        err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
-    x |= ((ZPOS64_T)i)<<8;
-
-    if (err==UNZ_OK)
-        err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
-    x |= ((ZPOS64_T)i)<<16;
-
-    if (err==UNZ_OK)
-        err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
-    x |= ((ZPOS64_T)i)<<24;
-
-    if (err==UNZ_OK)
-        err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
-    x |= ((ZPOS64_T)i)<<32;
-
-    if (err==UNZ_OK)
-        err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
-    x |= ((ZPOS64_T)i)<<40;
-
-    if (err==UNZ_OK)
-        err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
-    x |= ((ZPOS64_T)i)<<48;
-
-    if (err==UNZ_OK)
-        err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
-    x |= ((ZPOS64_T)i)<<56;
-
-    if (err==UNZ_OK)
-        *pX = x;
-    else
-        *pX = 0;
-    return err;
+        if (ZERROR64(*pzlib_filefunc_def,filestream))
+            return UNZ_ERRNO;
+        else
+            return UNZ_EOF;
+    }
 }
 
 /* My own strcmpi / strcasecmp */
-local int strcmpcasenosensitive_internal (const char* fileName1, const char* fileName2)
-{
+local int strcmpcasenosensitive_internal(const char* fileName1, const char* fileName2) {
     for (;;)
     {
         char c1=*(fileName1++);
@@ -379,19 +292,17 @@
 #endif
 
 /*
-   Compare two filename (fileName1,fileName2).
-   If iCaseSenisivity = 1, comparision is case sensitivity (like strcmp)
-   If iCaseSenisivity = 2, comparision is not case sensitivity (like strcmpi
+   Compare two filenames (fileName1,fileName2).
+   If iCaseSensitivity = 1, comparison is case sensitive (like strcmp)
+   If iCaseSensitivity = 2, comparison is not case sensitive (like strcmpi
                                                                 or strcasecmp)
-   If iCaseSenisivity = 0, case sensitivity is defaut of your operating system
+   If iCaseSensitivity = 0, case sensitivity is default of your operating system
         (like 1 on Unix, 2 on Windows)
 
 */
 extern int ZEXPORT unzStringFileNameCompare (const char*  fileName1,
-                                                 const char*  fileName2,
-                                                 int iCaseSensitivity)
-
-{
+                                             const char*  fileName2,
+                                             int iCaseSensitivity) {
     if (iCaseSensitivity==0)
         iCaseSensitivity=CASESENSITIVITYDEFAULTVALUE;
 
@@ -405,21 +316,23 @@
 #define BUFREADCOMMENT (0x400)
 #endif
 
+#ifndef CENTRALDIRINVALID
+#define CENTRALDIRINVALID ((ZPOS64_T)(-1))
+#endif
+
 /*
   Locate the Central directory of a zipfile (at the end, just before
     the global comment)
 */
-local ZPOS64_T unz64local_SearchCentralDir OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream));
-local ZPOS64_T unz64local_SearchCentralDir(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream)
-{
+local ZPOS64_T unz64local_SearchCentralDir(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream) {
     unsigned char* buf;
     ZPOS64_T uSizeFile;
     ZPOS64_T uBackRead;
     ZPOS64_T uMaxBack=0xffff; /* maximum size of global comment */
-    ZPOS64_T uPosFound=0;
+    ZPOS64_T uPosFound=CENTRALDIRINVALID;
 
     if (ZSEEK64(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0)
-        return 0;
+        return CENTRALDIRINVALID;
 
 
     uSizeFile = ZTELL64(*pzlib_filefunc_def,filestream);
@@ -429,7 +342,7 @@
 
     buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4);
     if (buf==NULL)
-        return 0;
+        return CENTRALDIRINVALID;
 
     uBackRead = 4;
     while (uBackRead<uMaxBack)
@@ -455,14 +368,14 @@
             if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) &&
                 ((*(buf+i+2))==0x05) && ((*(buf+i+3))==0x06))
             {
-                uPosFound = uReadPos+i;
+                uPosFound = uReadPos+(unsigned)i;
                 break;
             }
 
-        if (uPosFound!=0)
+        if (uPosFound!=CENTRALDIRINVALID)
             break;
     }
-    TRYFREE(buf);
+    free(buf);
     return uPosFound;
 }
 
@@ -471,23 +384,18 @@
   Locate the Central directory 64 of a zipfile (at the end, just before
     the global comment)
 */
-local ZPOS64_T unz64local_SearchCentralDir64 OF((
-    const zlib_filefunc64_32_def* pzlib_filefunc_def,
-    voidpf filestream));
-
 local ZPOS64_T unz64local_SearchCentralDir64(const zlib_filefunc64_32_def* pzlib_filefunc_def,
-                                      voidpf filestream)
-{
+                                             voidpf filestream) {
     unsigned char* buf;
     ZPOS64_T uSizeFile;
     ZPOS64_T uBackRead;
     ZPOS64_T uMaxBack=0xffff; /* maximum size of global comment */
-    ZPOS64_T uPosFound=0;
+    ZPOS64_T uPosFound=CENTRALDIRINVALID;
     uLong uL;
                 ZPOS64_T relativeOffset;
 
     if (ZSEEK64(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0)
-        return 0;
+        return CENTRALDIRINVALID;
 
 
     uSizeFile = ZTELL64(*pzlib_filefunc_def,filestream);
@@ -497,7 +405,7 @@
 
     buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4);
     if (buf==NULL)
-        return 0;
+        return CENTRALDIRINVALID;
 
     uBackRead = 4;
     while (uBackRead<uMaxBack)
@@ -523,51 +431,51 @@
             if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) &&
                 ((*(buf+i+2))==0x06) && ((*(buf+i+3))==0x07))
             {
-                uPosFound = uReadPos+i;
+                uPosFound = uReadPos+(unsigned)i;
                 break;
             }
 
-        if (uPosFound!=0)
+        if (uPosFound!=CENTRALDIRINVALID)
             break;
     }
-    TRYFREE(buf);
-    if (uPosFound == 0)
-        return 0;
+    free(buf);
+    if (uPosFound == CENTRALDIRINVALID)
+        return CENTRALDIRINVALID;
 
     /* Zip64 end of central directory locator */
     if (ZSEEK64(*pzlib_filefunc_def,filestream, uPosFound,ZLIB_FILEFUNC_SEEK_SET)!=0)
-        return 0;
+        return CENTRALDIRINVALID;
 
     /* the signature, already checked */
     if (unz64local_getLong(pzlib_filefunc_def,filestream,&uL)!=UNZ_OK)
-        return 0;
+        return CENTRALDIRINVALID;
 
     /* number of the disk with the start of the zip64 end of  central directory */
     if (unz64local_getLong(pzlib_filefunc_def,filestream,&uL)!=UNZ_OK)
-        return 0;
+        return CENTRALDIRINVALID;
     if (uL != 0)
-        return 0;
+        return CENTRALDIRINVALID;
 
     /* relative offset of the zip64 end of central directory record */
     if (unz64local_getLong64(pzlib_filefunc_def,filestream,&relativeOffset)!=UNZ_OK)
-        return 0;
+        return CENTRALDIRINVALID;
 
     /* total number of disks */
     if (unz64local_getLong(pzlib_filefunc_def,filestream,&uL)!=UNZ_OK)
-        return 0;
+        return CENTRALDIRINVALID;
     if (uL != 1)
-        return 0;
+        return CENTRALDIRINVALID;
 
     /* Goto end of central directory record */
     if (ZSEEK64(*pzlib_filefunc_def,filestream, relativeOffset,ZLIB_FILEFUNC_SEEK_SET)!=0)
-        return 0;
+        return CENTRALDIRINVALID;
 
      /* the signature */
     if (unz64local_getLong(pzlib_filefunc_def,filestream,&uL)!=UNZ_OK)
-        return 0;
+        return CENTRALDIRINVALID;
 
     if (uL != 0x06064b50)
-        return 0;
+        return CENTRALDIRINVALID;
 
     return relativeOffset;
 }
@@ -581,19 +489,18 @@
      Else, the return value is a unzFile Handle, usable with other function
        of this unzip package.
 */
-local unzFile unzOpenInternal (const void *path,
-                               zlib_filefunc64_32_def* pzlib_filefunc64_32_def,
-                               int is64bitOpenFunction)
-{
+local unzFile unzOpenInternal(const void *path,
+                              zlib_filefunc64_32_def* pzlib_filefunc64_32_def,
+                              int is64bitOpenFunction) {
     unz64_s us;
     unz64_s *s;
     ZPOS64_T central_pos;
     uLong   uL;
 
     uLong number_disk;          /* number of the current dist, used for
-                                   spaning ZIP, unsupported, always 0*/
+                                   spanning ZIP, unsupported, always 0*/
     uLong number_disk_with_CD;  /* number the the disk with central dir, used
-                                   for spaning ZIP, unsupported, always 0*/
+                                   for spanning ZIP, unsupported, always 0*/
     ZPOS64_T number_entry_CD;      /* total number of entries in
                                    the central dir
                                    (same than number_entry on nospan) */
@@ -621,7 +528,7 @@
         return NULL;
 
     central_pos = unz64local_SearchCentralDir64(&us.z_filefunc,us.filestream);
-    if (central_pos)
+    if (central_pos!=CENTRALDIRINVALID)
     {
         uLong uS;
         ZPOS64_T uL64;
@@ -683,7 +590,7 @@
     else
     {
         central_pos = unz64local_SearchCentralDir(&us.z_filefunc,us.filestream);
-        if (central_pos==0)
+        if (central_pos==CENTRALDIRINVALID)
             err=UNZ_ERRNO;
 
         us.isZip64 = 0;
@@ -762,9 +669,8 @@
 }
 
 
-extern unzFile ZEXPORT unzOpen2 (const char *path,
-                                        zlib_filefunc_def* pzlib_filefunc32_def)
-{
+extern unzFile ZEXPORT unzOpen2(const char *path,
+                                zlib_filefunc_def* pzlib_filefunc32_def) {
     if (pzlib_filefunc32_def != NULL)
     {
         zlib_filefunc64_32_def zlib_filefunc64_32_def_fill;
@@ -775,9 +681,8 @@
         return unzOpenInternal(path, NULL, 0);
 }
 
-extern unzFile ZEXPORT unzOpen2_64 (const void *path,
-                                     zlib_filefunc64_def* pzlib_filefunc_def)
-{
+extern unzFile ZEXPORT unzOpen2_64(const void *path,
+                                   zlib_filefunc64_def* pzlib_filefunc_def) {
     if (pzlib_filefunc_def != NULL)
     {
         zlib_filefunc64_32_def zlib_filefunc64_32_def_fill;
@@ -790,13 +695,11 @@
         return unzOpenInternal(path, NULL, 1);
 }
 
-extern unzFile ZEXPORT unzOpen (const char *path)
-{
+extern unzFile ZEXPORT unzOpen(const char *path) {
     return unzOpenInternal(path, NULL, 0);
 }
 
-extern unzFile ZEXPORT unzOpen64 (const void *path)
-{
+extern unzFile ZEXPORT unzOpen64(const void *path) {
     return unzOpenInternal(path, NULL, 1);
 }
 
@@ -805,8 +708,7 @@
   If there is files inside the .Zip opened with unzOpenCurrentFile (see later),
     these files MUST be closed with unzCloseCurrentFile before call unzClose.
   return UNZ_OK if there is no problem. */
-extern int ZEXPORT unzClose (unzFile file)
-{
+extern int ZEXPORT unzClose(unzFile file) {
     unz64_s* s;
     if (file==NULL)
         return UNZ_PARAMERROR;
@@ -816,7 +718,7 @@
         unzCloseCurrentFile(file);
 
     ZCLOSE64(s->z_filefunc, s->filestream);
-    TRYFREE(s);
+    free(s);
     return UNZ_OK;
 }
 
@@ -825,8 +727,7 @@
   Write info about the ZipFile in the *pglobal_info structure.
   No preparation of the structure is needed
   return UNZ_OK if there is no problem. */
-extern int ZEXPORT unzGetGlobalInfo64 (unzFile file, unz_global_info64* pglobal_info)
-{
+extern int ZEXPORT unzGetGlobalInfo64(unzFile file, unz_global_info64* pglobal_info) {
     unz64_s* s;
     if (file==NULL)
         return UNZ_PARAMERROR;
@@ -835,8 +736,7 @@
     return UNZ_OK;
 }
 
-extern int ZEXPORT unzGetGlobalInfo (unzFile file, unz_global_info* pglobal_info32)
-{
+extern int ZEXPORT unzGetGlobalInfo(unzFile file, unz_global_info* pglobal_info32) {
     unz64_s* s;
     if (file==NULL)
         return UNZ_PARAMERROR;
@@ -847,46 +747,33 @@
     return UNZ_OK;
 }
 /*
-   Translate date/time from Dos format to tm_unz (readable more easilty)
+   Translate date/time from Dos format to tm_unz (readable more easily)
 */
-local void unz64local_DosDateToTmuDate (ZPOS64_T ulDosDate, tm_unz* ptm)
-{
+local void unz64local_DosDateToTmuDate(ZPOS64_T ulDosDate, tm_unz* ptm) {
     ZPOS64_T uDate;
     uDate = (ZPOS64_T)(ulDosDate>>16);
-    ptm->tm_mday = (uInt)(uDate&0x1f) ;
-    ptm->tm_mon =  (uInt)((((uDate)&0x1E0)/0x20)-1) ;
-    ptm->tm_year = (uInt)(((uDate&0x0FE00)/0x0200)+1980) ;
+    ptm->tm_mday = (int)(uDate&0x1f) ;
+    ptm->tm_mon =  (int)((((uDate)&0x1E0)/0x20)-1) ;
+    ptm->tm_year = (int)(((uDate&0x0FE00)/0x0200)+1980) ;
 
-    ptm->tm_hour = (uInt) ((ulDosDate &0xF800)/0x800);
-    ptm->tm_min =  (uInt) ((ulDosDate&0x7E0)/0x20) ;
-    ptm->tm_sec =  (uInt) (2*(ulDosDate&0x1f)) ;
+    ptm->tm_hour = (int) ((ulDosDate &0xF800)/0x800);
+    ptm->tm_min =  (int) ((ulDosDate&0x7E0)/0x20) ;
+    ptm->tm_sec =  (int) (2*(ulDosDate&0x1f)) ;
 }
 
 /*
   Get Info about the current file in the zipfile, with internal only info
 */
-local int unz64local_GetCurrentFileInfoInternal OF((unzFile file,
-                                                  unz_file_info64 *pfile_info,
-                                                  unz_file_info64_internal
-                                                  *pfile_info_internal,
-                                                  char *szFileName,
-                                                  uLong fileNameBufferSize,
-                                                  void *extraField,
-                                                  uLong extraFieldBufferSize,
-                                                  char *szComment,
-                                                  uLong commentBufferSize));
-
-local int unz64local_GetCurrentFileInfoInternal (unzFile file,
-                                                  unz_file_info64 *pfile_info,
-                                                  unz_file_info64_internal
-                                                  *pfile_info_internal,
-                                                  char *szFileName,
-                                                  uLong fileNameBufferSize,
-                                                  void *extraField,
-                                                  uLong extraFieldBufferSize,
-                                                  char *szComment,
-                                                  uLong commentBufferSize)
-{
+local int unz64local_GetCurrentFileInfoInternal(unzFile file,
+                                                unz_file_info64 *pfile_info,
+                                                unz_file_info64_internal
+                                                *pfile_info_internal,
+                                                char *szFileName,
+                                                uLong fileNameBufferSize,
+                                                void *extraField,
+                                                uLong extraFieldBufferSize,
+                                                char *szComment,
+                                                uLong commentBufferSize) {
     unz64_s* s;
     unz_file_info64 file_info;
     unz_file_info64_internal file_info_internal;
@@ -993,7 +880,7 @@
 
         if (lSeek!=0)
         {
-            if (ZSEEK64(s->z_filefunc, s->filestream,lSeek,ZLIB_FILEFUNC_SEEK_CUR)==0)
+            if (ZSEEK64(s->z_filefunc, s->filestream,(ZPOS64_T)lSeek,ZLIB_FILEFUNC_SEEK_CUR)==0)
                 lSeek=0;
             else
                 err=UNZ_ERRNO;
@@ -1018,7 +905,7 @@
 
         if (lSeek!=0)
         {
-            if (ZSEEK64(s->z_filefunc, s->filestream,lSeek,ZLIB_FILEFUNC_SEEK_CUR)==0)
+            if (ZSEEK64(s->z_filefunc, s->filestream,(ZPOS64_T)lSeek,ZLIB_FILEFUNC_SEEK_CUR)==0)
                 lSeek=0;
             else
                 err=UNZ_ERRNO;
@@ -1038,33 +925,31 @@
             /* ZIP64 extra fields */
             if (headerId == 0x0001)
             {
-                                                        uLong uL;
+                if(file_info.uncompressed_size == MAXU32)
+                {
+                    if (unz64local_getLong64(&s->z_filefunc, s->filestream,&file_info.uncompressed_size) != UNZ_OK)
+                        err=UNZ_ERRNO;
+                }
 
-                                                                if(file_info.uncompressed_size == MAXU32)
-                                                                {
-                                                                        if (unz64local_getLong64(&s->z_filefunc, s->filestream,&file_info.uncompressed_size) != UNZ_OK)
-                                                                                        err=UNZ_ERRNO;
-                                                                }
-
-                                                                if(file_info.compressed_size == MAXU32)
-                                                                {
-                                                                        if (unz64local_getLong64(&s->z_filefunc, s->filestream,&file_info.compressed_size) != UNZ_OK)
-                                                                                  err=UNZ_ERRNO;
-                                                                }
+                if(file_info.compressed_size == MAXU32)
+                {
+                    if (unz64local_getLong64(&s->z_filefunc, s->filestream,&file_info.compressed_size) != UNZ_OK)
+                        err=UNZ_ERRNO;
+                }
 
-                                                                if(file_info_internal.offset_curfile == MAXU32)
-                                                                {
-                                                                        /* Relative Header offset */
-                                                                        if (unz64local_getLong64(&s->z_filefunc, s->filestream,&file_info_internal.offset_curfile) != UNZ_OK)
-                                                                                err=UNZ_ERRNO;
-                                                                }
+                if(file_info_internal.offset_curfile == MAXU32)
+                {
+                    /* Relative Header offset */
+                    if (unz64local_getLong64(&s->z_filefunc, s->filestream,&file_info_internal.offset_curfile) != UNZ_OK)
+                        err=UNZ_ERRNO;
+                }
 
-                                                                if(file_info.disk_num_start == MAXU32)
-                                                                {
-                                                                        /* Disk Start Number */
-                                                                        if (unz64local_getLong(&s->z_filefunc, s->filestream,&uL) != UNZ_OK)
-                                                                                err=UNZ_ERRNO;
-                                                                }
+                if(file_info.disk_num_start == 0xffff)
+                {
+                    /* Disk Start Number */
+                    if (unz64local_getLong(&s->z_filefunc, s->filestream,&file_info.disk_num_start) != UNZ_OK)
+                        err=UNZ_ERRNO;
+                }
 
             }
             else
@@ -1090,7 +975,7 @@
 
         if (lSeek!=0)
         {
-            if (ZSEEK64(s->z_filefunc, s->filestream,lSeek,ZLIB_FILEFUNC_SEEK_CUR)==0)
+            if (ZSEEK64(s->z_filefunc, s->filestream,(ZPOS64_T)lSeek,ZLIB_FILEFUNC_SEEK_CUR)==0)
                 lSeek=0;
             else
                 err=UNZ_ERRNO;
@@ -1121,24 +1006,22 @@
   No preparation of the structure is needed
   return UNZ_OK if there is no problem.
 */
-extern int ZEXPORT unzGetCurrentFileInfo64 (unzFile file,
-                                          unz_file_info64 * pfile_info,
-                                          char * szFileName, uLong fileNameBufferSize,
-                                          void *extraField, uLong extraFieldBufferSize,
-                                          char* szComment,  uLong commentBufferSize)
-{
+extern int ZEXPORT unzGetCurrentFileInfo64(unzFile file,
+                                           unz_file_info64 * pfile_info,
+                                           char * szFileName, uLong fileNameBufferSize,
+                                           void *extraField, uLong extraFieldBufferSize,
+                                           char* szComment,  uLong commentBufferSize) {
     return unz64local_GetCurrentFileInfoInternal(file,pfile_info,NULL,
-                                                szFileName,fileNameBufferSize,
-                                                extraField,extraFieldBufferSize,
-                                                szComment,commentBufferSize);
+                                                 szFileName,fileNameBufferSize,
+                                                 extraField,extraFieldBufferSize,
+                                                 szComment,commentBufferSize);
 }
 
-extern int ZEXPORT unzGetCurrentFileInfo (unzFile file,
-                                          unz_file_info * pfile_info,
-                                          char * szFileName, uLong fileNameBufferSize,
-                                          void *extraField, uLong extraFieldBufferSize,
-                                          char* szComment,  uLong commentBufferSize)
-{
+extern int ZEXPORT unzGetCurrentFileInfo(unzFile file,
+                                         unz_file_info * pfile_info,
+                                         char * szFileName, uLong fileNameBufferSize,
+                                         void *extraField, uLong extraFieldBufferSize,
+                                         char* szComment,  uLong commentBufferSize) {
     int err;
     unz_file_info64 file_info64;
     err = unz64local_GetCurrentFileInfoInternal(file,&file_info64,NULL,
@@ -1162,7 +1045,7 @@
         pfile_info->internal_fa = file_info64.internal_fa;
         pfile_info->external_fa = file_info64.external_fa;
 
-        pfile_info->tmu_date = file_info64.tmu_date,
+        pfile_info->tmu_date = file_info64.tmu_date;
 
 
         pfile_info->compressed_size = (uLong)file_info64.compressed_size;
@@ -1175,8 +1058,7 @@
   Set the current file of the zipfile to the first file.
   return UNZ_OK if there is no problem
 */
-extern int ZEXPORT unzGoToFirstFile (unzFile file)
-{
+extern int ZEXPORT unzGoToFirstFile(unzFile file) {
     int err=UNZ_OK;
     unz64_s* s;
     if (file==NULL)
@@ -1196,8 +1078,7 @@
   return UNZ_OK if there is no problem
   return UNZ_END_OF_LIST_OF_FILE if the actual file was the latest.
 */
-extern int ZEXPORT unzGoToNextFile (unzFile  file)
-{
+extern int ZEXPORT unzGoToNextFile(unzFile file) {
     unz64_s* s;
     int err;
 
@@ -1229,8 +1110,7 @@
   UNZ_OK if the file is found. It becomes the current file.
   UNZ_END_OF_LIST_OF_FILE if the file is not found
 */
-extern int ZEXPORT unzLocateFile (unzFile file, const char *szFileName, int iCaseSensitivity)
-{
+extern int ZEXPORT unzLocateFile(unzFile file, const char *szFileName, int iCaseSensitivity) {
     unz64_s* s;
     int err;
 
@@ -1305,8 +1185,7 @@
 } unz_file_pos;
 */
 
-extern int ZEXPORT unzGetFilePos64(unzFile file, unz64_file_pos*  file_pos)
-{
+extern int ZEXPORT unzGetFilePos64(unzFile file, unz64_file_pos* file_pos) {
     unz64_s* s;
 
     if (file==NULL || file_pos==NULL)
@@ -1321,10 +1200,7 @@
     return UNZ_OK;
 }
 
-extern int ZEXPORT unzGetFilePos(
-    unzFile file,
-    unz_file_pos* file_pos)
-{
+extern int ZEXPORT unzGetFilePos(unzFile file, unz_file_pos* file_pos) {
     unz64_file_pos file_pos64;
     int err = unzGetFilePos64(file,&file_pos64);
     if (err==UNZ_OK)
@@ -1335,8 +1211,7 @@
     return err;
 }
 
-extern int ZEXPORT unzGoToFilePos64(unzFile file, const unz64_file_pos* file_pos)
-{
+extern int ZEXPORT unzGoToFilePos64(unzFile file, const unz64_file_pos* file_pos) {
     unz64_s* s;
     int err;
 
@@ -1357,10 +1232,7 @@
     return err;
 }
 
-extern int ZEXPORT unzGoToFilePos(
-    unzFile file,
-    unz_file_pos* file_pos)
-{
+extern int ZEXPORT unzGoToFilePos(unzFile file, unz_file_pos* file_pos) {
     unz64_file_pos file_pos64;
     if (file_pos == NULL)
         return UNZ_PARAMERROR;
@@ -1382,10 +1254,9 @@
   store in *piSizeVar the size of extra info in local header
         (filename and size of extra field data)
 */
-local int unz64local_CheckCurrentFileCoherencyHeader (unz64_s* s, uInt* piSizeVar,
-                                                    ZPOS64_T * poffset_local_extrafield,
-                                                    uInt  * psize_local_extrafield)
-{
+local int unz64local_CheckCurrentFileCoherencyHeader(unz64_s* s, uInt* piSizeVar,
+                                                     ZPOS64_T * poffset_local_extrafield,
+                                                     uInt  * psize_local_extrafield) {
     uLong uMagic,uData,uFlags;
     uLong size_filename;
     uLong size_extra_field;
@@ -1469,9 +1340,8 @@
   Open for reading data the current file in the zipfile.
   If there is no error and the file is opened, the return value is UNZ_OK.
 */
-extern int ZEXPORT unzOpenCurrentFile3 (unzFile file, int* method,
-                                            int* level, int raw, const char* password)
-{
+extern int ZEXPORT unzOpenCurrentFile3(unzFile file, int* method,
+                                       int* level, int raw, const char* password) {
     int err=UNZ_OK;
     uInt iSizeVar;
     unz64_s* s;
@@ -1509,7 +1379,7 @@
 
     if (pfile_in_zip_read_info->read_buffer==NULL)
     {
-        TRYFREE(pfile_in_zip_read_info);
+        free(pfile_in_zip_read_info);
         return UNZ_INTERNALERROR;
     }
 
@@ -1566,7 +1436,8 @@
         pfile_in_zip_read_info->stream_initialised=Z_BZIP2ED;
       else
       {
-        TRYFREE(pfile_in_zip_read_info);
+        free(pfile_in_zip_read_info->read_buffer);
+        free(pfile_in_zip_read_info);
         return err;
       }
 #else
@@ -1586,7 +1457,8 @@
         pfile_in_zip_read_info->stream_initialised=Z_DEFLATED;
       else
       {
-        TRYFREE(pfile_in_zip_read_info);
+        free(pfile_in_zip_read_info->read_buffer);
+        free(pfile_in_zip_read_info);
         return err;
       }
         /* windowBits is passed < 0 to tell that there is no zlib header.
@@ -1638,25 +1510,21 @@
     return UNZ_OK;
 }
 
-extern int ZEXPORT unzOpenCurrentFile (unzFile file)
-{
+extern int ZEXPORT unzOpenCurrentFile(unzFile file) {
     return unzOpenCurrentFile3(file, NULL, NULL, 0, NULL);
 }
 
-extern int ZEXPORT unzOpenCurrentFilePassword (unzFile file, const char*  password)
-{
+extern int ZEXPORT unzOpenCurrentFilePassword(unzFile file, const char* password) {
     return unzOpenCurrentFile3(file, NULL, NULL, 0, password);
 }
 
-extern int ZEXPORT unzOpenCurrentFile2 (unzFile file, int* method, int* level, int raw)
-{
+extern int ZEXPORT unzOpenCurrentFile2(unzFile file, int* method, int* level, int raw) {
     return unzOpenCurrentFile3(file, method, level, raw, NULL);
 }
 
 /** Addition for GDAL : START */
 
-extern ZPOS64_T ZEXPORT unzGetCurrentFileZStreamPos64( unzFile file)
-{
+extern ZPOS64_T ZEXPORT unzGetCurrentFileZStreamPos64(unzFile file) {
     unz64_s* s;
     file_in_zip64_read_info_s* pfile_in_zip_read_info;
     s=(unz64_s*)file;
@@ -1676,13 +1544,12 @@
   buf contain buffer where data must be copied
   len the size of buf.
 
-  return the number of byte copied if somes bytes are copied
+  return the number of byte copied if some bytes are copied
   return 0 if the end of file was reached
   return <0 with error code if there is an error
     (UNZ_ERRNO for IO error, or zLib error for uncompress error)
 */
-extern int ZEXPORT unzReadCurrentFile  (unzFile file, voidp buf, unsigned len)
-{
+extern int ZEXPORT unzReadCurrentFile(unzFile file, voidp buf, unsigned len) {
     int err=UNZ_OK;
     uInt iRead = 0;
     unz64_s* s;
@@ -1767,7 +1634,7 @@
 
             if ((pfile_in_zip_read_info->stream.avail_in == 0) &&
                 (pfile_in_zip_read_info->rest_read_compressed == 0))
-                return (iRead==0) ? UNZ_EOF : iRead;
+                return (iRead==0) ? UNZ_EOF : (int)iRead;
 
             if (pfile_in_zip_read_info->stream.avail_out <
                             pfile_in_zip_read_info->stream.avail_in)
@@ -1857,6 +1724,9 @@
               err = Z_DATA_ERROR;
 
             uTotalOutAfter = pfile_in_zip_read_info->stream.total_out;
+            /* Detect overflow, because z_stream.total_out is uLong (32 bits) */
+            if (uTotalOutAfter<uTotalOutBefore)
+                uTotalOutAfter += 1LL << 32; /* Add maximum value of uLong + 1 */
             uOutThis = uTotalOutAfter-uTotalOutBefore;
 
             pfile_in_zip_read_info->total_out_64 = pfile_in_zip_read_info->total_out_64 + uOutThis;
@@ -1871,14 +1741,14 @@
             iRead += (uInt)(uTotalOutAfter - uTotalOutBefore);
 
             if (err==Z_STREAM_END)
-                return (iRead==0) ? UNZ_EOF : iRead;
+                return (iRead==0) ? UNZ_EOF : (int)iRead;
             if (err!=Z_OK)
                 break;
         }
     }
 
     if (err==Z_OK)
-        return iRead;
+        return (int)iRead;
     return err;
 }
 
@@ -1886,8 +1756,7 @@
 /*
   Give the current position in uncompressed data
 */
-extern z_off_t ZEXPORT unztell (unzFile file)
-{
+extern z_off_t ZEXPORT unztell(unzFile file) {
     unz64_s* s;
     file_in_zip64_read_info_s* pfile_in_zip_read_info;
     if (file==NULL)
@@ -1901,8 +1770,7 @@
     return (z_off_t)pfile_in_zip_read_info->stream.total_out;
 }
 
-extern ZPOS64_T ZEXPORT unztell64 (unzFile file)
-{
+extern ZPOS64_T ZEXPORT unztell64(unzFile file) {
 
     unz64_s* s;
     file_in_zip64_read_info_s* pfile_in_zip_read_info;
@@ -1921,8 +1789,7 @@
 /*
   return 1 if the end of file was reached, 0 elsewhere
 */
-extern int ZEXPORT unzeof (unzFile file)
-{
+extern int ZEXPORT unzeof(unzFile file) {
     unz64_s* s;
     file_in_zip64_read_info_s* pfile_in_zip_read_info;
     if (file==NULL)
@@ -1953,8 +1820,7 @@
   the return value is the number of bytes copied in buf, or (if <0)
     the error code
 */
-extern int ZEXPORT unzGetLocalExtrafield (unzFile file, voidp buf, unsigned len)
-{
+extern int ZEXPORT unzGetLocalExtrafield(unzFile file, voidp buf, unsigned len) {
     unz64_s* s;
     file_in_zip64_read_info_s* pfile_in_zip_read_info;
     uInt read_now;
@@ -2001,8 +1867,7 @@
   Close the file in zip opened with unzOpenCurrentFile
   Return UNZ_CRCERROR if all the file was read but the CRC is not good
 */
-extern int ZEXPORT unzCloseCurrentFile (unzFile file)
-{
+extern int ZEXPORT unzCloseCurrentFile(unzFile file) {
     int err=UNZ_OK;
 
     unz64_s* s;
@@ -2024,7 +1889,7 @@
     }
 
 
-    TRYFREE(pfile_in_zip_read_info->read_buffer);
+    free(pfile_in_zip_read_info->read_buffer);
     pfile_in_zip_read_info->read_buffer = NULL;
     if (pfile_in_zip_read_info->stream_initialised == Z_DEFLATED)
         inflateEnd(&pfile_in_zip_read_info->stream);
@@ -2035,7 +1900,7 @@
 
 
     pfile_in_zip_read_info->stream_initialised = 0;
-    TRYFREE(pfile_in_zip_read_info);
+    free(pfile_in_zip_read_info);
 
     s->pfile_in_zip_read=NULL;
 
@@ -2048,8 +1913,7 @@
   uSizeBuf is the size of the szComment buffer.
   return the number of byte copied or an error code <0
 */
-extern int ZEXPORT unzGetGlobalComment (unzFile file, char * szComment, uLong uSizeBuf)
-{
+extern int ZEXPORT unzGetGlobalComment(unzFile file, char * szComment, uLong uSizeBuf) {
     unz64_s* s;
     uLong uReadThis ;
     if (file==NULL)
@@ -2076,8 +1940,7 @@
 }
 
 /* Additions by RX '2004 */
-extern ZPOS64_T ZEXPORT unzGetOffset64(unzFile file)
-{
+extern ZPOS64_T ZEXPORT unzGetOffset64(unzFile file) {
     unz64_s* s;
 
     if (file==NULL)
@@ -2091,8 +1954,7 @@
     return s->pos_in_central_dir;
 }
 
-extern uLong ZEXPORT unzGetOffset (unzFile file)
-{
+extern uLong ZEXPORT unzGetOffset(unzFile file) {
     ZPOS64_T offset64;
 
     if (file==NULL)
@@ -2101,8 +1963,7 @@
     return (uLong)offset64;
 }
 
-extern int ZEXPORT unzSetOffset64(unzFile file, ZPOS64_T pos)
-{
+extern int ZEXPORT unzSetOffset64(unzFile file, ZPOS64_T pos) {
     unz64_s* s;
     int err;
 
@@ -2119,7 +1980,6 @@
     return err;
 }
 
-extern int ZEXPORT unzSetOffset (unzFile file, uLong pos)
-{
+extern int ZEXPORT unzSetOffset (unzFile file, uLong pos) {
     return unzSetOffset64(file,pos);
 }
--- a/OrthancFramework/Resources/ThirdParty/minizip/unzip.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/ThirdParty/minizip/unzip.h	Tue Sep 24 11:39:52 2024 +0200
@@ -83,12 +83,12 @@
 /* tm_unz contain date/time info */
 typedef struct tm_unz_s
 {
-    uInt tm_sec;            /* seconds after the minute - [0,59] */
-    uInt tm_min;            /* minutes after the hour - [0,59] */
-    uInt tm_hour;           /* hours since midnight - [0,23] */
-    uInt tm_mday;           /* day of the month - [1,31] */
-    uInt tm_mon;            /* months since January - [0,11] */
-    uInt tm_year;           /* years - [1980..2044] */
+    int tm_sec;             /* seconds after the minute - [0,59] */
+    int tm_min;             /* minutes after the hour - [0,59] */
+    int tm_hour;            /* hours since midnight - [0,23] */
+    int tm_mday;            /* day of the month - [1,31] */
+    int tm_mon;             /* months since January - [0,11] */
+    int tm_year;            /* years - [1980..2044] */
 } tm_unz;
 
 /* unz_global_info structure contain global data about the ZIPfile
@@ -150,21 +150,21 @@
     tm_unz tmu_date;
 } unz_file_info;
 
-extern int ZEXPORT unzStringFileNameCompare OF ((const char* fileName1,
-                                                 const char* fileName2,
-                                                 int iCaseSensitivity));
+extern int ZEXPORT unzStringFileNameCompare(const char* fileName1,
+                                            const char* fileName2,
+                                            int iCaseSensitivity);
 /*
-   Compare two filename (fileName1,fileName2).
-   If iCaseSenisivity = 1, comparision is case sensitivity (like strcmp)
-   If iCaseSenisivity = 2, comparision is not case sensitivity (like strcmpi
+   Compare two filenames (fileName1,fileName2).
+   If iCaseSensitivity = 1, comparison is case sensitive (like strcmp)
+   If iCaseSensitivity = 2, comparison is not case sensitive (like strcmpi
                                 or strcasecmp)
-   If iCaseSenisivity = 0, case sensitivity is defaut of your operating system
+   If iCaseSensitivity = 0, case sensitivity is default of your operating system
     (like 1 on Unix, 2 on Windows)
 */
 
 
-extern unzFile ZEXPORT unzOpen OF((const char *path));
-extern unzFile ZEXPORT unzOpen64 OF((const void *path));
+extern unzFile ZEXPORT unzOpen(const char *path);
+extern unzFile ZEXPORT unzOpen64(const void *path);
 /*
   Open a Zip file. path contain the full pathname (by example,
      on a Windows XP computer "c:\\zlib\\zlib113.zip" or on an Unix computer
@@ -181,41 +181,41 @@
 */
 
 
-extern unzFile ZEXPORT unzOpen2 OF((const char *path,
-                                    zlib_filefunc_def* pzlib_filefunc_def));
+extern unzFile ZEXPORT unzOpen2(const char *path,
+                                zlib_filefunc_def* pzlib_filefunc_def);
 /*
    Open a Zip file, like unzOpen, but provide a set of file low level API
       for read/write the zip file (see ioapi.h)
 */
 
-extern unzFile ZEXPORT unzOpen2_64 OF((const void *path,
-                                    zlib_filefunc64_def* pzlib_filefunc_def));
+extern unzFile ZEXPORT unzOpen2_64(const void *path,
+                                   zlib_filefunc64_def* pzlib_filefunc_def);
 /*
    Open a Zip file, like unz64Open, but provide a set of file low level API
       for read/write the zip file (see ioapi.h)
 */
 
-extern int ZEXPORT unzClose OF((unzFile file));
+extern int ZEXPORT unzClose(unzFile file);
 /*
   Close a ZipFile opened with unzOpen.
   If there is files inside the .Zip opened with unzOpenCurrentFile (see later),
     these files MUST be closed with unzCloseCurrentFile before call unzClose.
   return UNZ_OK if there is no problem. */
 
-extern int ZEXPORT unzGetGlobalInfo OF((unzFile file,
-                                        unz_global_info *pglobal_info));
+extern int ZEXPORT unzGetGlobalInfo(unzFile file,
+                                    unz_global_info *pglobal_info);
 
-extern int ZEXPORT unzGetGlobalInfo64 OF((unzFile file,
-                                        unz_global_info64 *pglobal_info));
+extern int ZEXPORT unzGetGlobalInfo64(unzFile file,
+                                      unz_global_info64 *pglobal_info);
 /*
   Write info about the ZipFile in the *pglobal_info structure.
   No preparation of the structure is needed
   return UNZ_OK if there is no problem. */
 
 
-extern int ZEXPORT unzGetGlobalComment OF((unzFile file,
-                                           char *szComment,
-                                           uLong uSizeBuf));
+extern int ZEXPORT unzGetGlobalComment(unzFile file,
+                                       char *szComment,
+                                       uLong uSizeBuf);
 /*
   Get the global comment string of the ZipFile, in the szComment buffer.
   uSizeBuf is the size of the szComment buffer.
@@ -226,22 +226,22 @@
 /***************************************************************************/
 /* Unzip package allow you browse the directory of the zipfile */
 
-extern int ZEXPORT unzGoToFirstFile OF((unzFile file));
+extern int ZEXPORT unzGoToFirstFile(unzFile file);
 /*
   Set the current file of the zipfile to the first file.
   return UNZ_OK if there is no problem
 */
 
-extern int ZEXPORT unzGoToNextFile OF((unzFile file));
+extern int ZEXPORT unzGoToNextFile(unzFile file);
 /*
   Set the current file of the zipfile to the next file.
   return UNZ_OK if there is no problem
   return UNZ_END_OF_LIST_OF_FILE if the actual file was the latest.
 */
 
-extern int ZEXPORT unzLocateFile OF((unzFile file,
-                     const char *szFileName,
-                     int iCaseSensitivity));
+extern int ZEXPORT unzLocateFile(unzFile file,
+                                 const char *szFileName,
+                                 int iCaseSensitivity);
 /*
   Try locate the file szFileName in the zipfile.
   For the iCaseSensitivity signification, see unzStringFileNameCompare
@@ -285,26 +285,26 @@
 
 /* ****************************************** */
 
-extern int ZEXPORT unzGetCurrentFileInfo64 OF((unzFile file,
-                         unz_file_info64 *pfile_info,
-                         char *szFileName,
-                         uLong fileNameBufferSize,
-                         void *extraField,
-                         uLong extraFieldBufferSize,
-                         char *szComment,
-                         uLong commentBufferSize));
+extern int ZEXPORT unzGetCurrentFileInfo64(unzFile file,
+                                           unz_file_info64 *pfile_info,
+                                           char *szFileName,
+                                           uLong fileNameBufferSize,
+                                           void *extraField,
+                                           uLong extraFieldBufferSize,
+                                           char *szComment,
+                                           uLong commentBufferSize);
 
-extern int ZEXPORT unzGetCurrentFileInfo OF((unzFile file,
-                         unz_file_info *pfile_info,
-                         char *szFileName,
-                         uLong fileNameBufferSize,
-                         void *extraField,
-                         uLong extraFieldBufferSize,
-                         char *szComment,
-                         uLong commentBufferSize));
+extern int ZEXPORT unzGetCurrentFileInfo(unzFile file,
+                                         unz_file_info *pfile_info,
+                                         char *szFileName,
+                                         uLong fileNameBufferSize,
+                                         void *extraField,
+                                         uLong extraFieldBufferSize,
+                                         char *szComment,
+                                         uLong commentBufferSize);
 /*
   Get Info about the current file
-  if pfile_info!=NULL, the *pfile_info structure will contain somes info about
+  if pfile_info!=NULL, the *pfile_info structure will contain some info about
         the current file
   if szFileName!=NULL, the filemane string will be copied in szFileName
             (fileNameBufferSize is the size of the buffer)
@@ -318,7 +318,7 @@
 
 /** Addition for GDAL : START */
 
-extern ZPOS64_T ZEXPORT unzGetCurrentFileZStreamPos64 OF((unzFile file));
+extern ZPOS64_T ZEXPORT unzGetCurrentFileZStreamPos64(unzFile file);
 
 /** Addition for GDAL : END */
 
@@ -328,24 +328,24 @@
    from it, and close it (you can close it before reading all the file)
    */
 
-extern int ZEXPORT unzOpenCurrentFile OF((unzFile file));
+extern int ZEXPORT unzOpenCurrentFile(unzFile file);
 /*
   Open for reading data the current file in the zipfile.
   If there is no error, the return value is UNZ_OK.
 */
 
-extern int ZEXPORT unzOpenCurrentFilePassword OF((unzFile file,
-                                                  const char* password));
+extern int ZEXPORT unzOpenCurrentFilePassword(unzFile file,
+                                              const char* password);
 /*
   Open for reading data the current file in the zipfile.
   password is a crypting password
   If there is no error, the return value is UNZ_OK.
 */
 
-extern int ZEXPORT unzOpenCurrentFile2 OF((unzFile file,
-                                           int* method,
-                                           int* level,
-                                           int raw));
+extern int ZEXPORT unzOpenCurrentFile2(unzFile file,
+                                       int* method,
+                                       int* level,
+                                       int raw);
 /*
   Same than unzOpenCurrentFile, but open for read raw the file (not uncompress)
     if raw==1
@@ -355,11 +355,11 @@
          but you CANNOT set method parameter as NULL
 */
 
-extern int ZEXPORT unzOpenCurrentFile3 OF((unzFile file,
-                                           int* method,
-                                           int* level,
-                                           int raw,
-                                           const char* password));
+extern int ZEXPORT unzOpenCurrentFile3(unzFile file,
+                                       int* method,
+                                       int* level,
+                                       int raw,
+                                       const char* password);
 /*
   Same than unzOpenCurrentFile, but open for read raw the file (not uncompress)
     if raw==1
@@ -370,41 +370,41 @@
 */
 
 
-extern int ZEXPORT unzCloseCurrentFile OF((unzFile file));
+extern int ZEXPORT unzCloseCurrentFile(unzFile file);
 /*
   Close the file in zip opened with unzOpenCurrentFile
   Return UNZ_CRCERROR if all the file was read but the CRC is not good
 */
 
-extern int ZEXPORT unzReadCurrentFile OF((unzFile file,
-                      voidp buf,
-                      unsigned len));
+extern int ZEXPORT unzReadCurrentFile(unzFile file,
+                                      voidp buf,
+                                      unsigned len);
 /*
   Read bytes from the current file (opened by unzOpenCurrentFile)
   buf contain buffer where data must be copied
   len the size of buf.
 
-  return the number of byte copied if somes bytes are copied
+  return the number of byte copied if some bytes are copied
   return 0 if the end of file was reached
   return <0 with error code if there is an error
     (UNZ_ERRNO for IO error, or zLib error for uncompress error)
 */
 
-extern z_off_t ZEXPORT unztell OF((unzFile file));
+extern z_off_t ZEXPORT unztell(unzFile file);
 
-extern ZPOS64_T ZEXPORT unztell64 OF((unzFile file));
+extern ZPOS64_T ZEXPORT unztell64(unzFile file);
 /*
   Give the current position in uncompressed data
 */
 
-extern int ZEXPORT unzeof OF((unzFile file));
+extern int ZEXPORT unzeof(unzFile file);
 /*
   return 1 if the end of file was reached, 0 elsewhere
 */
 
-extern int ZEXPORT unzGetLocalExtrafield OF((unzFile file,
-                                             voidp buf,
-                                             unsigned len));
+extern int ZEXPORT unzGetLocalExtrafield(unzFile file,
+                                         voidp buf,
+                                         unsigned len);
 /*
   Read extra field from the current file (opened by unzOpenCurrentFile)
   This is the local-header version of the extra field (sometimes, there is
--- a/OrthancFramework/Resources/ThirdParty/minizip/zip.c	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/ThirdParty/minizip/zip.c	Tue Sep 24 11:39:52 2024 +0200
@@ -14,7 +14,7 @@
    Oct-2009 - Mathias Svensson - Added Zip64 Support when creating new file archives
    Oct-2009 - Mathias Svensson - Did some code cleanup and refactoring to get better overview of some functions.
    Oct-2009 - Mathias Svensson - Added zipRemoveExtraInfoBlock to strip extra field data from its ZIP64 data
-                                 It is used when recreting zip archive with RAW when deleting items from a zip.
+                                 It is used when recreating zip archive with RAW when deleting items from a zip.
                                  ZIP64 data is automatically added to items that needs it, and existing ZIP64 data need to be removed.
    Oct-2009 - Mathias Svensson - Added support for BZIP2 as compression mode (bzip2 lib is required)
    Jan-2010 - back to unzip and minizip 1.0 name scheme, with compatibility layer
@@ -25,14 +25,13 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <stdint.h>
 #include <time.h>
 #include "zlib.h"
 #include "zip.h"
 
 #ifdef STDC
 #  include <stddef.h>
-#  include <string.h>
-#  include <stdlib.h>
 #endif
 #ifdef NO_ERRNO_H
     extern int errno;
@@ -47,7 +46,7 @@
 /* compile with -Dlocal if your debugger can't find static symbols */
 
 #ifndef VERSIONMADEBY
-# define VERSIONMADEBY   (0x0) /* platform depedent */
+# define VERSIONMADEBY   (0x0) /* platform dependent */
 #endif
 
 #ifndef Z_BUFSIZE
@@ -61,9 +60,6 @@
 #ifndef ALLOC
 # define ALLOC(size) (malloc(size))
 #endif
-#ifndef TRYFREE
-# define TRYFREE(p) {if (p) free(p);}
-#endif
 
 /*
 #define SIZECENTRALDIRITEM (0x2e)
@@ -138,37 +134,37 @@
     uInt pos_in_buffered_data;  /* last written byte in buffered_data */
 
     ZPOS64_T pos_local_header;     /* offset of the local header of the file
-                                     currenty writing */
+                                     currently writing */
     char* central_header;       /* central header data for the current file */
     uLong size_centralExtra;
     uLong size_centralheader;   /* size of the central header for cur file */
     uLong size_centralExtraFree; /* Extra bytes allocated to the centralheader but that are not used */
     uLong flag;                 /* flag of the file currently writing */
 
-    int  method;                /* compression method of file currenty wr.*/
+    int  method;                /* compression method of file currently wr.*/
     int  raw;                   /* 1 for directly writing raw data */
     Byte buffered_data[Z_BUFSIZE];/* buffer contain compressed data to be writ*/
     uLong dosDate;
     uLong crc32;
     int  encrypt;
-    int  zip64;               /* Add ZIP64 extened information in the extra field */
+    int  zip64;               /* Add ZIP64 extended information in the extra field */
     ZPOS64_T pos_zip64extrainfo;
     ZPOS64_T totalCompressedData;
     ZPOS64_T totalUncompressedData;
 #ifndef NOCRYPT
     unsigned long keys[3];     /* keys defining the pseudo-random sequence */
     const z_crc_t* pcrc_32_tab;
-    int crypt_header_size;
+    unsigned crypt_header_size;
 #endif
 } curfile64_info;
 
 typedef struct
 {
     zlib_filefunc64_32_def z_filefunc;
-    voidpf filestream;        /* io structore of the zipfile */
+    voidpf filestream;        /* io structure of the zipfile */
     linkedlist_data central_dir;/* datablock with central dir in construction*/
     int  in_opened_file_inzip;  /* 1 if a file in the zip is currently writ.*/
-    curfile64_info ci;            /* info on the file curretly writing */
+    curfile64_info ci;            /* info on the file currently writing */
 
     ZPOS64_T begin_pos;            /* position of the beginning of the zipfile */
     ZPOS64_T add_position_when_writing_offset;
@@ -186,8 +182,7 @@
 #include "crypt.h"
 #endif
 
-local linkedlist_datablock_internal* allocate_new_datablock()
-{
+local linkedlist_datablock_internal* allocate_new_datablock(void) {
     linkedlist_datablock_internal* ldi;
     ldi = (linkedlist_datablock_internal*)
                  ALLOC(sizeof(linkedlist_datablock_internal));
@@ -200,30 +195,26 @@
     return ldi;
 }
 
-local void free_datablock(linkedlist_datablock_internal* ldi)
-{
+local void free_datablock(linkedlist_datablock_internal* ldi) {
     while (ldi!=NULL)
     {
         linkedlist_datablock_internal* ldinext = ldi->next_datablock;
-        TRYFREE(ldi);
+        free(ldi);
         ldi = ldinext;
     }
 }
 
-local void init_linkedlist(linkedlist_data* ll)
-{
+local void init_linkedlist(linkedlist_data* ll) {
     ll->first_block = ll->last_block = NULL;
 }
 
-local void free_linkedlist(linkedlist_data* ll)
-{
+local void free_linkedlist(linkedlist_data* ll) {
     free_datablock(ll->first_block);
     ll->first_block = ll->last_block = NULL;
 }
 
 
-local int add_data_in_datablock(linkedlist_data* ll, const void* buf, uLong len)
-{
+local int add_data_in_datablock(linkedlist_data* ll, const void* buf, uLong len) {
     linkedlist_datablock_internal* ldi;
     const unsigned char* from_copy;
 
@@ -238,7 +229,7 @@
     }
 
     ldi = ll->last_block;
-    from_copy = (unsigned char*)buf;
+    from_copy = (const unsigned char*)buf;
 
     while (len>0)
     {
@@ -283,9 +274,7 @@
    nbByte == 1, 2 ,4 or 8 (byte, short or long, ZPOS64_T)
 */
 
-local int zip64local_putValue OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T x, int nbByte));
-local int zip64local_putValue (const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T x, int nbByte)
-{
+local int zip64local_putValue(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T x, int nbByte) {
     unsigned char buf[8];
     int n;
     for (n = 0; n < nbByte; n++)
@@ -301,15 +290,13 @@
         }
       }
 
-    if (ZWRITE64(*pzlib_filefunc_def,filestream,buf,nbByte)!=(uLong)nbByte)
+    if (ZWRITE64(*pzlib_filefunc_def,filestream,buf,(uLong)nbByte)!=(uLong)nbByte)
         return ZIP_ERRNO;
     else
         return ZIP_OK;
 }
 
-local void zip64local_putValue_inmemory OF((void* dest, ZPOS64_T x, int nbByte));
-local void zip64local_putValue_inmemory (void* dest, ZPOS64_T x, int nbByte)
-{
+local void zip64local_putValue_inmemory (void* dest, ZPOS64_T x, int nbByte) {
     unsigned char* buf=(unsigned char*)dest;
     int n;
     for (n = 0; n < nbByte; n++) {
@@ -329,25 +316,21 @@
 /****************************************************************************/
 
 
-local uLong zip64local_TmzDateToDosDate(const tm_zip* ptm)
-{
+local uLong zip64local_TmzDateToDosDate(const tm_zip* ptm) {
     uLong year = (uLong)ptm->tm_year;
     if (year>=1980)
         year-=1980;
     else if (year>=80)
         year-=80;
     return
-      (uLong) (((ptm->tm_mday) + (32 * (ptm->tm_mon+1)) + (512 * year)) << 16) |
-        ((ptm->tm_sec/2) + (32* ptm->tm_min) + (2048 * (uLong)ptm->tm_hour));
+      (uLong) (((uLong)(ptm->tm_mday) + (32 * (uLong)(ptm->tm_mon+1)) + (512 * year)) << 16) |
+        (((uLong)ptm->tm_sec/2) + (32 * (uLong)ptm->tm_min) + (2048 * (uLong)ptm->tm_hour));
 }
 
 
 /****************************************************************************/
 
-local int zip64local_getByte OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, int *pi));
-
-local int zip64local_getByte(const zlib_filefunc64_32_def* pzlib_filefunc_def,voidpf filestream,int* pi)
-{
+local int zip64local_getByte(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, int* pi) {
     unsigned char c;
     int err = (int)ZREAD64(*pzlib_filefunc_def,filestream,&c,1);
     if (err==1)
@@ -368,10 +351,7 @@
 /* ===========================================================================
    Reads a long in LSB order from the given gz_stream. Sets
 */
-local int zip64local_getShort OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong *pX));
-
-local int zip64local_getShort (const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong* pX)
-{
+local int zip64local_getShort(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong* pX) {
     uLong x ;
     int i = 0;
     int err;
@@ -390,10 +370,7 @@
     return err;
 }
 
-local int zip64local_getLong OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong *pX));
-
-local int zip64local_getLong (const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong* pX)
-{
+local int zip64local_getLong(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong* pX) {
     uLong x ;
     int i = 0;
     int err;
@@ -420,11 +397,8 @@
     return err;
 }
 
-local int zip64local_getLong64 OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T *pX));
 
-
-local int zip64local_getLong64 (const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T *pX)
-{
+local int zip64local_getLong64(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T *pX) {
   ZPOS64_T x;
   int i = 0;
   int err;
@@ -475,10 +449,7 @@
   Locate the Central directory of a zipfile (at the end, just before
     the global comment)
 */
-local ZPOS64_T zip64local_SearchCentralDir OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream));
-
-local ZPOS64_T zip64local_SearchCentralDir(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream)
-{
+local ZPOS64_T zip64local_SearchCentralDir(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream) {
   unsigned char* buf;
   ZPOS64_T uSizeFile;
   ZPOS64_T uBackRead;
@@ -522,14 +493,14 @@
       if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) &&
         ((*(buf+i+2))==0x05) && ((*(buf+i+3))==0x06))
       {
-        uPosFound = uReadPos+i;
+        uPosFound = uReadPos+(unsigned)i;
         break;
       }
 
     if (uPosFound!=0)
       break;
   }
-  TRYFREE(buf);
+  free(buf);
   return uPosFound;
 }
 
@@ -537,10 +508,7 @@
 Locate the End of Zip64 Central directory locator and from there find the CD of a zipfile (at the end, just before
 the global comment)
 */
-local ZPOS64_T zip64local_SearchCentralDir64 OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream));
-
-local ZPOS64_T zip64local_SearchCentralDir64(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream)
-{
+local ZPOS64_T zip64local_SearchCentralDir64(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream) {
   unsigned char* buf;
   ZPOS64_T uSizeFile;
   ZPOS64_T uBackRead;
@@ -586,7 +554,7 @@
       // Signature "0x07064b50" Zip64 end of central directory locater
       if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) && ((*(buf+i+2))==0x06) && ((*(buf+i+3))==0x07))
       {
-        uPosFound = uReadPos+i;
+        uPosFound = uReadPos+(unsigned)i;
         break;
       }
     }
@@ -595,7 +563,7 @@
         break;
   }
 
-  TRYFREE(buf);
+  free(buf);
   if (uPosFound == 0)
     return 0;
 
@@ -637,8 +605,7 @@
   return relativeOffset;
 }
 
-int LoadCentralDirectoryRecord(zip64_internal* pziinit)
-{
+local int LoadCentralDirectoryRecord(zip64_internal* pziinit) {
   int err=ZIP_OK;
   ZPOS64_T byte_before_the_zipfile;/* byte before the zipfile, (>0 for sfx)*/
 
@@ -647,10 +614,10 @@
   ZPOS64_T central_pos;
   uLong uL;
 
-  uLong number_disk;          /* number of the current dist, used for
-                              spaning ZIP, unsupported, always 0*/
-  uLong number_disk_with_CD;  /* number the the disk with central dir, used
-                              for spaning ZIP, unsupported, always 0*/
+  uLong number_disk;          /* number of the current disk, used for
+                              spanning ZIP, unsupported, always 0*/
+  uLong number_disk_with_CD;  /* number of the disk with central dir, used
+                              for spanning ZIP, unsupported, always 0*/
   ZPOS64_T number_entry;
   ZPOS64_T number_entry_CD;      /* total number of entries in
                                 the central dir
@@ -830,7 +797,7 @@
 
       size_central_dir_to_read-=read_this;
     }
-    TRYFREE(buf_read);
+    free(buf_read);
   }
   pziinit->begin_pos = byte_before_the_zipfile;
   pziinit->number_entry = number_entry_CD;
@@ -846,8 +813,7 @@
 
 
 /************************************************************/
-extern zipFile ZEXPORT zipOpen3 (const void *pathname, int append, zipcharpc* globalcomment, zlib_filefunc64_32_def* pzlib_filefunc64_32_def)
-{
+extern zipFile ZEXPORT zipOpen3(const void *pathname, int append, zipcharpc* globalcomment, zlib_filefunc64_32_def* pzlib_filefunc64_32_def) {
     zip64_internal ziinit;
     zip64_internal* zi;
     int err=ZIP_OK;
@@ -905,9 +871,9 @@
     if (err != ZIP_OK)
     {
 #    ifndef NO_ADDFILEINEXISTINGZIP
-        TRYFREE(ziinit.globalcomment);
+        free(ziinit.globalcomment);
 #    endif /* !NO_ADDFILEINEXISTINGZIP*/
-        TRYFREE(zi);
+        free(zi);
         return NULL;
     }
     else
@@ -917,8 +883,7 @@
     }
 }
 
-extern zipFile ZEXPORT zipOpen2 (const char *pathname, int append, zipcharpc* globalcomment, zlib_filefunc_def* pzlib_filefunc32_def)
-{
+extern zipFile ZEXPORT zipOpen2(const char *pathname, int append, zipcharpc* globalcomment, zlib_filefunc_def* pzlib_filefunc32_def) {
     if (pzlib_filefunc32_def != NULL)
     {
         zlib_filefunc64_32_def zlib_filefunc64_32_def_fill;
@@ -929,8 +894,7 @@
         return zipOpen3(pathname, append, globalcomment, NULL);
 }
 
-extern zipFile ZEXPORT zipOpen2_64 (const void *pathname, int append, zipcharpc* globalcomment, zlib_filefunc64_def* pzlib_filefunc_def)
-{
+extern zipFile ZEXPORT zipOpen2_64(const void *pathname, int append, zipcharpc* globalcomment, zlib_filefunc64_def* pzlib_filefunc_def) {
     if (pzlib_filefunc_def != NULL)
     {
         zlib_filefunc64_32_def zlib_filefunc64_32_def_fill;
@@ -945,18 +909,15 @@
 
 
 
-extern zipFile ZEXPORT zipOpen (const char* pathname, int append)
-{
+extern zipFile ZEXPORT zipOpen(const char* pathname, int append) {
     return zipOpen3((const void*)pathname,append,NULL,NULL);
 }
 
-extern zipFile ZEXPORT zipOpen64 (const void* pathname, int append)
-{
+extern zipFile ZEXPORT zipOpen64(const void* pathname, int append) {
     return zipOpen3(pathname,append,NULL,NULL);
 }
 
-int Write_LocalFileHeader(zip64_internal* zi, const char* filename, uInt size_extrafield_local, const void* extrafield_local)
-{
+local int Write_LocalFileHeader(zip64_internal* zi, const char* filename, uInt size_extrafield_local, const void* extrafield_local) {
   /* write the local header */
   int err;
   uInt size_filename = (uInt)strlen(filename);
@@ -1034,8 +995,8 @@
       // Remember position of Zip64 extended info for the local file header. (needed when we update size after done with file)
       zi->ci.pos_zip64extrainfo = ZTELL64(zi->z_filefunc,zi->filestream);
 
-      err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (short)HeaderID,2);
-      err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (short)DataSize,2);
+      err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (ZPOS64_T)HeaderID,2);
+      err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (ZPOS64_T)DataSize,2);
 
       err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (ZPOS64_T)UncompressedSize,8);
       err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (ZPOS64_T)CompressedSize,8);
@@ -1052,14 +1013,13 @@
  It is not done here because then we need to realloc a new buffer since parameters are 'const' and I want to minimize
  unnecessary allocations.
  */
-extern int ZEXPORT zipOpenNewFileInZip4_64 (zipFile file, const char* filename, const zip_fileinfo* zipfi,
-                                         const void* extrafield_local, uInt size_extrafield_local,
-                                         const void* extrafield_global, uInt size_extrafield_global,
-                                         const char* comment, int method, int level, int raw,
-                                         int windowBits,int memLevel, int strategy,
-                                         const char* password, uLong crcForCrypting,
-                                         uLong versionMadeBy, uLong flagBase, int zip64)
-{
+extern int ZEXPORT zipOpenNewFileInZip4_64(zipFile file, const char* filename, const zip_fileinfo* zipfi,
+                                           const void* extrafield_local, uInt size_extrafield_local,
+                                           const void* extrafield_global, uInt size_extrafield_global,
+                                           const char* comment, int method, int level, int raw,
+                                           int windowBits,int memLevel, int strategy,
+                                           const char* password, uLong crcForCrypting,
+                                           uLong versionMadeBy, uLong flagBase, int zip64) {
     zip64_internal* zi;
     uInt size_filename;
     uInt size_comment;
@@ -1083,6 +1043,17 @@
       return ZIP_PARAMERROR;
 #endif
 
+    // The filename and comment length must fit in 16 bits.
+    if ((filename!=NULL) && (strlen(filename)>0xffff))
+        return ZIP_PARAMERROR;
+    if ((comment!=NULL) && (strlen(comment)>0xffff))
+        return ZIP_PARAMERROR;
+    // The extra field length must fit in 16 bits. If the member also requires
+    // a Zip64 extra block, that will also need to fit within that 16-bit
+    // length, but that will be checked for later.
+    if ((size_extrafield_local>0xffff) || (size_extrafield_global>0xffff))
+        return ZIP_PARAMERROR;
+
     zi = (zip64_internal*)file;
 
     if (zi->in_opened_file_inzip == 1)
@@ -1262,35 +1233,33 @@
     return err;
 }
 
-extern int ZEXPORT zipOpenNewFileInZip4 (zipFile file, const char* filename, const zip_fileinfo* zipfi,
-                                         const void* extrafield_local, uInt size_extrafield_local,
-                                         const void* extrafield_global, uInt size_extrafield_global,
-                                         const char* comment, int method, int level, int raw,
-                                         int windowBits,int memLevel, int strategy,
-                                         const char* password, uLong crcForCrypting,
-                                         uLong versionMadeBy, uLong flagBase)
-{
-    return zipOpenNewFileInZip4_64 (file, filename, zipfi,
-                                 extrafield_local, size_extrafield_local,
-                                 extrafield_global, size_extrafield_global,
-                                 comment, method, level, raw,
-                                 windowBits, memLevel, strategy,
-                                 password, crcForCrypting, versionMadeBy, flagBase, 0);
+extern int ZEXPORT zipOpenNewFileInZip4(zipFile file, const char* filename, const zip_fileinfo* zipfi,
+                                        const void* extrafield_local, uInt size_extrafield_local,
+                                        const void* extrafield_global, uInt size_extrafield_global,
+                                        const char* comment, int method, int level, int raw,
+                                        int windowBits,int memLevel, int strategy,
+                                        const char* password, uLong crcForCrypting,
+                                        uLong versionMadeBy, uLong flagBase) {
+    return zipOpenNewFileInZip4_64(file, filename, zipfi,
+                                   extrafield_local, size_extrafield_local,
+                                   extrafield_global, size_extrafield_global,
+                                   comment, method, level, raw,
+                                   windowBits, memLevel, strategy,
+                                   password, crcForCrypting, versionMadeBy, flagBase, 0);
 }
 
-extern int ZEXPORT zipOpenNewFileInZip3 (zipFile file, const char* filename, const zip_fileinfo* zipfi,
-                                         const void* extrafield_local, uInt size_extrafield_local,
-                                         const void* extrafield_global, uInt size_extrafield_global,
-                                         const char* comment, int method, int level, int raw,
-                                         int windowBits,int memLevel, int strategy,
-                                         const char* password, uLong crcForCrypting)
-{
-    return zipOpenNewFileInZip4_64 (file, filename, zipfi,
-                                 extrafield_local, size_extrafield_local,
-                                 extrafield_global, size_extrafield_global,
-                                 comment, method, level, raw,
-                                 windowBits, memLevel, strategy,
-                                 password, crcForCrypting, VERSIONMADEBY, 0, 0);
+extern int ZEXPORT zipOpenNewFileInZip3(zipFile file, const char* filename, const zip_fileinfo* zipfi,
+                                        const void* extrafield_local, uInt size_extrafield_local,
+                                        const void* extrafield_global, uInt size_extrafield_global,
+                                        const char* comment, int method, int level, int raw,
+                                        int windowBits,int memLevel, int strategy,
+                                        const char* password, uLong crcForCrypting) {
+    return zipOpenNewFileInZip4_64(file, filename, zipfi,
+                                   extrafield_local, size_extrafield_local,
+                                   extrafield_global, size_extrafield_global,
+                                   comment, method, level, raw,
+                                   windowBits, memLevel, strategy,
+                                   password, crcForCrypting, VERSIONMADEBY, 0, 0);
 }
 
 extern int ZEXPORT zipOpenNewFileInZip3_64(zipFile file, const char* filename, const zip_fileinfo* zipfi,
@@ -1298,70 +1267,64 @@
                                          const void* extrafield_global, uInt size_extrafield_global,
                                          const char* comment, int method, int level, int raw,
                                          int windowBits,int memLevel, int strategy,
-                                         const char* password, uLong crcForCrypting, int zip64)
-{
-    return zipOpenNewFileInZip4_64 (file, filename, zipfi,
-                                 extrafield_local, size_extrafield_local,
-                                 extrafield_global, size_extrafield_global,
-                                 comment, method, level, raw,
-                                 windowBits, memLevel, strategy,
-                                 password, crcForCrypting, VERSIONMADEBY, 0, zip64);
+                                         const char* password, uLong crcForCrypting, int zip64) {
+    return zipOpenNewFileInZip4_64(file, filename, zipfi,
+                                   extrafield_local, size_extrafield_local,
+                                   extrafield_global, size_extrafield_global,
+                                   comment, method, level, raw,
+                                   windowBits, memLevel, strategy,
+                                   password, crcForCrypting, VERSIONMADEBY, 0, zip64);
 }
 
 extern int ZEXPORT zipOpenNewFileInZip2(zipFile file, const char* filename, const zip_fileinfo* zipfi,
                                         const void* extrafield_local, uInt size_extrafield_local,
                                         const void* extrafield_global, uInt size_extrafield_global,
-                                        const char* comment, int method, int level, int raw)
-{
-    return zipOpenNewFileInZip4_64 (file, filename, zipfi,
-                                 extrafield_local, size_extrafield_local,
-                                 extrafield_global, size_extrafield_global,
-                                 comment, method, level, raw,
-                                 -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
-                                 NULL, 0, VERSIONMADEBY, 0, 0);
+                                        const char* comment, int method, int level, int raw) {
+    return zipOpenNewFileInZip4_64(file, filename, zipfi,
+                                   extrafield_local, size_extrafield_local,
+                                   extrafield_global, size_extrafield_global,
+                                   comment, method, level, raw,
+                                   -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
+                                   NULL, 0, VERSIONMADEBY, 0, 0);
 }
 
 extern int ZEXPORT zipOpenNewFileInZip2_64(zipFile file, const char* filename, const zip_fileinfo* zipfi,
-                                        const void* extrafield_local, uInt size_extrafield_local,
-                                        const void* extrafield_global, uInt size_extrafield_global,
-                                        const char* comment, int method, int level, int raw, int zip64)
-{
-    return zipOpenNewFileInZip4_64 (file, filename, zipfi,
-                                 extrafield_local, size_extrafield_local,
-                                 extrafield_global, size_extrafield_global,
-                                 comment, method, level, raw,
-                                 -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
-                                 NULL, 0, VERSIONMADEBY, 0, zip64);
+                                           const void* extrafield_local, uInt size_extrafield_local,
+                                           const void* extrafield_global, uInt size_extrafield_global,
+                                           const char* comment, int method, int level, int raw, int zip64) {
+    return zipOpenNewFileInZip4_64(file, filename, zipfi,
+                                   extrafield_local, size_extrafield_local,
+                                   extrafield_global, size_extrafield_global,
+                                   comment, method, level, raw,
+                                   -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
+                                   NULL, 0, VERSIONMADEBY, 0, zip64);
 }
 
-extern int ZEXPORT zipOpenNewFileInZip64 (zipFile file, const char* filename, const zip_fileinfo* zipfi,
-                                        const void* extrafield_local, uInt size_extrafield_local,
-                                        const void*extrafield_global, uInt size_extrafield_global,
-                                        const char* comment, int method, int level, int zip64)
-{
-    return zipOpenNewFileInZip4_64 (file, filename, zipfi,
-                                 extrafield_local, size_extrafield_local,
-                                 extrafield_global, size_extrafield_global,
-                                 comment, method, level, 0,
-                                 -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
-                                 NULL, 0, VERSIONMADEBY, 0, zip64);
+extern int ZEXPORT zipOpenNewFileInZip64(zipFile file, const char* filename, const zip_fileinfo* zipfi,
+                                         const void* extrafield_local, uInt size_extrafield_local,
+                                         const void*extrafield_global, uInt size_extrafield_global,
+                                         const char* comment, int method, int level, int zip64) {
+    return zipOpenNewFileInZip4_64(file, filename, zipfi,
+                                   extrafield_local, size_extrafield_local,
+                                   extrafield_global, size_extrafield_global,
+                                   comment, method, level, 0,
+                                   -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
+                                   NULL, 0, VERSIONMADEBY, 0, zip64);
 }
 
-extern int ZEXPORT zipOpenNewFileInZip (zipFile file, const char* filename, const zip_fileinfo* zipfi,
-                                        const void* extrafield_local, uInt size_extrafield_local,
-                                        const void*extrafield_global, uInt size_extrafield_global,
-                                        const char* comment, int method, int level)
-{
-    return zipOpenNewFileInZip4_64 (file, filename, zipfi,
-                                 extrafield_local, size_extrafield_local,
-                                 extrafield_global, size_extrafield_global,
-                                 comment, method, level, 0,
-                                 -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
-                                 NULL, 0, VERSIONMADEBY, 0, 0);
+extern int ZEXPORT zipOpenNewFileInZip(zipFile file, const char* filename, const zip_fileinfo* zipfi,
+                                       const void* extrafield_local, uInt size_extrafield_local,
+                                       const void*extrafield_global, uInt size_extrafield_global,
+                                       const char* comment, int method, int level) {
+    return zipOpenNewFileInZip4_64(file, filename, zipfi,
+                                   extrafield_local, size_extrafield_local,
+                                   extrafield_global, size_extrafield_global,
+                                   comment, method, level, 0,
+                                   -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
+                                   NULL, 0, VERSIONMADEBY, 0, 0);
 }
 
-local int zip64FlushWriteBuffer(zip64_internal* zi)
-{
+local int zip64FlushWriteBuffer(zip64_internal* zi) {
     int err=ZIP_OK;
 
     if (zi->ci.encrypt != 0)
@@ -1399,8 +1362,7 @@
     return err;
 }
 
-extern int ZEXPORT zipWriteInFileInZip (zipFile file,const void* buf,unsigned int len)
-{
+extern int ZEXPORT zipWriteInFileInZip(zipFile file, const void* buf, unsigned int len) {
     zip64_internal* zi;
     int err=ZIP_OK;
 
@@ -1450,7 +1412,7 @@
     else
 #endif
     {
-      zi->ci.stream.next_in = (Bytef*)buf;
+      zi->ci.stream.next_in = (Bytef*)(uintptr_t)buf;
       zi->ci.stream.avail_in = len;
 
       while ((err==ZIP_OK) && (zi->ci.stream.avail_in>0))
@@ -1471,11 +1433,6 @@
           {
               uLong uTotalOutBefore = zi->ci.stream.total_out;
               err=deflate(&zi->ci.stream,  Z_NO_FLUSH);
-              if(uTotalOutBefore > zi->ci.stream.total_out)
-              {
-                int bBreak = 0;
-                bBreak++;
-              }
 
               zi->ci.pos_in_buffered_data += (uInt)(zi->ci.stream.total_out - uTotalOutBefore) ;
           }
@@ -1506,17 +1463,15 @@
     return err;
 }
 
-extern int ZEXPORT zipCloseFileInZipRaw (zipFile file, uLong uncompressed_size, uLong crc32)
-{
+extern int ZEXPORT zipCloseFileInZipRaw(zipFile file, uLong uncompressed_size, uLong crc32) {
     return zipCloseFileInZipRaw64 (file, uncompressed_size, crc32);
 }
 
-extern int ZEXPORT zipCloseFileInZipRaw64 (zipFile file, ZPOS64_T uncompressed_size, uLong crc32)
-{
+extern int ZEXPORT zipCloseFileInZipRaw64(zipFile file, ZPOS64_T uncompressed_size, uLong crc32) {
     zip64_internal* zi;
     ZPOS64_T compressed_size;
     uLong invalidValue = 0xffffffff;
-    short datasize = 0;
+    unsigned datasize = 0;
     int err=ZIP_OK;
 
     if (file == NULL)
@@ -1747,13 +1702,11 @@
     return err;
 }
 
-extern int ZEXPORT zipCloseFileInZip (zipFile file)
-{
+extern int ZEXPORT zipCloseFileInZip(zipFile file) {
     return zipCloseFileInZipRaw (file,0,0);
 }
 
-int Write_Zip64EndOfCentralDirectoryLocator(zip64_internal* zi, ZPOS64_T zip64eocd_pos_inzip)
-{
+local int Write_Zip64EndOfCentralDirectoryLocator(zip64_internal* zi, ZPOS64_T zip64eocd_pos_inzip) {
   int err = ZIP_OK;
   ZPOS64_T pos = zip64eocd_pos_inzip - zi->add_position_when_writing_offset;
 
@@ -1774,8 +1727,7 @@
     return err;
 }
 
-int Write_Zip64EndOfCentralDirectoryRecord(zip64_internal* zi, uLong size_centraldir, ZPOS64_T centraldir_pos_inzip)
-{
+local int Write_Zip64EndOfCentralDirectoryRecord(zip64_internal* zi, uLong size_centraldir, ZPOS64_T centraldir_pos_inzip) {
   int err = ZIP_OK;
 
   uLong Zip64DataSize = 44;
@@ -1813,8 +1765,8 @@
   }
   return err;
 }
-int Write_EndOfCentralDirectoryRecord(zip64_internal* zi, uLong size_centraldir, ZPOS64_T centraldir_pos_inzip)
-{
+
+local int Write_EndOfCentralDirectoryRecord(zip64_internal* zi, uLong size_centraldir, ZPOS64_T centraldir_pos_inzip) {
   int err = ZIP_OK;
 
   /*signature*/
@@ -1861,8 +1813,7 @@
    return err;
 }
 
-int Write_GlobalComment(zip64_internal* zi, const char* global_comment)
-{
+local int Write_GlobalComment(zip64_internal* zi, const char* global_comment) {
   int err = ZIP_OK;
   uInt size_global_comment = 0;
 
@@ -1879,8 +1830,7 @@
   return err;
 }
 
-extern int ZEXPORT zipClose (zipFile file, const char* global_comment)
-{
+extern int ZEXPORT zipClose(zipFile file, const char* global_comment) {
     zip64_internal* zi;
     int err = 0;
     uLong size_centraldir = 0;
@@ -1922,7 +1872,7 @@
     free_linkedlist(&(zi->central_dir));
 
     pos = centraldir_pos_inzip - zi->add_position_when_writing_offset;
-    if(pos >= 0xffffffff || zi->number_entry > 0xFFFF)
+    if(pos >= 0xffffffff || zi->number_entry >= 0xFFFF)
     {
       ZPOS64_T Zip64EOCDpos = ZTELL64(zi->z_filefunc,zi->filestream);
       Write_Zip64EndOfCentralDirectoryRecord(zi, size_centraldir, centraldir_pos_inzip);
@@ -1941,15 +1891,14 @@
             err = ZIP_ERRNO;
 
 #ifndef NO_ADDFILEINEXISTINGZIP
-    TRYFREE(zi->globalcomment);
+    free(zi->globalcomment);
 #endif
-    TRYFREE(zi);
+    free(zi);
 
     return err;
 }
 
-extern int ZEXPORT zipRemoveExtraInfoBlock (char* pData, int* dataLen, short sHeader)
-{
+extern int ZEXPORT zipRemoveExtraInfoBlock(char* pData, int* dataLen, short sHeader) {
   char* p = pData;
   int size = 0;
   char* pNewHeader;
@@ -1959,10 +1908,10 @@
 
   int retVal = ZIP_OK;
 
-  if(pData == NULL || *dataLen < 4)
+  if(pData == NULL || dataLen == NULL || *dataLen < 4)
     return ZIP_PARAMERROR;
 
-  pNewHeader = (char*)ALLOC(*dataLen);
+  pNewHeader = (char*)ALLOC((unsigned)*dataLen);
   pTmp = pNewHeader;
 
   while(p < (pData + *dataLen))
@@ -2001,7 +1950,7 @@
   else
     retVal = ZIP_ERRNO;
 
-  TRYFREE(pNewHeader);
+  free(pNewHeader);
 
   return retVal;
 }
--- a/OrthancFramework/Resources/ThirdParty/minizip/zip.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/ThirdParty/minizip/zip.h	Tue Sep 24 11:39:52 2024 +0200
@@ -88,12 +88,12 @@
 /* tm_zip contain date/time info */
 typedef struct tm_zip_s
 {
-    uInt tm_sec;            /* seconds after the minute - [0,59] */
-    uInt tm_min;            /* minutes after the hour - [0,59] */
-    uInt tm_hour;           /* hours since midnight - [0,23] */
-    uInt tm_mday;           /* day of the month - [1,31] */
-    uInt tm_mon;            /* months since January - [0,11] */
-    uInt tm_year;           /* years - [1980..2044] */
+    int tm_sec;             /* seconds after the minute - [0,59] */
+    int tm_min;             /* minutes after the hour - [0,59] */
+    int tm_hour;            /* hours since midnight - [0,23] */
+    int tm_mday;            /* day of the month - [1,31] */
+    int tm_mon;             /* months since January - [0,11] */
+    int tm_year;            /* years - [1980..2044] */
 } tm_zip;
 
 typedef struct
@@ -113,8 +113,8 @@
 #define APPEND_STATUS_CREATEAFTER   (1)
 #define APPEND_STATUS_ADDINZIP      (2)
 
-extern zipFile ZEXPORT zipOpen OF((const char *pathname, int append));
-extern zipFile ZEXPORT zipOpen64 OF((const void *pathname, int append));
+extern zipFile ZEXPORT zipOpen(const char *pathname, int append);
+extern zipFile ZEXPORT zipOpen64(const void *pathname, int append);
 /*
   Create a zipfile.
      pathname contain on Windows XP a filename like "c:\\zlib\\zlib113.zip" or on
@@ -131,50 +131,55 @@
 
 /* Note : there is no delete function into a zipfile.
    If you want delete file into a zipfile, you must open a zipfile, and create another
-   Of couse, you can use RAW reading and writing to copy the file you did not want delte
+   Of course, you can use RAW reading and writing to copy the file you did not want delete
 */
 
-extern zipFile ZEXPORT zipOpen2 OF((const char *pathname,
-                                   int append,
-                                   zipcharpc* globalcomment,
-                                   zlib_filefunc_def* pzlib_filefunc_def));
+extern zipFile ZEXPORT zipOpen2(const char *pathname,
+                                int append,
+                                zipcharpc* globalcomment,
+                                zlib_filefunc_def* pzlib_filefunc_def);
 
-extern zipFile ZEXPORT zipOpen2_64 OF((const void *pathname,
+extern zipFile ZEXPORT zipOpen2_64(const void *pathname,
                                    int append,
                                    zipcharpc* globalcomment,
-                                   zlib_filefunc64_def* pzlib_filefunc_def));
+                                   zlib_filefunc64_def* pzlib_filefunc_def);
+
+extern zipFile ZEXPORT zipOpen3(const void *pathname,
+                                int append,
+                                zipcharpc* globalcomment,
+                                zlib_filefunc64_32_def* pzlib_filefunc64_32_def);
 
-extern int ZEXPORT zipOpenNewFileInZip OF((zipFile file,
-                       const char* filename,
-                       const zip_fileinfo* zipfi,
-                       const void* extrafield_local,
-                       uInt size_extrafield_local,
-                       const void* extrafield_global,
-                       uInt size_extrafield_global,
-                       const char* comment,
-                       int method,
-                       int level));
+extern int ZEXPORT zipOpenNewFileInZip(zipFile file,
+                                       const char* filename,
+                                       const zip_fileinfo* zipfi,
+                                       const void* extrafield_local,
+                                       uInt size_extrafield_local,
+                                       const void* extrafield_global,
+                                       uInt size_extrafield_global,
+                                       const char* comment,
+                                       int method,
+                                       int level);
 
-extern int ZEXPORT zipOpenNewFileInZip64 OF((zipFile file,
-                       const char* filename,
-                       const zip_fileinfo* zipfi,
-                       const void* extrafield_local,
-                       uInt size_extrafield_local,
-                       const void* extrafield_global,
-                       uInt size_extrafield_global,
-                       const char* comment,
-                       int method,
-                       int level,
-                       int zip64));
+extern int ZEXPORT zipOpenNewFileInZip64(zipFile file,
+                                         const char* filename,
+                                         const zip_fileinfo* zipfi,
+                                         const void* extrafield_local,
+                                         uInt size_extrafield_local,
+                                         const void* extrafield_global,
+                                         uInt size_extrafield_global,
+                                         const char* comment,
+                                         int method,
+                                         int level,
+                                         int zip64);
 
 /*
   Open a file in the ZIP for writing.
   filename : the filename in zip (if NULL, '-' without quote will be used
   *zipfi contain supplemental information
   if extrafield_local!=NULL and size_extrafield_local>0, extrafield_local
-    contains the extrafield data the the local header
+    contains the extrafield data for the local header
   if extrafield_global!=NULL and size_extrafield_global>0, extrafield_global
-    contains the extrafield data the the local header
+    contains the extrafield data for the global header
   if comment != NULL, comment contain the comment string
   method contain the compression method (0 for store, Z_DEFLATED for deflate)
   level contain the level of compression (can be Z_DEFAULT_COMPRESSION)
@@ -184,70 +189,69 @@
 */
 
 
-extern int ZEXPORT zipOpenNewFileInZip2 OF((zipFile file,
-                                            const char* filename,
-                                            const zip_fileinfo* zipfi,
-                                            const void* extrafield_local,
-                                            uInt size_extrafield_local,
-                                            const void* extrafield_global,
-                                            uInt size_extrafield_global,
-                                            const char* comment,
-                                            int method,
-                                            int level,
-                                            int raw));
+extern int ZEXPORT zipOpenNewFileInZip2(zipFile file,
+                                        const char* filename,
+                                        const zip_fileinfo* zipfi,
+                                        const void* extrafield_local,
+                                        uInt size_extrafield_local,
+                                        const void* extrafield_global,
+                                        uInt size_extrafield_global,
+                                        const char* comment,
+                                        int method,
+                                        int level,
+                                        int raw);
 
 
-extern int ZEXPORT zipOpenNewFileInZip2_64 OF((zipFile file,
-                                            const char* filename,
-                                            const zip_fileinfo* zipfi,
-                                            const void* extrafield_local,
-                                            uInt size_extrafield_local,
-                                            const void* extrafield_global,
-                                            uInt size_extrafield_global,
-                                            const char* comment,
-                                            int method,
-                                            int level,
-                                            int raw,
-                                            int zip64));
+extern int ZEXPORT zipOpenNewFileInZip2_64(zipFile file,
+                                           const char* filename,
+                                           const zip_fileinfo* zipfi,
+                                           const void* extrafield_local,
+                                           uInt size_extrafield_local,
+                                           const void* extrafield_global,
+                                           uInt size_extrafield_global,
+                                           const char* comment,
+                                           int method,
+                                           int level,
+                                           int raw,
+                                           int zip64);
 /*
   Same than zipOpenNewFileInZip, except if raw=1, we write raw file
  */
 
-extern int ZEXPORT zipOpenNewFileInZip3 OF((zipFile file,
-                                            const char* filename,
-                                            const zip_fileinfo* zipfi,
-                                            const void* extrafield_local,
-                                            uInt size_extrafield_local,
-                                            const void* extrafield_global,
-                                            uInt size_extrafield_global,
-                                            const char* comment,
-                                            int method,
-                                            int level,
-                                            int raw,
-                                            int windowBits,
-                                            int memLevel,
-                                            int strategy,
-                                            const char* password,
-                                            uLong crcForCrypting));
+extern int ZEXPORT zipOpenNewFileInZip3(zipFile file,
+                                        const char* filename,
+                                        const zip_fileinfo* zipfi,
+                                        const void* extrafield_local,
+                                        uInt size_extrafield_local,
+                                        const void* extrafield_global,
+                                        uInt size_extrafield_global,
+                                        const char* comment,
+                                        int method,
+                                        int level,
+                                        int raw,
+                                        int windowBits,
+                                        int memLevel,
+                                        int strategy,
+                                        const char* password,
+                                        uLong crcForCrypting);
 
-extern int ZEXPORT zipOpenNewFileInZip3_64 OF((zipFile file,
-                                            const char* filename,
-                                            const zip_fileinfo* zipfi,
-                                            const void* extrafield_local,
-                                            uInt size_extrafield_local,
-                                            const void* extrafield_global,
-                                            uInt size_extrafield_global,
-                                            const char* comment,
-                                            int method,
-                                            int level,
-                                            int raw,
-                                            int windowBits,
-                                            int memLevel,
-                                            int strategy,
-                                            const char* password,
-                                            uLong crcForCrypting,
-                                            int zip64
-                                            ));
+extern int ZEXPORT zipOpenNewFileInZip3_64(zipFile file,
+                                           const char* filename,
+                                           const zip_fileinfo* zipfi,
+                                           const void* extrafield_local,
+                                           uInt size_extrafield_local,
+                                           const void* extrafield_global,
+                                           uInt size_extrafield_global,
+                                           const char* comment,
+                                           int method,
+                                           int level,
+                                           int raw,
+                                           int windowBits,
+                                           int memLevel,
+                                           int strategy,
+                                           const char* password,
+                                           uLong crcForCrypting,
+                                           int zip64);
 
 /*
   Same than zipOpenNewFileInZip2, except
@@ -256,47 +260,45 @@
     crcForCrypting : crc of file to compress (needed for crypting)
  */
 
-extern int ZEXPORT zipOpenNewFileInZip4 OF((zipFile file,
-                                            const char* filename,
-                                            const zip_fileinfo* zipfi,
-                                            const void* extrafield_local,
-                                            uInt size_extrafield_local,
-                                            const void* extrafield_global,
-                                            uInt size_extrafield_global,
-                                            const char* comment,
-                                            int method,
-                                            int level,
-                                            int raw,
-                                            int windowBits,
-                                            int memLevel,
-                                            int strategy,
-                                            const char* password,
-                                            uLong crcForCrypting,
-                                            uLong versionMadeBy,
-                                            uLong flagBase
-                                            ));
+extern int ZEXPORT zipOpenNewFileInZip4(zipFile file,
+                                        const char* filename,
+                                        const zip_fileinfo* zipfi,
+                                        const void* extrafield_local,
+                                        uInt size_extrafield_local,
+                                        const void* extrafield_global,
+                                        uInt size_extrafield_global,
+                                        const char* comment,
+                                        int method,
+                                        int level,
+                                        int raw,
+                                        int windowBits,
+                                        int memLevel,
+                                        int strategy,
+                                        const char* password,
+                                        uLong crcForCrypting,
+                                        uLong versionMadeBy,
+                                        uLong flagBase);
 
 
-extern int ZEXPORT zipOpenNewFileInZip4_64 OF((zipFile file,
-                                            const char* filename,
-                                            const zip_fileinfo* zipfi,
-                                            const void* extrafield_local,
-                                            uInt size_extrafield_local,
-                                            const void* extrafield_global,
-                                            uInt size_extrafield_global,
-                                            const char* comment,
-                                            int method,
-                                            int level,
-                                            int raw,
-                                            int windowBits,
-                                            int memLevel,
-                                            int strategy,
-                                            const char* password,
-                                            uLong crcForCrypting,
-                                            uLong versionMadeBy,
-                                            uLong flagBase,
-                                            int zip64
-                                            ));
+extern int ZEXPORT zipOpenNewFileInZip4_64(zipFile file,
+                                           const char* filename,
+                                           const zip_fileinfo* zipfi,
+                                           const void* extrafield_local,
+                                           uInt size_extrafield_local,
+                                           const void* extrafield_global,
+                                           uInt size_extrafield_global,
+                                           const char* comment,
+                                           int method,
+                                           int level,
+                                           int raw,
+                                           int windowBits,
+                                           int memLevel,
+                                           int strategy,
+                                           const char* password,
+                                           uLong crcForCrypting,
+                                           uLong versionMadeBy,
+                                           uLong flagBase,
+                                           int zip64);
 /*
   Same than zipOpenNewFileInZip4, except
     versionMadeBy : value for Version made by field
@@ -304,25 +306,25 @@
  */
 
 
-extern int ZEXPORT zipWriteInFileInZip OF((zipFile file,
-                       const void* buf,
-                       unsigned len));
+extern int ZEXPORT zipWriteInFileInZip(zipFile file,
+                                       const void* buf,
+                                       unsigned len);
 /*
   Write data in the zipfile
 */
 
-extern int ZEXPORT zipCloseFileInZip OF((zipFile file));
+extern int ZEXPORT zipCloseFileInZip(zipFile file);
 /*
   Close the current file in the zipfile
 */
 
-extern int ZEXPORT zipCloseFileInZipRaw OF((zipFile file,
-                                            uLong uncompressed_size,
-                                            uLong crc32));
+extern int ZEXPORT zipCloseFileInZipRaw(zipFile file,
+                                        uLong uncompressed_size,
+                                        uLong crc32);
 
-extern int ZEXPORT zipCloseFileInZipRaw64 OF((zipFile file,
-                                            ZPOS64_T uncompressed_size,
-                                            uLong crc32));
+extern int ZEXPORT zipCloseFileInZipRaw64(zipFile file,
+                                          ZPOS64_T uncompressed_size,
+                                          uLong crc32);
 
 /*
   Close the current file in the zipfile, for file opened with
@@ -330,14 +332,14 @@
   uncompressed_size and crc32 are value for the uncompressed size
 */
 
-extern int ZEXPORT zipClose OF((zipFile file,
-                const char* global_comment));
+extern int ZEXPORT zipClose(zipFile file,
+                            const char* global_comment);
 /*
   Close the zipfile
 */
 
 
-extern int ZEXPORT zipRemoveExtraInfoBlock OF((char* pData, int* dataLen, short sHeader));
+extern int ZEXPORT zipRemoveExtraInfoBlock(char* pData, int* dataLen, short sHeader);
 /*
   zipRemoveExtraInfoBlock -  Added by Mathias Svensson
 
--- a/OrthancFramework/Resources/Toolchains/CrossToolchain.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/Toolchains/CrossToolchain.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancFramework/Resources/Toolchains/MinGW-W64-Toolchain32.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/Toolchains/MinGW-W64-Toolchain32.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancFramework/Resources/Toolchains/MinGW-W64-Toolchain64.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/Toolchains/MinGW-W64-Toolchain64.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancFramework/Resources/Toolchains/MinGWToolchain.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/Toolchains/MinGWToolchain.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancFramework/Resources/WindowsResources.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/WindowsResources.py	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancFramework/Resources/WindowsResources.rc	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Resources/WindowsResources.rc	Tue Sep 24 11:39:52 2024 +0200
@@ -15,8 +15,8 @@
             VALUE "FileDescription", "${DESCRIPTION}"
             VALUE "FileVersion", "${VERSION_MAJOR}.${VERSION_MINOR}.0.${VERSION_PATCH}"
             VALUE "InternalName", "${PRODUCT}"
-            VALUE "LegalCopyright", "(c) 2012-${YEAR}, Sebastien Jodogne, University Hospital of Liege, Osimis S.A., and ICTEAM UCLouvain"
-            VALUE "LegalTrademarks", "Licensing information is available at http://www.orthanc-server.com/"
+            VALUE "LegalCopyright", "(c) 2012-${YEAR}, Sebastien Jodogne, University Hospital of Liege, Osimis S.A., Orthanc Team SRL, and ICTEAM UCLouvain"
+            VALUE "LegalTrademarks", "Licensing information is available at https://orthanc.uclouvain.be/book/faq/licensing.html"
             VALUE "OriginalFilename", "${FILENAME}"
             VALUE "ProductName", "${PRODUCT}"
             VALUE "ProductVersion", "${VERSION_MAJOR}.${VERSION_MINOR}"
--- a/OrthancFramework/SharedLibrary/CMakeLists.txt	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/SharedLibrary/CMakeLists.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -257,6 +258,8 @@
     ${ORTHANC_DICOM_SOURCES}
     )
 
+  DefineSourceBasenameForTarget(OrthancFramework)
+
   # CMake does not natively handle SIDE_MODULE, and believes that
   # Emscripten produces a ".js" file (whereas it creates only the
   # ".wasm"). Create a dummy ".js" for target to work.
@@ -273,6 +276,8 @@
       DllMain.cpp
       )
 
+    DefineSourceBasenameForTarget(OrthancFramework)
+
     # By default, hide all the symbols
     set_target_properties(OrthancFramework PROPERTIES C_VISIBILITY_PRESET hidden)
     set_target_properties(OrthancFramework PROPERTIES CXX_VISIBILITY_PRESET hidden)
@@ -301,10 +306,14 @@
       ${ORTHANC_DICOM_SOURCES}
       )
 
+    DefineSourceBasenameForTarget(OrthancFramework)
+
     # Add the "-fPIC" option to use the static library from Orthanc
     # plugins (the latter being shared libraries)
     set_property(TARGET OrthancFramework PROPERTY POSITION_INDEPENDENT_CODE ON)
   endif()
+
+  DefineSourceBasenameForTarget(OrthancFramework)
 endif()
 
 
--- a/OrthancFramework/SharedLibrary/DllMain.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/SharedLibrary/DllMain.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -27,35 +28,55 @@
    ${BOOST_SOURCES_DIR}/libs/thread/src/win32/tss_dll.cpp
    ${OPENSSL_SOURCES_DIR}/crypto/dllmain.c
 
- **/
+**/
 
 #if defined(_WIN32) || defined(__CYGWIN__)
-# ifdef __CYGWIN__
-#  include <windows.h>
-# endif
+
+#include <boost/thread/detail/tss_hooks.hpp>
+
+#include <windows.h>
+
+#include <crypto/cryptlib.h>
 
-#include "e_os.h"
-#include "crypto/cryptlib.h"
-
-BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
+#if defined(__BORLANDC__)
+extern "C" BOOL WINAPI DllEntryPoint(HINSTANCE /*hInstance*/, DWORD dwReason, LPVOID /*lpReserved*/)
+#elif defined(_WIN32_WCE)
+  extern "C" BOOL WINAPI DllMain(HANDLE /*hInstance*/, DWORD dwReason, LPVOID /*lpReserved*/)
+#else
+  extern "C" BOOL WINAPI DllMain(HINSTANCE /*hInstance*/, DWORD dwReason, LPVOID /*lpReserved*/)
+#endif
 {
-  switch (fdwReason)
+  switch(dwReason)
   {
     case DLL_PROCESS_ATTACH:
+    {
       //OPENSSL_cpuid_setup();  // TODO - Is this necessary?
+      boost::on_process_enter();
+      boost::on_thread_enter();
       break;
-        
+    }
+
     case DLL_THREAD_ATTACH:
+    {
+      boost::on_thread_enter();
       break;
-        
+    }
+
     case DLL_THREAD_DETACH:
+    {
       OPENSSL_thread_stop();
+      boost::on_thread_exit();
       break;
-        
+    }
+
     case DLL_PROCESS_DETACH:
+    {
+      boost::on_thread_exit();
+      boost::on_process_exit();
       break;
+    }
   }
-    
+
   return TRUE;
 }
 
--- a/OrthancFramework/SharedLibrary/OrthancFramework.h.in	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/SharedLibrary/OrthancFramework.h.in	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Cache/ICachePageProvider.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Cache/ICachePageProvider.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Cache/ICacheable.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Cache/ICacheable.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Cache/LeastRecentlyUsedIndex.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Cache/LeastRecentlyUsedIndex.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Cache/MemoryCache.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryCache.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Cache/MemoryCache.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryCache.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Cache/MemoryObjectCache.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryObjectCache.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Cache/MemoryObjectCache.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryObjectCache.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Cache/MemoryStringCache.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryStringCache.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -53,47 +54,235 @@
     }      
   };
 
+
+  MemoryStringCache::Accessor::Accessor(MemoryStringCache& cache)
+  : cache_(cache),
+    shouldAdd_(false)
+  {
+  }
+
+
+  MemoryStringCache::Accessor::~Accessor()
+  {
+    // if this accessor was the one in charge of loading and adding the data into the cache
+    // and it failed to add, remove the key from the list to make sure others accessor
+    // stop waiting for it.
+    if (shouldAdd_)
+    {
+      cache_.RemoveFromItemsBeingLoaded(keyToAdd_);
+    }
+  }
+
+
+  bool MemoryStringCache::Accessor::Fetch(std::string& value, const std::string& key)
+  {
+    // if multiple accessors are fetching at the same time:
+    // the first one will return false and will be in charge of adding to the cache.
+    // others will wait.
+    // if the first one fails to add, or, if the content was too large to fit in the cache,
+    // the next one will be in charge of adding ...
+    if (!cache_.Fetch(value, key))
+    {
+      shouldAdd_ = true;
+      keyToAdd_ = key;
+      return false;
+    }
+
+    shouldAdd_ = false;
+    keyToAdd_.clear();
+
+    return true;
+  }
+
+
+  void MemoryStringCache::Accessor::Add(const std::string& key, const std::string& value)
+  {
+    cache_.Add(key, value);
+    shouldAdd_ = false;
+  }
+
+
+  void MemoryStringCache::Accessor::Add(const std::string& key, const char* buffer, size_t size)
+  {
+    cache_.Add(key, buffer, size);
+    shouldAdd_ = false;
+  }
+
+
+  MemoryStringCache::MemoryStringCache() :
+    currentSize_(0),
+    maxSize_(100 * 1024 * 1024)  // 100 MB
+  {
+  }
+
+
+  MemoryStringCache::~MemoryStringCache()
+  {
+    Recycle(0);
+    assert(content_.IsEmpty());
+  }
+
+
   size_t MemoryStringCache::GetMaximumSize()
   {
-    return cache_.GetMaximumSize();
+    return maxSize_;
   }
 
+
   void MemoryStringCache::SetMaximumSize(size_t size)
   {
-    cache_.SetMaximumSize(size);
+    if (size == 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+      
+    // // Make sure no accessor is currently open (as its data may be
+    // // removed if recycling is needed)
+    // WriterLock contentLock(contentMutex_);
+
+    // Lock the global structure of the cache
+    boost::mutex::scoped_lock cacheLock(cacheMutex_);
+
+    Recycle(size);
+    maxSize_ = size;
   }
 
+
   void MemoryStringCache::Add(const std::string& key,
-                              const std::string& value)
+                               const std::string& value)
   {
-    cache_.Acquire(key, new StringValue(value));
+    std::unique_ptr<StringValue> item(new StringValue(value));
+    size_t size = value.size();
+
+    boost::mutex::scoped_lock cacheLock(cacheMutex_);
+
+    if (size > maxSize_)
+    {
+      // This object is too large to be stored in the cache, discard it
+    }
+    else if (content_.Contains(key))
+    {
+      // Value already stored, don't overwrite the old value but put it on top of the cache
+      content_.MakeMostRecent(key);
+    }
+    else
+    {
+      Recycle(maxSize_ - size);   // Post-condition: currentSize_ <= maxSize_ - size
+      assert(currentSize_ + size <= maxSize_);
+
+      content_.Add(key, item.release());
+      currentSize_ += size;
+    }
+
+    RemoveFromItemsBeingLoadedInternal(key);
   }
 
+
   void MemoryStringCache::Add(const std::string& key,
                               const void* buffer,
                               size_t size)
   {
-    cache_.Acquire(key, new StringValue(reinterpret_cast<const char*>(buffer), size));
+    Add(key, std::string(reinterpret_cast<const char*>(buffer), size));
   }
 
+
   void MemoryStringCache::Invalidate(const std::string &key)
   {
-    cache_.Invalidate(key);
+    boost::mutex::scoped_lock cacheLock(cacheMutex_);
+
+    StringValue* item = NULL;
+    if (content_.Contains(key, item))
+    {
+      assert(item != NULL);
+      const size_t size = item->GetMemoryUsage();
+      delete item;
+
+      content_.Invalidate(key);
+          
+      assert(currentSize_ >= size);
+      currentSize_ -= size;
+    }
+
+    RemoveFromItemsBeingLoadedInternal(key);
   }
-  
+
+
   bool MemoryStringCache::Fetch(std::string& value,
                                 const std::string& key)
   {
-    MemoryObjectCache::Accessor reader(cache_, key, false /* multiple readers are allowed */);
+    boost::mutex::scoped_lock cacheLock(cacheMutex_);
+
+    StringValue* item;
 
-    if (reader.IsValid())
+    // if another client is currently loading the item, wait for it.
+    while (itemsBeingLoaded_.find(key) != itemsBeingLoaded_.end() && !content_.Contains(key, item))
     {
-      value = dynamic_cast<StringValue&>(reader.GetValue()).GetContent();
+      cacheCond_.wait(cacheLock);
+    }
+
+    if (content_.Contains(key, item))
+    {
+      value = dynamic_cast<StringValue&>(*item).GetContent();
+      content_.MakeMostRecent(key);
+
       return true;
     }
     else
     {
+      // note that this accessor will be in charge of loading and adding.
+      itemsBeingLoaded_.insert(key);
       return false;
     }
   }
+
+
+  void MemoryStringCache::RemoveFromItemsBeingLoaded(const std::string& key)
+  {
+    boost::mutex::scoped_lock cacheLock(cacheMutex_);
+    RemoveFromItemsBeingLoadedInternal(key);
+  }
+
+
+  void MemoryStringCache::RemoveFromItemsBeingLoadedInternal(const std::string& key)
+  {
+    // notify all waiting users, some of them potentially waiting for this item
+    itemsBeingLoaded_.erase(key);
+    cacheCond_.notify_all();
+  }
+
+  void MemoryStringCache::Recycle(size_t targetSize)
+  {
+    // WARNING: "cacheMutex_" must be locked
+    while (currentSize_ > targetSize)
+    {
+      assert(!content_.IsEmpty());
+        
+      StringValue* item = NULL;
+      content_.RemoveOldest(item);
+
+      assert(item != NULL);
+      const size_t size = item->GetMemoryUsage();
+      delete item;
+
+      assert(currentSize_ >= size);
+      currentSize_ -= size;
+    }
+
+    // Post-condition: "currentSize_ <= targetSize"
+  }
+
+  size_t MemoryStringCache::GetCurrentSize() const
+  {
+    boost::mutex::scoped_lock cacheLock(cacheMutex_);
+
+    return currentSize_;
+  }
+    
+  size_t MemoryStringCache::GetNumberOfItems() const
+  {
+    boost::mutex::scoped_lock cacheLock(cacheMutex_);
+    return content_.GetSize();
+
+  }
+
 }
--- a/OrthancFramework/Sources/Cache/MemoryStringCache.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryStringCache.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -23,28 +24,79 @@
 
 #pragma once
 
-#include "MemoryObjectCache.h"
+#include "../OrthancFramework.h"
+#include "ICacheable.h"
+#include "LeastRecentlyUsedIndex.h"
+
+#include <boost/thread/condition_variable.hpp>
+#include <boost/thread/mutex.hpp>
+
 
 namespace Orthanc
 {
   /**
-   * Facade object around "MemoryObjectCache" that caches a dictionary
+   * Class that caches a dictionary
    * of strings, using the "fetch/add" paradigm of memcached.
    * 
+   * Starting from 1.12.2, if multiple clients are trying to access
+   * an inexistent item at the same time, only one of them will load it
+   * and the others will wait until the first one has loaded the data.
+   * 
+   * The MemoryStringCache is only accessible through an Accessor.
+   * 
    * Note: this class is thread safe
    **/
   class ORTHANC_PUBLIC MemoryStringCache : public boost::noncopyable
   {
+  public:
+    class ORTHANC_PUBLIC Accessor : public boost::noncopyable
+    {
+    protected:
+      MemoryStringCache& cache_;
+
+    private:
+      bool                shouldAdd_;  // when this accessor is the one who should load and add the data
+      std::string         keyToAdd_;
+
+
+    public:
+      explicit Accessor(MemoryStringCache& cache);
+      ~Accessor();
+
+      bool Fetch(std::string& value, const std::string& key);
+      void Add(const std::string& key, const std::string& value);
+      void Add(const std::string& key,const char* buffer, size_t size);
+    };
+
   private:
     class StringValue;
 
-    MemoryObjectCache  cache_;
+    mutable boost::mutex      cacheMutex_;  // note: we can not use recursive_mutex with condition_variable
+    boost::condition_variable cacheCond_;
+    std::set<std::string>     itemsBeingLoaded_;
+
+    size_t currentSize_;
+    size_t maxSize_;
+    LeastRecentlyUsedIndex<std::string, StringValue*>  content_;
+
+    void Recycle(size_t targetSize);
 
   public:
+    MemoryStringCache();
+
+    ~MemoryStringCache();
+
     size_t GetMaximumSize();
     
     void SetMaximumSize(size_t size);
 
+    void Invalidate(const std::string& key);
+
+    size_t GetCurrentSize() const;
+    
+    size_t GetNumberOfItems() const;
+
+  private:
     void Add(const std::string& key,
              const std::string& value);
 
@@ -52,9 +104,12 @@
              const void* buffer,
              size_t size);
 
-    void Invalidate(const std::string& key);
-
     bool Fetch(std::string& value,
                const std::string& key);
+
+    void RemoveFromItemsBeingLoaded(const std::string& key);
+    void RemoveFromItemsBeingLoadedInternal(const std::string& key);
+
+    void AddToItemsBeingLoadedInternal(const std::string& key);
   };
 }
--- a/OrthancFramework/Sources/Cache/SharedArchive.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Cache/SharedArchive.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -102,7 +103,7 @@
 
   std::string SharedArchive::Add(IDynamicObject* obj)
   {
-    boost::mutex::scoped_lock lock(mutex_);
+    boost::recursive_mutex::scoped_lock lock(mutex_);
 
     if (archive_.size() == maxSize_)
     {
@@ -122,7 +123,7 @@
 
   void SharedArchive::Remove(const std::string& id)
   {
-    boost::mutex::scoped_lock lock(mutex_);
+    boost::recursive_mutex::scoped_lock lock(mutex_);
     RemoveInternal(id);      
   }
 
@@ -132,7 +133,7 @@
     items.clear();
 
     {
-      boost::mutex::scoped_lock lock(mutex_);
+      boost::recursive_mutex::scoped_lock lock(mutex_);
 
       for (Archive::const_iterator it = archive_.begin();
            it != archive_.end(); ++it)
--- a/OrthancFramework/Sources/Cache/SharedArchive.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Cache/SharedArchive.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -44,9 +45,9 @@
   private:
     typedef std::map<std::string, IDynamicObject*>  Archive;
 
-    size_t         maxSize_;
-    boost::mutex   mutex_;
-    Archive        archive_;
+    size_t                  maxSize_;
+    boost::recursive_mutex  mutex_;
+    Archive                 archive_;
     LeastRecentlyUsedIndex<std::string> lru_;
 
     void RemoveInternal(const std::string& id);
@@ -55,8 +56,8 @@
     class ORTHANC_PUBLIC Accessor : public boost::noncopyable
     {
     private:
-      boost::mutex::scoped_lock  lock_;
-      IDynamicObject*            item_;
+      boost::recursive_mutex::scoped_lock lock_;
+      IDynamicObject*                     item_;
 
     public:
       Accessor(SharedArchive& that,
--- a/OrthancFramework/Sources/ChunkedBuffer.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/ChunkedBuffer.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/ChunkedBuffer.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/ChunkedBuffer.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Compatibility.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Compatibility.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Compression/DeflateBaseCompressor.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Compression/DeflateBaseCompressor.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Compression/DeflateBaseCompressor.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Compression/DeflateBaseCompressor.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Compression/GzipCompressor.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Compression/GzipCompressor.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Compression/GzipCompressor.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Compression/GzipCompressor.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Compression/HierarchicalZipWriter.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Compression/HierarchicalZipWriter.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Compression/HierarchicalZipWriter.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Compression/HierarchicalZipWriter.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Compression/IBufferCompressor.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Compression/IBufferCompressor.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Compression/IBufferCompressor.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Compression/IBufferCompressor.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Compression/ZipReader.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Compression/ZipReader.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Compression/ZipReader.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Compression/ZipReader.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Compression/ZipWriter.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Compression/ZipWriter.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -553,7 +554,7 @@
       if (!pimpl_->file_)
       {
         throw OrthancException(ErrorCode_CannotWriteFile,
-                               "Cannot create new ZIP archive: " + path_);
+                               "Cannot create new ZIP archive");  // we do not log the path anymore since it can contain PHI
       }
     }
   }
@@ -632,10 +633,10 @@
                                    compressionLevel_);
     }
 
-    if (result != 0)
+    if (result != ZIP_OK)
     {
       throw OrthancException(ErrorCode_CannotWriteFile,
-                             "Cannot add new file inside ZIP archive: " + std::string(path));
+                             "Cannot add new file inside ZIP archive - error code = " + boost::lexical_cast<std::string>(result)); // we do not log the path anymore since it can contain PHI
     }
 
     hasFileInZip_ = true;
@@ -666,10 +667,11 @@
     {
       int bytes = static_cast<int32_t>(length <= maxBytesInAStep ? length : maxBytesInAStep);
 
-      if (zipWriteInFileInZip(pimpl_->file_, p, bytes))
+      int result = zipWriteInFileInZip(pimpl_->file_, p, bytes);
+      if (result != ZIP_OK)
       {
         throw OrthancException(ErrorCode_CannotWriteFile,
-                               "Cannot write data to ZIP archive: " + path_);
+                               "Cannot write data to ZIP archive - error code =" + boost::lexical_cast<std::string>(result));  // we do not log the path anymore since it can contain PHI
       }
       
       p += bytes;
--- a/OrthancFramework/Sources/Compression/ZipWriter.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Compression/ZipWriter.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Compression/ZlibCompressor.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Compression/ZlibCompressor.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Compression/ZlibCompressor.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Compression/ZlibCompressor.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomFormat/DicomArray.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomArray.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -86,7 +87,21 @@
     {
       DicomTag t = elements_[i]->GetTag();
       const DicomValue& v = elements_[i]->GetValue();
-      std::string s = v.IsNull() ? "(null)" : v.GetContent();
+
+      std::string s;
+      if (v.IsNull())
+      {
+        s = "(null)";
+      }
+      else if (v.IsSequence())
+      {
+        s = "(sequence)";
+      }
+      else
+      {
+        s = v.GetContent();
+      }
+
       printf("0x%04x 0x%04x [%s]\n", t.GetGroup(), t.GetElement(), s.c_str());
     }
   }
--- a/OrthancFramework/Sources/DicomFormat/DicomArray.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomArray.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomFormat/DicomElement.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomElement.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomFormat/DicomElement.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomElement.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -59,60 +60,66 @@
 
     try
     {
-      std::string p = values.GetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION).GetContent();
-      Toolbox::ToUpperCase(p);
+      std::string p;
+      if (values.LookupStringValue(p, DICOM_TAG_PHOTOMETRIC_INTERPRETATION, false)) {
+        Toolbox::ToUpperCase(p);
 
-      if (p == "RGB")
-      {
-        photometric_ = PhotometricInterpretation_RGB;
-      }
-      else if (p == "MONOCHROME1")
-      {
-        photometric_ = PhotometricInterpretation_Monochrome1;
-      }
-      else if (p == "MONOCHROME2")
-      {
-        photometric_ = PhotometricInterpretation_Monochrome2;
-      }
-      else if (p == "PALETTE COLOR")
-      {
-        photometric_ = PhotometricInterpretation_Palette;
-      }
-      else if (p == "HSV")
-      {
-        photometric_ = PhotometricInterpretation_HSV;
-      }
-      else if (p == "ARGB")
-      {
-        photometric_ = PhotometricInterpretation_ARGB;
-      }
-      else if (p == "CMYK")
-      {
-        photometric_ = PhotometricInterpretation_CMYK;
-      }
-      else if (p == "YBR_FULL")
-      {
-        photometric_ = PhotometricInterpretation_YBRFull;
-      }
-      else if (p == "YBR_FULL_422")
-      {
-        photometric_ = PhotometricInterpretation_YBRFull422;
-      }
-      else if (p == "YBR_PARTIAL_420")
-      {
-        photometric_ = PhotometricInterpretation_YBRPartial420;
-      }
-      else if (p == "YBR_PARTIAL_422")
-      {
-        photometric_ = PhotometricInterpretation_YBRPartial422;
-      }
-      else if (p == "YBR_ICT")
-      {
-        photometric_ = PhotometricInterpretation_YBR_ICT;
-      }
-      else if (p == "YBR_RCT")
-      {
-        photometric_ = PhotometricInterpretation_YBR_RCT;
+        if (p == "RGB")
+        {
+          photometric_ = PhotometricInterpretation_RGB;
+        }
+        else if (p == "MONOCHROME1")
+        {
+          photometric_ = PhotometricInterpretation_Monochrome1;
+        }
+        else if (p == "MONOCHROME2")
+        {
+          photometric_ = PhotometricInterpretation_Monochrome2;
+        }
+        else if (p == "PALETTE COLOR")
+        {
+          photometric_ = PhotometricInterpretation_Palette;
+        }
+        else if (p == "HSV")
+        {
+          photometric_ = PhotometricInterpretation_HSV;
+        }
+        else if (p == "ARGB")
+        {
+          photometric_ = PhotometricInterpretation_ARGB;
+        }
+        else if (p == "CMYK")
+        {
+          photometric_ = PhotometricInterpretation_CMYK;
+        }
+        else if (p == "YBR_FULL")
+        {
+          photometric_ = PhotometricInterpretation_YBRFull;
+        }
+        else if (p == "YBR_FULL_422")
+        {
+          photometric_ = PhotometricInterpretation_YBRFull422;
+        }
+        else if (p == "YBR_PARTIAL_420")
+        {
+          photometric_ = PhotometricInterpretation_YBRPartial420;
+        }
+        else if (p == "YBR_PARTIAL_422")
+        {
+          photometric_ = PhotometricInterpretation_YBRPartial422;
+        }
+        else if (p == "YBR_ICT")
+        {
+          photometric_ = PhotometricInterpretation_YBR_ICT;
+        }
+        else if (p == "YBR_RCT")
+        {
+          photometric_ = PhotometricInterpretation_YBR_RCT;
+        }
+        else
+        {
+          photometric_ = PhotometricInterpretation_Unknown;
+        }
       }
       else
       {
@@ -423,4 +430,46 @@
   {
     return 256;
   }
+
+
+  ValueRepresentation DicomImageInformation::GuessPixelDataValueRepresentation(const DicomTransferSyntax& transferSyntax,
+                                                                               unsigned int bitsAllocated)
+  {
+    /**
+     * This approach is validated in "Tests/GuessPixelDataVR.py":
+     * https://orthanc.uclouvain.be/hg/orthanc-tests/file/default/Tests/GuessPixelDataVR.py
+     **/
+
+    if (transferSyntax == DicomTransferSyntax_LittleEndianExplicit ||
+        transferSyntax == DicomTransferSyntax_BigEndianExplicit)
+    {
+      /**
+       * Same rules apply to Little Endian Explicit and Big Endian
+       * Explicit (now retired). The VR of the pixel data directly
+       * depends upon the "Bits Allocated (0028,0100)" tag:
+       * https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_A.2.html
+       * https://dicom.nema.org/medical/dicom/2016b/output/chtml/part05/sect_A.3.html
+       **/
+      if (bitsAllocated > 8)
+      {
+        return ValueRepresentation_OtherWord;
+      }
+      else
+      {
+        return ValueRepresentation_OtherByte;
+      }
+    }
+    else if (transferSyntax == DicomTransferSyntax_LittleEndianImplicit)
+    {
+      // Assume "OW" for DICOM Implicit VR Little Endian Transfer Syntax
+      // https://dicom.nema.org/medical/dicom/current/output/chtml/part05/chapter_A.html#sect_A.1
+      return ValueRepresentation_OtherWord;
+    }
+    else
+    {
+      // Assume "OB" for all the compressed transfer syntaxes
+      // https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_A.4.html
+      return ValueRepresentation_OtherByte;
+    }
+  }
 }
--- a/OrthancFramework/Sources/DicomFormat/DicomImageInformation.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomImageInformation.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -95,5 +96,8 @@
      * was implicitly used in Orthanc <= 1.7.2.
      **/
     static unsigned int GetUsefulTagLength();
+
+    static ValueRepresentation GuessPixelDataValueRepresentation(const DicomTransferSyntax& transferSyntax,
+                                                                 unsigned int bitsAllocated);
   };
 }
--- a/OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -34,11 +35,17 @@
 #include "../OrthancException.h"
 #include "../Toolbox.h"
 #include "DicomArray.h"
+#include "DicomImageInformation.h"
 
 #if ORTHANC_ENABLE_DCMTK == 1
 #include "../DicomParsing/FromDcmtkBridge.h"
 #endif
 
+#if !defined(__EMSCRIPTEN__)
+// Multithreading is not supported in WebAssembly
+#  include <boost/thread/shared_mutex.hpp>
+#endif
+
 namespace Orthanc
 {
   // WARNING: the DEFAULT list of main dicom tags below are the list as they 
@@ -58,8 +65,7 @@
     DICOM_TAG_PATIENT_BIRTH_DATE,
     DICOM_TAG_PATIENT_SEX,
     DICOM_TAG_OTHER_PATIENT_IDS,
-    DICOM_TAG_PATIENT_ID,
-
+    DICOM_TAG_PATIENT_ID
   };
   
   static const DicomTag DEFAULT_STUDY_MAIN_DICOM_TAGS[] =
@@ -72,7 +78,8 @@
     DICOM_TAG_STUDY_DESCRIPTION,
     DICOM_TAG_ACCESSION_NUMBER,
     DICOM_TAG_STUDY_INSTANCE_UID,
-    // New in db v6
+    
+    // New in db v6 (Orthanc 0.9.5)
     DICOM_TAG_REQUESTED_PROCEDURE_DESCRIPTION,
     DICOM_TAG_INSTITUTION_NAME,
     DICOM_TAG_REQUESTING_PHYSICIAN,
@@ -99,7 +106,7 @@
     DICOM_TAG_NUMBER_OF_TIME_SLICES,
     DICOM_TAG_SERIES_INSTANCE_UID,
 
-        // New in db v6
+    // New in db v6 (Orthanc 0.9.5)
     DICOM_TAG_IMAGE_ORIENTATION_PATIENT,
     DICOM_TAG_SERIES_TYPE,
     DICOM_TAG_OPERATOR_NAME,
@@ -119,7 +126,7 @@
     DICOM_TAG_TEMPORAL_POSITION_IDENTIFIER,
     DICOM_TAG_SOP_INSTANCE_UID,
 
-    // New in db v6
+    // New in db v6 (Orthanc 0.9.5)
     DICOM_TAG_IMAGE_POSITION_PATIENT,
     DICOM_TAG_IMAGE_COMMENTS,
 
@@ -133,18 +140,20 @@
     DICOM_TAG_IMAGE_ORIENTATION_PATIENT  // New in Orthanc 1.4.2
   };
 
-
-
-
-  class DicomMap::MainDicomTagsConfiguration
+  class DicomMap::MainDicomTagsConfiguration : public boost::noncopyable
   {
   private:
-    friend DicomMap;
+#if !defined(__EMSCRIPTEN__)
+    typedef boost::unique_lock<boost::shared_mutex> WriterLock;
+    typedef boost::shared_lock<boost::shared_mutex> ReaderLock;
 
-    std::set<DicomTag>               patientsMainDicomTagsByLevel_;
-    std::set<DicomTag>               studiesMainDicomTagsByLevel_;
-    std::set<DicomTag>               seriesMainDicomTagsByLevel_;
-    std::set<DicomTag>               instancesMainDicomTagsByLevel_;
+    boost::shared_mutex mutex_;
+#endif
+    
+    std::set<DicomTag> patientsMainDicomTagsByLevel_;
+    std::set<DicomTag> studiesMainDicomTagsByLevel_;
+    std::set<DicomTag> seriesMainDicomTagsByLevel_;
+    std::set<DicomTag> instancesMainDicomTagsByLevel_;
 
     std::set<DicomTag> allMainDicomTags_;
 
@@ -156,27 +165,6 @@
       ResetDefaultMainDicomTags();
     }
 
-    void ResetDefaultMainDicomTags()
-    {
-      patientsMainDicomTagsByLevel_.clear();
-      studiesMainDicomTagsByLevel_.clear();
-      seriesMainDicomTagsByLevel_.clear();
-      instancesMainDicomTagsByLevel_.clear();
-
-      allMainDicomTags_.clear();
-
-      // by default, initialize with the previous static list (up to 1.10.0)
-      LoadDefaultMainDicomTags(ResourceType_Patient);
-      LoadDefaultMainDicomTags(ResourceType_Study);
-      LoadDefaultMainDicomTags(ResourceType_Series);
-      LoadDefaultMainDicomTags(ResourceType_Instance);
-
-      defaultSignatures_[ResourceType_Patient] = signatures_[ResourceType_Patient];
-      defaultSignatures_[ResourceType_Study] = signatures_[ResourceType_Study];
-      defaultSignatures_[ResourceType_Series] = signatures_[ResourceType_Series];
-      defaultSignatures_[ResourceType_Instance] = signatures_[ResourceType_Instance];
-    }
-
     std::string ComputeSignature(const std::set<DicomTag>& tags)
     {
       // std::set are sorted by default (which is important for us !)
@@ -227,13 +215,11 @@
 
       for (size_t i = 0; i < size; i++)
       {
-        AddMainDicomTag(tags[i], level);
+        AddMainDicomTagInternal(tags[i], level);
       }
-
     }
 
-
-    std::set<DicomTag>& GetMainDicomTagsByLevel(ResourceType level)
+    std::set<DicomTag>& GetMainDicomTagsByLevelInternal(ResourceType level)
     {
       switch (level)
       {
@@ -254,6 +240,22 @@
       }
     }
 
+    void AddMainDicomTagInternal(const DicomTag& tag,
+                                 ResourceType level)
+    {
+      std::set<DicomTag>& existingLevelTags = GetMainDicomTagsByLevelInternal(level);
+      
+      if (existingLevelTags.find(tag) != existingLevelTags.end())
+      {
+        throw OrthancException(ErrorCode_MainDicomTagsMultiplyDefined, tag.Format() + " is already defined");
+      }
+
+      existingLevelTags.insert(tag);
+      allMainDicomTags_.insert(tag);
+
+      signatures_[level] = ComputeSignature(GetMainDicomTagsByLevelInternal(level));
+    }
+
   public:
     // Singleton pattern
     static MainDicomTagsConfiguration& GetInstance()
@@ -262,40 +264,99 @@
       return parameters;
     }
 
-    void AddMainDicomTag(const DicomTag& tag, ResourceType level)
+    void ResetDefaultMainDicomTags()
     {
-      const std::set<DicomTag>& existingLevelTags = GetMainDicomTagsByLevel(level);
+#if !defined(__EMSCRIPTEN__)
+      WriterLock lock(mutex_);
+#endif
       
-      if (existingLevelTags.find(tag) != existingLevelTags.end())
-      {
-        throw OrthancException(ErrorCode_MainDicomTagsMultiplyDefined, tag.Format() + " is already defined");
-      }
+      patientsMainDicomTagsByLevel_.clear();
+      studiesMainDicomTagsByLevel_.clear();
+      seriesMainDicomTagsByLevel_.clear();
+      instancesMainDicomTagsByLevel_.clear();
+
+      allMainDicomTags_.clear();
 
+      // by default, initialize with the previous static list (up to 1.10.0)
+      LoadDefaultMainDicomTags(ResourceType_Patient);
+      LoadDefaultMainDicomTags(ResourceType_Study);
+      LoadDefaultMainDicomTags(ResourceType_Series);
+      LoadDefaultMainDicomTags(ResourceType_Instance);
 
-      GetMainDicomTagsByLevel(level).insert(tag);
-      allMainDicomTags_.insert(tag);
-      signatures_[level] = ComputeSignature(GetMainDicomTagsByLevel(level));
+      defaultSignatures_[ResourceType_Patient] = signatures_[ResourceType_Patient];
+      defaultSignatures_[ResourceType_Study] = signatures_[ResourceType_Study];
+      defaultSignatures_[ResourceType_Series] = signatures_[ResourceType_Series];
+      defaultSignatures_[ResourceType_Instance] = signatures_[ResourceType_Instance];
     }
 
-    const std::set<DicomTag>& GetAllMainDicomTags() const
+    void AddMainDicomTag(const DicomTag& tag,
+                         ResourceType level)
     {
-      return allMainDicomTags_;
+#if !defined(__EMSCRIPTEN__)
+      WriterLock lock(mutex_);
+#endif
+      
+      AddMainDicomTagInternal(tag, level);
     }
 
-    const std::string& GetMainDicomTagsSignature(ResourceType level)
+    void GetAllMainDicomTags(std::set<DicomTag>& target)
+    {
+#if !defined(__EMSCRIPTEN__)
+      ReaderLock lock(mutex_);
+#endif
+      
+      target = allMainDicomTags_;
+    }
+
+    void GetMainDicomTagsByLevel(std::set<DicomTag>& target,
+                                 ResourceType level)
     {
+#if !defined(__EMSCRIPTEN__)
+      ReaderLock lock(mutex_);
+#endif
+      
+      target = GetMainDicomTagsByLevelInternal(level);
+    }
+
+    std::string GetMainDicomTagsSignature(ResourceType level)
+    {
+#if !defined(__EMSCRIPTEN__)
+      ReaderLock lock(mutex_);
+#endif
+      
       assert(signatures_.find(level) != signatures_.end());
-
       return signatures_[level];
     }
 
-    const std::string& GetDefaultMainDicomTagsSignature(ResourceType level)
+    std::string GetDefaultMainDicomTagsSignature(ResourceType level)
     {
+#if !defined(__EMSCRIPTEN__)
+      ReaderLock lock(mutex_);
+#endif
+      
       assert(defaultSignatures_.find(level) != defaultSignatures_.end());
-
       return defaultSignatures_[level];
     }
 
+    bool IsMainDicomTag(const DicomTag& tag)
+    {
+#if !defined(__EMSCRIPTEN__)
+      ReaderLock lock(mutex_);
+#endif
+      
+      return allMainDicomTags_.find(tag) != allMainDicomTags_.end();
+    }
+
+    bool IsMainDicomTag(const DicomTag& tag,
+                        ResourceType level)
+    {
+#if !defined(__EMSCRIPTEN__)
+      ReaderLock lock(mutex_);
+#endif
+      
+      const std::set<DicomTag>& mainDicomTags = GetMainDicomTagsByLevelInternal(level);
+      return mainDicomTags.find(tag) != mainDicomTags.end();
+    }
   };
 
 
@@ -359,7 +420,7 @@
     SetValueInternal(group, element, new DicomValue(str, isBinary));
   }
 
-  void DicomMap::SetValue(const DicomTag& tag, const Json::Value& value)
+  void DicomMap::SetSequenceValue(const DicomTag& tag, const Json::Value& value)
   {
     SetValueInternal(tag.GetGroup(), tag.GetElement(), new DicomValue(value));
   }
@@ -414,7 +475,8 @@
 
   void DicomMap::ExtractResourceInformation(DicomMap& result, ResourceType level) const
   {
-    const std::set<DicomTag>& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsByLevel(level);
+    std::set<DicomTag> mainDicomTags;
+    DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsByLevel(mainDicomTags, level);
     ExtractTagsInternal(result, content_, mainDicomTags);
   }
 
@@ -527,69 +589,81 @@
     }
   }
 
-  static void SetupFindTemplate(DicomMap& result,
-                                const std::set<DicomTag>& mainDicomTags)
+  void DicomMap::SetupFindPatientTemplate(DicomMap& result)
   {
     result.Clear();
 
-    for (std::set<DicomTag>::const_iterator itmt = mainDicomTags.begin();
-         itmt != mainDicomTags.end(); ++itmt)
-    {
-      result.SetValue(*itmt, "", false);
-    }
-  }
+    // Identifying tags
+    result.SetValue(DICOM_TAG_PATIENT_ID, "", false);
 
-  void DicomMap::SetupFindPatientTemplate(DicomMap& result)
-  {
-    const std::set<DicomTag>& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsByLevel(ResourceType_Patient);
-    SetupFindTemplate(result, mainDicomTags);
+    // Other tags in the "Patient" module
+    result.SetValue(DICOM_TAG_OTHER_PATIENT_IDS, "", false);
+    result.SetValue(DICOM_TAG_PATIENT_BIRTH_DATE, "", false);
+    result.SetValue(DICOM_TAG_PATIENT_NAME, "", false);
+    result.SetValue(DICOM_TAG_PATIENT_SEX, "", false);
   }
 
   void DicomMap::SetupFindStudyTemplate(DicomMap& result)
   {
-    const std::set<DicomTag>& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsByLevel(ResourceType_Study);
-    SetupFindTemplate(result, mainDicomTags);
+    result.Clear();
+
+    // Identifying tags
+    result.SetValue(DICOM_TAG_PATIENT_ID, "", false);
     result.SetValue(DICOM_TAG_ACCESSION_NUMBER, "", false);
-    result.SetValue(DICOM_TAG_PATIENT_ID, "", false);
+    result.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "", false);
 
-    // These main DICOM tags are only indirectly related to the
-    // General Study Module, remove them
-    result.Remove(DICOM_TAG_INSTITUTION_NAME);
-    result.Remove(DICOM_TAG_REQUESTING_PHYSICIAN);
-    result.Remove(DICOM_TAG_REQUESTED_PROCEDURE_DESCRIPTION);
+    // Other tags in the "General Study" module
+    result.SetValue(DICOM_TAG_REFERRING_PHYSICIAN_NAME, "", false);
+    result.SetValue(DICOM_TAG_STUDY_DATE, "", false);
+    result.SetValue(DICOM_TAG_STUDY_DESCRIPTION, "", false);
+    result.SetValue(DICOM_TAG_STUDY_ID, "", false);
+    result.SetValue(DICOM_TAG_STUDY_TIME, "", false);
   }
 
   void DicomMap::SetupFindSeriesTemplate(DicomMap& result)
   {
-    const std::set<DicomTag>& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsByLevel(ResourceType_Series);
-    SetupFindTemplate(result, mainDicomTags);
+    result.Clear();
+
+    // Identifying tags
+    result.SetValue(DICOM_TAG_PATIENT_ID, "", false);
     result.SetValue(DICOM_TAG_ACCESSION_NUMBER, "", false);
-    result.SetValue(DICOM_TAG_PATIENT_ID, "", false);
     result.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "", false);
+    result.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, "", false);
 
-    // These tags are considered as "main" by Orthanc, but are not in the Series module
-    result.Remove(DicomTag(0x0008, 0x0070));  // Manufacturer
-    result.Remove(DicomTag(0x0008, 0x1010));  // Station name
-    result.Remove(DicomTag(0x0018, 0x0024));  // Sequence name
-    result.Remove(DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES);
-    result.Remove(DICOM_TAG_IMAGES_IN_ACQUISITION);
-    result.Remove(DICOM_TAG_NUMBER_OF_SLICES);
-    result.Remove(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS);
-    result.Remove(DICOM_TAG_NUMBER_OF_TIME_SLICES);
-    result.Remove(DICOM_TAG_IMAGE_ORIENTATION_PATIENT);
-    result.Remove(DICOM_TAG_SERIES_TYPE);
-    result.Remove(DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_DESCRIPTION);
-    result.Remove(DICOM_TAG_CONTRAST_BOLUS_AGENT);
+    // Other tags in the "General Series" module
+    result.SetValue(DICOM_TAG_BODY_PART_EXAMINED, "", false);
+    result.SetValue(DICOM_TAG_MODALITY, "", false);
+    result.SetValue(DICOM_TAG_OPERATOR_NAME, "", false);
+    result.SetValue(DICOM_TAG_PERFORMED_PROCEDURE_STEP_DESCRIPTION, "", false);
+    result.SetValue(DICOM_TAG_PROTOCOL_NAME, "", false);
+    result.SetValue(DICOM_TAG_SERIES_DATE, "", false);
+    result.SetValue(DICOM_TAG_SERIES_DESCRIPTION, "", false);
+    result.SetValue(DICOM_TAG_SERIES_NUMBER, "", false);
+    result.SetValue(DICOM_TAG_SERIES_TIME, "", false);
   }
 
   void DicomMap::SetupFindInstanceTemplate(DicomMap& result)
   {
-    const std::set<DicomTag>& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsByLevel(ResourceType_Instance);
-    SetupFindTemplate(result, mainDicomTags);
+    result.Clear();
+
+    // Identifying tags
+    result.SetValue(DICOM_TAG_PATIENT_ID, "", false);
     result.SetValue(DICOM_TAG_ACCESSION_NUMBER, "", false);
-    result.SetValue(DICOM_TAG_PATIENT_ID, "", false);
     result.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "", false);
     result.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, "", false);
+    result.SetValue(DICOM_TAG_SOP_INSTANCE_UID, "", false);
+
+    // Other tags in the "SOP Common" module
+    result.SetValue(DICOM_TAG_ACQUISITION_NUMBER, "", false);
+    result.SetValue(DICOM_TAG_IMAGE_COMMENTS, "", false);
+    result.SetValue(DICOM_TAG_IMAGE_INDEX, "", false);
+    result.SetValue(DICOM_TAG_IMAGE_ORIENTATION_PATIENT, "", false);
+    result.SetValue(DICOM_TAG_IMAGE_POSITION_PATIENT, "", false);
+    result.SetValue(DICOM_TAG_INSTANCE_CREATION_DATE, "", false);
+    result.SetValue(DICOM_TAG_INSTANCE_CREATION_TIME, "", false);
+    result.SetValue(DICOM_TAG_INSTANCE_NUMBER, "", false);
+    result.SetValue(DICOM_TAG_NUMBER_OF_FRAMES, "", false);
+    result.SetValue(DICOM_TAG_TEMPORAL_POSITION_IDENTIFIER, "", false);
   }
 
 
@@ -605,8 +679,7 @@
 
   bool DicomMap::IsMainDicomTag(const DicomTag& tag, ResourceType level)
   {
-    const std::set<DicomTag>& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsByLevel(level);
-    return mainDicomTags.find(tag) != mainDicomTags.end();
+    return DicomMap::MainDicomTagsConfiguration::GetInstance().IsMainDicomTag(tag, level);
   }
 
   bool DicomMap::IsMainDicomTag(const DicomTag& tag)
@@ -706,14 +779,15 @@
   }
 
 
-  const std::set<DicomTag>& DicomMap::GetMainDicomTags(ResourceType level)
+  void DicomMap::GetMainDicomTags(std::set<DicomTag>& target,
+                                  ResourceType level)
   {
-    return DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsByLevel(level);
+    DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsByLevel(target, level);
   }
 
-  const std::set<DicomTag>& DicomMap::GetAllMainDicomTags()
+  void DicomMap::GetAllMainDicomTags(std::set<DicomTag>& target)
   {
-    return DicomMap::MainDicomTagsConfiguration::GetInstance().GetAllMainDicomTags();
+    DicomMap::MainDicomTagsConfiguration::GetInstance().GetAllMainDicomTags(target);
   }
 
   void DicomMap::AddMainDicomTag(const DicomTag& tag, ResourceType level)
@@ -726,12 +800,12 @@
     DicomMap::MainDicomTagsConfiguration::GetInstance().ResetDefaultMainDicomTags();
   }
 
-  const std::string& DicomMap::GetMainDicomTagsSignature(ResourceType level)
+  std::string DicomMap::GetMainDicomTagsSignature(ResourceType level)
   {
     return DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsSignature(level);
   }
 
-  const std::string& DicomMap::GetDefaultMainDicomTagsSignature(ResourceType level)
+  std::string DicomMap::GetDefaultMainDicomTagsSignature(ResourceType level)
   {
     return DicomMap::MainDicomTagsConfiguration::GetInstance().GetDefaultMainDicomTagsSignature(level);
   }
@@ -1384,7 +1458,7 @@
         }
         else
         {
-          SetValue(tag, value["Value"]);
+          SetSequenceValue(tag, value["Value"]);
         }
       }
     }
@@ -1409,7 +1483,8 @@
   void DicomMap::MergeMainDicomTags(const DicomMap& other,
                                     ResourceType level)
   {
-    const std::set<DicomTag>& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsByLevel(level);
+    std::set<DicomTag> mainDicomTags;
+    DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsByLevel(mainDicomTags, level);
 
     for (std::set<DicomTag>::const_iterator itmt = mainDicomTags.begin();
          itmt != mainDicomTags.end(); ++itmt)
@@ -1438,11 +1513,9 @@
 
   bool DicomMap::HasOnlyMainDicomTags() const
   {
-    const std::set<DicomTag>& allMainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetAllMainDicomTags();
-
     for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it)
     {
-      if (allMainDicomTags.find(it->first) == allMainDicomTags.end())
+      if (!DicomMap::MainDicomTagsConfiguration::GetInstance().IsMainDicomTag(it->first))
       {
         return false;
       }
@@ -1459,7 +1532,7 @@
     {
       if (it->second->IsSequence())
       {
-        result.SetValue(it->first, it->second->GetSequenceContent());
+        result.SetSequenceValue(it->first, it->second->GetSequenceContent());
       }
     }
   }
@@ -1710,7 +1783,8 @@
   void DicomMap::DumpMainDicomTags(Json::Value& target,
                                    ResourceType level) const
   {
-    const std::set<DicomTag>& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsByLevel(level);
+    std::set<DicomTag> mainDicomTags;
+    DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsByLevel(mainDicomTags, level);
     
     target = Json::objectValue;
 
@@ -1736,6 +1810,19 @@
   }
   
 
+  ValueRepresentation DicomMap::GuessPixelDataValueRepresentation(DicomTransferSyntax transferSyntax) const
+  {
+    const DicomValue* value = TestAndGetValue(DICOM_TAG_BITS_ALLOCATED);
+
+    uint32_t bitsAllocated;
+    if (value == NULL ||
+        !value->ParseUnsignedInteger32(bitsAllocated))
+    {
+      bitsAllocated = 8;
+    }
+
+    return DicomImageInformation::GuessPixelDataValueRepresentation(transferSyntax, bitsAllocated);
+  }
   
 
   void DicomMap::Print(FILE* fp) const
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -31,10 +32,6 @@
 #include <map>
 #include <json/value.h>
 
-#if ORTHANC_BUILD_UNIT_TESTS == 1
-#  include <gtest/gtest_prod.h>
-#endif
-
 namespace Orthanc
 {
   class ORTHANC_PUBLIC DicomMap : public boost::noncopyable
@@ -48,10 +45,6 @@
     friend class FromDcmtkBridge;
     friend class ParsedDicomFile;
 
-#if ORTHANC_BUILD_UNIT_TESTS == 1
-    friend class DicomMapMainTagsTests;
-#endif
-
     Content content_;
 
     // Warning: This takes the ownership of "value"
@@ -59,12 +52,11 @@
                           uint16_t element, 
                           DicomValue* value);
 
-    // used for unit tests only
-    static void ResetDefaultMainDicomTags();
-
   public:
     ~DicomMap();
 
+    static void ResetDefaultMainDicomTags();
+
     size_t GetSize() const;
     
     DicomMap* Clone() const;
@@ -94,8 +86,8 @@
                   const std::string& str,
                   bool isBinary);
 
-    void SetValue(const DicomTag& tag,
-                  const Json::Value& value);
+    void SetSequenceValue(const DicomTag& tag,
+                          const Json::Value& value);
 
     bool HasTag(uint16_t group, uint16_t element) const;
 
@@ -154,14 +146,15 @@
 
     static bool HasComputedTags(const std::set<DicomTag>& tags);
 
-    static const std::set<DicomTag>& GetMainDicomTags(ResourceType level);
+    static void GetMainDicomTags(std::set<DicomTag>& target,
+                                 ResourceType level);
 
     // returns a string uniquely identifying the list of main dicom tags for a level
-    static const std::string& GetMainDicomTagsSignature(ResourceType level);
+    static std::string GetMainDicomTagsSignature(ResourceType level);
 
-    static const std::string& GetDefaultMainDicomTagsSignature(ResourceType level);
+    static std::string GetDefaultMainDicomTagsSignature(ResourceType level);
 
-    static const std::set<DicomTag>& GetAllMainDicomTags();
+    static void GetAllMainDicomTags(std::set<DicomTag>& target);
 
     // adds a main dicom tag to the definition of main dicom tags for each level.
     // this should be done once at startup before you use MainDicomTags methods
@@ -238,6 +231,8 @@
     void DumpMainDicomTags(Json::Value& target,
                            ResourceType level) const;
 
+    ValueRepresentation GuessPixelDataValueRepresentation(DicomTransferSyntax transferSyntax) const;
+
     void Print(FILE* fp) const;  // For debugging only
   };
 }
--- a/OrthancFramework/Sources/DicomFormat/DicomPath.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomPath.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomFormat/DicomPath.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomPath.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomFormat/DicomStreamReader.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomStreamReader.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -579,13 +580,17 @@
   class DicomStreamReader::PixelDataVisitor : public DicomStreamReader::IVisitor
   {
   private:
-    bool      hasPixelData_;
-    uint64_t  pixelDataOffset_;
+    bool                 hasPixelData_;
+    uint64_t             pixelDataOffset_;
+    ValueRepresentation  pixelDataVR_;
+    DicomTransferSyntax  transferSyntax_;
     
   public:
     PixelDataVisitor() :
       hasPixelData_(false),
-      pixelDataOffset_(0)
+      pixelDataOffset_(0),
+      pixelDataVR_(ValueRepresentation_Unknown),
+      transferSyntax_(DicomTransferSyntax_LittleEndianImplicit) // Default DICOM transfer syntax
     {
     }
     
@@ -597,6 +602,7 @@
 
     virtual void VisitTransferSyntax(DicomTransferSyntax transferSyntax) ORTHANC_OVERRIDE
     {
+      transferSyntax_ = transferSyntax;
     }
     
     virtual bool VisitDatasetTag(const DicomTag& tag,
@@ -609,6 +615,23 @@
       {
         hasPixelData_ = true;
         pixelDataOffset_ = fileOffset;
+
+        if (transferSyntax_ == DicomTransferSyntax_LittleEndianImplicit)
+        {
+          // Implicit Little Endian has always "OW" VR for pixel data
+          // https://dicom.nema.org/medical/dicom/current/output/chtml/part05/chapter_A.html
+          pixelDataVR_ = ValueRepresentation_OtherWord;
+        }
+        else if (transferSyntax_ == DicomTransferSyntax_LittleEndianExplicit ||
+                 transferSyntax_ == DicomTransferSyntax_BigEndianExplicit)
+        {
+          pixelDataVR_ = vr;
+        }
+        else
+        {
+          // Compressed transfer syntaxes must always be OB
+          pixelDataVR_ = ValueRepresentation_OtherByte;
+        }
       }
 
       // Stop processing once pixel data has been passed
@@ -625,7 +648,13 @@
       return pixelDataOffset_;
     }
 
+    ValueRepresentation GetPixelDataVR() const
+    {
+      return pixelDataVR_;
+    }
+
     static bool LookupPixelDataOffset(uint64_t& offset,
+                                      ValueRepresentation& vr,
                                       std::istream& stream)
     {
       PixelDataVisitor visitor;
@@ -672,6 +701,7 @@
             s[3] == char(0x00))
         {
           offset = visitor.GetPixelDataOffset();
+          vr = visitor.GetPixelDataVR();
           return true;
         }
         else
@@ -688,20 +718,22 @@
 
   
   bool DicomStreamReader::LookupPixelDataOffset(uint64_t& offset,
+                                                ValueRepresentation& vr,
                                                 const std::string& dicom)
   {
     std::stringstream stream(dicom);
-    return PixelDataVisitor::LookupPixelDataOffset(offset, stream);
+    return PixelDataVisitor::LookupPixelDataOffset(offset, vr, stream);
   }
   
 
   bool DicomStreamReader::LookupPixelDataOffset(uint64_t& offset,
+                                                ValueRepresentation& vr,
                                                 const void* buffer,
                                                 size_t size)
   {
     boost::iostreams::array_source source(reinterpret_cast<const char*>(buffer), size);
     boost::iostreams::stream<boost::iostreams::array_source> stream(source);
-    return PixelDataVisitor::LookupPixelDataOffset(offset, stream);
+    return PixelDataVisitor::LookupPixelDataOffset(offset, vr, stream);
   }
 }
 
--- a/OrthancFramework/Sources/DicomFormat/DicomStreamReader.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomStreamReader.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -128,10 +129,12 @@
 
     uint64_t GetProcessedBytes() const;
 
-    static bool LookupPixelDataOffset(uint64_t& offset,
+    static bool LookupPixelDataOffset(uint64_t& offset /* out */,
+                                      ValueRepresentation& vr /* out */,
                                       const std::string& dicom);
 
-    static bool LookupPixelDataOffset(uint64_t& offset,
+    static bool LookupPixelDataOffset(uint64_t& offset /* out */,
+                                      ValueRepresentation& vr /* out */,
                                       const void* buffer,
                                       size_t size);
   };
--- a/OrthancFramework/Sources/DicomFormat/DicomTag.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomTag.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomFormat/DicomTag.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomTag.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -147,6 +148,7 @@
   static const DicomTag DICOM_TAG_SERIES_TIME(0x0008, 0x0031);
   static const DicomTag DICOM_TAG_STUDY_DATE(0x0008, 0x0020);
   static const DicomTag DICOM_TAG_STUDY_TIME(0x0008, 0x0030);
+  static const DicomTag DICOM_TAG_TIMEZONE_OFFSET_FROM_UTC(0x0008, 0x0201);
 
   // Various tags
   static const DicomTag DICOM_TAG_SERIES_TYPE(0x0054, 0x1000);
--- a/OrthancFramework/Sources/DicomFormat/DicomValue.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomValue.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -66,6 +67,10 @@
     type_(Type_SequenceAsJson),
     sequenceJson_(value)
   {
+    if (value.type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
   }
   
   const std::string& DicomValue::GetContent() const
--- a/OrthancFramework/Sources/DicomFormat/DicomValue.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomValue.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomFormat/StreamBlockReader.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/StreamBlockReader.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomFormat/StreamBlockReader.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/StreamBlockReader.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -33,6 +34,10 @@
 #include "../OrthancException.h"
 #include "NetworkingCompatibility.h"
 
+#ifdef _WIN32
+#  include <winsock.h>
+#endif
+
 #include <dcmtk/dcmnet/diutil.h>  // For dcmConnectionTimeout()
 #include <dcmtk/dcmdata/dcdeftag.h>
 
@@ -104,6 +109,8 @@
     
   void DicomAssociation::CloseInternal()
   {
+    CLOG(INFO, DICOM) << "Closing DICOM association";
+
 #if ORTHANC_ENABLE_SSL == 1
     tls_.reset(NULL);  // Transport layer must be destroyed before the association itself
 #endif
@@ -293,11 +300,12 @@
       {
         assert(net_ != NULL &&
                params_ != NULL);
-        
         tls_.reset(Internals::InitializeDicomTls(net_, NET_REQUESTOR, parameters.GetOwnPrivateKeyPath(),
                                                  parameters.GetOwnCertificatePath(),
                                                  parameters.GetTrustedCertificatesPath(),
-                                                 parameters.IsRemoteCertificateRequired()));
+                                                 parameters.IsRemoteCertificateRequired(),
+                                                 parameters.GetMinimumTlsVersion(),
+                                                 parameters.GetAcceptedCiphers()));
       }
       catch (OrthancException&)
       {
@@ -389,8 +397,10 @@
       LST_Position(l, (LST_NODE*)pc);
       while (pc)
       {
-        if (pc->result == ASC_P_ACCEPTANCE)
+        if (pc->result == ASC_P_ACCEPTANCE && strlen(pc->abstractSyntax) > 0)
         {
+          CLOG(TRACE, DICOM) << "DicomAssociation::Open, adding SOPClassUID " << pc->abstractSyntax << " - TS " << pc->acceptedTransferSyntax << " - PC ID " << boost::lexical_cast<std::string>(static_cast<int>(pc->presentationContextID));
+
           DicomTransferSyntax transferSyntax;
           if (LookupTransferSyntax(transferSyntax, pc->acceptedTransferSyntax))
           {
--- a/OrthancFramework/Sources/DicomNetworking/DicomAssociation.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociation.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -43,7 +44,8 @@
 static std::string   defaultTrustedCertificatesPath_;
 static unsigned int  defaultMaximumPduLength_ = ASC_DEFAULTMAXPDU;
 static bool          defaultRemoteCertificateRequired_ = true;
-
+static unsigned int  minimumTlsVersion_ = 0;
+static std::set<std::string> acceptedCiphers_;
 
 namespace Orthanc
 {
@@ -193,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");
@@ -251,7 +253,26 @@
     return remoteCertificateRequired_;
   }
 
+  unsigned int DicomAssociationParameters::GetMinimumTlsVersion()
+  {
+    return minimumTlsVersion_;
+  }
   
+  void DicomAssociationParameters::SetMinimumTlsVersion(unsigned int version)
+  {
+    minimumTlsVersion_ = version;
+  }
+
+  void DicomAssociationParameters::SetAcceptedCiphers(const std::set<std::string>& acceptedCiphers)
+  {
+    acceptedCiphers_ = acceptedCiphers;
+  }
+
+  const std::set<std::string>& DicomAssociationParameters::GetAcceptedCiphers()
+  {
+    return acceptedCiphers_;
+  }
+
 
   static const char* const LOCAL_AET = "LocalAet";
   static const char* const REMOTE = "Remote";
--- a/OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -127,5 +128,13 @@
     static void SetDefaultRemoteCertificateRequired(bool required);
 
     static bool GetDefaultRemoteCertificateRequired();
+
+    static void SetMinimumTlsVersion(unsigned int version);
+
+    static unsigned int GetMinimumTlsVersion();
+
+    static void SetAcceptedCiphers(const std::set<std::string>& acceptedCiphers);
+
+    static const std::set<std::string>& GetAcceptedCiphers();
   };
 }
--- a/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -189,7 +190,7 @@
     // same fix is required for Agfa Impax. This was generalized for
     // generic manufacturer since it seems to affect PhilipsADW,
     // GEWAServer as well:
-    // https://bugs.orthanc-server.com/show_bug.cgi?id=31
+    // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=31
 
     switch (manufacturer)
     {
--- a/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/DicomServer.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomServer.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -61,6 +62,7 @@
                                  unsigned int maximumPduLength,
                                  bool useDicomTls)
   {
+    Logging::SetCurrentThreadName("DICOM-SERVER");
     CLOG(INFO, DICOM) << "DICOM server started";
 
     while (server->continue_)
@@ -105,7 +107,8 @@
     applicationEntityFilter_(NULL),
     useDicomTls_(false),
     maximumPduLength_(ASC_DEFAULTMAXPDU),
-    remoteCertificateRequired_(true)
+    remoteCertificateRequired_(true),
+    minimumTlsVersion_(0)
   {
   }
 
@@ -409,7 +412,7 @@
       {
         pimpl_->tls_.reset(Internals::InitializeDicomTls(
                              pimpl_->network_, NET_ACCEPTOR, ownPrivateKeyPath_, ownCertificatePath_,
-                             trustedCertificatesPath_, remoteCertificateRequired_));
+                             trustedCertificatesPath_, remoteCertificateRequired_, minimumTlsVersion_, acceptedCiphers_));
       }
       catch (OrthancException&)
       {
@@ -429,7 +432,7 @@
 
     CLOG(INFO, DICOM) << "The embedded DICOM server will use " << threadsCount_ << " threads";
 
-    pimpl_->workers_.reset(new RunnableWorkersPool(threadsCount_));
+    pimpl_->workers_.reset(new RunnableWorkersPool(threadsCount_, "DICOM-"));
     pimpl_->thread_ = boost::thread(ServerThread, this, maximumPduLength_, useDicomTls_);
   }
 
@@ -492,6 +495,18 @@
     return useDicomTls_;
   }
 
+  void DicomServer::SetMinimumTlsVersion(unsigned int version)
+  {
+    minimumTlsVersion_ = version;
+    DicomAssociationParameters::SetMinimumTlsVersion(version);
+  }
+
+  void DicomServer::SetAcceptedCiphers(const std::set<std::string>& ciphers)
+  {
+    acceptedCiphers_ = ciphers;
+    DicomAssociationParameters::SetAcceptedCiphers(ciphers);
+  }
+
   void DicomServer::SetOwnCertificatePath(const std::string& privateKeyPath,
                                           const std::string& certificatePath)
   {
--- a/OrthancFramework/Sources/DicomNetworking/DicomServer.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomServer.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -90,6 +91,8 @@
     std::string  trustedCertificatesPath_;
     unsigned int maximumPduLength_;
     bool         remoteCertificateRequired_;  // New in 1.9.3
+    unsigned int minimumTlsVersion_;          // New in 1.12.4
+    std::set<std::string> acceptedCiphers_;   // New in 1.12.4
 
 
     static void ServerThread(DicomServer* server,
@@ -153,6 +156,9 @@
     void SetDicomTlsEnabled(bool enabled);
     bool IsDicomTlsEnabled() const;
 
+    void SetMinimumTlsVersion(unsigned int version);
+    void SetAcceptedCiphers(const std::set<std::string>& ciphers);
+
     void SetOwnCertificatePath(const std::string& privateKeyPath,
                                const std::string& certificatePath);
     const std::string& GetOwnPrivateKeyPath() const;    
--- a/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -262,26 +263,33 @@
 
     if (LookupPresentationContext(presentationContextId, sopClassUid, transferSyntax))
     {
+      CLOG(INFO, DICOM) << "Found an accepted presentation context for SOPClassUID " << sopClassUid << " and transfer syntax " << GetTransferSyntaxUid(transferSyntax);
       return true;
     }
 
     // The association must be re-negotiated
     if (association_->IsOpen())
     {
-      CLOG(INFO, DICOM) << "Re-negotiating DICOM association with "
-                        << parameters_.GetRemoteModality().GetApplicationEntityTitle();
+      CLOG(INFO, DICOM) << "No accepted presentation context found, re-negotiating DICOM association with "
+                        << parameters_.GetRemoteModality().GetApplicationEntityTitle()
+                        << " for SOPClassUID " << sopClassUid << " TransferSyntax =" << GetTransferSyntaxUid(transferSyntax);
 
-      // Don't renegociate if we know that the remote modality was
+      // Check if we know that the remote modality was
       // already proposed this individual transfer syntax (**)
-      if (proposedOriginalClasses_.find(std::make_pair(sopClassUid, transferSyntax)) !=
-          proposedOriginalClasses_.end())
+      if (proposedOriginalClasses_.find(std::make_pair(sopClassUid, transferSyntax)) != proposedOriginalClasses_.end())
       {
         CLOG(INFO, DICOM) << "The remote modality has already rejected SOP class UID \""
                           << sopClassUid << "\" with transfer syntax \""
-                          << GetTransferSyntaxUid(transferSyntax) << "\", don't renegotiate";
-        return false;
+                          << GetTransferSyntaxUid(transferSyntax) << "\", but we will renegotiate anyway";
+        // always renegotiating since 1.12.2 // return false;
       }
     }
+    else
+    {
+      CLOG(INFO, DICOM) << "Negotiating DICOM association with "
+                        << parameters_.GetRemoteModality().GetApplicationEntityTitle()
+                        << " for SOPClassUID " << sopClassUid << " TransferSyntax =" << GetTransferSyntaxUid(transferSyntax);
+    }
 
     association_->ClearPresentationContexts();
     proposedOriginalClasses_.clear();
@@ -374,6 +382,8 @@
     DicomTransferSyntax transferSyntax;
     LookupParameters(sopClassUid, sopInstanceUid, transferSyntax, dicom);
 
+    LOG(INFO) << "Performing C-Store on instance of SOPClassUID '" << sopClassUid << "'";
+
     uint8_t presID;
     if (!NegotiatePresentationContext(presID, sopClassUid, transferSyntax, proposeUncompressedSyntaxes_,
                                       DicomTransferSyntax_LittleEndianExplicit))
@@ -440,7 +450,8 @@
     if (response.DimseStatus != 0x0000 &&  // Success
         response.DimseStatus != 0xB000 &&  // Warning - Coercion of Data Elements
         response.DimseStatus != 0xB007 &&  // Warning - Data Set does not match SOP Class
-        response.DimseStatus != 0xB006)    // Warning - Elements Discarded
+        response.DimseStatus != 0xB006 &&  // Warning - Elements Discarded
+        response.DimseStatus != 0x0111)    // Warning - Duplicate SOPInstanceUID (https://discourse.orthanc-server.org/t/ignore-dimse-status-0x0111-when-sending-partial-duplicate-studies/4555/3)
     {
       char buf[16];
       sprintf(buf, "%04X", response.DimseStatus);
@@ -472,6 +483,7 @@
   }
 
 
+#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
   void DicomStoreUserConnection::LookupTranscoding(std::set<DicomTransferSyntax>& acceptedSyntaxes,
                                                    const std::string& sopClassUid,
                                                    DicomTransferSyntax sourceSyntax,
@@ -479,14 +491,17 @@
                                                    DicomTransferSyntax preferred)
   {
     acceptedSyntaxes.clear();
+    std::map<DicomTransferSyntax, uint8_t> contexts;
 
     // Make sure a negotiation has already occurred for this transfer
-    // syntax. We don't use the return code: Transcoding is possible
-    // even if the "sourceSyntax" is not supported.
-    uint8_t presID;
-    NegotiatePresentationContext(presID, sopClassUid, sourceSyntax, hasPreferred, preferred);
+    // syntax if we have not negotiated yet. 
+    // We don't use the return code: Transcoding is possible even if the "sourceSyntax" is not supported.
+    if (!association_->IsOpen() || !association_->LookupAcceptedPresentationContext(contexts, sopClassUid))
+    {
+      uint8_t presID;
+      NegotiatePresentationContext(presID, sopClassUid, sourceSyntax, hasPreferred, preferred);
+    }
 
-    std::map<DicomTransferSyntax, uint8_t> contexts;
     if (association_->LookupAcceptedPresentationContext(contexts, sopClassUid))
     {
       for (std::map<DicomTransferSyntax, uint8_t>::const_iterator
@@ -496,8 +511,10 @@
       }
     }
   }
+#endif
+  
 
-
+#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
   void DicomStoreUserConnection::Transcode(std::string& sopClassUid /* out */,
                                            std::string& sopInstanceUid /* out */,
                                            IDicomTranscoder& transcoder,
@@ -521,6 +538,12 @@
     std::set<DicomTransferSyntax> accepted;
     LookupTranscoding(accepted, sopClassUid, sourceSyntax, true, preferredTransferSyntax);
 
+    if (accepted.size() == 0)
+    {
+      throw OrthancException(ErrorCode_NoPresentationContext, "Cannot C-Store an instance of SOPClassUID " + 
+                             sopClassUid + ", the destination has not accepted any TransferSyntax for this SOPClassUID.");
+    }
+
     if (accepted.find(sourceSyntax) != accepted.end())
     {
       // No need for transcoding
@@ -541,6 +564,8 @@
       bool isDestructiveCompressionAllowed = false;
       std::set<DicomTransferSyntax> attemptedSyntaxes;
 
+      LOG(INFO) << "Transcoding is required to C-Store an instance of SOPClassUID '" << sopClassUid << "', preferredTransferSyntax is " << GetTransferSyntaxUid(preferredTransferSyntax);
+
       if (accepted.find(preferredTransferSyntax) != accepted.end())
       {
         // New in Orthanc 1.9.0: The preferred transfer syntax is
@@ -626,14 +651,17 @@
           s += " " + std::string(GetTransferSyntaxUid(*it));
         }
         
-        throw OrthancException(ErrorCode_NotImplemented, "Cannot transcode from " +
+        throw OrthancException(ErrorCode_NotImplemented, "Cannot transcode instance of SOPClassUID " + 
+                               sopClassUid + " from " +
                                std::string(GetTransferSyntaxUid(sourceSyntax)) +
                                " to one of [" + s + " ]");
       }
     }
   }
-
+#endif
   
+  
+#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
   void DicomStoreUserConnection::Transcode(std::string& sopClassUid /* out */,
                                            std::string& sopInstanceUid /* out */,
                                            IDicomTranscoder& transcoder,
@@ -646,4 +674,5 @@
     Transcode(sopClassUid, sopInstanceUid, transcoder, buffer, size, DicomTransferSyntax_LittleEndianExplicit,
               hasMoveOriginator, moveOriginatorAET, moveOriginatorID);
   }
+#endif
 }
--- a/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -96,11 +97,13 @@
                                       bool hasPreferred,
                                       DicomTransferSyntax preferred);
 
+#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
     void LookupTranscoding(std::set<DicomTransferSyntax>& acceptedSyntaxes,
                            const std::string& sopClassUid,
                            DicomTransferSyntax sourceSyntax,
                            bool hasPreferred,
                            DicomTransferSyntax preferred);
+#endif
 
   public:
     explicit DicomStoreUserConnection(const DicomAssociationParameters& params);
@@ -142,6 +145,7 @@
                           DicomTransferSyntax& transferSyntax,
                           DcmFileFormat& dicom);
 
+#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
     void Transcode(std::string& sopClassUid /* out */,
                    std::string& sopInstanceUid /* out */,
                    IDicomTranscoder& transcoder,
@@ -151,7 +155,9 @@
                    bool hasMoveOriginator,
                    const std::string& moveOriginatorAET,
                    uint16_t moveOriginatorID);
-
+#endif
+    
+#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
     void Transcode(std::string& sopClassUid /* out */,
                    std::string& sopInstanceUid /* out */,
                    IDicomTranscoder& transcoder,
@@ -160,5 +166,6 @@
                    bool hasMoveOriginator,
                    const std::string& moveOriginatorAET,
                    uint16_t moveOriginatorID);
+#endif
   };
 }
--- a/OrthancFramework/Sources/DicomNetworking/IApplicationEntityFilter.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IApplicationEntityFilter.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -25,6 +26,7 @@
 
 #include "../Enumerations.h"
 
+#include <boost/noncopyable.hpp>
 #include <string>
 
 namespace Orthanc
--- a/OrthancFramework/Sources/DicomNetworking/IFindRequestHandler.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IFindRequestHandler.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/IFindRequestHandlerFactory.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IFindRequestHandlerFactory.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/IGetRequestHandler.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IGetRequestHandler.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/IGetRequestHandlerFactory.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IGetRequestHandlerFactory.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/IMoveRequestHandler.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IMoveRequestHandler.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/IMoveRequestHandlerFactory.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IMoveRequestHandlerFactory.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/IStorageCommitmentRequestHandler.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IStorageCommitmentRequestHandler.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/IStorageCommitmentRequestHandlerFactory.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IStorageCommitmentRequestHandlerFactory.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandler.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandler.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandlerFactory.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandlerFactory.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/IWorklistRequestHandler.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IWorklistRequestHandler.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/IWorklistRequestHandlerFactory.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IWorklistRequestHandlerFactory.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -481,7 +482,7 @@
 #if DCMTK_VERSION_NUMBER >= 362
           // The global variable "numberOfDcmAllStorageSOPClassUIDs" is
           // only published if DCMTK >= 3.6.2:
-          // https://bugs.orthanc-server.com/show_bug.cgi?id=137
+          // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=137
           assert(static_cast<int>(count) == numberOfDcmAllStorageSOPClassUIDs);
 #endif
       
@@ -521,10 +522,12 @@
                  * Accept in the order "least wanted" to "most wanted"
                  * transfer syntax.  Accepting a transfer syntax will
                  * override previously accepted transfer syntaxes.
+                 * Since Orthanc 1.11.2+, we give priority to the transfer
+                 * syntaxes proposed in the presentation context.
                  **/
-                for (int k = static_cast<int>(storageTransferSyntaxesC.size()) - 1; k >= 0; k--)
+                for (int j = static_cast<int>(pc.transferSyntaxCount)-1; j >=0; j--)
                 {
-                  for (int j = 0; j < static_cast<int>(pc.transferSyntaxCount); j++)
+                  for (int k = static_cast<int>(storageTransferSyntaxesC.size()) - 1; k >= 0; k--)
                   {
                     /**
                      * If the transfer syntax was proposed then we can accept it
--- a/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -27,7 +28,9 @@
 #include "../../Logging.h"
 #include "../../OrthancException.h"
 #include "../../SystemToolbox.h"
-
+#include "../../Toolbox.h"
+#include <openssl/ssl.h>
+#include <openssl/err.h>
 
 #if DCMTK_VERSION_NUMBER < 364
 #  define DCF_Filetype_PEM  SSL_FILETYPE_PEM
@@ -58,12 +61,47 @@
 #endif
 
 
+#if DCMTK_VERSION_NUMBER >= 367
+    static OFCondition MyConvertOpenSSLError(unsigned long errorCode, OFBool logAsError)
+    {
+      return DcmTLSTransportLayer::convertOpenSSLError(errorCode, logAsError);
+    }
+#else
+    static OFCondition MyConvertOpenSSLError(unsigned long errorCode, OFBool logAsError)
+    {
+      if (errorCode == 0)
+      {
+        return EC_Normal;
+      }
+      else
+      {
+        const char *err = ERR_reason_error_string(errorCode);
+        if (err == NULL)
+        {
+          err = "OpenSSL error";
+        }
+
+        if (logAsError)
+        {
+          DCMTLS_ERROR("OpenSSL error " << STD_NAMESPACE hex << STD_NAMESPACE setfill('0')
+                       << STD_NAMESPACE setw(8) << errorCode << ": " << err);
+        }
+
+        // The "2" below corresponds to the same error code as "DCMTLS_EC_FailedToSetCiphersuites"
+        return OFCondition(OFM_dcmtls, 2, OF_error, err);
+      }
+    }
+#endif
+
+
     DcmTLSTransportLayer* InitializeDicomTls(T_ASC_Network *network,
                                              T_ASC_NetworkRole role,
                                              const std::string& ownPrivateKeyPath,
                                              const std::string& ownCertificatePath,
                                              const std::string& trustedCertificatesPath,
-                                             bool requireRemoteCertificate)
+                                             bool requireRemoteCertificate,
+                                             unsigned int minimalTlsVersion,
+                                             const std::set<std::string>& ciphers)
     {
       if (network == NULL)
       {
@@ -76,7 +114,7 @@
         throw OrthancException(ErrorCode_ParameterOutOfRange, "Unknown role");
       }
     
-      if (!SystemToolbox::IsRegularFile(trustedCertificatesPath))
+      if (requireRemoteCertificate && !SystemToolbox::IsRegularFile(trustedCertificatesPath))
       {
         throw OrthancException(ErrorCode_InexistentFile, "Cannot read file with trusted certificates for DICOM TLS: " +
                                trustedCertificatesPath);
@@ -120,7 +158,7 @@
         new DcmTLSTransportLayer(tmpRole /*opt_networkRole*/, NULL /*opt_readSeedFile*/,
                                  OFFalse /*initializeOpenSSL, done by Orthanc::Toolbox::InitializeOpenSsl()*/));
 
-      if (IsFailure(tls->addTrustedCertificateFile(trustedCertificatesPath.c_str(), DCF_Filetype_PEM /*opt_keyFileFormat*/)))
+      if (requireRemoteCertificate && IsFailure(tls->addTrustedCertificateFile(trustedCertificatesPath.c_str(), DCF_Filetype_PEM /*opt_keyFileFormat*/)))
       {
         throw OrthancException(ErrorCode_BadFileFormat, "Cannot parse PEM file with trusted certificates for DICOM TLS: " +
                                trustedCertificatesPath);
@@ -132,7 +170,18 @@
                                ownPrivateKeyPath);
       }
 
-      if (IsFailure(tls->setCertificateFile(ownCertificatePath.c_str(), DCF_Filetype_PEM /*opt_keyFileFormat*/)))
+      if (IsFailure(tls->setCertificateFile(
+                      ownCertificatePath.c_str(), DCF_Filetype_PEM /*opt_keyFileFormat*/
+#if DCMTK_VERSION_NUMBER >= 368
+                      /**
+                       * DICOM BCP 195 RFC 8996 TLS Profile, based on RFC 8996 and RFC 9325.
+                       * This profile only negotiates TLS 1.2 or newer, and will not fall back to
+                       * previous TLS versions. It provides the higher security level offered by the
+                       * 2021 revised edition of BCP 195.
+                       **/
+                      , TSP_Profile_BCP_195_RFC_8996
+#endif
+                      )))
       {
         throw OrthancException(ErrorCode_BadFileFormat, "Cannot parse PEM file with own certificate for DICOM TLS: " +
                                ownCertificatePath);
@@ -145,14 +194,94 @@
       }
 
 #if DCMTK_VERSION_NUMBER >= 364
-      if (IsFailure(tls->setTLSProfile(TSP_Profile_BCP195 /*opt_tlsProfile*/)))
+      if (minimalTlsVersion == 0) // use the default values (same behavior as before 1.12.4)
       {
-        throw OrthancException(ErrorCode_InternalError, "Cannot set the DICOM TLS profile");
+        if (ciphers.size() > 0)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat, "The cipher suites can not be specified when using the default BCP profile");
+        }
+
+        if (IsFailure(tls->setTLSProfile(TSP_Profile_BCP195 /*opt_tlsProfile*/)))
+        {
+          throw OrthancException(ErrorCode_InternalError, "Cannot set the DICOM TLS profile");
+        }
+      
+        if (IsFailure(tls->activateCipherSuites()))
+        {
+          throw OrthancException(ErrorCode_InternalError, "Cannot activate the cipher suites for DICOM TLS");
+        }
       }
-    
-      if (IsFailure(tls->activateCipherSuites()))
+      else
       {
-        throw OrthancException(ErrorCode_InternalError, "Cannot activate the cipher suites for DICOM TLS");
+        // Fine tune the SSL context
+        if (IsFailure(tls->setTLSProfile(TSP_Profile_None)))
+        {
+          throw OrthancException(ErrorCode_InternalError, "Cannot set the DICOM TLS profile");
+        }
+
+        DcmTLSTransportLayer::native_handle_type sslNativeHandle = tls->getNativeHandle();
+        SSL_CTX_clear_options(sslNativeHandle, SSL_OP_NO_SSL_MASK);
+        if (minimalTlsVersion > 1) 
+        {
+          SSL_CTX_set_options(sslNativeHandle, SSL_OP_NO_SSLv3);
+        }
+        if (minimalTlsVersion > 2) 
+        {
+          SSL_CTX_set_options(sslNativeHandle, SSL_OP_NO_TLSv1);
+        }
+        if (minimalTlsVersion > 3) 
+        {
+          SSL_CTX_set_options(sslNativeHandle, SSL_OP_NO_TLSv1_1);
+        }
+        if (minimalTlsVersion > 4) 
+        {
+          SSL_CTX_set_options(sslNativeHandle, SSL_OP_NO_TLSv1_2);
+        }
+
+        std::set<std::string> ciphersTls;
+        std::set<std::string> ciphersTls13;
+
+        // DCMTK 3.8 is missing a method to add TLS13 cipher suite in the DcmTLSTransportLayer interface.
+        // And, anyway, since we do not run dcmtkPrepare.cmake, DCMTK is not aware of TLS v1.3 cipher suite names.
+        for (std::set<std::string>::const_iterator it = ciphers.begin(); it != ciphers.end(); ++it)
+        {
+          bool isValid = false;
+          if (DcmTLSCiphersuiteHandler::lookupCiphersuiteByOpenSSLName(it->c_str()) != DcmTLSCiphersuiteHandler::unknownCipherSuiteIndex)
+          {
+            ciphersTls.insert(it->c_str());
+            isValid = true;
+          }
+          
+          // list of TLS v1.3 ciphers according to https://www.openssl.org/docs/man3.3/man1/openssl-ciphers.html
+          if (strstr("TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_CCM_SHA256:TLS_AES_128_CCM_8_SHA256", it->c_str()) != NULL)
+          {
+            ciphersTls13.insert(it->c_str());
+            isValid = true;
+          }
+
+          if (!isValid)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat, "The cipher suite " + *it + " is not recognized as valid cipher suite by OpenSSL ");
+          }
+        }
+
+        std::string joinedCiphersTls;
+        std::string joinedCiphersTls13;
+        Toolbox::JoinStrings(joinedCiphersTls, ciphersTls, ":");
+        Toolbox::JoinStrings(joinedCiphersTls13, ciphersTls13, ":");
+
+        if (joinedCiphersTls.size() > 0 && SSL_CTX_set_cipher_list(sslNativeHandle, joinedCiphersTls.c_str()) != 1)
+        {
+          OFCondition cond = MyConvertOpenSSLError(ERR_get_error(), OFTrue);
+          throw OrthancException(ErrorCode_InternalError, "Unable to configure cipher suite.  OpenSSL error: " + boost::lexical_cast<std::string>(cond.code()) + " - " + cond.text());
+        }
+
+        if (joinedCiphersTls13.size() > 0 && SSL_CTX_set_ciphersuites(sslNativeHandle, joinedCiphersTls13.c_str()) != 1)
+        {
+          OFCondition cond = MyConvertOpenSSLError(ERR_get_error(), OFTrue);
+          throw OrthancException(ErrorCode_InternalError, "Unable to configure cipher suite for TLS 1.3.  OpenSSL error: " + boost::lexical_cast<std::string>(cond.code()) + " - " + cond.text());
+        }
+
       }
 #else
       CLOG(INFO, DICOM) << "Using the following cipher suites for DICOM TLS: " << opt_ciphersuites;
@@ -169,8 +298,8 @@
       }
       else
       {
-        // Check remote certificate if present, succeed if no certificate is present
-        tls->setCertificateVerification(DCV_checkCertificate /*opt_certVerification*/);
+        // From 1.12.4, do not even request remote certificate (prior to 1.12.4, we were requesting a certificates, checking it if present and succeeding if not present)
+        tls->setCertificateVerification(DCV_ignoreCertificate /*opt_certVerification*/);
       }
       
       if (ASC_setTransportLayer(network, tls.get(), 0).bad())
--- a/OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -38,7 +39,7 @@
 
 #include <dcmtk/dcmnet/dimse.h>
 #include <dcmtk/dcmtls/tlslayer.h>
-
+#include <set>
 
 namespace Orthanc
 {
@@ -50,6 +51,9 @@
       const std::string& ownPrivateKeyPath,        // This is the first argument of "+tls" option from DCMTK command-line tools
       const std::string& ownCertificatePath,       // This is the second argument of "+tls" option
       const std::string& trustedCertificatesPath,  // This is the "--add-cert-file" ("+cf") option
-      bool requireRemoteCertificate);              // "true" means "--require-peer-cert", "false" means "--verify-peer-cert"
+      bool requireRemoteCertificate,               // "true" means "--require-peer-cert", "false" means "--ignore-peer-cert"
+      unsigned int minimalTlsVersion,              // 0 = default BCP195, 5 = TLS1.3 only
+      const std::set<std::string>& acceptedCiphers
+    );
   }
 }
--- a/OrthancFramework/Sources/DicomNetworking/Internals/FindScp.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/FindScp.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -174,11 +175,15 @@
       // http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#sect_C.4.1.1.3
       // https://groups.google.com/d/msg/orthanc-users/D3kpPuX8yV0/_zgHOzkMEQAJ
 
+      // GroupLength are removed as well since they make no sense in the filtering as well as in the response.  
+      // Note that it seems that only some GE devices include them.
+
       DicomArray a(source);
 
       for (size_t i = 0; i < a.GetSize(); i++)
       {
-        if (a.GetElement(i).GetTag().GetGroup() >= 0x0008)
+        if (a.GetElement(i).GetTag().GetGroup() >= 0x0008 
+          && a.GetElement(i).GetTag().GetElement() != 0x0000)
         {
           target.SetValue(a.GetElement(i).GetTag(), a.GetElement(i).GetValue());
         }
--- a/OrthancFramework/Sources/DicomNetworking/Internals/FindScp.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/FindScp.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/Internals/GetScp.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/GetScp.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/Internals/GetScp.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/GetScp.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/Internals/MoveScp.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/MoveScp.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -86,7 +87,7 @@
 /**
  * Macro specifying whether to apply the patch suggested in issue 66:
  * "Orthanc responses C-MOVE with zero Move Originator Message ID"
- * https://bugs.orthanc-server.com/show_bug.cgi?id=66
+ * https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=66
  **/
 
 #define APPLY_FIX_ISSUE_66   1
--- a/OrthancFramework/Sources/DicomNetworking/Internals/MoveScp.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/MoveScp.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/NetworkingCompatibility.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/NetworkingCompatibility.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -32,7 +33,6 @@
  * http://msdn.microsoft.com/en-us/library/windows/desktop/ms738527(v=vs.85).aspx
  **/
 #  define HOST_NAME_MAX 256
-#  include <winsock.h>
 #endif 
 
 
--- a/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -186,7 +187,6 @@
     manufacturer_ = StringToModalityManufacturer(manufacturer);
   }
 
-
   void RemoteModalityParameters::UnserializeArray(const Json::Value& serialized)
   {
     assert(serialized.type() == Json::arrayValue);
--- a/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/TimeoutDicomConnectionManager.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/TimeoutDicomConnectionManager.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomNetworking/TimeoutDicomConnectionManager.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/TimeoutDicomConnectionManager.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -37,6 +38,7 @@
 #include "FromDcmtkBridge.h"
 #include "../Logging.h"
 #include "../OrthancException.h"
+#include "../Toolbox.h"
 
 #include <dcmtk/dcmdata/dcdeftag.h>
 #include <dcmtk/dcmjpeg/djrploss.h>  // for DJ_RPLossy
@@ -83,12 +85,33 @@
     return lossyQuality_;
   }
 
+  bool TryTranscode(std::vector<std::string>& failureReasons, /* out */
+                    DicomTransferSyntax& selectedSyntax, /* out*/
+                    DcmFileFormat& dicom, /* in/out */
+                    const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                    DicomTransferSyntax trySyntax)
+  {
+    if (allowedSyntaxes.find(trySyntax) != allowedSyntaxes.end())
+    {
+      if (FromDcmtkBridge::Transcode(dicom, trySyntax, NULL))
+      {
+        selectedSyntax = trySyntax;
+        return true;
+      }
+
+      failureReasons.push_back(std::string("Internal error while transcoding to ") + GetTransferSyntaxUid(trySyntax));
+    }
+    return false;
+  }
 
   bool DcmtkTranscoder::InplaceTranscode(DicomTransferSyntax& selectedSyntax /* out */,
-                                         DcmFileFormat& dicom,
+                                         std::string& failureReason /* out */,
+                                         DcmFileFormat& dicom, /* in/out */
                                          const std::set<DicomTransferSyntax>& allowedSyntaxes,
                                          bool allowNewSopInstanceUid) 
   {
+    std::vector<std::string> failureReasons;
+
     if (dicom.getDataset() == NULL)
     {
       throw OrthancException(ErrorCode_InternalError);
@@ -109,62 +132,75 @@
       // No transcoding is needed
       return true;
     }
-      
-    if (allowedSyntaxes.find(DicomTransferSyntax_LittleEndianImplicit) != allowedSyntaxes.end() &&
-        FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_LittleEndianImplicit, NULL))
+    
+    if (TryTranscode(failureReasons, selectedSyntax, dicom, allowedSyntaxes, DicomTransferSyntax_LittleEndianImplicit))
+    {
+      return true;
+    }
+
+    if (TryTranscode(failureReasons, selectedSyntax, dicom, allowedSyntaxes, DicomTransferSyntax_LittleEndianExplicit))
     {
-      selectedSyntax = DicomTransferSyntax_LittleEndianImplicit;
+      return true;
+    }
+
+    if (TryTranscode(failureReasons, selectedSyntax, dicom, allowedSyntaxes, DicomTransferSyntax_BigEndianExplicit))
+    {
+      return true;
+    }
+
+    if (TryTranscode(failureReasons, selectedSyntax, dicom, allowedSyntaxes, DicomTransferSyntax_DeflatedLittleEndianExplicit))
+    {
       return true;
     }
 
-    if (allowedSyntaxes.find(DicomTransferSyntax_LittleEndianExplicit) != allowedSyntaxes.end() &&
-        FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_LittleEndianExplicit, NULL))
-    {
-      selectedSyntax = DicomTransferSyntax_LittleEndianExplicit;
-      return true;
-    }
-      
-    if (allowedSyntaxes.find(DicomTransferSyntax_BigEndianExplicit) != allowedSyntaxes.end() &&
-        FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_BigEndianExplicit, NULL))
-    {
-      selectedSyntax = DicomTransferSyntax_BigEndianExplicit;
-      return true;
-    }
-
-    if (allowedSyntaxes.find(DicomTransferSyntax_DeflatedLittleEndianExplicit) != allowedSyntaxes.end() &&
-        FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_DeflatedLittleEndianExplicit, NULL))
-    {
-      selectedSyntax = DicomTransferSyntax_DeflatedLittleEndianExplicit;
-      return true;
-    }
 
 #if ORTHANC_ENABLE_DCMTK_JPEG == 1
-    if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess1) != allowedSyntaxes.end() &&
-        allowNewSopInstanceUid &&
-        (!hasBitsStored || bitsStored == 8))
+    if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess1) != allowedSyntaxes.end())
     {
-      // Check out "dcmjpeg/apps/dcmcjpeg.cc"
-      DJ_RPLossy parameters(lossyQuality_);
-        
-      if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess1, &parameters))
+      if (!allowNewSopInstanceUid)
+      {
+        failureReasons.push_back(std::string("Can not transcode to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGProcess1) + " without generating new SOPInstanceUID");
+      }
+      else if (hasBitsStored && bitsStored != 8)
+      {
+        failureReasons.push_back(std::string("Can not transcode to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGProcess1) + " if BitsStored != 8");
+      }
+      else
       {
-        selectedSyntax = DicomTransferSyntax_JPEGProcess1;
-        return true;
+        // Check out "dcmjpeg/apps/dcmcjpeg.cc"
+        DJ_RPLossy parameters(lossyQuality_);
+          
+        if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess1, &parameters))
+        {
+          selectedSyntax = DicomTransferSyntax_JPEGProcess1;
+          return true;
+        }
+        failureReasons.push_back(std::string("Internal error while transcoding to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGProcess1));
       }
     }
 #endif
       
 #if ORTHANC_ENABLE_DCMTK_JPEG == 1
-    if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess2_4) != allowedSyntaxes.end() &&
-        allowNewSopInstanceUid &&
-        (!hasBitsStored || bitsStored <= 12))
+    if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess2_4) != allowedSyntaxes.end())
     {
-      // Check out "dcmjpeg/apps/dcmcjpeg.cc"
-      DJ_RPLossy parameters(lossyQuality_);
-      if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess2_4, &parameters))
+      if (!allowNewSopInstanceUid)
+      {
+        failureReasons.push_back(std::string("Can not transcode to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGProcess2_4) + " without generating new SOPInstanceUID");
+      }
+      else if (hasBitsStored && bitsStored > 12)
       {
-        selectedSyntax = DicomTransferSyntax_JPEGProcess2_4;
-        return true;
+        failureReasons.push_back(std::string("Can not transcode to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGProcess2_4) + " if BitsStored != 8");
+      }
+      else
+      {
+        // Check out "dcmjpeg/apps/dcmcjpeg.cc"
+        DJ_RPLossy parameters(lossyQuality_);
+        if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess2_4, &parameters))
+        {
+          selectedSyntax = DicomTransferSyntax_JPEGProcess2_4;
+          return true;
+        }
+        failureReasons.push_back(std::string("Internal error while transcoding to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGProcess2_4));
       }
     }
 #endif
@@ -180,6 +216,7 @@
         selectedSyntax = DicomTransferSyntax_JPEGProcess14;
         return true;
       }
+      failureReasons.push_back(std::string("Internal error while transcoding to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGProcess14));
     }
 #endif
       
@@ -194,6 +231,7 @@
         selectedSyntax = DicomTransferSyntax_JPEGProcess14SV1;
         return true;
       }
+      failureReasons.push_back(std::string("Internal error while transcoding to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGProcess14SV1));
     }
 #endif
       
@@ -213,6 +251,7 @@
         selectedSyntax = DicomTransferSyntax_JPEGLSLossless;
         return true;
       }
+      failureReasons.push_back(std::string("Internal error while transcoding to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGLSLossless));
     }
 #endif
       
@@ -233,9 +272,11 @@
         selectedSyntax = DicomTransferSyntax_JPEGLSLossy;
         return true;
       }
+      failureReasons.push_back(std::string("Internal error while transcoding to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGLSLossy));
     }
 #endif
 
+    Orthanc::Toolbox::JoinStrings(failureReason, failureReasons, ", ");
     return false;
   }
 
@@ -285,28 +326,27 @@
       return false;
     }
 
+    std::string failureReason;
+    std::string s;
+    for (std::set<DicomTransferSyntax>::const_iterator
+            it = allowedSyntaxes.begin(); it != allowedSyntaxes.end(); ++it)
     {
-      std::string s;
-      for (std::set<DicomTransferSyntax>::const_iterator
-             it = allowedSyntaxes.begin(); it != allowedSyntaxes.end(); ++it)
+      if (!s.empty())
       {
-        if (!s.empty())
-        {
-          s += ", ";
-        }
-
-        s += GetTransferSyntaxUid(*it);
+        s += ", ";
       }
 
-      if (s.empty())
-      {
-        s = "<none>";
-      }
-      
-      LOG(INFO) << "DCMTK transcoding from " << GetTransferSyntaxUid(sourceSyntax)
-                << " to one of: " << s;
+      s += GetTransferSyntaxUid(*it);
     }
 
+    if (s.empty())
+    {
+      s = "<none>";
+    }
+
+    LOG(INFO) << "DCMTK transcoding from " << GetTransferSyntaxUid(sourceSyntax)
+              << " to one of: " << s;
+
 #if !defined(NDEBUG)
     const std::string sourceSopInstanceUid = GetSopInstanceUid(source.GetParsed());
 #endif
@@ -319,7 +359,7 @@
       target.AcquireBuffer(source);
       return true;
     }
-    else if (InplaceTranscode(targetSyntax, source.GetParsed(),
+    else if (InplaceTranscode(targetSyntax, failureReason, source.GetParsed(),
                               allowedSyntaxes, allowNewSopInstanceUid))
     {   
       // Sanity check
@@ -347,6 +387,8 @@
     else
     {
       // Cannot transcode
+      LOG(WARNING) << "DCMTK was unable to transcode from " << GetTransferSyntaxUid(sourceSyntax)
+                   << " to one of: " << s << " " << failureReason;
       return false;
     }
   }
--- a/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -41,6 +42,7 @@
     unsigned int  lossyQuality_;
     
     bool InplaceTranscode(DicomTransferSyntax& selectedSyntax /* out */,
+                          std::string& failureReason /* out */,
                           DcmFileFormat& dicom,
                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
                           bool allowNewSopInstanceUid);
--- a/OrthancFramework/Sources/DicomParsing/DicomDirWriter.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DicomDirWriter.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomParsing/DicomDirWriter.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DicomDirWriter.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomParsing/DicomModification.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DicomModification.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -43,6 +44,9 @@
 static const std::string ORTHANC_DEIDENTIFICATION_METHOD_2021b =
   "Orthanc " ORTHANC_VERSION " - PS 3.15-2021b Table E.1-1 Basic Profile";
 
+static const std::string ORTHANC_DEIDENTIFICATION_METHOD_2023b =
+  "Orthanc " ORTHANC_VERSION " - PS 3.15-2023b Table E.1-1 Basic Profile";
+
 namespace Orthanc
 {
   namespace
@@ -314,7 +318,7 @@
                * data sets including:
                * https://wiki.cancerimagingarchive.net/display/Public/Lung+CT+Segmentation+Challenge+2017)
                * Tested in "test_anonymize_relationships_5". Introduced
-               * in: https://hg.orthanc-server.com/orthanc/rev/3513
+               * in: https://orthanc.uclouvain.be/hg/orthanc/rev/3513
                **/
               newValue = that_.MapDicomIdentifier(value, ResourceType_Study);
             }
@@ -427,7 +431,8 @@
 
       if (it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2008 ||
           it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2017c ||
-          it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2021b)
+          it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2021b ||
+          it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2023b)
       {
         delete it->second;
         replacements_.erase(it);
@@ -521,6 +526,7 @@
   
   DicomModification::DicomModification() :
     removePrivateTags_(false),
+    keepLabels_(false),
     level_(ResourceType_Instance),
     allowManualIdentifiers_(true),
     keepStudyInstanceUid_(false),
@@ -540,6 +546,7 @@
 
   void DicomModification::Keep(const DicomTag& tag)
   {
+    keep_.insert(tag);
     removals_.erase(tag);
     clearings_.erase(tag);
     uids_.erase(tag);
@@ -636,6 +643,11 @@
     return replacements_.find(tag) != replacements_.end();
   }
 
+  bool DicomModification::IsKept(const DicomTag& tag) const
+  {
+    return keep_.find(tag) != keep_.end();
+  }
+
   const Json::Value& DicomModification::GetReplacement(const DicomTag& tag) const
   {
     Replacements::const_iterator it = replacements_.find(tag);
@@ -682,6 +694,16 @@
     return removePrivateTags_;
   }
 
+  void DicomModification::SetKeepLabels(bool keep)
+  {
+    keepLabels_ = keep;
+  }
+
+  bool DicomModification::AreLabelsKept() const
+  {
+    return keepLabels_;
+  }
+
   void DicomModification::SetLevel(ResourceType level)
   {
     uidMap_.clear();
@@ -714,7 +736,7 @@
      * Values below come from the hardcoded UID of Orthanc 1.9.3
      * in DicomModification::RelationshipsVisitor::VisitString() and
      * DicomModification::RelationshipsVisitor::RemoveRelationships()
-     * https://hg.orthanc-server.com/orthanc/file/Orthanc-1.9.3/OrthancFramework/Sources/DicomParsing/DicomModification.cpp#l117
+     * https://orthanc.uclouvain.be/hg/orthanc/file/Orthanc-1.9.3/OrthancFramework/Sources/DicomParsing/DicomModification.cpp#l117
      **/
     uids_.clear();
 
@@ -825,9 +847,6 @@
      * generated automatically by calling:
      * "../../../OrthancServer/Resources/GenerateAnonymizationProfile.py
      * https://raw.githubusercontent.com/jodogne/dicom-specification/master/2021b/part15.xml"
-     *
-     * http://dicom.nema.org/medical/dicom/2021b/output/chtml/part15/chapter_E.html#table_E.1-1a
-     * http://dicom.nema.org/medical/dicom/2021b/output/chtml/part15/chapter_E.html#table_E.1-1
      **/
     
 #include "DicomModification_Anonymization2021b.impl.h"
@@ -837,10 +856,31 @@
   }
   
 
+  void DicomModification::SetupAnonymization2023b()
+  {
+    /**
+     * This is Table E.1-1 from PS 3.15-2023b (DICOM Part 15: Security
+     * and System Management Profiles), "basic profile" column. It was
+     * generated automatically by calling:
+     * "../../../OrthancServer/Resources/GenerateAnonymizationProfile.py
+     * https://raw.githubusercontent.com/jodogne/dicom-specification/master/2023b/part15.xml"
+     *
+     * http://dicom.nema.org/medical/dicom/current/output/chtml/part15/chapter_E.html#table_E.1-1a
+     * http://dicom.nema.org/medical/dicom/current/output/chtml/part15/chapter_E.html#table_E.1-1
+     **/
+    
+#include "DicomModification_Anonymization2023b.impl.h"
+    
+    // Set the DeidentificationMethod tag
+    ReplaceInternal(DICOM_TAG_DEIDENTIFICATION_METHOD, ORTHANC_DEIDENTIFICATION_METHOD_2023b);
+  }
+  
+
   void DicomModification::SetupAnonymization(DicomVersion version)
   {
     isAnonymization_ = true;
     
+    keep_.clear();
     removals_.clear();
     clearings_.clear();
     removedRanges_.clear();
@@ -867,6 +907,10 @@
         SetupAnonymization2021b();
         break;
 
+      case DicomVersion_2023b:
+        SetupAnonymization2023b();
+        break;
+
       default:
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
@@ -918,22 +962,12 @@
         IsRemoved(DICOM_TAG_SERIES_INSTANCE_UID) ||
         IsRemoved(DICOM_TAG_SOP_INSTANCE_UID))
     {
-      throw OrthancException(ErrorCode_BadRequest);
+      throw OrthancException(ErrorCode_BadRequest, "It is forbidden to remove one of the main Dicom identifiers");
     }
     
-
-    // Sanity checks at the patient level
-    bool isReplacedPatientId = (IsReplaced(DICOM_TAG_PATIENT_ID) ||
-                                uids_.find(DICOM_TAG_PATIENT_ID) != uids_.end());
-    
-    if (level_ == ResourceType_Patient && !isReplacedPatientId)
-    {
-      throw OrthancException(ErrorCode_BadRequest,
-                             "When modifying a patient, her PatientID is required to be modified");
-    }
-
     if (!allowManualIdentifiers_)
     {
+       // Sanity checks at the patient level
       if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
       {
         throw OrthancException(ErrorCode_BadRequest,
@@ -951,18 +985,8 @@
         throw OrthancException(ErrorCode_BadRequest,
                                "When modifying a patient, the SopInstanceUID cannot be manually modified");
       }
-    }
 
-
-    // Sanity checks at the study level
-    if (level_ == ResourceType_Study && isReplacedPatientId)
-    {
-      throw OrthancException(ErrorCode_BadRequest,
-                             "When modifying a study, the parent PatientID cannot be manually modified");
-    }
-
-    if (!allowManualIdentifiers_)
-    {
+      // Sanity checks at the study level
       if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
       {
         throw OrthancException(ErrorCode_BadRequest,
@@ -974,24 +998,8 @@
         throw OrthancException(ErrorCode_BadRequest,
                                "When modifying a study, the SopInstanceUID cannot be manually modified");
       }
-    }
 
-
-    // Sanity checks at the series level
-    if (level_ == ResourceType_Series && isReplacedPatientId)
-    {
-      throw OrthancException(ErrorCode_BadRequest,
-                             "When modifying a series, the parent PatientID cannot be manually modified");
-    }
-
-    if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
-    {
-      throw OrthancException(ErrorCode_BadRequest,
-                             "When modifying a series, the parent StudyInstanceUID cannot be manually modified");
-    }
-
-    if (!allowManualIdentifiers_)
-    {
+      // Sanity checks at the series level
       if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
       {
         throw OrthancException(ErrorCode_BadRequest,
@@ -1000,25 +1008,6 @@
     }
 
 
-    // Sanity checks at the instance level
-    if (level_ == ResourceType_Instance && isReplacedPatientId)
-    {
-      throw OrthancException(ErrorCode_BadRequest,
-                             "When modifying an instance, the parent PatientID cannot be manually modified");
-    }
-
-    if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
-    {
-      throw OrthancException(ErrorCode_BadRequest,
-                             "When modifying an instance, the parent StudyInstanceUID cannot be manually modified");
-    }
-
-    if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
-    {
-      throw OrthancException(ErrorCode_BadRequest,
-                             "When modifying an instance, the parent SeriesInstanceUID cannot be manually modified");
-    }
-
     // (0) Create a summary of the source file, if a custom generator
     // is provided
     if (identifierGenerator_ != NULL)
@@ -1296,6 +1285,11 @@
       SetRemovePrivateTags(true);
     }
 
+    if (GetBooleanValue("KeepLabels", request, false))
+    {
+      SetKeepLabels(true);
+    }
+
     if (request.isMember("Remove"))
     {
       ParseListOfTags(*this, request["Remove"], TagOperation_Remove, force);
@@ -1321,6 +1315,62 @@
     {
       privateCreator_ = SerializationToolbox::ReadString(request, "PrivateCreator");
     }
+
+    if (!force)
+    {
+      /**
+       * Sanity checks about the manual replacement of DICOM
+       * identifiers. Those checks were part of
+       * "DicomModification::Apply()" in Orthanc <= 1.11.2, and
+       * couldn't be disabled even if using the "Force" flag. Check
+       * out:
+       * https://groups.google.com/g/orthanc-users/c/xMUUZAnBa5g/m/WCEu-U2NBQAJ
+       **/
+      bool isReplacedPatientId = (IsReplaced(DICOM_TAG_PATIENT_ID) ||
+                                  uids_.find(DICOM_TAG_PATIENT_ID) != uids_.end());
+    
+      if (level_ == ResourceType_Patient && !isReplacedPatientId)
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a patient, her PatientID is required to be modified.");
+      }
+
+      if (level_ == ResourceType_Study && isReplacedPatientId)
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a study, the parent PatientID cannot be manually modified");
+      }
+
+      if (level_ == ResourceType_Series && isReplacedPatientId)
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a series, the parent PatientID cannot be manually modified");
+      }
+
+      if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a series, the parent StudyInstanceUID cannot be manually modified");
+      }
+
+      if (level_ == ResourceType_Instance && isReplacedPatientId)
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying an instance, the parent PatientID cannot be manually modified");
+      }
+      
+      if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying an instance, the parent StudyInstanceUID cannot be manually modified");
+      }
+
+      if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying an instance, the parent SeriesInstanceUID cannot be manually modified");
+      }
+    }
   }
 
 
@@ -1336,7 +1386,9 @@
       
     // DicomVersion version = DicomVersion_2008;   // For Orthanc <= 1.2.0
     // DicomVersion version = DicomVersion_2017c;  // For Orthanc between 1.3.0 and 1.9.3
-    DicomVersion version = DicomVersion_2021b;     // For Orthanc >= 1.9.4
+    // DicomVersion version = DicomVersion_2021b;  // For Orthanc >= 1.9.4
+    DicomVersion version = DicomVersion_2023b;     // For Orthanc >= 1.12.1
+    
     if (request.isMember("DicomVersion"))
     {
       if (request["DicomVersion"].type() != Json::stringValue)
@@ -1356,6 +1408,11 @@
       SetRemovePrivateTags(false);
     }
 
+    if (GetBooleanValue("KeepLabels", request, false))
+    {
+      SetKeepLabels(true);
+    }
+
     if (request.isMember("Remove"))
     {
       ParseListOfTags(*this, request["Remove"], TagOperation_Remove, force);
@@ -1585,7 +1642,7 @@
        * 1.5.0 and 1.6.1. This compatibility was broken between 1.7.0
        * and 1.9.3: Indeed, an exception was thrown in "ReadBoolean()"
        * if "KEEP_SOP_INSTANCE_UID" was absent, because of changeset:
-       * https://hg.orthanc-server.com/orthanc/rev/3860
+       * https://orthanc.uclouvain.be/hg/orthanc/rev/3860
        **/
       keepSopInstanceUid_ = false;
     }
@@ -1830,4 +1887,13 @@
             (tag == DICOM_TAG_SOP_INSTANCE_UID &&
              !keepSopInstanceUid_));
   }
+
+  void DicomModification::GetReplacedTags(std::set<DicomTag>& target) const
+  {
+    target.clear();
+    for (Replacements::const_iterator it = replacements_.begin(); it != replacements_.end(); ++it)
+    {
+      target.insert(it->first);
+    }
+  }
 }
--- a/OrthancFramework/Sources/DicomParsing/DicomModification.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DicomModification.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -128,8 +129,10 @@
     
     SetOfTags removals_;
     SetOfTags clearings_;
+    SetOfTags keep_;
     Replacements replacements_;
     bool removePrivateTags_;
+    bool keepLabels_;
     ResourceType level_;
     UidMap uidMap_;
     SetOfTags privateTagsToKeep_;
@@ -178,6 +181,8 @@
 
     void SetupAnonymization2021b();
 
+    void SetupAnonymization2023b();
+
     void UnserializeUidMap(ResourceType level,
                            const Json::Value& serialized,
                            const char* field);
@@ -208,14 +213,22 @@
 
     bool IsReplaced(const DicomTag& tag) const;
 
+    void GetReplacedTags(std::set<DicomTag>& target) const;
+
     const Json::Value& GetReplacement(const DicomTag& tag) const;
 
     std::string GetReplacementAsString(const DicomTag& tag) const;
 
+    bool IsKept(const DicomTag& tag) const;
+
     void SetRemovePrivateTags(bool removed);
 
     bool ArePrivateTagsRemoved() const;
 
+    void SetKeepLabels(bool keep);
+
+    bool AreLabelsKept() const;
+
     void SetLevel(ResourceType level);
 
     ResourceType GetLevel() const;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomParsing/DicomModification_Anonymization2023b.impl.h	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,610 @@
+// RelationshipsVisitor handles (0x0008, 0x1140)  /* X/Z/U* */   // Referenced Image Sequence
+// RelationshipsVisitor handles (0x0008, 0x2112)  /* X/Z/U* */   // Source Image Sequence
+// Tag (0x0008, 0x0018) is set in Apply()         /* U */        // SOP Instance UID
+// Tag (0x0010, 0x0010) is set below (*)          /* Z */        // Patient's Name
+// Tag (0x0010, 0x0020) is set below (*)          /* Z */        // Patient ID
+// Tag (0x0020, 0x000d) is set in Apply()         /* U */        // Study Instance UID
+// Tag (0x0020, 0x000e) is set in Apply()         /* U */        // Series Instance UID
+clearings_.insert(DicomTag(0x0008, 0x0020));                     // Study Date
+clearings_.insert(DicomTag(0x0008, 0x0023));  /* Z/D */          // Content Date
+clearings_.insert(DicomTag(0x0008, 0x0030));                     // Study Time
+clearings_.insert(DicomTag(0x0008, 0x0033));  /* Z/D */          // Content Time
+clearings_.insert(DicomTag(0x0008, 0x0050));                     // Accession Number
+clearings_.insert(DicomTag(0x0008, 0x0090));                     // Referring Physician's Name
+clearings_.insert(DicomTag(0x0008, 0x009c));                     // Consulting Physician's Name
+clearings_.insert(DicomTag(0x0008, 0x0106));  /* D */            // Context Group Version
+clearings_.insert(DicomTag(0x0008, 0x0107));  /* D */            // Context Group Local Version
+clearings_.insert(DicomTag(0x0010, 0x0030));                     // Patient's Birth Date
+clearings_.insert(DicomTag(0x0010, 0x0040));                     // Patient's Sex
+clearings_.insert(DicomTag(0x0012, 0x0010));  /* D */            // Clinical Trial Sponsor Name
+clearings_.insert(DicomTag(0x0012, 0x0020));  /* D */            // Clinical Trial Protocol ID
+clearings_.insert(DicomTag(0x0012, 0x0021));                     // Clinical Trial Protocol Name
+clearings_.insert(DicomTag(0x0012, 0x0030));                     // Clinical Trial Site ID
+clearings_.insert(DicomTag(0x0012, 0x0031));                     // Clinical Trial Site Name
+clearings_.insert(DicomTag(0x0012, 0x0040));  /* D */            // Clinical Trial Subject ID
+clearings_.insert(DicomTag(0x0012, 0x0042));  /* D */            // Clinical Trial Subject Reading ID
+clearings_.insert(DicomTag(0x0012, 0x0050));                     // Clinical Trial Time Point ID
+clearings_.insert(DicomTag(0x0012, 0x0060));                     // Clinical Trial Coordinating Center Name
+clearings_.insert(DicomTag(0x0012, 0x0081));  /* D */            // Clinical Trial Protocol Ethics Committee Name
+clearings_.insert(DicomTag(0x0018, 0x0010));  /* Z/D */          // Contrast/Bolus Agent
+clearings_.insert(DicomTag(0x0018, 0x11bb));  /* D */            // Acquisition Field Of View Label
+clearings_.insert(DicomTag(0x0018, 0x1203));                     // Calibration DateTime
+clearings_.insert(DicomTag(0x0018, 0x9074));  /* D */            // Frame Acquisition DateTime
+clearings_.insert(DicomTag(0x0018, 0x9151));  /* D */            // Frame Reference DateTime
+clearings_.insert(DicomTag(0x0018, 0x9367));  /* D */            // X-Ray Source ID
+clearings_.insert(DicomTag(0x0018, 0x9369));  /* D */            // Source Start DateTime
+clearings_.insert(DicomTag(0x0018, 0x936a));  /* D */            // Source End DateTime
+clearings_.insert(DicomTag(0x0018, 0x9371));  /* D */            // X-Ray Detector ID
+clearings_.insert(DicomTag(0x0018, 0x9623));  /* D */            // Functional Sync Pulse
+clearings_.insert(DicomTag(0x0018, 0x9701));  /* D */            // Decay Correction DateTime
+clearings_.insert(DicomTag(0x0018, 0x9804));  /* D */            // Exclusion Start DateTime
+clearings_.insert(DicomTag(0x0018, 0x9919));  /* Z/D */          // Instruction Performed DateTime
+clearings_.insert(DicomTag(0x0020, 0x0010));                     // Study ID
+clearings_.insert(DicomTag(0x0034, 0x0001));  /* D */            // Flow Identifier Sequence
+clearings_.insert(DicomTag(0x0034, 0x0002));  /* D */            // Flow Identifier
+clearings_.insert(DicomTag(0x0034, 0x0005));  /* D */            // Source Identifier
+clearings_.insert(DicomTag(0x0034, 0x0007));  /* D */            // Frame Origin Timestamp
+clearings_.insert(DicomTag(0x003a, 0x0314));  /* D */            // Impedance Measurement DateTime
+clearings_.insert(DicomTag(0x0040, 0x0512));  /* D */            // Container Identifier
+clearings_.insert(DicomTag(0x0040, 0x0513));                     // Issuer of the Container Identifier Sequence
+clearings_.insert(DicomTag(0x0040, 0x0551));  /* D */            // Specimen Identifier
+clearings_.insert(DicomTag(0x0040, 0x0562));                     // Issuer of the Specimen Identifier Sequence
+clearings_.insert(DicomTag(0x0040, 0x0610));                     // Specimen Preparation Sequence
+clearings_.insert(DicomTag(0x0040, 0x1101));  /* D */            // Person Identification Code Sequence
+clearings_.insert(DicomTag(0x0040, 0x2016));                     // Placer Order Number / Imaging Service Request
+clearings_.insert(DicomTag(0x0040, 0x2017));                     // Filler Order Number / Imaging Service Request
+clearings_.insert(DicomTag(0x0040, 0xa027));  /* D */            // Verifying Organization
+clearings_.insert(DicomTag(0x0040, 0xa030));  /* D */            // Verification DateTime
+clearings_.insert(DicomTag(0x0040, 0xa073));  /* D */            // Verifying Observer Sequence
+clearings_.insert(DicomTag(0x0040, 0xa075));  /* D */            // Verifying Observer Name
+clearings_.insert(DicomTag(0x0040, 0xa082));                     // Participation DateTime
+clearings_.insert(DicomTag(0x0040, 0xa088));                     // Verifying Observer Identification Code Sequence
+clearings_.insert(DicomTag(0x0040, 0xa120));  /* D */            // DateTime
+clearings_.insert(DicomTag(0x0040, 0xa121));  /* D */            // Date
+clearings_.insert(DicomTag(0x0040, 0xa122));  /* D */            // Time
+clearings_.insert(DicomTag(0x0040, 0xa123));  /* D */            // Person Name
+clearings_.insert(DicomTag(0x0040, 0xa13a));  /* D */            // Referenced DateTime
+clearings_.insert(DicomTag(0x0040, 0xa730));  /* D */            // Content Sequence
+clearings_.insert(DicomTag(0x0042, 0x0011));  /* D */            // Encapsulated Document
+clearings_.insert(DicomTag(0x0044, 0x0104));  /* D */            // Assertion DateTime
+clearings_.insert(DicomTag(0x0068, 0x6226));  /* D */            // Effective DateTime
+clearings_.insert(DicomTag(0x0068, 0x6270));  /* D */            // Information Issue DateTime
+clearings_.insert(DicomTag(0x006a, 0x0003));  /* D */            // Annotation Group UID
+clearings_.insert(DicomTag(0x006a, 0x0005));  /* D */            // Annotation Group Label
+clearings_.insert(DicomTag(0x0070, 0x0001));  /* D */            // Graphic Annotation Sequence
+clearings_.insert(DicomTag(0x0070, 0x0084));  /* Z/D */          // Content Creator's Name
+clearings_.insert(DicomTag(0x0072, 0x000a));  /* D */            // Hanging Protocol Creation DateTime
+clearings_.insert(DicomTag(0x0072, 0x005e));  /* D */            // Selector AE Value
+clearings_.insert(DicomTag(0x0072, 0x005f));  /* D */            // Selector AS Value
+clearings_.insert(DicomTag(0x0072, 0x0061));  /* D */            // Selector DA Value
+clearings_.insert(DicomTag(0x0072, 0x0063));  /* D */            // Selector DT Value
+clearings_.insert(DicomTag(0x0072, 0x0065));  /* D */            // Selector OB Value
+clearings_.insert(DicomTag(0x0072, 0x0066));  /* D */            // Selector LO Value
+clearings_.insert(DicomTag(0x0072, 0x0068));  /* D */            // Selector LT Value
+clearings_.insert(DicomTag(0x0072, 0x006a));  /* D */            // Selector PN Value
+clearings_.insert(DicomTag(0x0072, 0x006b));  /* D */            // Selector TM Value
+clearings_.insert(DicomTag(0x0072, 0x006c));  /* D */            // Selector SH Value
+clearings_.insert(DicomTag(0x0072, 0x006d));  /* D */            // Selector UN Value
+clearings_.insert(DicomTag(0x0072, 0x006e));  /* D */            // Selector ST Value
+clearings_.insert(DicomTag(0x0072, 0x0070));  /* D */            // Selector UT Value
+clearings_.insert(DicomTag(0x0072, 0x0071));  /* D */            // Selector UR Value
+clearings_.insert(DicomTag(0x0400, 0x0105));  /* D */            // Digital Signature DateTime
+clearings_.insert(DicomTag(0x0400, 0x0115));  /* D */            // Certificate of Signer
+clearings_.insert(DicomTag(0x0400, 0x0562));  /* D */            // Attribute Modification DateTime
+clearings_.insert(DicomTag(0x0400, 0x0563));  /* D */            // Modifying System
+clearings_.insert(DicomTag(0x0400, 0x0564));                     // Source of Previous Values
+clearings_.insert(DicomTag(0x0400, 0x0565));  /* D */            // Reason for the Attribute Modification
+clearings_.insert(DicomTag(0x2100, 0x0140));  /* D */            // Destination AE
+clearings_.insert(DicomTag(0x3006, 0x0002));  /* D */            // Structure Set Label
+clearings_.insert(DicomTag(0x3006, 0x0008));                     // Structure Set Date
+clearings_.insert(DicomTag(0x3006, 0x0009));                     // Structure Set Time
+clearings_.insert(DicomTag(0x3006, 0x0026));                     // ROI Name
+clearings_.insert(DicomTag(0x3006, 0x00a6));                     // ROI Interpreter
+clearings_.insert(DicomTag(0x3008, 0x0024));  /* D */            // Treatment Control Point Date
+clearings_.insert(DicomTag(0x3008, 0x0025));  /* D */            // Treatment Control Point Time
+clearings_.insert(DicomTag(0x3008, 0x0162));  /* D */            // Safe Position Exit Date
+clearings_.insert(DicomTag(0x3008, 0x0164));  /* D */            // Safe Position Exit Time
+clearings_.insert(DicomTag(0x3008, 0x0166));  /* D */            // Safe Position Return Date
+clearings_.insert(DicomTag(0x3008, 0x0168));  /* D */            // Safe Position Return Time
+clearings_.insert(DicomTag(0x300a, 0x0002));  /* D */            // RT Plan Label
+clearings_.insert(DicomTag(0x300a, 0x022c));  /* D */            // Source Strength Reference Date
+clearings_.insert(DicomTag(0x300a, 0x022e));  /* D */            // Source Strength Reference Time
+clearings_.insert(DicomTag(0x300a, 0x0608));  /* D */            // Treatment Position Group Label
+clearings_.insert(DicomTag(0x300a, 0x0611));                     // RT Accessory Holder Slot ID
+clearings_.insert(DicomTag(0x300a, 0x0615));                     // RT Accessory Device Slot ID
+clearings_.insert(DicomTag(0x300a, 0x0619));  /* D */            // Radiation Dose Identification Label
+clearings_.insert(DicomTag(0x300a, 0x0623));  /* D */            // Radiation Dose In-Vivo Measurement Label
+clearings_.insert(DicomTag(0x300a, 0x062a));  /* D */            // RT Tolerance Set Label
+clearings_.insert(DicomTag(0x300a, 0x067c));  /* D */            // Radiation Generation Mode Label
+clearings_.insert(DicomTag(0x300a, 0x067d));                     // Radiation Generation Mode Description
+clearings_.insert(DicomTag(0x300a, 0x0734));  /* D */            // Treatment Tolerance Violation Description
+clearings_.insert(DicomTag(0x300a, 0x0736));  /* D */            // Treatment Tolerance Violation DateTime
+clearings_.insert(DicomTag(0x300a, 0x073a));  /* D */            // Recorded RT Control Point DateTime
+clearings_.insert(DicomTag(0x300a, 0x0741));  /* D */            // Interlock DateTime
+clearings_.insert(DicomTag(0x300a, 0x0742));  /* D */            // Interlock Description
+clearings_.insert(DicomTag(0x300a, 0x0760));  /* D */            // Override DateTime
+clearings_.insert(DicomTag(0x300a, 0x0783));  /* D */            // Interlock Origin Description
+clearings_.insert(DicomTag(0x300c, 0x0127));  /* D */            // Beam Hold Transition DateTime
+clearings_.insert(DicomTag(0x300e, 0x0004));                     // Review Date
+clearings_.insert(DicomTag(0x300e, 0x0005));                     // Review Time
+clearings_.insert(DicomTag(0x3010, 0x000f));                     // Conceptual Volume Combination Description
+clearings_.insert(DicomTag(0x3010, 0x0017));                     // Conceptual Volume Description
+clearings_.insert(DicomTag(0x3010, 0x001b));                     // Device Alternate Identifier
+clearings_.insert(DicomTag(0x3010, 0x002d));  /* D */            // Device Label
+clearings_.insert(DicomTag(0x3010, 0x0033));  /* D */            // User Content Label
+clearings_.insert(DicomTag(0x3010, 0x0034));  /* D */            // User Content Long Label
+clearings_.insert(DicomTag(0x3010, 0x0035));  /* D */            // Entity Label
+clearings_.insert(DicomTag(0x3010, 0x0038));  /* D */            // Entity Long Label
+clearings_.insert(DicomTag(0x3010, 0x0043));                     // Manufacturer's Device Identifier
+clearings_.insert(DicomTag(0x3010, 0x0054));  /* D */            // RT Prescription Label
+clearings_.insert(DicomTag(0x3010, 0x005a));                     // RT Physician Intent Narrative
+clearings_.insert(DicomTag(0x3010, 0x005c));                     // Reason for Superseding
+clearings_.insert(DicomTag(0x3010, 0x007a));                     // Treatment Technique Notes
+clearings_.insert(DicomTag(0x3010, 0x007b));                     // Prescription Notes
+clearings_.insert(DicomTag(0x3010, 0x007f));                     // Fractionation Notes
+clearings_.insert(DicomTag(0x3010, 0x0081));                     // Prescription Notes Sequence
+removals_.insert(DicomTag(0x0000, 0x1000));                      // Affected SOP Instance UID
+removals_.insert(DicomTag(0x0008, 0x0012));   /* X/D */          // Instance Creation Date
+removals_.insert(DicomTag(0x0008, 0x0013));   /* X/Z/D */        // Instance Creation Time
+removals_.insert(DicomTag(0x0008, 0x0015));                      // Instance Coercion DateTime
+removals_.insert(DicomTag(0x0008, 0x0021));   /* X/D */          // Series Date
+removals_.insert(DicomTag(0x0008, 0x0022));   /* X/Z */          // Acquisition Date
+removals_.insert(DicomTag(0x0008, 0x0024));                      // Overlay Date
+removals_.insert(DicomTag(0x0008, 0x0025));                      // Curve Date
+removals_.insert(DicomTag(0x0008, 0x002a));   /* X/Z/D */        // Acquisition DateTime
+removals_.insert(DicomTag(0x0008, 0x0031));   /* X/D */          // Series Time
+removals_.insert(DicomTag(0x0008, 0x0032));   /* X/Z */          // Acquisition Time
+removals_.insert(DicomTag(0x0008, 0x0034));                      // Overlay Time
+removals_.insert(DicomTag(0x0008, 0x0035));                      // Curve Time
+removals_.insert(DicomTag(0x0008, 0x0054));                      // Retrieve AE Title
+removals_.insert(DicomTag(0x0008, 0x0055));                      // Station AE Title
+removals_.insert(DicomTag(0x0008, 0x0080));   /* X/Z/D */        // Institution Name
+removals_.insert(DicomTag(0x0008, 0x0081));                      // Institution Address
+removals_.insert(DicomTag(0x0008, 0x0082));   /* X/Z/D */        // Institution Code Sequence
+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 Identification Sequence
+removals_.insert(DicomTag(0x0008, 0x009d));                      // Consulting Physician Identification Sequence
+removals_.insert(DicomTag(0x0008, 0x0201));                      // Timezone Offset From UTC
+removals_.insert(DicomTag(0x0008, 0x1000));                      // Network ID
+removals_.insert(DicomTag(0x0008, 0x1010));   /* X/Z/D */        // 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, 0x1041));                      // Institutional Department Type Code Sequence
+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 Physician's Name
+removals_.insert(DicomTag(0x0008, 0x1052));                      // Performing Physician Identification Sequence
+removals_.insert(DicomTag(0x0008, 0x1060));                      // Name of Physician(s) Reading Study
+removals_.insert(DicomTag(0x0008, 0x1062));                      // Physician(s) Reading Study Identification Sequence
+removals_.insert(DicomTag(0x0008, 0x1070));   /* X/Z/D */        // Operators' Name
+removals_.insert(DicomTag(0x0008, 0x1072));   /* X/D */          // Operator Identification Sequence
+removals_.insert(DicomTag(0x0008, 0x1080));                      // Admitting Diagnoses Description
+removals_.insert(DicomTag(0x0008, 0x1084));                      // Admitting Diagnoses Code Sequence
+removals_.insert(DicomTag(0x0008, 0x1088));                      // Pyramid Description
+removals_.insert(DicomTag(0x0008, 0x1110));   /* X/Z */          // Referenced Study Sequence
+removals_.insert(DicomTag(0x0008, 0x1111));   /* X/Z/D */        // Referenced Performed Procedure Step Sequence
+removals_.insert(DicomTag(0x0008, 0x1120));                      // Referenced Patient Sequence
+removals_.insert(DicomTag(0x0008, 0x2111));                      // Derivation Description
+removals_.insert(DicomTag(0x0008, 0x4000));                      // Identifying Comments
+removals_.insert(DicomTag(0x0010, 0x0021));                      // Issuer of Patient ID
+removals_.insert(DicomTag(0x0010, 0x0032));                      // Patient's Birth Time
+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, 0x1100));                      // Referenced Patient Photo Sequence
+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));                      // Patient's Telephone Numbers
+removals_.insert(DicomTag(0x0010, 0x2155));                      // Patient's Telecom Information
+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 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));   /* X/Z */          // 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(0x0012, 0x0051));                      // Clinical Trial Time Point Description
+removals_.insert(DicomTag(0x0012, 0x0071));                      // Clinical Trial Series ID
+removals_.insert(DicomTag(0x0012, 0x0072));                      // Clinical Trial Series Description
+removals_.insert(DicomTag(0x0012, 0x0082));                      // Clinical Trial Protocol Ethics Committee Approval Number
+removals_.insert(DicomTag(0x0012, 0x0086));                      // Ethics Committee Approval Effectiveness Start Date
+removals_.insert(DicomTag(0x0012, 0x0087));                      // Ethics Committee Approval Effectiveness End Date
+removals_.insert(DicomTag(0x0014, 0x407c));                      // Calibration Time
+removals_.insert(DicomTag(0x0014, 0x407e));                      // Calibration Date
+removals_.insert(DicomTag(0x0016, 0x002b));                      // Maker Note
+removals_.insert(DicomTag(0x0016, 0x004b));                      // Device Setting Description
+removals_.insert(DicomTag(0x0016, 0x004d));                      // Camera Owner Name
+removals_.insert(DicomTag(0x0016, 0x004e));                      // Lens Specification
+removals_.insert(DicomTag(0x0016, 0x004f));                      // Lens Make
+removals_.insert(DicomTag(0x0016, 0x0050));                      // Lens Model
+removals_.insert(DicomTag(0x0016, 0x0051));                      // Lens Serial Number
+removals_.insert(DicomTag(0x0016, 0x0070));                      // GPS Version ID
+removals_.insert(DicomTag(0x0016, 0x0071));                      // GPS Latitude Ref
+removals_.insert(DicomTag(0x0016, 0x0072));                      // GPS Latitude
+removals_.insert(DicomTag(0x0016, 0x0073));                      // GPS Longitude Ref
+removals_.insert(DicomTag(0x0016, 0x0074));                      // GPS Longitude
+removals_.insert(DicomTag(0x0016, 0x0075));                      // GPS Altitude Ref
+removals_.insert(DicomTag(0x0016, 0x0076));                      // GPS Altitude
+removals_.insert(DicomTag(0x0016, 0x0077));                      // GPS Time Stamp
+removals_.insert(DicomTag(0x0016, 0x0078));                      // GPS Satellites
+removals_.insert(DicomTag(0x0016, 0x0079));                      // GPS Status
+removals_.insert(DicomTag(0x0016, 0x007a));                      // GPS Measure Mode
+removals_.insert(DicomTag(0x0016, 0x007b));                      // GPS DOP
+removals_.insert(DicomTag(0x0016, 0x007c));                      // GPS Speed Ref
+removals_.insert(DicomTag(0x0016, 0x007d));                      // GPS Speed
+removals_.insert(DicomTag(0x0016, 0x007e));                      // GPS Track Ref
+removals_.insert(DicomTag(0x0016, 0x007f));                      // GPS Track
+removals_.insert(DicomTag(0x0016, 0x0080));                      // GPS Img Direction Ref
+removals_.insert(DicomTag(0x0016, 0x0081));                      // GPS Img Direction
+removals_.insert(DicomTag(0x0016, 0x0082));                      // GPS Map Datum
+removals_.insert(DicomTag(0x0016, 0x0083));                      // GPS Dest Latitude Ref
+removals_.insert(DicomTag(0x0016, 0x0084));                      // GPS Dest Latitude
+removals_.insert(DicomTag(0x0016, 0x0085));                      // GPS Dest Longitude Ref
+removals_.insert(DicomTag(0x0016, 0x0086));                      // GPS Dest Longitude
+removals_.insert(DicomTag(0x0016, 0x0087));                      // GPS Dest Bearing Ref
+removals_.insert(DicomTag(0x0016, 0x0088));                      // GPS Dest Bearing
+removals_.insert(DicomTag(0x0016, 0x0089));                      // GPS Dest Distance Ref
+removals_.insert(DicomTag(0x0016, 0x008a));                      // GPS Dest Distance
+removals_.insert(DicomTag(0x0016, 0x008b));                      // GPS Processing Method
+removals_.insert(DicomTag(0x0016, 0x008c));                      // GPS Area Information
+removals_.insert(DicomTag(0x0016, 0x008d));                      // GPS Date Stamp
+removals_.insert(DicomTag(0x0016, 0x008e));                      // GPS Differential
+removals_.insert(DicomTag(0x0018, 0x0027));                      // Intervention Drug Stop Time
+removals_.insert(DicomTag(0x0018, 0x0035));                      // Intervention Drug Start Time
+removals_.insert(DicomTag(0x0018, 0x1000));   /* X/Z/D */        // Device Serial Number
+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, 0x1009));                      // Unique Device Identifier
+removals_.insert(DicomTag(0x0018, 0x100a));                      // UDI Sequence
+removals_.insert(DicomTag(0x0018, 0x1012));                      // Date of Secondary Capture
+removals_.insert(DicomTag(0x0018, 0x1014));                      // Time of Secondary Capture
+removals_.insert(DicomTag(0x0018, 0x1030));   /* X/D */          // Protocol Name
+removals_.insert(DicomTag(0x0018, 0x1042));                      // Contrast/Bolus Start Time
+removals_.insert(DicomTag(0x0018, 0x1043));                      // Contrast/Bolus Stop Time
+removals_.insert(DicomTag(0x0018, 0x1072));                      // Radiopharmaceutical Start Time
+removals_.insert(DicomTag(0x0018, 0x1073));                      // Radiopharmaceutical Stop Time
+removals_.insert(DicomTag(0x0018, 0x1078));                      // Radiopharmaceutical Start DateTime
+removals_.insert(DicomTag(0x0018, 0x1079));                      // Radiopharmaceutical Stop DateTime
+removals_.insert(DicomTag(0x0018, 0x1200));                      // Date of Last Calibration
+removals_.insert(DicomTag(0x0018, 0x1201));                      // Time of Last Calibration
+removals_.insert(DicomTag(0x0018, 0x1202));                      // DateTime of Last Calibration
+removals_.insert(DicomTag(0x0018, 0x1400));   /* X/D */          // Acquisition Device Processing Description
+removals_.insert(DicomTag(0x0018, 0x4000));                      // Acquisition Comments
+removals_.insert(DicomTag(0x0018, 0x5011));                      // Transducer Identification Sequence
+removals_.insert(DicomTag(0x0018, 0x700a));   /* X/D */          // Detector ID
+removals_.insert(DicomTag(0x0018, 0x700c));   /* X/D */          // Date of Last Detector Calibration
+removals_.insert(DicomTag(0x0018, 0x700e));   /* X/D */          // Time of Last Detector Calibration
+removals_.insert(DicomTag(0x0018, 0x9185));                      // Respiratory Motion Compensation Technique Description
+removals_.insert(DicomTag(0x0018, 0x9373));                      // X-Ray Detector Label
+removals_.insert(DicomTag(0x0018, 0x937b));                      // Multi-energy Acquisition Description
+removals_.insert(DicomTag(0x0018, 0x937f));                      // Decomposition Description
+removals_.insert(DicomTag(0x0018, 0x9424));                      // Acquisition Protocol Description
+removals_.insert(DicomTag(0x0018, 0x9516));   /* X/D */          // Start Acquisition DateTime
+removals_.insert(DicomTag(0x0018, 0x9517));   /* X/D */          // End Acquisition DateTime
+removals_.insert(DicomTag(0x0018, 0x9937));                      // Requested Series Description
+removals_.insert(DicomTag(0x0018, 0xa002));                      // Contribution DateTime
+removals_.insert(DicomTag(0x0018, 0xa003));                      // Contribution Description
+removals_.insert(DicomTag(0x0020, 0x0027));                      // Pyramid Label
+removals_.insert(DicomTag(0x0020, 0x3401));                      // Modifying Device ID
+removals_.insert(DicomTag(0x0020, 0x3403));                      // Modified Image Date
+removals_.insert(DicomTag(0x0020, 0x3405));                      // Modified Image Time
+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(0x0028, 0x4000));                      // Image Presentation Comments
+removals_.insert(DicomTag(0x0032, 0x0012));                      // Study ID Issuer
+removals_.insert(DicomTag(0x0032, 0x0032));                      // Study Verified Date
+removals_.insert(DicomTag(0x0032, 0x0033));                      // Study Verified Time
+removals_.insert(DicomTag(0x0032, 0x0034));                      // Study Read Date
+removals_.insert(DicomTag(0x0032, 0x0035));                      // Study Read Time
+removals_.insert(DicomTag(0x0032, 0x1000));                      // Scheduled Study Start Date
+removals_.insert(DicomTag(0x0032, 0x1001));                      // Scheduled Study Start Time
+removals_.insert(DicomTag(0x0032, 0x1010));                      // Scheduled Study Stop Date
+removals_.insert(DicomTag(0x0032, 0x1011));                      // Scheduled Study Stop Time
+removals_.insert(DicomTag(0x0032, 0x1020));                      // Scheduled Study Location
+removals_.insert(DicomTag(0x0032, 0x1021));                      // Scheduled Study Location AE Title
+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, 0x1040));                      // Study Arrival Date
+removals_.insert(DicomTag(0x0032, 0x1041));                      // Study Arrival Time
+removals_.insert(DicomTag(0x0032, 0x1050));                      // Study Completion Date
+removals_.insert(DicomTag(0x0032, 0x1051));                      // Study Completion Time
+removals_.insert(DicomTag(0x0032, 0x1060));   /* X/Z */          // Requested Procedure Description
+removals_.insert(DicomTag(0x0032, 0x1066));                      // Reason for Visit
+removals_.insert(DicomTag(0x0032, 0x1067));                      // Reason for Visit Code Sequence
+removals_.insert(DicomTag(0x0032, 0x1070));                      // Requested Contrast Agent
+removals_.insert(DicomTag(0x0032, 0x4000));                      // Study Comments
+removals_.insert(DicomTag(0x0038, 0x0004));                      // Referenced Patient Alias Sequence
+removals_.insert(DicomTag(0x0038, 0x0010));                      // Admission ID
+removals_.insert(DicomTag(0x0038, 0x0011));                      // Issuer of Admission ID
+removals_.insert(DicomTag(0x0038, 0x0014));                      // Issuer of Admission ID Sequence
+removals_.insert(DicomTag(0x0038, 0x001a));                      // Scheduled Admission Date
+removals_.insert(DicomTag(0x0038, 0x001b));                      // Scheduled Admission Time
+removals_.insert(DicomTag(0x0038, 0x001c));                      // Scheduled Discharge Date
+removals_.insert(DicomTag(0x0038, 0x001d));                      // Scheduled Discharge Time
+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, 0x0030));                      // Discharge Date
+removals_.insert(DicomTag(0x0038, 0x0032));                      // Discharge 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, 0x0064));                      // Issuer of Service Episode ID Sequence
+removals_.insert(DicomTag(0x0038, 0x0300));                      // Current Patient Location
+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(0x003a, 0x0329));                      // Waveform Filter Description
+removals_.insert(DicomTag(0x003a, 0x032b));                      // Filter Lookup Table Description
+removals_.insert(DicomTag(0x0040, 0x0001));                      // Scheduled Station AE Title
+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's Name
+removals_.insert(DicomTag(0x0040, 0x0007));                      // Scheduled Procedure Step Description
+removals_.insert(DicomTag(0x0040, 0x0009));                      // Scheduled Procedure Step ID
+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, 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, 0x0250));                      // Performed Procedure Step End Date
+removals_.insert(DicomTag(0x0040, 0x0251));                      // Performed Procedure Step End Time
+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 the Performed Procedure Step
+removals_.insert(DicomTag(0x0040, 0x0310));                      // Comments on Radiation Dose
+removals_.insert(DicomTag(0x0040, 0x050a));                      // Specimen Accession Number
+removals_.insert(DicomTag(0x0040, 0x051a));                      // Container Description
+removals_.insert(DicomTag(0x0040, 0x0555));   /* X/Z */          // Acquisition Context Sequence
+removals_.insert(DicomTag(0x0040, 0x0600));                      // Specimen Short Description
+removals_.insert(DicomTag(0x0040, 0x0602));                      // Specimen Detailed Description
+removals_.insert(DicomTag(0x0040, 0x06fa));                      // Slide Identifier
+removals_.insert(DicomTag(0x0040, 0x1001));                      // Requested Procedure ID
+removals_.insert(DicomTag(0x0040, 0x1002));                      // Reason for the Requested Procedure
+removals_.insert(DicomTag(0x0040, 0x1004));                      // Patient Transport Arrangements
+removals_.insert(DicomTag(0x0040, 0x1005));                      // Requested Procedure Location
+removals_.insert(DicomTag(0x0040, 0x100a));                      // Reason for Requested Procedure Code Sequence
+removals_.insert(DicomTag(0x0040, 0x1010));                      // Names of Intended Recipients of Results
+removals_.insert(DicomTag(0x0040, 0x1011));                      // Intended Recipients of Results Identification Sequence
+removals_.insert(DicomTag(0x0040, 0x1102));                      // Person's Address
+removals_.insert(DicomTag(0x0040, 0x1103));                      // Person's Telephone Numbers
+removals_.insert(DicomTag(0x0040, 0x1104));                      // Person's Telecom Information
+removals_.insert(DicomTag(0x0040, 0x1400));                      // Requested Procedure Comments
+removals_.insert(DicomTag(0x0040, 0x2001));                      // Reason for the Imaging Service Request
+removals_.insert(DicomTag(0x0040, 0x2004));                      // Issue Date of Imaging Service Request
+removals_.insert(DicomTag(0x0040, 0x2005));                      // Issue Time of Imaging Service Request
+removals_.insert(DicomTag(0x0040, 0x2008));                      // Order Entered By
+removals_.insert(DicomTag(0x0040, 0x2009));                      // Order Enterer's Location
+removals_.insert(DicomTag(0x0040, 0x2010));                      // Order Callback Phone Number
+removals_.insert(DicomTag(0x0040, 0x2011));                      // Order Callback Telecom Information
+removals_.insert(DicomTag(0x0040, 0x2400));                      // Imaging Service Request Comments
+removals_.insert(DicomTag(0x0040, 0x3001));                      // Confidentiality Constraint on Patient Data Description
+removals_.insert(DicomTag(0x0040, 0x4005));                      // Scheduled Procedure Step Start DateTime
+removals_.insert(DicomTag(0x0040, 0x4008));                      // Scheduled Procedure Step Expiration DateTime
+removals_.insert(DicomTag(0x0040, 0x4010));                      // Scheduled Procedure Step Modification DateTime
+removals_.insert(DicomTag(0x0040, 0x4011));                      // Expected Completion DateTime
+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, 0x4028));                      // Performed Station Name 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 Performer's Organization
+removals_.insert(DicomTag(0x0040, 0x4037));                      // Human Performer's Name
+removals_.insert(DicomTag(0x0040, 0x4050));                      // Performed Procedure Step Start DateTime
+removals_.insert(DicomTag(0x0040, 0x4051));                      // Performed Procedure Step End DateTime
+removals_.insert(DicomTag(0x0040, 0x4052));                      // Procedure Step Cancellation DateTime
+removals_.insert(DicomTag(0x0040, 0xa023));                      // Findings Group Recording Date (Trial)
+removals_.insert(DicomTag(0x0040, 0xa024));                      // Findings Group Recording Time (Trial)
+removals_.insert(DicomTag(0x0040, 0xa032));   /* X/D */          // Observation DateTime
+removals_.insert(DicomTag(0x0040, 0xa033));                      // Observation Start DateTime
+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, 0xa110));                      // Date of Document or Verbal Transaction (Trial)
+removals_.insert(DicomTag(0x0040, 0xa112));                      // Time of Document or Verbal Transaction (Trial)
+removals_.insert(DicomTag(0x0040, 0xa192));                      // Observation Date (Trial)
+removals_.insert(DicomTag(0x0040, 0xa193));                      // Observation Time (Trial)
+removals_.insert(DicomTag(0x0040, 0xa307));                      // Current Observer (Trial)
+removals_.insert(DicomTag(0x0040, 0xa352));                      // Verbal Source (Trial)
+removals_.insert(DicomTag(0x0040, 0xa353));                      // Address (Trial)
+removals_.insert(DicomTag(0x0040, 0xa354));                      // Telephone Number (Trial)
+removals_.insert(DicomTag(0x0040, 0xa358));                      // Verbal Source Identifier Code Sequence (Trial)
+removals_.insert(DicomTag(0x0040, 0xdb06));                      // Template Version
+removals_.insert(DicomTag(0x0040, 0xdb07));                      // Template Local Version
+removals_.insert(DicomTag(0x0040, 0xe004));                      // HL7 Document Effective Time
+removals_.insert(DicomTag(0x0044, 0x0004));                      // Approval Status DateTime
+removals_.insert(DicomTag(0x0044, 0x000b));                      // Product Expiration DateTime
+removals_.insert(DicomTag(0x0044, 0x0010));                      // Substance Administration DateTime
+removals_.insert(DicomTag(0x0044, 0x0105));                      // Assertion Expiration DateTime
+removals_.insert(DicomTag(0x0050, 0x001b));                      // Container Component ID
+removals_.insert(DicomTag(0x0050, 0x0020));                      // Device Description
+removals_.insert(DicomTag(0x0050, 0x0021));                      // Long Device Description
+removals_.insert(DicomTag(0x006a, 0x0006));                      // Annotation Group Description
+removals_.insert(DicomTag(0x0070, 0x0082));                      // Presentation Creation Date
+removals_.insert(DicomTag(0x0070, 0x0083));                      // Presentation Creation Time
+removals_.insert(DicomTag(0x0070, 0x0086));                      // Content Creator's Identification Code Sequence
+removals_.insert(DicomTag(0x0074, 0x1234));                      // Receiving AE
+removals_.insert(DicomTag(0x0074, 0x1236));                      // Requesting AE
+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 Keywords
+removals_.insert(DicomTag(0x0100, 0x0420));                      // SOP Authorization DateTime
+removals_.insert(DicomTag(0x0400, 0x0310));                      // Certified Timestamp
+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, 0x0551));                      // Nonconforming Modified Attributes Sequence
+removals_.insert(DicomTag(0x0400, 0x0552));                      // Nonconforming Data Element Value
+removals_.insert(DicomTag(0x0400, 0x0561));                      // Original Attributes Sequence
+removals_.insert(DicomTag(0x0400, 0x0600));                      // Instance Origin Status
+removals_.insert(DicomTag(0x2030, 0x0020));                      // Text String
+removals_.insert(DicomTag(0x2100, 0x0040));                      // Creation Date
+removals_.insert(DicomTag(0x2100, 0x0050));                      // Creation Time
+removals_.insert(DicomTag(0x2100, 0x0070));                      // Originator
+removals_.insert(DicomTag(0x2200, 0x0002));   /* X/Z */          // Label Text
+removals_.insert(DicomTag(0x2200, 0x0005));   /* X/Z */          // Barcode Value
+removals_.insert(DicomTag(0x3002, 0x0121));                      // Position Acquisition Template Name
+removals_.insert(DicomTag(0x3002, 0x0123));                      // Position Acquisition Template Description
+removals_.insert(DicomTag(0x3006, 0x0004));                      // Structure Set Name
+removals_.insert(DicomTag(0x3006, 0x0006));                      // Structure Set Description
+removals_.insert(DicomTag(0x3006, 0x0028));                      // ROI Description
+removals_.insert(DicomTag(0x3006, 0x0038));                      // ROI Generation Description
+removals_.insert(DicomTag(0x3006, 0x0085));                      // ROI Observation Label
+removals_.insert(DicomTag(0x3006, 0x0088));                      // ROI Observation Description
+removals_.insert(DicomTag(0x3008, 0x0054));   /* X/D */          // First Treatment Date
+removals_.insert(DicomTag(0x3008, 0x0056));   /* X/D */          // Most Recent Treatment Date
+removals_.insert(DicomTag(0x3008, 0x0105));   /* X/Z */          // Source Serial Number
+removals_.insert(DicomTag(0x3008, 0x0250));   /* X/D */          // Treatment Date
+removals_.insert(DicomTag(0x3008, 0x0251));   /* X/D */          // Treatment Time
+removals_.insert(DicomTag(0x300a, 0x0003));                      // RT Plan Name
+removals_.insert(DicomTag(0x300a, 0x0004));                      // RT Plan Description
+removals_.insert(DicomTag(0x300a, 0x0006));   /* X/D */          // RT Plan Date
+removals_.insert(DicomTag(0x300a, 0x0007));   /* X/D */          // RT Plan Time
+removals_.insert(DicomTag(0x300a, 0x000b));                      // Treatment Sites
+removals_.insert(DicomTag(0x300a, 0x000e));                      // Prescription Description
+removals_.insert(DicomTag(0x300a, 0x0016));                      // Dose Reference Description
+removals_.insert(DicomTag(0x300a, 0x0072));                      // Fraction Group Description
+removals_.insert(DicomTag(0x300a, 0x00b2));   /* X/Z */          // Treatment Machine Name
+removals_.insert(DicomTag(0x300a, 0x00c3));                      // Beam Description
+removals_.insert(DicomTag(0x300a, 0x00dd));                      // Bolus Description
+removals_.insert(DicomTag(0x300a, 0x0196));                      // Fixation Device Description
+removals_.insert(DicomTag(0x300a, 0x01a6));                      // Shielding Device Description
+removals_.insert(DicomTag(0x300a, 0x01b2));                      // Setup Technique Description
+removals_.insert(DicomTag(0x300a, 0x0216));                      // Source Manufacturer
+removals_.insert(DicomTag(0x300a, 0x02eb));                      // Compensator Description
+removals_.insert(DicomTag(0x300a, 0x0676));                      // Equipment Frame of Reference Description
+removals_.insert(DicomTag(0x300a, 0x078e));                      // Patient Treatment Preparation Procedure Parameter Description
+removals_.insert(DicomTag(0x300a, 0x0792));                      // Patient Treatment Preparation Method Description
+removals_.insert(DicomTag(0x300a, 0x0794));                      // Patient Setup Photo Description
+removals_.insert(DicomTag(0x300a, 0x079a));                      // Displacement Reference Label
+removals_.insert(DicomTag(0x300c, 0x0113));                      // Reason for Omission Description
+removals_.insert(DicomTag(0x300e, 0x0008));   /* X/Z */          // Reviewer Name
+removals_.insert(DicomTag(0x3010, 0x0036));                      // Entity Name
+removals_.insert(DicomTag(0x3010, 0x0037));                      // Entity Description
+removals_.insert(DicomTag(0x3010, 0x004c));   /* X/D */          // Intended Phase Start Date
+removals_.insert(DicomTag(0x3010, 0x004d));   /* X/D */          // Intended Phase End Date
+removals_.insert(DicomTag(0x3010, 0x0056));   /* X/D */          // RT Treatment Approach Label
+removals_.insert(DicomTag(0x3010, 0x0061));                      // Prior Treatment Dose Description
+removals_.insert(DicomTag(0x3010, 0x0077));   /* X/D */          // Treatment Site
+removals_.insert(DicomTag(0x3010, 0x0085));                      // Intended Fraction Start Time
+removals_.insert(DicomTag(0x4000, 0x0010));                      // Arbitrary
+removals_.insert(DicomTag(0x4000, 0x4000));                      // Text Comments
+removals_.insert(DicomTag(0x4008, 0x0040));                      // Results ID
+removals_.insert(DicomTag(0x4008, 0x0042));                      // Results ID Issuer
+removals_.insert(DicomTag(0x4008, 0x0100));                      // Interpretation Recorded Date
+removals_.insert(DicomTag(0x4008, 0x0101));                      // Interpretation Recorded Time
+removals_.insert(DicomTag(0x4008, 0x0102));                      // Interpretation Recorder
+removals_.insert(DicomTag(0x4008, 0x0108));                      // Interpretation Transcription Date
+removals_.insert(DicomTag(0x4008, 0x0109));                      // Interpretation Transcription Time
+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, 0x0112));                      // Interpretation Approval Date
+removals_.insert(DicomTag(0x4008, 0x0113));                      // Interpretation Approval Time
+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, 0x0200));                      // Interpretation ID
+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 Signatures Sequence
+removals_.insert(DicomTag(0xfffc, 0xfffc));                      // Data Set Trailing Padding
+removedRanges_.push_back(DicomTagRange(0x5000, 0x50ff, 0x0000, 0xffff));  // Curve Data
+removedRanges_.push_back(DicomTagRange(0x6000, 0x60ff, 0x3000, 0x3000));  // Overlay Data
+removedRanges_.push_back(DicomTagRange(0x6000, 0x60ff, 0x4000, 0x4000));  // Overlay Comments
+uids_.insert(DicomTag(0x0000, 0x1001));                          // Requested SOP Instance UID
+uids_.insert(DicomTag(0x0002, 0x0003));                          // Media Storage SOP Instance UID
+uids_.insert(DicomTag(0x0004, 0x1511));                          // Referenced SOP Instance UID in File
+uids_.insert(DicomTag(0x0008, 0x0014));                          // Instance Creator UID
+uids_.insert(DicomTag(0x0008, 0x0017));                          // Acquisition UID
+uids_.insert(DicomTag(0x0008, 0x0019));                          // Pyramid UID
+uids_.insert(DicomTag(0x0008, 0x0058));                          // Failed SOP Instance UID List
+uids_.insert(DicomTag(0x0008, 0x1155));                          // Referenced SOP Instance UID
+uids_.insert(DicomTag(0x0008, 0x1195));                          // Transaction UID
+uids_.insert(DicomTag(0x0008, 0x3010));                          // Irradiation Event UID
+uids_.insert(DicomTag(0x0018, 0x1002));                          // Device UID
+uids_.insert(DicomTag(0x0018, 0x100b));                          // Manufacturer's Device Class UID
+uids_.insert(DicomTag(0x0018, 0x2042));                          // Target UID
+uids_.insert(DicomTag(0x0020, 0x0052));                          // Frame of Reference UID
+uids_.insert(DicomTag(0x0020, 0x0200));                          // Synchronization Frame of Reference UID
+uids_.insert(DicomTag(0x0020, 0x9161));                          // Concatenation UID
+uids_.insert(DicomTag(0x0020, 0x9164));                          // Dimension Organization UID
+uids_.insert(DicomTag(0x0028, 0x1199));                          // Palette Color Lookup Table UID
+uids_.insert(DicomTag(0x0028, 0x1214));                          // Large Palette Color Lookup Table UID
+uids_.insert(DicomTag(0x003a, 0x0310));                          // Multiplex Group UID
+uids_.insert(DicomTag(0x0040, 0x0554));                          // Specimen UID
+uids_.insert(DicomTag(0x0040, 0x4023));                          // Referenced General Purpose Scheduled Procedure Step Transaction UID
+uids_.insert(DicomTag(0x0040, 0xa124));                          // UID
+uids_.insert(DicomTag(0x0040, 0xa171));                          // Observation UID
+uids_.insert(DicomTag(0x0040, 0xa172));                          // Referenced Observation UID (Trial)
+uids_.insert(DicomTag(0x0040, 0xa402));                          // Observation Subject UID (Trial)
+uids_.insert(DicomTag(0x0040, 0xdb0c));                          // Template Extension Organization UID
+uids_.insert(DicomTag(0x0040, 0xdb0d));                          // Template Extension Creator UID
+uids_.insert(DicomTag(0x0062, 0x0021));                          // Tracking UID
+uids_.insert(DicomTag(0x0064, 0x0003));                          // Source Frame of Reference UID
+uids_.insert(DicomTag(0x0070, 0x031a));                          // Fiducial UID
+uids_.insert(DicomTag(0x0070, 0x1101));                          // Presentation Display Collection UID
+uids_.insert(DicomTag(0x0070, 0x1102));                          // Presentation Sequence Collection UID
+uids_.insert(DicomTag(0x0088, 0x0140));                          // Storage Media File-set UID
+uids_.insert(DicomTag(0x0400, 0x0100));                          // Digital Signature UID
+uids_.insert(DicomTag(0x3006, 0x0024));                          // Referenced Frame of Reference UID
+uids_.insert(DicomTag(0x3006, 0x00c2));                          // Related Frame of Reference UID
+uids_.insert(DicomTag(0x300a, 0x0013));                          // Dose Reference UID
+uids_.insert(DicomTag(0x300a, 0x0083));                          // Referenced Dose Reference UID
+uids_.insert(DicomTag(0x300a, 0x0609));                          // Treatment Position Group UID
+uids_.insert(DicomTag(0x300a, 0x0650));                          // Patient Setup UID
+uids_.insert(DicomTag(0x300a, 0x0700));                          // Treatment Session UID
+uids_.insert(DicomTag(0x300a, 0x0785));                          // Referenced Treatment Position Group UID
+uids_.insert(DicomTag(0x3010, 0x0006));                          // Conceptual Volume UID
+uids_.insert(DicomTag(0x3010, 0x000b));                          // Referenced Conceptual Volume UID
+uids_.insert(DicomTag(0x3010, 0x0013));                          // Constituent Conceptual Volume UID
+uids_.insert(DicomTag(0x3010, 0x0015));                          // Source Conceptual Volume UID
+uids_.insert(DicomTag(0x3010, 0x0031));                          // Referenced Fiducials UID
+uids_.insert(DicomTag(0x3010, 0x003b));                          // RT Treatment Phase UID
+uids_.insert(DicomTag(0x3010, 0x006e));                          // Dosimetric Objective UID
+uids_.insert(DicomTag(0x3010, 0x006f));                          // Referenced Dosimetric Objective UID
--- a/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -436,7 +437,7 @@
         /**
          * The test on "size > 0" is new in Orthanc 1.9.3, and fixes
          * issue #195 (No need for BulkDataURI when Data Element is
-         * empty): https://bugs.orthanc-server.com/show_bug.cgi?id=195
+         * empty): https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=195
          **/
         if (size > 0 ||
             tag == DICOM_TAG_PIXEL_DATA ||
--- a/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -93,7 +94,9 @@
 #endif
 
 #if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
-#  include <OrthancFrameworkResources.h>
+#  if !defined(ORTHANC_FRAMEWORK_INCLUDE_RESOURCES) || (ORTHANC_FRAMEWORK_INCLUDE_RESOURCES == 1)
+#    include <OrthancFrameworkResources.h>
+#  endif
 #endif
 
 #if ORTHANC_ENABLE_DCMTK_JPEG == 1
@@ -289,7 +292,7 @@
 
   void FromDcmtkBridge::InitializeDictionary(bool loadPrivateDictionary)
   {
-    CLOG(INFO, DICOM) << "Using DCTMK version: " << DCMTK_VERSION_NUMBER;
+    CLOG(INFO, DICOM) << "Using DCMTK version: " << DCMTK_VERSION_NUMBER;
     
 #if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
     {
@@ -580,8 +583,8 @@
                           ignoreTagLength, 1);
           }
 
-          target.SetValue(DicomTag(element->getTag().getGTag(), element->getTag().getETag()),
-                          jsonSequence);
+          target.SetSequenceValue(DicomTag(element->getTag().getGTag(), element->getTag().getETag()),
+                                  jsonSequence);
         }
       }
     }
@@ -1253,9 +1256,22 @@
   }
 
 
+  static bool GetTagFromNameInternal(DicomTag& tag, const std::string& tagName)
+  {
+    // conversion from old tag names (ex: RETIRED_OtherPatientIDs is the new name for OtherPatientIDs that is still a valid name for DICOM_TAG_OTHER_PATIENT_IDS)
+    if (tagName == "OtherPatientIDs")
+    {
+      tag = DICOM_TAG_OTHER_PATIENT_IDS;
+      return true;
+    }
+
+    return false;
+  }
+
   std::string FromDcmtkBridge::GetTagName(const DicomTag& t,
                                           const std::string& privateCreator)
   {
+    
     DcmTag tag(t.GetGroup(), t.GetElement());
 
     if (!privateCreator.empty())
@@ -1315,6 +1331,12 @@
     }
     else
     {
+      DicomTag dcmTag(0, 0);
+      if (GetTagFromNameInternal(dcmTag, name))
+      {
+        return dcmTag;
+      }
+
       CLOG(INFO, DICOM) << "Unknown DICOM tag: \"" << name << "\"";
       throw OrthancException(ErrorCode_UnknownDicomTag, name, false);
     }
@@ -1553,7 +1575,8 @@
   
   static bool SaveToMemoryBufferInternal(std::string& buffer,
                                          DcmFileFormat& dicom,
-                                         E_TransferSyntax xfer)
+                                         E_TransferSyntax xfer,
+                                         std::string& errorMessage)
   {
     E_EncodingType encodingType = /*opt_sequenceType*/ EET_ExplicitLength;
 
@@ -1592,14 +1615,24 @@
     {
       // Error
       buffer.clear();
+      errorMessage = std::string(c.text());
       return false;
     }
   }
-  
 
   bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer,
                                            DcmDataset& dataSet)
   {
+    std::string errorMessageNotUsed;
+    return SaveToMemoryBuffer(buffer, dataSet, errorMessageNotUsed);
+  }
+
+
+
+  bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer,
+                                           DcmDataset& dataSet,
+                                           std::string& errorMessage)
+  {
     // Determine the transfer syntax which shall be used to write the
     // information to the file. If not possible, switch to the Little
     // Endian syntax, with explicit length.
@@ -1626,7 +1659,7 @@
     ff.validateMetaInfo(xfer);
     ff.removeInvalidGroups();
 
-    return SaveToMemoryBufferInternal(buffer, ff, xfer);
+    return SaveToMemoryBufferInternal(buffer, ff, xfer, errorMessage);
   }
 
 
@@ -1797,7 +1830,7 @@
     {
       // This solves issue 140 (Modifying private tags with REST API
       // changes VR from LO to UN)
-      // https://bugs.orthanc-server.com/show_bug.cgi?id=140
+      // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=140
       LOG(WARNING) << "Private creator should not be empty while creating a private tag: " << tag.Format();
     }
     
@@ -1945,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;
         }
 
@@ -1960,7 +2007,14 @@
         case EVR_OL:  // other long (requires byte-swapping)
 #endif
         {
-          ok = element.putUint32(boost::lexical_cast<Uint32>(*decoded)).good();
+          if (decoded->find('\\') != std::string::npos)
+          {
+            ok = element.putString(decoded->c_str()).good();
+          }
+          else
+          {
+            ok = element.putUint32(boost::lexical_cast<Uint32>(*decoded)).good();
+          }
           break;
         }
 
@@ -1983,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;
         }
 
@@ -1999,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;
         }
 
@@ -2439,7 +2514,7 @@
 
 #if ORTHANC_ENABLE_DCMTK_JPEG == 1
     CLOG(INFO, DICOM) << "Registering JPEG codecs in DCMTK";
-    DJDecoderRegistration::registerCodecs(); 
+    DJDecoderRegistration::registerCodecs();
 # if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
     DJEncoderRegistration::registerCodecs();
 # endif
@@ -2548,6 +2623,19 @@
     {
       evr = EVR_OB;
     }
+    else if (evr == EVR_xs) // SS or US depending on context
+    {
+      // So far we assume that it's alway US (as a best guess: https://forum.dcmtk.org/viewtopic.php?t=932)
+      // However, e.g. in a LUTDescriptor (3 values), the middle value can be a SS depending on other tag values while first and third value are always US.
+      // This patch, although not perfect fixes  https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=214.
+      // It might need some rework once we encounter a LUTDescriptor with a SS value. ref: https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.11.2.html#sect_C.11.2.1.1
+      evr = EVR_US;  
+    }
+    else if (evr == EVR_lt) // US, SS or OW depending on context, used for LUT Data (thus the name)
+    {
+      // best guess is OW: final user should be able to interpret it correctly depending on the context
+      evr = EVR_OW;      
+    }
 
     if (evr == EVR_UNKNOWN ||  // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR)
         evr == EVR_UNKNOWN2B)  // used internally for elements with unknown VR with 2-byte length field in explicit VR
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -203,6 +204,10 @@
     static bool SaveToMemoryBuffer(std::string& buffer,
                                    DcmDataset& dataSet);
 
+    static bool SaveToMemoryBuffer(std::string& buffer,
+                                   DcmDataset& dataSet,
+                                   std::string& errorMessage);
+
     static bool Transcode(DcmFileFormat& dicom,
                           DicomTransferSyntax syntax,
                           const DcmRepresentationParameter* representation);
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge_TransferSyntaxes.impl.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge_TransferSyntaxes.impl.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomParsing/ITagVisitor.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ITagVisitor.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -644,6 +645,51 @@
   }
 
 
+  static ImageAccessor* DecodePlanarConfiguration(const ImageAccessor& source)
+  {
+    /**
+     * This function will interleave the RGB channels, if the source
+     * DICOM image has the "Planar Configuration" (0028,0006) tag that
+     * equals 1. This process was not applied to images using the RLE
+     * codec, which led to the following issue:
+     * https://groups.google.com/g/orthanc-users/c/CSVWfRasSR0/m/y1XDRXVnAgAJ
+     **/
+
+    const unsigned int height = source.GetHeight();
+    const unsigned int width = source.GetWidth();
+    const size_t size = static_cast<size_t>(height) * static_cast<size_t>(width);
+
+    if (source.GetFormat() != PixelFormat_RGB24 ||
+        3 * width != source.GetPitch())
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    std::unique_ptr<ImageAccessor> target(new Image(PixelFormat_RGB24, width, height, false));
+
+    const uint8_t* red = reinterpret_cast<const uint8_t*>(source.GetConstBuffer());
+    const uint8_t* green = red + size;
+    const uint8_t* blue = red + 2 * size;
+
+    for (unsigned int y = 0; y < height; y++)
+    {
+      uint8_t* interleaved = reinterpret_cast<uint8_t*>(target->GetRow(y));
+      for (unsigned int x = 0; x < width; x++)
+      {
+        interleaved[0] = *red;
+        interleaved[1] = *green;
+        interleaved[2] = *blue;
+        interleaved += 3;
+        red++;
+        green++;
+        blue++;
+      }
+    }
+
+    return target.release();
+  }
+
+
   ImageAccessor* DicomImageDecoder::ApplyCodec
   (const DcmCodec& codec,
    const DcmCodecParameter& parameters,
@@ -700,7 +746,25 @@
                                "Cannot decode a non-palette image");
       }
 
-      return target.release();
+      std::string colorModel = Orthanc::Toolbox::StripSpaces(decompressedColorModel.c_str());
+
+      if (target->GetFormat() == PixelFormat_RGB24 &&
+          (colorModel == "RGB" || colorModel == "YBR_FULL") &&
+          info.IsPlanar())
+      {
+        std::unique_ptr<ImageAccessor> output(DecodePlanarConfiguration(*target));
+
+        if (colorModel == "YBR_FULL")
+        {
+          ImageProcessing::ConvertJpegYCbCrToRgb(*output);
+        }
+
+        return output.release();
+      }
+      else
+      {
+        return target.release();
+      }
     }
   }
 
@@ -885,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	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomDir.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomDir.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomDir.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomDir.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -76,6 +77,7 @@
 #include "Internals/DicomImageDecoder.h"
 #include "ToDcmtkBridge.h"
 
+#include "../DicomFormat/DicomImageInformation.h"
 #include "../Images/Image.h"
 #include "../Images/ImageProcessing.h"
 #include "../Images/PamReader.h"
@@ -920,7 +922,7 @@
        * equals the empty string, then proceed. In Orthanc <= 1.5.6,
        * an exception "Bad file format" was generated.
        * https://groups.google.com/d/msg/orthanc-users/aphG_h1AHVg/rfOTtTPTAgAJ
-       * https://hg.orthanc-server.com/orthanc/rev/4c45e018bd3de3cfa21d6efc6734673aaaee4435
+       * https://orthanc.uclouvain.be/hg/orthanc/rev/4c45e018bd3de3cfa21d6efc6734673aaaee4435
        **/
       patientId.clear();
     }        
@@ -938,9 +940,10 @@
 
   void ParsedDicomFile::SaveToMemoryBuffer(std::string& buffer)
   {
-    if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer, *GetDcmtkObject().getDataset()))
+    std::string errorMessage;
+    if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer, *GetDcmtkObject().getDataset(), errorMessage))
     {
-      throw OrthancException(ErrorCode_InternalError, "Cannot write DICOM file to memory");
+      throw OrthancException(ErrorCode_InternalError, "Cannot write DICOM file to memory, DCMTK error: " + errorMessage);
     }
   }
 
@@ -1003,6 +1006,10 @@
       {
         SetEncoding(encoding);
       }
+      else if (permissive)
+      {
+        SetEncoding(defaultEncoding);
+      }
       else
       {
         throw OrthancException(ErrorCode_ParameterOutOfRange,
@@ -1202,7 +1209,42 @@
         break;
 
       case MimeType_Pdf:
-        EmbedPdf(content);
+      {
+        if (content.size() < 5 ||  // (*)
+            strncmp("%PDF-", content.c_str(), 5) != 0)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat, "Not a PDF file");
+        }
+        
+        EncapsulateDocument(MimeType_Pdf, content);
+
+        // In Orthanc <= 1.9.7, the "Modality" would have always be overwritten as "OT"
+        // https://groups.google.com/g/orthanc-users/c/eNSddNrQDtM/m/wc1HahimAAAJ
+
+        SetIfAbsent(DICOM_TAG_SOP_CLASS_UID, UID_EncapsulatedPDFStorage);
+        SetIfAbsent(DICOM_TAG_MODALITY, "OT");
+        SetIfAbsent(FromDcmtkBridge::Convert(DCM_ConversionType), "WSD");
+        //SetIfAbsent(FromDcmtkBridge::Convert(DCM_SeriesNumber), "1");
+
+        break;
+      }
+
+      case MimeType_Mtl:
+        EncapsulateDocument(mime, content);
+        SetIfAbsent(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.104.5");
+        SetIfAbsent(DICOM_TAG_MODALITY, "M3D");
+        break;
+
+      case MimeType_Obj:
+        EncapsulateDocument(mime, content);
+        SetIfAbsent(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.104.4");
+        SetIfAbsent(DICOM_TAG_MODALITY, "M3D");
+        break;
+
+      case MimeType_Stl:
+        EncapsulateDocument(mime, content);
+        SetIfAbsent(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.104.3");
+        SetIfAbsent(DICOM_TAG_MODALITY, "M3D");
         break;
 
       default:
@@ -1525,28 +1567,16 @@
   }
 
 
-  void ParsedDicomFile::EmbedPdf(const std::string& pdf)
+  void ParsedDicomFile::EncapsulateDocument(MimeType mime,
+                                            const std::string& document)
   {
-    if (pdf.size() < 5 ||  // (*)
-        strncmp("%PDF-", pdf.c_str(), 5) != 0)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat, "Not a PDF file");
-    }
-
     InvalidateCache();
 
-    // In Orthanc <= 1.9.7, the "Modality" would have always be overwritten as "OT"
-    // https://groups.google.com/g/orthanc-users/c/eNSddNrQDtM/m/wc1HahimAAAJ
-    
-    ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, UID_EncapsulatedPDFStorage);
-    SetIfAbsent(FromDcmtkBridge::Convert(DCM_Modality), "OT");
-    SetIfAbsent(FromDcmtkBridge::Convert(DCM_ConversionType), "WSD");
-    SetIfAbsent(FromDcmtkBridge::Convert(DCM_MIMETypeOfEncapsulatedDocument), MIME_PDF);
-    //SetIfAbsent(FromDcmtkBridge::Convert(DCM_SeriesNumber), "1");
+    ReplacePlainString(FromDcmtkBridge::Convert(DCM_MIMETypeOfEncapsulatedDocument), EnumerationToString(mime));
 
     std::unique_ptr<DcmPolymorphOBOW> element(new DcmPolymorphOBOW(DCM_EncapsulatedDocument));
 
-    size_t s = pdf.size();
+    size_t s = document.size();
     if (s & 1)
     {
       // The size of the buffer must be even
@@ -1560,10 +1590,12 @@
       throw OrthancException(ErrorCode_NotEnoughMemory);
     }
 
-    // Blank pad byte (no access violation, as "pdf.size() >= 5" because of (*) )
-    bytes[s - 1] = 0;
+    if (s > 0)
+    {
+      bytes[s - 1] = 0;
+    }
 
-    memcpy(bytes, pdf.c_str(), pdf.size());
+    memcpy(bytes, document.c_str(), document.size());
       
     DcmPolymorphOBOW* obj = element.release();
     result = GetDcmtkObject().getDataset()->insert(obj);
@@ -2121,6 +2153,84 @@
     }
   }
 
+  
+  void ParsedDicomFile::InjectEmptyPixelData(ValueRepresentation vr)
+  {
+    DcmItem& dataset = *GetDcmtkObject().getDataset();
+
+    DcmElement *element = NULL;
+    if (!dataset.findAndGetElement(DCM_PixelData, element).good() ||
+        element == NULL)
+    {
+      // The pixel data is indeed nonexistent, insert it now
+      switch (vr)
+      {
+        case ValueRepresentation_OtherByte:
+          if (!dataset.putAndInsertUint8Array(DCM_PixelData, NULL, 0).good())
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+          break;
+
+        case ValueRepresentation_OtherWord:
+          if (!dataset.putAndInsertUint16Array(DCM_PixelData, NULL, 0).good())
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+  }
+
+
+  void ParsedDicomFile::RemoveFromPixelData()
+  {
+    DcmItem& dataset = *GetDcmtkObject().getDataset();
+
+    // We need to go backward, otherwise "dataset.card()" is invalidated
+    for (unsigned long i = dataset.card(); i > 0; i--)
+    {
+      DcmElement* element = dataset.getElement(i - 1);
+      if (element == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      if (element->getTag().getGroup() > DCM_PixelData.getGroup() ||
+          (element->getTag().getGroup() == DCM_PixelData.getGroup() &&
+           element->getTag().getElement() >= DCM_PixelData.getElement()))
+      {
+        std::unique_ptr<DcmElement> removal(dataset.remove(i - 1));
+      }
+    }
+  }
+  
+
+  ValueRepresentation ParsedDicomFile::GuessPixelDataValueRepresentation() const
+  {
+    DicomTransferSyntax ts;
+    if (LookupTransferSyntax(ts))
+    {
+      DcmItem& dataset = *GetDcmtkObjectConst().getDataset();
+
+      uint16_t bitsAllocated;
+      if (!dataset.findAndGetUint16(DCM_BitsAllocated, bitsAllocated).good())
+      {
+        bitsAllocated = 8;
+      }
+
+      return DicomImageInformation::GuessPixelDataValueRepresentation(ts, bitsAllocated);
+    }
+    else
+    {
+      // Assume "OB" if the transfer syntax is unknown
+      return ValueRepresentation_OtherByte;
+    }
+  }
+
 
 #if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
   // Alias for binary compatibility with Orthanc Framework 1.7.2 => don't use it anymore
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -97,6 +98,9 @@
 
     bool EmbedContentInternal(const std::string& dataUriScheme);
 
+    void EncapsulateDocument(MimeType mime,
+                             const std::string& document);
+
     // For internal use only, in order to provide const-correctness on
     // the top of DCMTK API
     DcmFileFormat& GetDcmtkObjectConst() const;
@@ -205,6 +209,7 @@
     void SaveToFile(const std::string& path);
 #endif
 
+    // This method must only be used on the PixelData and EncapsulatedDocument tags
     void EmbedContent(const std::string& dataUriScheme);
 
     void EmbedImage(const ImageAccessor& accessor);
@@ -236,8 +241,6 @@
 
     bool HasTag(const DicomTag& tag) const;
 
-    void EmbedPdf(const std::string& pdf);
-
     bool ExtractPdf(std::string& pdf) const;
 
     void GetRawFrame(std::string& target, // OUT
@@ -309,5 +312,12 @@
 
     ImageAccessor* DecodeAllOverlays(int& originX,
                                      int& originY) const;
+
+    void InjectEmptyPixelData(ValueRepresentation vr);
+
+    // Remove all the tags after pixel data
+    void RemoveFromPixelData();
+
+    ValueRepresentation GuessPixelDataValueRepresentation() const;
   };
 }
--- a/OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Endianness.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Endianness.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -144,7 +145,7 @@
    * This alternative implementation only hid an underlying problem
    * with pointer alignment on some architectures, and was thus
    * reverted. Check out issue #99:
-   * https://bugs.orthanc-server.com/show_bug.cgi?id=99
+   * https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=99
    **/
   return (a << 8) | (a >> 8);
 }
--- a/OrthancFramework/Sources/EnumerationDictionary.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/EnumerationDictionary.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -105,5 +106,10 @@
         return found->second;
       }
     }
+
+    const std::map<std::string, Enumeration>& GetAllEntries() const
+    {
+      return stringToEnumeration_;
+    }
   };
 }
--- a/OrthancFramework/Sources/Enumerations.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Enumerations.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -24,36 +25,17 @@
 #include "PrecompiledHeaders.h"
 #include "Enumerations.h"
 
+#include "Logging.h"
+#include "MultiThreading/Mutex.h"
 #include "OrthancException.h"
 #include "Toolbox.h"
-#include "Logging.h"
-
-#include <boost/thread/mutex.hpp>
+
 #include <string.h>
 #include <cassert>
+#include <boost/algorithm/string/replace.hpp>
 
 namespace Orthanc
 {
-  static const char* const MIME_CSS = "text/css";
-  static const char* const MIME_DICOM = "application/dicom";
-  static const char* const MIME_GIF = "image/gif";
-  static const char* const MIME_GZIP = "application/gzip";
-  static const char* const MIME_HTML = "text/html";
-  static const char* const MIME_JAVASCRIPT = "application/javascript";
-  static const char* const MIME_JPEG2000 = "image/jp2";
-  static const char* const MIME_NACL = "application/x-nacl";
-  static const char* const MIME_PLAIN_TEXT = "text/plain";
-  static const char* const MIME_PNACL = "application/x-pnacl";
-  static const char* const MIME_SVG = "image/svg+xml";
-  static const char* const MIME_WEB_ASSEMBLY = "application/wasm";
-  static const char* const MIME_WOFF = "application/x-font-woff";
-  static const char* const MIME_WOFF2 = "font/woff2";
-  static const char* const MIME_XML_2 = "text/xml";
-  static const char* const MIME_ZIP = "application/zip";
-  static const char* const MIME_DICOM_WEB_JSON = "application/dicom+json";
-  static const char* const MIME_DICOM_WEB_XML = "application/dicom+xml";
-  static const char* const MIME_ICO = "image/x-icon";
-
   // This function is autogenerated by the script
   // "Resources/CodeGeneration/GenerateErrorCodes.py"
   const char* EnumerationToString(ErrorCode error)
@@ -198,6 +180,12 @@
       case ErrorCode_MainDicomTagsMultiplyDefined:
         return "A main DICOM Tag has been defined multiple times for the same resource level";
 
+      case ErrorCode_ForbiddenAccess:
+        return "Access to a resource is forbidden";
+
+      case ErrorCode_DuplicateResource:
+        return "Duplicate resource";
+
       case ErrorCode_SQLiteNotOpened:
         return "SQLite: The database is not opened";
 
@@ -877,15 +865,15 @@
     {
       case DicomVersion_2008:
         return "2008";
-        break;
 
       case DicomVersion_2017c:
         return "2017c";
-        break;
 
       case DicomVersion_2021b:
         return "2021b";
-        break;
+
+      case DicomVersion_2023b:
+        return "2023b";
 
       default: 
         throw OrthancException(ErrorCode_ParameterOutOfRange);
@@ -1110,6 +1098,15 @@
       case MimeType_Ico:
         return MIME_ICO;
 
+      case MimeType_Obj:
+        return MIME_OBJ;
+
+      case MimeType_Mtl:
+        return MIME_MTL;
+
+      case MimeType_Stl:
+        return MIME_STL;
+
       default:
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
@@ -1641,6 +1638,10 @@
     {
       return DicomVersion_2021b;
     }
+    else if (version == "2023b")
+    {
+      return DicomVersion_2023b;
+    }
     else
     {
       throw OrthancException(ErrorCode_ParameterOutOfRange,
@@ -1728,8 +1729,12 @@
       target = MimeType_Dicom;
       return true;
     }
-    else if (source == MIME_JPEG)
+    else if (source == MIME_JPEG ||
+             source == "image/jpg")
     {
+      // Note the tolerance for "image/jpg", which is *not* a standard MIME type
+      // https://groups.google.com/g/orthanc-users/c/Y5x37UFKiDg/m/1zI260KTAwAJ
+      // https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types
       target = MimeType_Jpeg;
       return true;
     }
@@ -1844,6 +1849,21 @@
       target = MimeType_Ico;
       return true;
     }
+    else if (source == MIME_OBJ)
+    {
+      target = MimeType_Obj;
+      return true;
+    }
+    else if (source == MIME_MTL)
+    {
+      target = MimeType_Mtl;
+      return true;
+    }
+    else if (source == MIME_STL)
+    {
+      target = MimeType_Stl;
+      return true;
+    }
     else
     {
       return false;
@@ -1929,6 +1949,11 @@
     std::string s = Toolbox::StripSpaces(specificCharacterSet);
     Toolbox::ToUpperCase(s);
 
+    // handle common spelling mistakes
+    boost::replace_all(s, "ISO_IR_", "ISO_IR ");
+    boost::replace_all(s, "ISO_2022_IR_", "ISO 2022 IR ");
+
+
     // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.12.1.1.2
     // https://github.com/dcm4che/dcm4che/blob/master/dcm4che-core/src/main/java/org/dcm4che3/data/SpecificCharacterSet.java
     if (s == "ISO_IR 6" ||
@@ -2240,6 +2265,12 @@
       case ErrorCode_Revision:
         return HttpStatus_409_Conflict;
 
+      case ErrorCode_ForbiddenAccess:
+        return HttpStatus_403_Forbidden;
+
+      case ErrorCode_DuplicateResource:
+        return HttpStatus_409_Conflict;
+
       case ErrorCode_CreateDicomNotString:
         return HttpStatus_400_BadRequest;
 
@@ -2333,12 +2364,12 @@
   }  
 
 
-  static boost::mutex  defaultEncodingMutex_;  // Should not be necessary
-  static Encoding      defaultEncoding_ = ORTHANC_DEFAULT_DICOM_ENCODING;
+  static Mutex     defaultEncodingMutex_;  // Should not be necessary
+  static Encoding  defaultEncoding_ = ORTHANC_DEFAULT_DICOM_ENCODING;
   
   Encoding GetDefaultDicomEncoding()
   {
-    boost::mutex::scoped_lock lock(defaultEncodingMutex_);
+    Mutex::ScopedLock lock(defaultEncodingMutex_);
     return defaultEncoding_;
   }
 
@@ -2347,7 +2378,7 @@
     std::string name = EnumerationToString(encoding);
     
     {
-      boost::mutex::scoped_lock lock(defaultEncodingMutex_);
+      Mutex::ScopedLock lock(defaultEncodingMutex_);
       defaultEncoding_ = encoding;
     }
 
@@ -2444,6 +2475,21 @@
       throw OrthancException(ErrorCode_InternalError);
     }
   }
+
+  DicomTransferSyntax GetTransferSyntax(const std::string& value)
+  {
+    DicomTransferSyntax syntax;
+    if (LookupTransferSyntax(syntax, value))
+    {
+      return syntax;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Unknown transfer syntax: " + value);
+    }
+  }
+
 }
 
 
--- a/OrthancFramework/Sources/Enumerations.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Enumerations.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -42,6 +43,32 @@
   static const char* const MIME_XML = "application/xml";
   static const char* const MIME_XML_UTF8 = "application/xml; charset=utf-8";
 
+  // Added in Orthanc 1.12.1
+  static const char* const MIME_OBJ = "model/obj";
+  static const char* const MIME_MTL = "model/mtl";
+  static const char* const MIME_STL = "model/stl";
+
+  static const char* const MIME_CSS = "text/css";
+  static const char* const MIME_DICOM = "application/dicom";
+  static const char* const MIME_GIF = "image/gif";
+  static const char* const MIME_GZIP = "application/gzip";
+  static const char* const MIME_HTML = "text/html";
+  static const char* const MIME_JAVASCRIPT = "application/javascript";
+  static const char* const MIME_JPEG2000 = "image/jp2";
+  static const char* const MIME_NACL = "application/x-nacl";
+  static const char* const MIME_PLAIN_TEXT = "text/plain";
+  static const char* const MIME_PNACL = "application/x-pnacl";
+  static const char* const MIME_SVG = "image/svg+xml";
+  static const char* const MIME_WEB_ASSEMBLY = "application/wasm";
+  static const char* const MIME_WOFF = "application/x-font-woff";
+  static const char* const MIME_WOFF2 = "font/woff2";
+  static const char* const MIME_XML_2 = "text/xml";
+  static const char* const MIME_ZIP = "application/zip";
+  static const char* const MIME_DICOM_WEB_JSON = "application/dicom+json";
+  static const char* const MIME_DICOM_WEB_XML = "application/dicom+xml";
+  static const char* const MIME_ICO = "image/x-icon";
+
+
   /**
    * "No Internet Media Type (aka MIME type, content type) for PBM has
    * been registered with IANA, but the unofficial value
@@ -79,7 +106,10 @@
     MimeType_PrometheusText,  // Prometheus text-based exposition format (for metrics)
     MimeType_DicomWebJson,
     MimeType_DicomWebXml,
-    MimeType_Ico
+    MimeType_Ico,
+    MimeType_Mtl,             // MTL - New in Orthanc 1.12.1
+    MimeType_Obj,             // OBJ - New in Orthanc 1.12.1
+    MimeType_Stl              // STL - New in Orthanc 1.12.1
   };
 
   
@@ -140,6 +170,8 @@
     ErrorCode_DatabaseCannotSerialize = 42    /*!< Database could not serialize access due to concurrent update, the transaction should be retried */,
     ErrorCode_Revision = 43    /*!< A bad revision number was provided, which might indicate conflict between multiple writers */,
     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_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 */,
@@ -620,7 +652,8 @@
   {
     DicomVersion_2008,
     DicomVersion_2017c,
-    DicomVersion_2021b
+    DicomVersion_2021b,
+    DicomVersion_2023b
   };
 
   enum ModalityManufacturer
@@ -904,9 +937,12 @@
                             const std::string& uid);
 
   ORTHANC_PUBLIC
+  DicomTransferSyntax GetTransferSyntax(const std::string& uid);
+
+  ORTHANC_PUBLIC
   const char* GetResourceTypeText(ResourceType type,
                                   bool isPlural,
-                                  bool isLowerCase);
+                                  bool isUpperCase);
 
   ORTHANC_PUBLIC
   void GetAllDicomTransferSyntaxes(std::set<DicomTransferSyntax>& target);
--- a/OrthancFramework/Sources/Enumerations_TransferSyntaxes.impl.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Enumerations_TransferSyntaxes.impl.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/FileBuffer.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/FileBuffer.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/FileBuffer.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/FileBuffer.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/FileStorage/FileInfo.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/FileStorage/FileInfo.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/FileStorage/FileInfo.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/FileStorage/FileInfo.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -23,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
@@ -122,8 +124,9 @@
                                  size_t size,
                                  FileContentType type)
   {
+    Toolbox::ElapsedTimer timer;
     LOG(INFO) << "Creating attachment \"" << uuid << "\" of \"" << GetDescriptionInternal(type) 
-              << "\" type (size: " << (size / (1024 * 1024) + 1) << "MB)";
+              << "\" type";
 
     boost::filesystem::path path;
     
@@ -133,37 +136,69 @@
     {
       // 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& ex)
+      {
+        if (retryCount >= maxRetryCount)
+        {
+          throw ex;
+        }
       }
     }
-    else
-    {
-      if (!boost::filesystem::create_directories(path.parent_path()))
-      {
-        throw OrthancException(ErrorCode_FileStorageCannotWrite);
-      }
-    }
-
-    SystemToolbox::WriteFile(content, size, path.string(), fsyncOnWrite_);
   }
 
 
   IMemoryBuffer* FilesystemStorage::Read(const std::string& uuid,
                                          FileContentType type)
   {
+    Toolbox::ElapsedTimer timer;
     LOG(INFO) << "Reading attachment \"" << uuid << "\" of \"" << GetDescriptionInternal(type) 
               << "\" content type";
 
     std::string content;
     SystemToolbox::ReadFile(content, GetPath(uuid).string());
 
+    LOG(INFO) << "Read attachment \"" << uuid << "\" (" << timer.GetHumanTransferSpeed(true, content.size()) << ")";
+
     return StringMemoryBuffer::CreateFromSwap(content);
   }
 
@@ -173,6 +208,7 @@
                                               uint64_t start /* inclusive */,
                                               uint64_t end /* exclusive */)
   {
+    Toolbox::ElapsedTimer timer;
     LOG(INFO) << "Reading attachment \"" << uuid << "\" of \"" << GetDescriptionInternal(type) 
               << "\" content type (range from " << start << " to " << end << ")";
 
@@ -180,6 +216,7 @@
     SystemToolbox::ReadFileRange(
       content, GetPath(uuid).string(), start, end, true /* throw if overflow */);
 
+    LOG(INFO) << "Read range of attachment \"" << uuid << "\" (" << timer.GetHumanTransferSpeed(true, content.size()) << ")";
     return StringMemoryBuffer::CreateFromSwap(content);
   }
 
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/FileStorage/IStorageArea.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/FileStorage/IStorageArea.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/FileStorage/MemoryStorageArea.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -27,6 +28,7 @@
 #include "../Logging.h"
 #include "../OrthancException.h"
 #include "../StringMemoryBuffer.h"
+#include "../Toolbox.h"
 
 namespace Orthanc
 {
@@ -47,9 +49,9 @@
                                  FileContentType type)
   {
     LOG(INFO) << "Creating attachment \"" << uuid << "\" of \"" << static_cast<int>(type)
-              << "\" type (size: " << (size / (1024 * 1024) + 1) << "MB)";
+              << "\" type (size: " << Toolbox::GetHumanFileSize(size) << ")";
 
-    boost::mutex::scoped_lock lock(mutex_);
+    Mutex::ScopedLock lock(mutex_);
 
     if (size != 0 &&
         content == NULL)
@@ -73,7 +75,7 @@
     LOG(INFO) << "Reading attachment \"" << uuid << "\" of \""
               << static_cast<int>(type) << "\" content type";
 
-    boost::mutex::scoped_lock lock(mutex_);
+    Mutex::ScopedLock lock(mutex_);
 
     Content::const_iterator found = content_.find(uuid);
 
@@ -111,7 +113,7 @@
     }
     else
     {
-      boost::mutex::scoped_lock lock(mutex_);
+      Mutex::ScopedLock lock(mutex_);
 
       Content::const_iterator found = content_.find(uuid);
 
@@ -152,7 +154,7 @@
   {
     LOG(INFO) << "Deleting attachment \"" << uuid << "\" of type " << static_cast<int>(type);
 
-    boost::mutex::scoped_lock lock(mutex_);
+    Mutex::ScopedLock lock(mutex_);
 
     Content::iterator found = content_.find(uuid);
     
--- a/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -26,8 +27,8 @@
 #include "IStorageArea.h"
 
 #include "../Compatibility.h"  // For ORTHANC_OVERRIDE
+#include "../MultiThreading/Mutex.h"
 
-#include <boost/thread/mutex.hpp>
 #include <map>
 
 namespace Orthanc
@@ -37,8 +38,8 @@
   private:
     typedef std::map<std::string, std::string*>  Content;
     
-    boost::mutex  mutex_;
-    Content       content_;
+    Mutex    mutex_;
+    Content  content_;
     
   public:
     virtual ~MemoryStorageArea();
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -38,9 +39,13 @@
 #endif
 
 
-static const std::string METRICS_CREATE = "orthanc_storage_create_duration_ms";
-static const std::string METRICS_READ = "orthanc_storage_read_duration_ms";
-static const std::string METRICS_REMOVE = "orthanc_storage_remove_duration_ms";
+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
@@ -62,18 +67,36 @@
   };
 
 
-  StorageAccessor::StorageAccessor(IStorageArea &area, StorageCache* cache) :
+  StorageAccessor::StorageAccessor(IStorageArea& area) :
     area_(area),
-    cache_(cache),
+    cache_(NULL),
+    metrics_(NULL)
+  {
+  }
+  
+
+  StorageAccessor::StorageAccessor(IStorageArea& area, 
+                                   StorageCache& cache) :
+    area_(area),
+    cache_(&cache),
     metrics_(NULL)
   {
   }
 
-  StorageAccessor::StorageAccessor(IStorageArea &area, 
-                                   StorageCache* cache,
-                                   MetricsRegistry &metrics) :
+
+  StorageAccessor::StorageAccessor(IStorageArea& area,
+                                   MetricsRegistry& metrics) :
     area_(area),
-    cache_(cache),
+    cache_(NULL),
+    metrics_(&metrics)
+  {
+  }
+
+  StorageAccessor::StorageAccessor(IStorageArea& area, 
+                                   StorageCache& cache,
+                                   MetricsRegistry& metrics) :
+    area_(area),
+    cache_(&cache),
     metrics_(&metrics)
   {
   }
@@ -99,13 +122,20 @@
     {
       case CompressionType_None:
       {
-        MetricsTimer timer(*this, METRICS_CREATE);
+        {
+          MetricsTimer timer(*this, METRICS_CREATE_DURATION);
+          area_.CreateInstance(customData, instance, uuid, data, size, type, false);
+        }
 
-        area_.CreateInstance(customData, instance, uuid, data, size, type, false);
+        if (metrics_ != NULL)
+        {
+          metrics_->IncrementIntegerValue(METRICS_WRITTEN_BYTES, size);
+        }
         
         if (cache_ != NULL)
         {
-          cache_->Add(uuid, type, data, size);
+          StorageCache::Accessor cacheAccessor(*cache_);
+          cacheAccessor.Add(uuid, type, data, size);
         }
 
         return FileInfo(uuid, type, size, md5, customData);
@@ -126,7 +156,7 @@
         }
 
         {
-          MetricsTimer timer(*this, METRICS_CREATE);
+          MetricsTimer timer(*this, METRICS_CREATE_DURATION);
 
           if (compressed.size() > 0)
           {
@@ -138,9 +168,15 @@
           }
         }
 
+        if (metrics_ != NULL)
+        {
+          metrics_->IncrementIntegerValue(METRICS_WRITTEN_BYTES, compressed.size());
+        }
+
         if (cache_ != NULL)
         {
-          cache_->Add(uuid, type, data, size);  // always add uncompressed data to cache
+          StorageCache::Accessor cacheAccessor(*cache_);
+          cacheAccessor.Add(uuid, type, data, size);    // always add uncompressed data to cache
         }
 
         return FileInfo(uuid, type, size, md5,
@@ -173,15 +209,23 @@
     {
       case CompressionType_None:
       {
-        MetricsTimer timer(*this, METRICS_CREATE);
-
-        area_.CreateAttachment(customData, resourceId, resourceType, uuid, data, size, type, false);
+        {
+          MetricsTimer timer(*this, METRICS_CREATE_DURATION);
+          area_.CreateAttachment(customData, resourceId, resourceType, uuid, data, size, type, false);
+        }
+        
+        if (metrics_ != NULL)
+        {
+          metrics_->IncrementIntegerValue(METRICS_WRITTEN_BYTES, size);
+        }
         
         if (cache_ != NULL)
         {
-          cache_->Add(uuid, type, data, size);
+          StorageCache::Accessor cacheAccessor(*cache_);
+          cacheAccessor.Add(uuid, type, data, size);
         }
 
+
         return FileInfo(uuid, type, size, md5, customData);
       }
 
@@ -200,7 +244,7 @@
         }
 
         {
-          MetricsTimer timer(*this, METRICS_CREATE);
+          MetricsTimer timer(*this, METRICS_CREATE_DURATION);
 
           if (compressed.size() > 0)
           {
@@ -212,9 +256,15 @@
           }
         }
 
+        if (metrics_ != NULL)
+        {
+          metrics_->IncrementIntegerValue(METRICS_WRITTEN_BYTES, compressed.size());
+        }
+
         if (cache_ != NULL)
         {
-          cache_->Add(uuid, type, data, size);  // always add uncompressed data to cache
+          StorageCache::Accessor cacheAccessor(*cache_);
+          cacheAccessor.Add(uuid, type, data, size);    // always add uncompressed data to cache
         }
 
         return FileInfo(uuid, type, size, md5,
@@ -230,46 +280,81 @@
   void StorageAccessor::Read(std::string& content,
                              const FileInfo& info)
   {
-    if (cache_ == NULL ||
-        !cache_->Fetch(content, info.GetUuid(), info.GetContentType()))
+    if (cache_ == NULL)
     {
-      switch (info.GetCompressionType())
+      ReadWholeInternal(content, info);
+    }
+    else
+    {
+      StorageCache::Accessor cacheAccessor(*cache_);
+
+      if (!cacheAccessor.Fetch(content, info.GetUuid(), info.GetContentType()))
       {
-        case CompressionType_None:
+        if (metrics_ != NULL)
         {
-          MetricsTimer timer(*this, METRICS_READ);
-          std::unique_ptr<IMemoryBuffer> buffer(area_.Read(info.GetUuid(), info.GetContentType(), info.GetCustomData()));
-          buffer->MoveToString(content);
-
-          break;
+          metrics_->IncrementIntegerValue(METRICS_CACHE_MISS_COUNT, 1);
         }
 
-        case CompressionType_ZlibWithSize:
-        {
-          ZlibCompressor zlib;
+        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);
+      }
+    }
+  }
 
-          std::unique_ptr<IMemoryBuffer> compressed;
-          
-          {
-            MetricsTimer timer(*this, METRICS_READ);
-            compressed.reset(area_.Read(info.GetUuid(), info.GetContentType(), info.GetCustomData()));
-          }
-          
-          zlib.Uncompress(content, compressed->GetData(), compressed->GetSize());
+  void StorageAccessor::ReadWholeInternal(std::string& content,
+                                          const FileInfo& info)
+  {
+    switch (info.GetCompressionType())
+    {
+      case CompressionType_None:
+      {
+        std::unique_ptr<IMemoryBuffer> buffer;
 
-          break;
+        {
+          MetricsTimer timer(*this, METRICS_READ_DURATION);
+          buffer.reset(area_.Read(info.GetUuid(), info.GetContentType(), info.GetCustomData()));
         }
 
-        default:
+        if (metrics_ != NULL)
         {
-          throw OrthancException(ErrorCode_NotImplemented);
+          metrics_->IncrementIntegerValue(METRICS_READ_BYTES, buffer->GetSize());
         }
+
+        buffer->MoveToString(content);
+
+        break;
       }
 
-      // always store the uncompressed data in cache
-      if (cache_ != NULL)
+      case CompressionType_ZlibWithSize:
       {
-        cache_->Add(info.GetUuid(), info.GetContentType(), content);
+        ZlibCompressor zlib;
+
+        std::unique_ptr<IMemoryBuffer> compressed;
+        
+        {
+          MetricsTimer timer(*this, METRICS_READ_DURATION);
+          compressed.reset(area_.Read(info.GetUuid(), info.GetContentType(), info.GetCustomData()));
+        }
+        
+        if (metrics_ != NULL)
+        {
+          metrics_->IncrementIntegerValue(METRICS_READ_BYTES, compressed->GetSize());
+        }
+
+        zlib.Uncompress(content, compressed->GetData(), compressed->GetSize());
+
+        break;
+      }
+
+      default:
+      {
+        throw OrthancException(ErrorCode_NotImplemented);
       }
     }
 
@@ -280,12 +365,48 @@
   void StorageAccessor::ReadRaw(std::string& content,
                                 const FileInfo& info)
   {
-    if (cache_ == NULL || !cache_->Fetch(content, info.GetUuid(), info.GetContentType()))
+    if (cache_ == NULL || info.GetCompressionType() != CompressionType_None)
     {
-      MetricsTimer timer(*this, METRICS_READ);
-      std::unique_ptr<IMemoryBuffer> buffer(area_.Read(info.GetUuid(), info.GetContentType(), info.GetCustomData()));
-      buffer->MoveToString(content);
+      ReadRawInternal(content, info);
     }
+    else
+    {// use the cache only if the data is uncompressed.
+      StorageCache::Accessor cacheAccessor(*cache_);
+
+      if (!cacheAccessor.Fetch(content, info.GetUuid(), info.GetContentType()))
+      {
+        if (metrics_ != NULL)
+        {
+          metrics_->IncrementIntegerValue(METRICS_CACHE_MISS_COUNT, 1);
+        }
+
+        ReadRawInternal(content, info);
+
+        cacheAccessor.Add(info.GetUuid(), info.GetContentType(), content);
+      }
+      else if (metrics_ != NULL)
+      {
+        metrics_->IncrementIntegerValue(METRICS_CACHE_HIT_COUNT, 1);
+      }
+    }
+  }
+
+  void StorageAccessor::ReadRawInternal(std::string& content,
+                                        const FileInfo& info)
+  {
+    std::unique_ptr<IMemoryBuffer> buffer;
+
+    {
+      MetricsTimer timer(*this, METRICS_READ_DURATION);
+      buffer.reset(area_.Read(info.GetUuid(), info.GetContentType(), info.GetCustomData()));
+    }
+
+    if (metrics_ != NULL)
+    {
+      metrics_->IncrementIntegerValue(METRICS_READ_BYTES, buffer->GetSize());
+    }
+
+    buffer->MoveToString(content);
   }
 
 
@@ -299,7 +420,7 @@
     }
 
     {
-      MetricsTimer timer(*this, METRICS_REMOVE);
+      MetricsTimer timer(*this, METRICS_REMOVE_DURATION);
       area_.Remove(fileUuid, type, customData);
     }
   }
@@ -311,26 +432,84 @@
   }
 
 
+  void ReadStartRangeFromAreaInternal(std::string& target,
+                                      IStorageArea& area,
+                                      const std::string& fileUuid,
+                                      FileContentType contentType,
+                                      uint64_t end /* exclusive */)
+  {
+
+  }
+
   void StorageAccessor::ReadStartRange(std::string& target,
-                                       const std::string& fileUuid,
-                                       FileContentType contentType,
-                                       uint64_t end /* exclusive */,
-                                       const std::string& customData)
+                                       const FileInfo& info,
+                                       uint64_t end /* exclusive */)
   {
-    if (cache_ == NULL || !cache_->FetchStartRange(target, fileUuid, contentType, end))
+    if (cache_ == NULL)
+    {
+      ReadStartRangeInternal(target, info, end);
+    }
+    else
     {
-      MetricsTimer timer(*this, METRICS_READ);
-      std::unique_ptr<IMemoryBuffer> buffer(area_.ReadRange(fileUuid, contentType, 0, end, customData));
-      assert(buffer->GetSize() == end);
-      buffer->MoveToString(target);
+      StorageCache::Accessor accessorStartRange(*cache_);
+      if (!accessorStartRange.FetchStartRange(target, info.GetUuid(), info.GetContentType(), end))
+      {
+        // the start range is not in cache, let's check if the whole file is
+        StorageCache::Accessor accessorWhole(*cache_);
+        if (!accessorWhole.Fetch(target, info.GetUuid(), info.GetContentType()))
+        {
+          if (metrics_ != NULL)
+          {
+            metrics_->IncrementIntegerValue(METRICS_CACHE_MISS_COUNT, 1);
+          }
 
-      if (cache_ != NULL)
+          // if nothing is in the cache, let's read and cache only the start
+          ReadStartRangeInternal(target, info, end);
+          accessorStartRange.AddStartRange(info.GetUuid(), info.GetContentType(), target);
+        }
+        else
+        {
+          if (metrics_ != NULL)
+          {
+            metrics_->IncrementIntegerValue(METRICS_CACHE_HIT_COUNT, 1);
+          }
+
+          // we have read the whole file, check size and resize if needed
+          if (target.size() < end)
+          {
+            throw OrthancException(ErrorCode_CorruptedFile);
+          }
+
+          target.resize(end);
+        }
+      }
+      else if (metrics_ != NULL)
       {
-        cache_->AddStartRange(fileUuid, contentType, target);
+        metrics_->IncrementIntegerValue(METRICS_CACHE_HIT_COUNT, 1);
       }
     }
   }
 
+  void StorageAccessor::ReadStartRangeInternal(std::string& target,
+                                                const FileInfo& info,
+                                                uint64_t end /* exclusive */)
+  {
+    std::unique_ptr<IMemoryBuffer> buffer;
+
+    {
+      MetricsTimer timer(*this, METRICS_READ_DURATION);
+      buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, end, info.GetCustomData()));
+      assert(buffer->GetSize() == end);
+    }
+
+    if (metrics_ != NULL)
+    {
+      metrics_->IncrementIntegerValue(METRICS_READ_BYTES, buffer->GetSize());
+    }
+
+    buffer->MoveToString(target);
+  }
+
 
 #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
   void StorageAccessor::SetupSender(BufferHttpSender& sender,
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -78,11 +79,16 @@
 #endif
 
   public:
-    explicit StorageAccessor(IStorageArea& area,
-                             StorageCache* cache);
+    explicit StorageAccessor(IStorageArea& area);
 
     StorageAccessor(IStorageArea& area,
-                    StorageCache* cache,
+                    StorageCache& cache);
+
+    StorageAccessor(IStorageArea& area,
+                    MetricsRegistry& metrics);
+
+    StorageAccessor(IStorageArea& area,
+                    StorageCache& cache,
                     MetricsRegistry& metrics);
 
     // FileInfo Write(const void* data,
@@ -134,10 +140,8 @@
                  const FileInfo& info);
 
     void ReadStartRange(std::string& target,
-                        const std::string& fileUuid,
-                        FileContentType fullFileContentType,
-                        uint64_t end /* exclusive */,
-                        const std::string& customData);
+                        const FileInfo& info,
+                        uint64_t end /* exclusive */);
 
     void Remove(const std::string& fileUuid,
                 FileContentType type,
@@ -172,5 +176,17 @@
     //                    size_t size,
     //                    FileContentType type,
     //                    bool compression);
+
+  private:
+    void ReadStartRangeInternal(std::string& target,
+                                const FileInfo& info,
+                                uint64_t end /* exclusive */);
+
+    void ReadWholeInternal(std::string& content,
+                           const FileInfo& info);
+
+    void ReadRawInternal(std::string& content,
+                         const FileInfo& info);
+
   };
 }
--- a/OrthancFramework/Sources/FileStorage/StorageCache.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageCache.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -38,65 +39,91 @@
   {
     return uuid + ":" + boost::lexical_cast<std::string>(contentType) + ":1";
   }
-  
+
+
   static std::string GetCacheKeyStartRange(const std::string& uuid,
                                            FileContentType contentType)
   {
     return uuid + ":" + boost::lexical_cast<std::string>(contentType) + ":0";
   }
-  
+
+
+  static std::string GetCacheKeyTranscodedInstance(const std::string& uuid,
+                                                   DicomTransferSyntax transferSyntax)
+  {
+    return uuid + ":ts:" + GetTransferSyntaxUid(transferSyntax);
+  }
+
+
   void StorageCache::SetMaximumSize(size_t size)
   {
     cache_.SetMaximumSize(size);
   }
   
 
-  void StorageCache::Add(const std::string& uuid, 
-                         FileContentType contentType,
-                         const std::string& value)
-  {
-    const std::string key = GetCacheKeyFullFile(uuid, contentType);
-    cache_.Add(key, value);
-  }
-  
-
-  void StorageCache::Add(const std::string& uuid, 
-                         FileContentType contentType,
-                         const void* buffer,
-                         size_t size)
-  {
-    const std::string key = GetCacheKeyFullFile(uuid, contentType);
-    cache_.Add(key, buffer, size);
-  }
-
-
-  void StorageCache::AddStartRange(const std::string& uuid, 
-                                   FileContentType contentType,
-                                   const std::string& value)
-  {
-    const std::string key = GetCacheKeyStartRange(uuid, contentType);
-    cache_.Add(key, value);
-  }
-
-
   void StorageCache::Invalidate(const std::string& uuid,
                                 FileContentType contentType)
   {
-    // invalidate both full file + start range file
+    std::set<DicomTransferSyntax> transferSyntaxes;
+
+    {
+      boost::mutex::scoped_lock lock(subKeysMutex_);
+      transferSyntaxes = subKeysTransferSyntax_;
+    }
+
+    // invalidate full file, start range file and possible transcoded instances
     const std::string keyFullFile = GetCacheKeyFullFile(uuid, contentType);
     cache_.Invalidate(keyFullFile);
 
     const std::string keyPartialFile = GetCacheKeyStartRange(uuid, contentType);
     cache_.Invalidate(keyPartialFile);
+    
+    for (std::set<DicomTransferSyntax>::const_iterator it = transferSyntaxes.begin(); it != transferSyntaxes.end(); ++it)
+    {
+      const std::string keyTransferSyntax = GetCacheKeyTranscodedInstance(uuid, *it);
+      cache_.Invalidate(keyTransferSyntax);
+    }
   }
-  
+
+
+  StorageCache::Accessor::Accessor(StorageCache& cache)
+  : MemoryStringCache::Accessor(cache.cache_),
+    storageCache_(cache)
+  {
+  }
 
-  bool StorageCache::Fetch(std::string& value, 
-                           const std::string& uuid,
-                           FileContentType contentType)
+  void StorageCache::Accessor::Add(const std::string& uuid, 
+                                   FileContentType contentType,
+                                   const std::string& value)
+  {
+
+    std::string key = GetCacheKeyFullFile(uuid, contentType);
+    MemoryStringCache::Accessor::Add(key, value);
+  }
+
+  void StorageCache::Accessor::AddStartRange(const std::string& uuid, 
+                                             FileContentType contentType,
+                                             const std::string& value)
+  {
+    const std::string key = GetCacheKeyStartRange(uuid, contentType);
+    MemoryStringCache::Accessor::Add(key, value);
+  }
+
+  void StorageCache::Accessor::Add(const std::string& uuid, 
+                                   FileContentType contentType,
+                                   const void* buffer,
+                                   size_t size)
   {
     const std::string key = GetCacheKeyFullFile(uuid, contentType);
-    if (cache_.Fetch(value, key))
+    MemoryStringCache::Accessor::Add(key, reinterpret_cast<const char*>(buffer), size);
+  }                                   
+
+  bool StorageCache::Accessor::Fetch(std::string& value, 
+                                     const std::string& uuid,
+                                     FileContentType contentType)
+  {
+    const std::string key = GetCacheKeyFullFile(uuid, contentType);
+    if (MemoryStringCache::Accessor::Fetch(value, key))
     {
       LOG(INFO) << "Read attachment \"" << uuid << "\" with content type "
                 << boost::lexical_cast<std::string>(contentType) << " from cache";
@@ -108,14 +135,44 @@
     }
   }
 
-  bool StorageCache::FetchStartRange(std::string& value, 
-                                     const std::string& uuid,
-                                     FileContentType contentType,
-                                     uint64_t end)
+  bool StorageCache::Accessor::FetchTranscodedInstance(std::string& value, 
+                                                       const std::string& uuid,
+                                                       DicomTransferSyntax targetSyntax)
   {
-    // first try to get the start of file only from cache
+    const std::string key = GetCacheKeyTranscodedInstance(uuid, targetSyntax);
+    if (MemoryStringCache::Accessor::Fetch(value, key))
+    {
+      LOG(INFO) << "Read instance \"" << uuid << "\" transcoded to "
+                << GetTransferSyntaxUid(targetSyntax) << " from cache";
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  void StorageCache::Accessor::AddTranscodedInstance(const std::string& uuid,
+                                                     DicomTransferSyntax targetSyntax,
+                                                     const void* buffer,
+                                                     size_t size)
+  {
+    {
+      boost::mutex::scoped_lock lock(storageCache_.subKeysMutex_);
+      storageCache_.subKeysTransferSyntax_.insert(targetSyntax);
+    }
+
+    const std::string key = GetCacheKeyTranscodedInstance(uuid, targetSyntax);
+    MemoryStringCache::Accessor::Add(key, reinterpret_cast<const char*>(buffer), size);
+  }
+
+  bool StorageCache::Accessor::FetchStartRange(std::string& value, 
+                                               const std::string& uuid,
+                                               FileContentType contentType,
+                                               uint64_t end /* exclusive */)
+  {
     const std::string keyPartialFile = GetCacheKeyStartRange(uuid, contentType);
-    if (cache_.Fetch(value, keyPartialFile) && value.size() >= end)
+    if (MemoryStringCache::Accessor::Fetch(value, keyPartialFile) && value.size() >= end)
     {
       if (value.size() > end)  // the start range that has been cached is larger than the requested value
       {
@@ -126,23 +183,19 @@
                 << boost::lexical_cast<std::string>(contentType) << " from cache";
       return true;
     }
-    else
-    {
-      // try to get the full file from cache
-      if (Fetch(value, uuid, contentType))
-      {
-        if (value.size() < end)
-        {
-          throw OrthancException(ErrorCode_CorruptedFile);
-        }
+
+    return false;
+  }
+
 
-        value.resize(end);
-        return true;
-      }
-      else
-      {
-        return false;
-      }
-    }
+  size_t StorageCache::GetCurrentSize() const
+  {
+    return cache_.GetCurrentSize();
   }
+  
+  size_t StorageCache::GetNumberOfItems() const
+  {
+    return cache_.GetNumberOfItems();
+  }
+
 }
--- a/OrthancFramework/Sources/FileStorage/StorageCache.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageCache.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -37,12 +38,67 @@
    **/
    class ORTHANC_PUBLIC StorageCache : public boost::noncopyable
     {
+    public:
+
+      // The StorageCache is only accessible through this accessor.
+      // It will make sure that only one user will fill load data and fill
+      // the cache if multiple users try to access the same item at the same time.
+      // This scenario happens a lot when multiple workers from a viewer access 
+      // the same file.
+      class Accessor : public MemoryStringCache::Accessor
+      {
+        StorageCache& storageCache_;
+      public:
+        explicit Accessor(StorageCache& cache);
+
+        void Add(const std::string& uuid, 
+                 FileContentType contentType,
+                 const std::string& value);
+
+        void AddStartRange(const std::string& uuid, 
+                           FileContentType contentType,
+                           const std::string& value);
+
+        void Add(const std::string& uuid, 
+                 FileContentType contentType,
+                 const void* buffer,
+                 size_t size);
+
+        bool Fetch(std::string& value, 
+                   const std::string& uuid,
+                   FileContentType contentType);
+
+        bool FetchStartRange(std::string& value, 
+                             const std::string& uuid,
+                             FileContentType contentType,
+                             uint64_t end /* exclusive */);
+
+        bool FetchTranscodedInstance(std::string& value, 
+                                     const std::string& uuid,
+                                     DicomTransferSyntax targetSyntax);
+
+        void AddTranscodedInstance(const std::string& uuid,
+                                   DicomTransferSyntax targetSyntax,
+                                   const void* buffer,
+                                   size_t size);
+      };
+
     private:
-      MemoryStringCache   cache_;
-      
+      MemoryStringCache             cache_;
+      std::set<DicomTransferSyntax> subKeysTransferSyntax_;
+      boost::mutex                  subKeysMutex_;
+
     public:
       void SetMaximumSize(size_t size);
 
+      void Invalidate(const std::string& uuid,
+                      FileContentType contentType);
+
+      size_t GetCurrentSize() const;
+      
+      size_t GetNumberOfItems() const;
+
+    private:
       void Add(const std::string& uuid, 
                FileContentType contentType,
                const std::string& value);
@@ -56,9 +112,6 @@
                const void* buffer,
                size_t size);
 
-      void Invalidate(const std::string& uuid,
-                      FileContentType contentType);
-
       bool Fetch(std::string& value, 
                  const std::string& uuid,
                  FileContentType contentType);
--- a/OrthancFramework/Sources/HttpClient.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpClient.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -46,7 +47,7 @@
 
 extern "C"
 {
-  static CURLcode GetHttpStatus(CURLcode code, CURL* curl, long* status)
+  static CURLcode GetHttpStatus(CURLcode code, CURL* curl, long* status, const std::string& url)
   {
     if (code == CURLE_OK)
     {
@@ -55,8 +56,6 @@
     }
     else
     {
-      LOG(ERROR) << "Error code " << static_cast<int>(code)
-                 << " in libcurl: " << curl_easy_strerror(code);
       *status = 0;
       return code;
     }
@@ -68,10 +67,10 @@
 #if defined(__GNUC__) || defined(__clang__)
 __attribute__((noinline)) 
 #endif
-static CURLcode OrthancHttpClientPerformSSL(CURL* curl, long* status)
+static CURLcode OrthancHttpClientPerformSSL(CURL* curl, long* status, const std::string& url)
 {
 #if ORTHANC_ENABLE_SSL == 1
-  return GetHttpStatus(curl_easy_perform(curl), curl, status);
+  return GetHttpStatus(curl_easy_perform(curl), curl, status, url);
 #else
   throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
                                   "Orthanc was compiled without SSL support, "
@@ -101,6 +100,24 @@
     return code;
   }
 
+  static CURLcode CheckCode(CURLcode code, const std::string& url)
+  {
+    if (code == CURLE_NOT_BUILT_IN)
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Your libcurl does not contain a required feature, "
+                             "please recompile Orthanc with -DUSE_SYSTEM_CURL=OFF");
+    }
+
+    if (code != CURLE_OK)
+    {
+      throw OrthancException(ErrorCode_NetworkProtocol,
+                             "libCURL error: " + std::string(curl_easy_strerror(code)) + " while accessing " + url);
+    }
+
+    return code;
+  }
+
 
   // RAII pattern around a "curl_slist"
   class HttpClient::CurlHeaders : public boost::noncopyable
@@ -666,7 +683,7 @@
 
     SetPkcs11Enabled(service.IsPkcs11Enabled());
 
-    SetUrl(service.GetUrl() + uri);
+    SetUrl(Toolbox::JoinUri(service.GetUrl(), uri));
 
     for (WebServiceParameters::Dictionary::const_iterator 
            it = service.GetHttpHeaders().begin();
@@ -1045,11 +1062,11 @@
     
     if (boost::starts_with(url_, "https://"))
     {
-      code = OrthancHttpClientPerformSSL(pimpl_->curl_, &status);
+      code = OrthancHttpClientPerformSSL(pimpl_->curl_, &status, url_);
     }
     else
     {
-      code = GetHttpStatus(curl_easy_perform(pimpl_->curl_), pimpl_->curl_, &status);
+      code = GetHttpStatus(curl_easy_perform(pimpl_->curl_), pimpl_->curl_, &status, url_);
     }
 
     const boost::posix_time::ptime end = boost::posix_time::microsec_clock::universal_time();
@@ -1063,7 +1080,7 @@
       CLOG(INFO, HTTP) << "cURL status code: " << code;
     }
 
-    CheckCode(code);
+    CheckCode(code, url_);  // throws on HTTP error
 
     if (status == 0)
     {
--- a/OrthancFramework/Sources/HttpClient.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpClient.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/BufferHttpSender.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/BufferHttpSender.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/BufferHttpSender.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/BufferHttpSender.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/CStringMatcher.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/CStringMatcher.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/CStringMatcher.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/CStringMatcher.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -153,7 +154,7 @@
     {
       FilesystemHttpSender sender(p);
       sender.SetContentType(SystemToolbox::AutodetectMimeType(p.string()));
-      output.Answer(sender);   // TODO COMPRESSION
+      output.Answer(sender);
     }
     else if (listDirectoryContent_ &&
              fs::exists(p) && 
--- a/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/FilesystemHttpSender.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/FilesystemHttpSender.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/FilesystemHttpSender.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/FilesystemHttpSender.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/HttpContentNegociation.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpContentNegociation.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -49,28 +50,68 @@
     {
       return true;
     }
-        
-    if (subtype == "*" && type == type_)
+    else if (subtype == "*" && type == type_)
     {
       return true;
     }
-
-    return type == type_ && subtype == subtype_;
+    else
+    {
+      return type == type_ && subtype == subtype_;
+    }
   }
 
 
-  struct HttpContentNegociation::Reference : public boost::noncopyable
+  class HttpContentNegociation::Reference : public boost::noncopyable
   {
+  private:
     const Handler&  handler_;
     uint8_t         level_;
     float           quality_;
+    Dictionary      parameters_;
 
+    static float GetQuality(const Dictionary& parameters)
+    {
+      Dictionary::const_iterator found = parameters.find("q");
+
+      if (found != parameters.end())
+      {
+        float quality;
+        bool ok = false;
+
+        try
+        {
+          quality = boost::lexical_cast<float>(found->second);
+          ok = (quality >= 0.0f && quality <= 1.0f);
+        }
+        catch (boost::bad_lexical_cast&)
+        {
+        }
+
+        if (ok)
+        {
+          return quality;
+        }
+        else
+        {
+          throw OrthancException(
+            ErrorCode_BadRequest,
+            "Quality parameter out of range in a HTTP request (must be between 0 and 1): " + found->second);
+        }
+      }
+      else
+      {
+        return 1.0f;  // Default quality
+      }
+    }
+
+  public:
     Reference(const Handler& handler,
               const std::string& type,
               const std::string& subtype,
-              float quality) :
+              const Dictionary& parameters) :
       handler_(handler),
-      quality_(quality)
+      quality_(GetQuality(parameters)),
+      parameters_(parameters)
     {
       if (type == "*" && subtype == "*")
       {
@@ -85,6 +126,11 @@
         level_ = 2;
       }
     }
+
+    void Call() const
+    {
+      handler_.Call(parameters_);
+    }
       
     bool operator< (const Reference& other) const
     {
@@ -92,13 +138,14 @@
       {
         return true;
       }
-
-      if (level_ > other.level_)
+      else if (level_ > other.level_)
       {
         return false;
       }
-
-      return quality_ < other.quality_;
+      else
+      {
+        return quality_ < other.quality_;
+      }
     }
   };
 
@@ -123,58 +170,21 @@
   }
 
 
-  float HttpContentNegociation::GetQuality(const Tokens& parameters)
-  {
-    for (size_t i = 1; i < parameters.size(); i++)
-    {
-      std::string key, value;
-      if (SplitPair(key, value, parameters[i], '=') &&
-          key == "q")
-      {
-        float quality;
-        bool ok = false;
-
-        try
-        {
-          quality = boost::lexical_cast<float>(value);
-          ok = (quality >= 0.0f && quality <= 1.0f);
-        }
-        catch (boost::bad_lexical_cast&)
-        {
-        }
-
-        if (ok)
-        {
-          return quality;
-        }
-        else
-        {
-          throw OrthancException(
-            ErrorCode_BadRequest,
-            "Quality parameter out of range in a HTTP request (must be between 0 and 1): " + value);
-        }
-      }
-    }
-
-    return 1.0f;  // Default quality
-  }
-
-
-  void HttpContentNegociation::SelectBestMatch(std::unique_ptr<Reference>& best,
+  void HttpContentNegociation::SelectBestMatch(std::unique_ptr<Reference>& target,
                                                const Handler& handler,
                                                const std::string& type,
                                                const std::string& subtype,
-                                               float quality)
+                                               const Dictionary& parameters)
   {
-    std::unique_ptr<Reference> match(new Reference(handler, type, subtype, quality));
+    std::unique_ptr<Reference> match(new Reference(handler, type, subtype, parameters));
 
-    if (best.get() == NULL ||
-        *best < *match)
+    if (target.get() == NULL ||
+        *target < *match)
     {
 #if __cplusplus < 201103L
-      best.reset(match.release());
+      target.reset(match.release());
 #else
-      best = std::move(match);
+      target = std::move(match);
 #endif
     }
   }
@@ -198,9 +208,9 @@
   }
 
     
-  bool HttpContentNegociation::Apply(const HttpHeaders& headers)
+  bool HttpContentNegociation::Apply(const Dictionary& headers)
   {
-    HttpHeaders::const_iterator accept = headers.find("accept");
+    Dictionary::const_iterator accept = headers.find("accept");
     if (accept != headers.end())
     {
       return Apply(accept->second);
@@ -226,22 +236,44 @@
     for (Tokens::const_iterator it = mediaRanges.begin();
          it != mediaRanges.end(); ++it)
     {
-      Tokens parameters;
-      Toolbox::TokenizeString(parameters, *it, ';');
+      Tokens tokens;
+      Toolbox::TokenizeString(tokens, *it, ';');
 
-      if (parameters.size() > 0)
+      if (tokens.size() > 0)
       {
-        float quality = GetQuality(parameters);
+        Dictionary parameters;
+        for (size_t i = 1; i < tokens.size(); i++)
+        {
+          std::string key, value;
+          
+          if (SplitPair(key, value, tokens[i], '='))
+          {
+            // Remove the enclosing quotes, if present
+            if (!value.empty() &&
+                value[0] == '"' &&
+                value[value.size() - 1] == '"')
+            {
+              value = value.substr(1, value.size() - 2);
+            }
+          }
+          else
+          {
+            key = Toolbox::StripSpaces(tokens[i]);
+            value = "";
+          }
 
+          parameters[key] = value;
+        }
+        
         std::string type, subtype;
-        if (SplitPair(type, subtype, parameters[0], '/'))
+        if (SplitPair(type, subtype, tokens[0], '/'))
         {
           for (Handlers::const_iterator it2 = handlers_.begin();
                it2 != handlers_.end(); ++it2)
           {
             if (it2->IsMatch(type, subtype))
             {
-              SelectBestMatch(bestMatch, *it2, type, subtype, quality);
+              SelectBestMatch(bestMatch, *it2, type, subtype, parameters);
             }
           }
         }
@@ -254,7 +286,7 @@
     }
     else
     {
-      bestMatch->handler_.Call();
+      bestMatch->Call();
       return true;
     }
   }
--- a/OrthancFramework/Sources/HttpServer/HttpContentNegociation.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpContentNegociation.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -39,7 +40,7 @@
   class ORTHANC_PUBLIC HttpContentNegociation : public boost::noncopyable
   {
   public:
-    typedef std::map<std::string, std::string>  HttpHeaders;
+    typedef std::map<std::string, std::string>  Dictionary;
 
     class IHandler : public boost::noncopyable
     {
@@ -49,7 +50,8 @@
       }
 
       virtual void Handle(const std::string& type,
-                          const std::string& subtype) = 0;
+                          const std::string& subtype,
+                          const Dictionary& parameters) = 0;
     };
 
   private:
@@ -66,9 +68,9 @@
       bool IsMatch(const std::string& type,
                    const std::string& subtype) const;
 
-      void Call() const
+      void Call(const Dictionary& parameters) const
       {
-        handler_.Handle(type_, subtype_);
+        handler_.Handle(type_, subtype_, parameters);
       }
    };
 
@@ -86,19 +88,17 @@
                           const std::string& source,
                           char separator);
 
-    static float GetQuality(const Tokens& parameters);
-
-    static void SelectBestMatch(std::unique_ptr<Reference>& best,
+    static void SelectBestMatch(std::unique_ptr<Reference>& target,
                                 const Handler& handler,
                                 const std::string& type,
                                 const std::string& subtype,
-                                float quality);
+                                const Dictionary& parameters);
 
   public:
     void Register(const std::string& mime,
                   IHandler& handler);
     
-    bool Apply(const HttpHeaders& headers);
+    bool Apply(const Dictionary& headers);
 
     bool Apply(const std::string& accept);
   };
--- a/OrthancFramework/Sources/HttpServer/HttpFileSender.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpFileSender.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -53,7 +54,8 @@
 
     if (contentType_.empty())
     {
-      contentType_ = SystemToolbox::AutodetectMimeType(filename);
+      MimeType mimeType = SystemToolbox::AutodetectMimeType(filename);
+      contentType_ = EnumerationToString(mimeType);
     }
   }
 
--- a/OrthancFramework/Sources/HttpServer/HttpFileSender.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpFileSender.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/HttpOutput.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpOutput.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -30,6 +31,7 @@
 #include "../Logging.h"
 #include "../OrthancException.h"
 #include "../Toolbox.h"
+#include "../SystemToolbox.h"
 
 #include <iostream>
 #include <vector>
@@ -43,18 +45,24 @@
 #  endif
 #endif
 
+static const std::string X_CONTENT_TYPE_OPTIONS = "X-Content-Type-Options";
+
 
 namespace Orthanc
 {
   HttpOutput::StateMachine::StateMachine(IHttpOutputStream& stream,
-                                         bool isKeepAlive) : 
+                                         bool isKeepAlive,
+                                         unsigned int keepAliveTimeout) : 
     stream_(stream),
     state_(State_WritingHeader),
+    isContentCompressible_(false),
     status_(HttpStatus_200_Ok),
     hasContentLength_(false),
     contentLength_(0),
     contentPosition_(0),
-    keepAlive_(isKeepAlive)
+    keepAlive_(isKeepAlive),
+    keepAliveTimeout_(keepAliveTimeout),
+    hasXContentTypeOptions_(false)
   {
   }
 
@@ -68,7 +76,7 @@
 
     if (hasContentLength_ && contentPosition_ != contentLength_)
     {
-      LOG(ERROR) << "This HTTP answer has not sent the proper number of bytes in its body";
+      LOG(ERROR) << "This HTTP answer has not sent the proper number of bytes in its body.  The remote client has likely closed the connection.";
     }
   }
 
@@ -100,6 +108,17 @@
     AddHeader("Content-Type", contentType);
   }
 
+  void HttpOutput::StateMachine::SetContentCompressible(bool isContentCompressible)
+  {
+    isContentCompressible_ = isContentCompressible;
+  }
+
+  bool HttpOutput::StateMachine::IsContentCompressible() const
+  {
+    // We assume that all files that compress correctly (mainly JSON, XML) are clearly identified.
+    return isContentCompressible_;
+  }
+
   void HttpOutput::StateMachine::SetContentFilename(const char* filename)
   {
     // TODO Escape double quotes
@@ -127,6 +146,11 @@
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
     }
 
+    if (header == X_CONTENT_TYPE_OPTIONS)
+    {
+      hasXContentTypeOptions_ = true;
+    }
+
     headers_.push_back(header + ": " + value + "\r\n");
   }
 
@@ -189,7 +213,7 @@
          * HTTP header, so we can't use the milliseconds granularity.
          **/
         s += ("Keep-Alive: timeout=" +
-              boost::lexical_cast<std::string>(CIVETWEB_KEEP_ALIVE_TIMEOUT_SECONDS) + "\r\n");
+              boost::lexical_cast<std::string>(keepAliveTimeout_) + "\r\n");
       }
       else
       {
@@ -202,6 +226,13 @@
         s += *it;
       }
 
+      if (!hasXContentTypeOptions_)
+      {
+        // Always include this header to prevent MIME Confusion attacks:
+        // https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-content-type-options
+        s += X_CONTENT_TYPE_OPTIONS + ": nosniff\r\n";
+      }
+
       if (status_ != HttpStatus_200_Ok)
       {
         hasContentLength_ = false;
@@ -273,13 +304,11 @@
 
   HttpCompression HttpOutput::GetPreferredCompression(size_t bodySize) const
   {
-#if 0
-    // TODO Do not compress small files?
-    if (bodySize < 512)
+    // Do not compress small files since there is no real size benefit.
+    if (bodySize < 2048)
     {
       return HttpCompression_None;
     }
-#endif
 
     // Prefer "gzip" over "deflate" if the choice is offered
 
@@ -299,8 +328,9 @@
 
 
   HttpOutput::HttpOutput(IHttpOutputStream &stream,
-                         bool isKeepAlive) :
-    stateMachine_(stream, isKeepAlive),
+                         bool isKeepAlive,
+                         unsigned int keepAliveTimeout) :
+    stateMachine_(stream, isKeepAlive, keepAliveTimeout),
     isDeflateAllowed_(false),
     isGzipAllowed_(false)
   {
@@ -337,8 +367,8 @@
 
 
   void HttpOutput::SendStatus(HttpStatus status,
-			      const char* message,
-			      size_t messageSize)
+                              const char* message,
+                              size_t messageSize)
   {
     if (status == HttpStatus_301_MovedPermanently ||
         //status == HttpStatus_401_Unauthorized ||
@@ -349,6 +379,13 @@
     }
     
     stateMachine_.SetHttpStatus(status);
+
+    if (messageSize > 0)
+    {
+      // Assume that the body always contains a textual description of the error
+      stateMachine_.SetContentType("text/plain");
+    }
+
     stateMachine_.SendBody(message, messageSize);
   }
 
@@ -365,11 +402,13 @@
   void HttpOutput::SetContentType(MimeType contentType)
   {
     stateMachine_.SetContentType(EnumerationToString(contentType));
+    stateMachine_.SetContentCompressible(SystemToolbox::IsContentCompressible(contentType));
   }
 
   void HttpOutput::SetContentType(const std::string &contentType)
   {
     stateMachine_.SetContentType(contentType.c_str());
+    stateMachine_.SetContentCompressible(SystemToolbox::IsContentCompressible(contentType));
   }
 
   void HttpOutput::SetContentFilename(const char *filename)
@@ -390,8 +429,13 @@
 
   void HttpOutput::Redirect(const std::string& path)
   {
+    /**
+     * "HttpStatus_301_MovedPermanently" was used in Orthanc <=
+     * 1.12.3. This caused issues on changes in the configuration of
+     * Orthanc.
+     **/
     stateMachine_.ClearHeaders();
-    stateMachine_.SetHttpStatus(HttpStatus_301_MovedPermanently);
+    stateMachine_.SetHttpStatus(HttpStatus_307_TemporaryRedirect);
     stateMachine_.AddHeader("Location", path);
     stateMachine_.SendBody(NULL, 0);
   }
@@ -439,7 +483,7 @@
 
     HttpCompression compression = GetPreferredCompression(length);
 
-    if (compression == HttpCompression_None)
+    if (compression == HttpCompression_None || !IsContentCompressible())
     {
       stateMachine_.SetContentLength(length);
       stateMachine_.SendBody(buffer, length);
@@ -539,7 +583,7 @@
      * Full history is available at the following locations:
      * - In changeset 2248:69b0f4e8a49b:
      *   # hg history -v -r 2248
-     * - https://bugs.orthanc-server.com/show_bug.cgi?id=54
+     * - https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=54
      * - https://groups.google.com/d/msg/orthanc-users/65zhIM5xbKI/TU5Q1_LhAwAJ
      **/
     std::string tmp;
@@ -561,7 +605,7 @@
      * within the encapsulations, and must be no longer than 70
      * characters, not counting the two leading hyphens."
      * https://tools.ietf.org/html/rfc1521
-     * https://bugs.orthanc-server.com/show_bug.cgi?id=165
+     * https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=165
      **/
     if (boundary.size() != 36 + 1 + 36)  // one UUID contains 36 characters
     {
--- a/OrthancFramework/Sources/HttpServer/HttpOutput.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpOutput.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -56,12 +57,15 @@
       IHttpOutputStream& stream_;
       State state_;
 
+      bool isContentCompressible_;
       HttpStatus status_;
       bool hasContentLength_;
       uint64_t contentLength_;
       uint64_t contentPosition_;
       bool keepAlive_;
+      unsigned int keepAliveTimeout_;
       std::list<std::string> headers_;
+      bool hasXContentTypeOptions_;
 
       std::string multipartBoundary_;
       std::string multipartContentType_;
@@ -70,7 +74,8 @@
 
     public:
       StateMachine(IHttpOutputStream& stream,
-                   bool isKeepAlive);
+                   bool isKeepAlive,
+                   unsigned int keepAliveTimeout);
 
       ~StateMachine();
 
@@ -80,6 +85,8 @@
 
       void SetContentType(const char* contentType);
 
+      void SetContentCompressible(bool isCompressible);
+
       void SetContentFilename(const char* filename);
 
       void SetCookie(const std::string& cookie,
@@ -108,6 +115,8 @@
         return state_;
       }
 
+      bool IsContentCompressible() const;
+
       void CheckHeadersCompatibilityWithMultipart() const;
 
       void StartStream(const std::string& contentType);
@@ -126,7 +135,8 @@
 
   public:
     HttpOutput(IHttpOutputStream& stream,
-               bool isKeepAlive);
+               bool isKeepAlive,
+               unsigned int keepAliveTimeout);
 
     void SetDeflateAllowed(bool allowed);
 
@@ -136,6 +146,11 @@
 
     bool IsGzipAllowed() const;
 
+    bool IsContentCompressible() const
+    {
+      return stateMachine_.IsContentCompressible();
+    }
+
     void SendStatus(HttpStatus status,
 		    const char* message,
 		    size_t messageSize);
--- a/OrthancFramework/Sources/HttpServer/HttpServer.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpServer.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -35,6 +36,7 @@
 #include "IHttpHandler.h"
 #include "MultipartStreamReader.h"
 #include "StringHttpOutput.h"
+#include <algorithm>
 
 #if ORTHANC_ENABLE_PUGIXML == 1
 #  include "IWebDavBucket.h"
@@ -109,12 +111,25 @@
       {
         if (length > 0)
         {
-          int status = mg_write(connection_, buffer, length);
-          if (status != static_cast<int>(length))
+          // mg_write does not support buffers > 2GB (INT_MAX) -> need to split it
+          size_t offset = 0;
+          size_t remainingSize = length;
+
+          while (remainingSize > 0)
           {
-            // status == 0 when the connection has been closed, -1 on error
-            throw OrthancException(ErrorCode_NetworkProtocol);
-          }
+            size_t packetSize = std::min(remainingSize, static_cast<size_t>(INT_MAX));
+
+            int status = mg_write(connection_, &(reinterpret_cast<const char*>(buffer)[offset]), packetSize);  
+
+            if (status != static_cast<int>(packetSize))
+            {
+              // status == 0 when the connection has been closed, -1 on error
+              throw OrthancException(ErrorCode_NetworkProtocol);
+            }
+
+            offset += packetSize;
+            remainingSize -= packetSize;
+          }  
         }
       }
 
@@ -339,7 +354,7 @@
                         size_t size)
     {
       StringHttpOutput stringOutput;
-      HttpOutput fakeOutput(stringOutput, false);
+      HttpOutput fakeOutput(stringOutput, false /* assume no keep-alive */, 0);
       HttpToolbox::GetArguments getArguments;
       
       if (!handler_.Handle(fakeOutput, RequestOrigin_RestApi, remoteIp_.c_str(), username_.c_str(), 
@@ -1442,13 +1457,13 @@
       if (server == NULL)
       {
         MongooseOutputStream stream(connection);
-        HttpOutput output(stream, false /* assume no keep-alive */);
+        HttpOutput output(stream, false /* assume no keep-alive */, 0);
         output.SendStatus(HttpStatus_500_InternalServerError);
         return;
       }
 
       MongooseOutputStream stream(connection);
-      HttpOutput output(stream, server->IsKeepAliveEnabled());
+      HttpOutput output(stream, server->IsKeepAliveEnabled(), server->GetKeepAliveTimeout());
       HttpMethod method = HttpMethod_Get;
 
       try
@@ -1519,6 +1534,7 @@
     }
   }
 
+  static uint16_t threadCounter = 0;
 
 #if MONGOOSE_USE_CALLBACKS == 0
   static void* Callback(enum mg_event event,
@@ -1543,6 +1559,11 @@
   {
     const struct mg_request_info *request = mg_get_request_info(connection);
 
+    if (!Logging::HasCurrentThreadName())
+    {
+      Logging::SetCurrentThreadName(std::string("HTTP-") + boost::lexical_cast<std::string>(threadCounter++));
+    }
+
     ProtectedCallback(connection, request);
 
     return 1;  // Do not let Mongoose handle the request by itself
@@ -1574,6 +1595,7 @@
     port_(8000),
     filter_(NULL),
     keepAlive_(false),
+    keepAliveTimeout_(1),
     httpCompression_(true),
     exceptionFormatter_(NULL),
     realm_(ORTHANC_REALM),
@@ -1628,6 +1650,9 @@
 
   void HttpServer::Start()
   {
+    // reset thread counter used to generate HTTP thread names.
+    threadCounter = 0;
+
 #if ORTHANC_ENABLE_MONGOOSE == 1
     CLOG(INFO, HTTP) << "Starting embedded Web server using Mongoose";
 #elif ORTHANC_ENABLE_CIVETWEB == 1
@@ -1641,7 +1666,7 @@
       std::string port = boost::lexical_cast<std::string>(port_);
       std::string numThreads = boost::lexical_cast<std::string>(threadsCount_);
       std::string requestTimeoutMilliseconds = boost::lexical_cast<std::string>(requestTimeout_ * 1000);
-      std::string keepAliveTimeoutMilliseconds = boost::lexical_cast<std::string>(CIVETWEB_KEEP_ALIVE_TIMEOUT_SECONDS * 1000);
+      std::string keepAliveTimeoutMilliseconds = boost::lexical_cast<std::string>(keepAliveTimeout_ * 1000);
       std::string sslMinimumVersion = boost::lexical_cast<std::string>(sslMinimumVersion_);
 
       if (ssl_)
@@ -1945,6 +1970,20 @@
 #endif
   }
 
+  void HttpServer::SetKeepAliveTimeout(unsigned int timeout)
+  {
+    Stop();
+    keepAliveTimeout_ = timeout;
+    CLOG(INFO, HTTP) << "HTTP keep alive Timeout is now " << keepAliveTimeout_ << " seconds";
+
+#if ORTHANC_ENABLE_MONGOOSE == 1
+    if (enabled)
+    {
+      CLOG(WARNING, HTTP) << "You should disable HTTP keep alive, as you are using Mongoose";
+    }
+#endif
+  }
+
   const std::string &HttpServer::GetSslCertificate() const
   {
     return certificate_;
@@ -1984,6 +2023,11 @@
     return keepAlive_;
   }
 
+  unsigned int HttpServer::GetKeepAliveTimeout() const
+  {
+    return keepAliveTimeout_;
+  }
+
   void HttpServer::SetRemoteAccessAllowed(bool allowed)
   {
     Stop();
--- a/OrthancFramework/Sources/HttpServer/HttpServer.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpServer.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -106,6 +107,7 @@
     uint16_t port_;
     IIncomingHttpRequestFilter* filter_;
     bool keepAlive_;
+    unsigned int keepAliveTimeout_;
     bool httpCompression_;
     IHttpExceptionFormatter* exceptionFormatter_;
     std::string realm_;
@@ -157,8 +159,12 @@
 
     bool IsKeepAliveEnabled() const;
 
+    unsigned int GetKeepAliveTimeout() const;
+
     void SetKeepAliveEnabled(bool enabled);
 
+    void SetKeepAliveTimeout(unsigned int timeout);
+
     const std::string& GetSslCertificate() const;
 
     void SetSslCertificate(const char* path);
--- a/OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/HttpToolbox.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpToolbox.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/HttpToolbox.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpToolbox.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/IHttpHandler.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/IHttpHandler.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -45,7 +46,7 @@
     HttpToolbox::ParseGetQuery(curi, getArguments, uri.c_str());
 
     StringHttpOutput stream;
-    HttpOutput http(stream, false /* no keep alive */);
+    HttpOutput http(stream, false /* assume no keep-alive */, 0);
 
     if (handler.Handle(http, origin, LOCALHOST, "", HttpMethod_Get, curi, 
                        httpHeaders, getArguments, NULL /* no body for GET */, 0))
@@ -82,7 +83,7 @@
     Toolbox::SplitUriComponents(curi, uri);
 
     StringHttpOutput stream;
-    HttpOutput http(stream, false /* no keep alive */);
+    HttpOutput http(stream, false /* assume no keep-alive */, 0);
 
     if (handler.Handle(http, origin, LOCALHOST, "", method, curi, 
                        httpHeaders, getArguments, bodyData, bodySize))
@@ -141,7 +142,7 @@
     HttpToolbox::GetArguments getArguments;  // No GET argument for DELETE
 
     StringHttpOutput stream;
-    HttpOutput http(stream, false /* no keep alive */);
+    HttpOutput http(stream, false /* assume no keep-alive */, 0);
 
     if (handler.Handle(http, origin, LOCALHOST, "", HttpMethod_Delete, curi, 
                        httpHeaders, getArguments, NULL /* no body for DELETE */, 0))
--- a/OrthancFramework/Sources/HttpServer/IHttpHandler.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/IHttpHandler.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/IHttpOutputStream.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/IHttpOutputStream.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/IHttpStreamAnswer.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/IHttpStreamAnswer.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/IIncomingHttpRequestFilter.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/IIncomingHttpRequestFilter.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/IWebDavBucket.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/IWebDavBucket.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/IWebDavBucket.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/IWebDavBucket.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/MultipartStreamReader.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/MultipartStreamReader.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -384,7 +385,7 @@
         {
           boundary = Toolbox::StripSpaces(items[1]);
 
-          // https://bugs.orthanc-server.com/show_bug.cgi?id=190
+          // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=190
           RemoveSurroundingQuotes(boundary);
           
           valid = !boundary.empty();
@@ -394,7 +395,7 @@
           subType = Toolbox::StripSpaces(items[1]);
           Toolbox::ToLowerCase(subType);
 
-          // https://bugs.orthanc-server.com/show_bug.cgi?id=54
+          // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=54
           // https://tools.ietf.org/html/rfc7231#section-3.1.1.1
           RemoveSurroundingQuotes(subType);
         }
--- a/OrthancFramework/Sources/HttpServer/MultipartStreamReader.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/MultipartStreamReader.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/StringHttpOutput.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/StringHttpOutput.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/StringHttpOutput.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/StringHttpOutput.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/StringMatcher.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/StringMatcher.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/StringMatcher.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/StringMatcher.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/WebDavStorage.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/WebDavStorage.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/HttpServer/WebDavStorage.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/HttpServer/WebDavStorage.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/IDynamicObject.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/IDynamicObject.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/IMemoryBuffer.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/IMemoryBuffer.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/Font.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/Font.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -37,6 +38,7 @@
 #include "Image.h"
 #include "ImageProcessing.h"
 
+#include <cassert>
 #include <stdio.h>
 #include <memory>
 #include <boost/lexical_cast.hpp>
--- a/OrthancFramework/Sources/Images/Font.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/Font.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/FontRegistry.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/FontRegistry.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/FontRegistry.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/FontRegistry.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/IImageWriter.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/IImageWriter.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/IImageWriter.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/IImageWriter.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/Image.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/Image.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/Image.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/Image.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/ImageAccessor.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/ImageAccessor.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/ImageAccessor.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/ImageAccessor.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/ImageBuffer.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/ImageBuffer.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/ImageBuffer.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/ImageBuffer.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/ImageProcessing.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/ImageProcessing.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/ImageProcessing.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/ImageProcessing.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/ImageTraits.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/ImageTraits.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/JpegErrorManager.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/JpegErrorManager.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/JpegErrorManager.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/JpegErrorManager.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/JpegReader.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/JpegReader.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -158,8 +159,7 @@
     jpeg_create_decompress(&cinfo);
     jpeg_mem_src(&cinfo, 
       const_cast<unsigned char*>(
-        reinterpret_cast<const unsigned char*>(buffer)),
-      static_cast<unsigned long>(size));
+        reinterpret_cast<const unsigned char*>(buffer)), size);
 
     try
     {
--- a/OrthancFramework/Sources/Images/JpegReader.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/JpegReader.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/JpegWriter.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/JpegWriter.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -177,7 +178,17 @@
     Internals::JpegErrorManager jerr;
 
     unsigned char* data = NULL;
+
+#if ((JPEG_LIB_VERSION_MAJOR < 9) ||                                    \
+     (JPEG_LIB_VERSION_MAJOR == 9 && JPEG_LIB_VERSION_MINOR <= 3))
+    /**
+     * jpeg_mem_dest() has "unsigned long*" as its 3rd parameter until
+     * jpeg-9c. Since jpeg-9d, this is a "size_t*".
+     **/
     unsigned long size;
+#else
+    size_t size;
+#endif
 
     if (setjmp(jerr.GetJumpBuffer())) 
     {
@@ -202,7 +213,7 @@
 
     // Everything went fine, "setjmp()" didn't get called
 
-    jpeg.assign(reinterpret_cast<const char*>(data), size);
+    jpeg.assign(reinterpret_cast<const char*>(data), static_cast<size_t>(size));
     free(data);
   }
 
--- a/OrthancFramework/Sources/Images/JpegWriter.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/JpegWriter.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/NumpyWriter.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/NumpyWriter.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -154,7 +155,7 @@
   {
     if (compress)
     {
-#if ORTHANC_ENABLE_ZLIB == 1
+#if (ORTHANC_ENABLE_ZLIB == 1) && (ORTHANC_SANDBOXED == 0)
       // This is the default name of the first array if arrays are
       // specified as positional arguments in "numpy.savez()"
       // https://numpy.org/doc/stable/reference/generated/numpy.savez.html
@@ -172,7 +173,7 @@
       writer.Write(uncompressed);
       writer.Close();
 #else
-      throw OrthancException(ErrorCode_InternalError, "Orthanc was compiled without support for zlib");
+      throw OrthancException(ErrorCode_InternalError, "Orthanc was compiled without support for ZIP");
 #endif
     }
     else
--- a/OrthancFramework/Sources/Images/NumpyWriter.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/NumpyWriter.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/PamReader.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/PamReader.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -242,7 +243,7 @@
            * in Orthanc <= 1.8.0 (i.e. make a "memcpy()" to a local
            * uint16_t variable) doesn't seem work for WebAssembly. We
            * thus use a plain old C implementation. Check out issue
-           * #99: https://bugs.orthanc-server.com/show_bug.cgi?id=99
+           * #99: https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=99
            *
            * Here is the crash log on WebAssembly (2019-08-05):
            * 
--- a/OrthancFramework/Sources/Images/PamReader.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/PamReader.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/PamWriter.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/PamWriter.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -28,6 +29,7 @@
 #include "../OrthancException.h"
 #include "../Toolbox.h"
 
+#include <cassert>
 #include <boost/lexical_cast.hpp>
 
 
@@ -130,7 +132,7 @@
            * in Orthanc <= 1.8.0 (i.e. make a "memcpy()" to a local
            * uint16_t variable) doesn't seem work for WebAssembly. We
            * thus use a plain old C implementation. Check out issue
-           * #99: https://bugs.orthanc-server.com/show_bug.cgi?id=99
+           * #99: https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=99
            **/
           const uint8_t* a = reinterpret_cast<const uint8_t*>(p);
           uint8_t* b = reinterpret_cast<uint8_t*>(q);
--- a/OrthancFramework/Sources/Images/PamWriter.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/PamWriter.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/PixelTraits.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/PixelTraits.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/PngReader.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/PngReader.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/PngReader.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/PngReader.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/PngWriter.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/PngWriter.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Images/PngWriter.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Images/PngWriter.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/JobsEngine/GenericJobUnserializer.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/GenericJobUnserializer.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/JobsEngine/GenericJobUnserializer.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/GenericJobUnserializer.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/JobsEngine/IJob.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/IJob.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -62,5 +63,13 @@
                            MimeType& mime,
                            std::string& filename,
                            const std::string& key) = 0;
+
+    // This function can only be called if the job has reached its
+    // "success" state
+    virtual bool DeleteOutput(const std::string& key) = 0;
+
+    // This function can only be called if the job has reached its
+    // "success" state
+    virtual void DeleteAllOutputs() {}
   };
 }
--- a/OrthancFramework/Sources/JobsEngine/IJobUnserializer.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/IJobUnserializer.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/JobsEngine/JobInfo.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobInfo.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/JobsEngine/JobInfo.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobInfo.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/JobsEngine/JobStatus.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobStatus.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/JobsEngine/JobStatus.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobStatus.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/JobsEngine/JobStepResult.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobStepResult.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/JobsEngine/JobStepResult.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobStepResult.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/JobsEngine/JobsEngine.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobsEngine.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -122,7 +123,7 @@
                           size_t workerIndex)
   {
     assert(engine != NULL);
-
+    Logging::SetCurrentThreadName(std::string("JOBS-WORKER-") + boost::lexical_cast<std::string>(workerIndex));
     CLOG(INFO, JOBS) << "Worker thread " << workerIndex << " has started";
 
     while (engine->IsRunning())
--- a/OrthancFramework/Sources/JobsEngine/JobsEngine.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobsEngine.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -649,6 +650,42 @@
   }
 
 
+  bool JobsRegistry::DeleteJobInfo(const std::string& id)
+  {
+    LOG(INFO) << "Deleting job: " << id;
+
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    JobsIndex::iterator found = jobsIndex_.find(id);
+
+    if (found == jobsIndex_.end())
+    {
+      LOG(WARNING) << "Unknown job to delete: " << id;
+      return false;
+    }
+    else
+    {
+      for (CompletedJobs::iterator it = completedJobs_.begin();
+           it != completedJobs_.end(); ++it)
+      {
+        if (*it == found->second)
+        {
+          found->second->GetJob().DeleteAllOutputs();
+          delete found->second;
+          
+          completedJobs_.erase(it);
+          jobsIndex_.erase(id);
+          return true;
+        }
+      }
+
+      LOG(WARNING) << "Can not delete a job that is not complete: " << id;
+      return false;
+    }
+  }
+
+
   bool JobsRegistry::GetJobOutput(std::string& output,
                                   MimeType& mime,
                                   std::string& filename,
@@ -679,6 +716,33 @@
     }
   }
 
+  bool JobsRegistry::DeleteJobOutput(const std::string& job,
+                                     const std::string& key)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    JobsIndex::const_iterator found = jobsIndex_.find(job);
+
+    if (found == jobsIndex_.end())
+    {
+      return false;
+    }
+    else
+    {
+      const JobHandler& handler = *found->second;
+
+      if (handler.GetState() == JobState_Success)
+      {
+        return handler.GetJob().DeleteOutput(key);
+      }
+      else
+      {
+        return false;
+      }
+    }
+  }
+
 
   void JobsRegistry::SubmitInternal(std::string& id,
                                     JobHandler* handler)
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -147,12 +148,17 @@
     bool GetJobInfo(JobInfo& target,
                     const std::string& id);
 
+    bool DeleteJobInfo(const std::string& id);
+
     bool GetJobOutput(std::string& output,
                       MimeType& mime,
                       std::string& filename,
                       const std::string& job,
                       const std::string& key);
 
+    bool DeleteJobOutput(const std::string& job,
+                         const std::string& key);
+
     void Serialize(Json::Value& target);
 
     void Submit(std::string& id,
--- a/OrthancFramework/Sources/JobsEngine/Operations/IJobOperation.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/IJobOperation.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/JobsEngine/Operations/IJobOperationValue.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/IJobOperationValue.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/JobsEngine/Operations/NullOperationValue.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/NullOperationValue.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/JobsEngine/Operations/NullOperationValue.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/NullOperationValue.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -447,13 +448,6 @@
     return true;
   }
 
-  bool SequenceOfOperationsJob::GetOutput(std::string& output,
-                                          MimeType& mime,
-                                          std::string& filename,
-                                          const std::string& key)
-  {
-    return false;
-  }
 
   void SequenceOfOperationsJob::AwakeTrailingSleep()
   {
--- a/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -127,7 +128,15 @@
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
                            std::string& filename,
-                           const std::string& key) ORTHANC_OVERRIDE;
+                           const std::string& key) ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+
+    virtual bool DeleteOutput(const std::string& key) ORTHANC_OVERRIDE
+    {
+      return false;
+    }
 
     void AwakeTrailingSleep();
   };
--- a/OrthancFramework/Sources/JobsEngine/Operations/StringOperationValue.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/StringOperationValue.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/JobsEngine/Operations/StringOperationValue.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/StringOperationValue.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -269,14 +270,6 @@
     return true;
   }
 
-  bool SetOfCommandsJob::GetOutput(std::string &output,
-                                   MimeType &mime,
-                                   std::string& filename,
-                                   const std::string &key)
-  {
-    return false;
-  }
-
 
   SetOfCommandsJob::SetOfCommandsJob(ICommandUnserializer* unserializer,
                                      const Json::Value& source) :
--- a/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -106,6 +107,14 @@
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
                            std::string& filename,
-                           const std::string& key) ORTHANC_OVERRIDE;
+                           const std::string& key) ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+
+    virtual bool DeleteOutput(const std::string& key) ORTHANC_OVERRIDE
+    {
+      return false;
+    }
   };
 }
--- a/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Logging.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Logging.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -26,6 +27,7 @@
 
 #include "OrthancException.h"
 
+#include <cassert>
 #include <stdint.h>
 
 
@@ -310,6 +312,10 @@
     {
     }
 
+    void InitializePluginContext(void* pluginContext, const char* pluginName)
+    {
+    }
+
     void Initialize()
     {
     }
@@ -438,6 +444,10 @@
     {
     }
 
+    void InitializePluginContext(void* pluginContext, const char* pluginName)
+    {
+    }
+
     void Initialize()
     {
     }
@@ -472,6 +482,7 @@
  * mimics behavior from Google Log.
  *********************************************************/
 
+#include <boost/thread/thread.hpp>
 #include <cassert>
 
 namespace
@@ -486,6 +497,7 @@
     _OrthancPluginService_LogInfo = 1,
     _OrthancPluginService_LogWarning = 2,
     _OrthancPluginService_LogError = 3,
+    _OrthancPluginService_LogMessage = 45,
     _OrthancPluginService_INTERNAL = 0x7fffffff
   } _OrthancPluginService;
 
@@ -498,11 +510,23 @@
                                    _OrthancPluginService service,
                                    const void* params);
   } OrthancPluginContext;
+
+  typedef struct
+  {
+    const char*               message;
+    const char*               plugin;
+    const char*               file;
+    uint32_t                  line;
+    uint32_t                  category;  // can be a LogCategory or a OrthancPluginLogCategory
+    uint32_t                  level;     // can be a LogLevel or a OrthancPluginLogLevel
+  } _OrthancPluginLogMessage;
+
 }
   
 
 #include "Enumerations.h"
 #include "SystemToolbox.h"
+#include "Toolbox.h"
 
 #include <fstream>
 #include <boost/filesystem.hpp>
@@ -534,16 +558,27 @@
 
 
 
-static std::unique_ptr<LoggingStreamsContext> loggingStreamsContext_;
-static boost::mutex                           loggingStreamsMutex_;
-static Orthanc::Logging::NullStream           nullStream_;
-static OrthancPluginContext*                  pluginContext_ = NULL;
+static std::unique_ptr<LoggingStreamsContext>   loggingStreamsContext_;
+static boost::mutex                             loggingStreamsMutex_;
+static Orthanc::Logging::NullStream             nullStream_;
+static OrthancPluginContext*                    pluginContext_ = NULL;    // this is != NULL only when running from a plugin
+static std::string                              pluginName_;              // this string can only be non-empty if running from a plugin
+static bool                                     hasOrthancAdvancedLogging_ = false;  // Whether the Orthanc runtime is >= 1.12.4
+static boost::recursive_mutex                   threadNamesMutex_;
+static std::map<boost::thread::id, std::string> threadNames_;
+static bool                                     enableThreadNames_ = true;
+
 
 
 namespace Orthanc
 {
   namespace Logging
   {
+    void EnableThreadNames(bool enabled)
+    {
+      enableThreadNames_ = enabled;
+    }
+
     static void GetLogPath(boost::filesystem::path& log,
                            boost::filesystem::path& link,
                            const std::string& suffix,
@@ -612,10 +647,51 @@
         throw OrthancException(ErrorCode_CannotWriteFile);
       }
     }
-    
+
+    void SetCurrentThreadNameInternal(const boost::thread::id& id, const std::string& name)
+    {
+      boost::recursive_mutex::scoped_lock lock(threadNamesMutex_);
+
+      if (name.size() > 16)
+      {
+        throw OrthancException(ErrorCode_InternalError, std::string("Thread name can not exceed 16 characters: ") + name);
+      }
+
+      threadNames_[id] = name;
+    }
+
+    void SetCurrentThreadName(const std::string& name)
+    {
+      boost::recursive_mutex::scoped_lock lock(threadNamesMutex_);
+      SetCurrentThreadNameInternal(boost::this_thread::get_id(), name);
+    }
+
+    bool HasCurrentThreadName()
+    {
+      boost::thread::id threadId = boost::this_thread::get_id();
+
+      boost::mutex::scoped_lock lock(loggingStreamsMutex_);
+      return threadNames_.find(threadId) != threadNames_.end();
+    }
+
+    static std::string GetCurrentThreadName()
+    {
+      boost::thread::id threadId = boost::this_thread::get_id();
+
+      boost::recursive_mutex::scoped_lock lock(threadNamesMutex_);
+
+      if (threadNames_.find(threadId) == threadNames_.end())
+      {
+        // set the threadId as the thread name
+        SetCurrentThreadNameInternal(threadId, boost::lexical_cast<std::string>(threadId));
+      }
+
+      return threadNames_[threadId];
+    }    
 
     static void GetLinePrefix(std::string& prefix,
                               LogLevel level,
+                              const char* pluginName,  // when logging in the core but coming from a plugin, pluginName_ is NULL but this argument is != NULL
                               const char* file,
                               int line,
                               LogCategory category)
@@ -679,7 +755,23 @@
               static_cast<int>(duration.seconds()),
               static_cast<int>(duration.fractional_seconds()));
 
-      prefix = (std::string(date) + path.filename().string() + ":" +
+      char threadName[20]; // thread names are limited to 16 char + a space
+      if (enableThreadNames_)
+      {
+        sprintf(threadName, "%16s ", GetCurrentThreadName().c_str());
+      }
+      else
+      {
+        threadName[0] = '\0';
+      }
+
+      std::string internalPluginName = "";
+      if (pluginName != NULL)
+      {
+        internalPluginName = std::string(pluginName) + ":/";
+      }
+
+      prefix = (std::string(date) + threadName + internalPluginName + path.filename().string() + ":" +
                 boost::lexical_cast<std::string>(line) + "] ");
 
       if (level != LogLevel_ERROR &&
@@ -695,14 +787,29 @@
     {
       assert(sizeof(_OrthancPluginService) == sizeof(int32_t));
 
+      if (pluginContext == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+
       boost::mutex::scoped_lock lock(loggingStreamsMutex_);
       loggingStreamsContext_.reset(NULL);
       pluginContext_ = reinterpret_cast<OrthancPluginContext*>(pluginContext);
 
+      // The value "hasOrthancAdvancedLogging_" is cached to avoid computing it on every logged message
+      hasOrthancAdvancedLogging_ = Toolbox::IsVersionAbove(pluginContext_->orthancVersion, 1, 12, 4);
+
       EnableInfoLevel(true);  // allow the plugin to log at info level (but the Orthanc Core still decides of the level)
     }
 
 
+    void InitializePluginContext(void* pluginContext, const std::string& pluginName)
+    {
+      InitializePluginContext(pluginContext);
+      pluginName_ = pluginName;
+    }
+
+
     void Initialize()
     {
       boost::mutex::scoped_lock lock(loggingStreamsMutex_);
@@ -775,9 +882,17 @@
     }
 
 
-    void InternalLogger::Setup(LogCategory category,
-                               const char* file,
-                               int line)
+    InternalLogger::InternalLogger(LogLevel level,
+                                   LogCategory category,
+                                   const char* pluginName,
+                                   const char* file,
+                                   int line) :
+      lock_(loggingStreamsMutex_, boost::defer_lock_t()),
+      level_(level),
+      stream_(&nullStream_),  // By default, logging to "/dev/null" is simulated
+      category_(category),
+      file_(file),
+      line_(line)
     {
       if (pluginContext_ != NULL)
       {
@@ -808,7 +923,7 @@
         }
 
         std::string prefix;
-        GetLinePrefix(prefix, level_, file, line, category);
+        GetLinePrefix(prefix, level_, pluginName, file, line, category);
 
         {
           // We lock the global mutex. The mutex is locked until the
@@ -817,7 +932,9 @@
       
           if (loggingStreamsContext_.get() == NULL)
           {
-            fprintf(stderr, "ERROR: Trying to log a message after the finalization of the logging engine\n");
+            // Have you called Orthanc::Logging::InitializePluginContext()?
+            fprintf(stderr, "ERROR: Trying to log a message after the finalization of the logging engine "
+                    "(or did you forgot to initialize it?)\n");
             lock_.unlock();
             return;
           }
@@ -867,28 +984,6 @@
     }
 
 
-    InternalLogger::InternalLogger(LogLevel level,
-                                   LogCategory category,
-                                   const char* file,
-                                   int line) :
-      lock_(loggingStreamsMutex_, boost::defer_lock_t()),
-      level_(level),
-      stream_(&nullStream_)  // By default, logging to "/dev/null" is simulated
-    {
-      Setup(category, file, line);
-    }
-
-
-    InternalLogger::InternalLogger(LogLevel level,
-                                   const char* file,
-                                   int line) :
-      lock_(loggingStreamsMutex_, boost::defer_lock_t()),
-      level_(level),
-      stream_(&nullStream_)  // By default, logging to "/dev/null" is simulated
-    {
-      Setup(LogCategory_GENERIC, file, line);
-    }
-
 
     InternalLogger::~InternalLogger()
     {
@@ -900,22 +995,37 @@
 
         if (pluginContext_ != NULL)
         {
-          switch (level_)
+          if (!pluginName_.empty() &&
+              hasOrthancAdvancedLogging_)
           {
-            case LogLevel_ERROR:
-              pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogError, message.c_str());
-              break;
+            _OrthancPluginLogMessage m;
+            m.category = category_;
+            m.level = level_;
+            m.file = file_;
+            m.line = line_;
+            m.plugin = pluginName_.c_str();
+            m.message = message.c_str();
+            pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogMessage, &m);
+          }
+          else
+          {
+            switch (level_)
+            {
+              case LogLevel_ERROR:
+                pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogError, message.c_str());
+                break;
 
-            case LogLevel_WARNING:
-              pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogWarning, message.c_str());
-              break;
+              case LogLevel_WARNING:
+                pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogWarning, message.c_str());
+                break;
 
-            case LogLevel_INFO:
-              pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogInfo, message.c_str());
-              break;
+              case LogLevel_INFO:
+                pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogInfo, message.c_str());
+                break;
 
-            default:
-              break;
+              default:
+                break;
+            }
           }
         }
       }
--- a/OrthancFramework/Sources/Logging.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Logging.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -28,6 +29,7 @@
 #include "Compatibility.h"
 
 #include <iostream>
+#include <stdint.h>
 
 #if !defined(ORTHANC_ENABLE_LOGGING)
 #  error The macro ORTHANC_ENABLE_LOGGING must be defined
@@ -46,12 +48,13 @@
 {
   namespace Logging
   {
+    // Note: these values must match the ones in OrthancCPlugin.h
     enum LogLevel
     {
-      LogLevel_ERROR,
-      LogLevel_WARNING,
-      LogLevel_INFO,
-      LogLevel_TRACE
+      LogLevel_ERROR     = 0,
+      LogLevel_WARNING   = 1,
+      LogLevel_INFO      = 2,
+      LogLevel_TRACE     = 3
     };
 
     /**
@@ -59,6 +62,7 @@
      * mask. As a consequence, there can be up to 31 log categories
      * (not 32, as the value GENERIC is reserved for the log commands
      * that don't fall in a specific category).
+     * Note: these values must match the ones in OrthancCPlugin.h
      **/
     enum LogCategory
     {
@@ -78,6 +82,9 @@
     // "pluginContext" must be of type "OrthancPluginContext"
     ORTHANC_PUBLIC void InitializePluginContext(void* pluginContext);
 
+    ORTHANC_PUBLIC void InitializePluginContext(void* pluginContext,
+                                                const std::string& pluginName);
+
     ORTHANC_PUBLIC void Initialize();
 
     ORTHANC_PUBLIC void Finalize();
@@ -86,6 +93,12 @@
 
     ORTHANC_PUBLIC void Flush();
 
+    ORTHANC_PUBLIC void SetCurrentThreadName(const std::string& name);
+
+    ORTHANC_PUBLIC bool HasCurrentThreadName();
+
+    ORTHANC_PUBLIC void EnableThreadNames(bool enabled);
+
     ORTHANC_PUBLIC void EnableInfoLevel(bool enabled);
 
     ORTHANC_PUBLIC void EnableTraceLevel(bool enabled);
@@ -156,16 +169,36 @@
 #  define LOG(level)            ::Orthanc::Logging::NullStream()
 #  define VLOG(unused)          ::Orthanc::Logging::NullStream()
 #  define CLOG(level, category) ::Orthanc::Logging::NullStream()
+#  define LOG_FROM_PLUGIN(level, category, pluginName, file, line)  ::Orthanc::Logging::NullStream()
 #else /* ORTHANC_ENABLE_LOGGING == 1 */
-#  define LOG(level)     ::Orthanc::Logging::InternalLogger     \
-  (::Orthanc::Logging::LogLevel_ ## level,                      \
-   ::Orthanc::Logging::LogCategory_GENERIC, __FILE__, __LINE__)
-#  define VLOG(unused)   ::Orthanc::Logging::InternalLogger     \
-  (::Orthanc::Logging::LogLevel_TRACE,                          \
-   ::Orthanc::Logging::LogCategory_GENERIC, __FILE__, __LINE__)
+
+#if !defined(__ORTHANC_FILE__)
+#  if defined(_MSC_VER)
+#    pragma message("Warning: Macro __ORTHANC_FILE__ is not defined, this will leak the full path of the source files in the binaries")
+#  else
+#    warning Warning: Macro __ORTHANC_FILE__ is not defined, this will leak the full path of the source files in the binaries
+#  endif
+#  define __ORTHANC_FILE__ __FILE__
+#endif
+
+#  define LOG(level)     ::Orthanc::Logging::InternalLogger             \
+  (::Orthanc::Logging::LogLevel_ ## level,                              \
+   ::Orthanc::Logging::LogCategory_GENERIC, NULL /* no plugin */,       \
+   __ORTHANC_FILE__, __LINE__)
+
+#  define VLOG(unused)   ::Orthanc::Logging::InternalLogger             \
+  (::Orthanc::Logging::LogLevel_TRACE,                                  \
+   ::Orthanc::Logging::LogCategory_GENERIC, NULL /* no plugin */,       \
+   __ORTHANC_FILE__, __LINE__)
+
 #  define CLOG(level, category) ::Orthanc::Logging::InternalLogger      \
   (::Orthanc::Logging::LogLevel_ ## level,                              \
-   ::Orthanc::Logging::LogCategory_ ## category, __FILE__, __LINE__)
+   ::Orthanc::Logging::LogCategory_ ## category, NULL /* no plugin */,  \
+   __ORTHANC_FILE__, __LINE__)
+
+#  define LOG_FROM_PLUGIN(level, category, pluginName, file, line)      \
+  ::Orthanc::Logging::InternalLogger(level, category, pluginName, file, line)
+
 #endif
 
 
@@ -192,6 +225,7 @@
     public:
       InternalLogger(LogLevel level,
                      LogCategory category,
+                     const char* pluginName /* ignored */,
                      const char* file  /* ignored */,
                      int line  /* ignored */) :
         level_(level),
@@ -199,15 +233,6 @@
       {
       }
 
-      // For backward binary compatibility with Orthanc Framework <= 1.8.0
-      InternalLogger(LogLevel level,
-                     const char* file  /* ignored */,
-                     int line  /* ignored */) :
-        level_(level),
-        category_(LogCategory_GENERIC)
-      {
-      }
-
       ~InternalLogger();
       
       template <typename T>
@@ -242,19 +267,14 @@
       LogLevel                            level_;
       std::unique_ptr<std::stringstream>  pluginStream_;
       std::ostream*                       stream_;
-
-      void Setup(LogCategory category,
-                 const char* file,
-                 int line);
+      LogCategory                         category_;
+      const char*                         file_;
+      uint32_t                            line_;
 
     public:
       InternalLogger(LogLevel level,
                      LogCategory category,
-                     const char* file,
-                     int line);
-
-      // For backward binary compatibility with Orthanc Framework <= 1.8.0
-      InternalLogger(LogLevel level,
+                     const char* pluginName,
                      const char* file,
                      int line);
 
--- a/OrthancFramework/Sources/Lua/LuaContext.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Lua/LuaContext.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -396,10 +397,6 @@
       const std::string s = value.asString();
       lua_pushlstring(lua_, s.c_str(), s.size());
     }
-    else if (value.isDouble())
-    {
-      lua_pushnumber(lua_, value.asDouble());
-    }
     else if (value.isInt())
     {
       lua_pushinteger(lua_, value.asInt());
@@ -408,6 +405,10 @@
     {
       lua_pushinteger(lua_, value.asUInt());
     }
+    else if (value.isDouble())
+    {
+      lua_pushnumber(lua_, value.asDouble());
+    }
     else if (value.isBool())
     {
       lua_pushboolean(lua_, value.asBool());
--- a/OrthancFramework/Sources/Lua/LuaContext.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Lua/LuaContext.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Lua/LuaFunctionCall.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Lua/LuaFunctionCall.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Lua/LuaFunctionCall.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Lua/LuaFunctionCall.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/MallocMemoryBuffer.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/MallocMemoryBuffer.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/MallocMemoryBuffer.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/MallocMemoryBuffer.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/MetricsRegistry.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/MetricsRegistry.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -28,6 +29,8 @@
 #include "Compatibility.h"
 #include "OrthancException.h"
 
+#include <boost/math/special_functions/round.hpp>
+
 namespace Orthanc
 {
   static const boost::posix_time::ptime GetNow()
@@ -35,134 +38,280 @@
     return boost::posix_time::microsec_clock::universal_time();
   }
 
-  class MetricsRegistry::Item
+  namespace
   {
-  private:
-    MetricsType               type_;
-    boost::posix_time::ptime  time_;
-    bool                      hasValue_;
-    float                     value_;
-    
-    void Touch(float value,
-               const boost::posix_time::ptime& now)
+    template <typename T>
+    class TimestampedValue : public boost::noncopyable
     {
-      hasValue_ = true;
-      value_ = value;
-      time_ = now;
-    }
+    private:
+      boost::posix_time::ptime  time_;
+      bool                      hasValue_;
+      T                         value_;
+
+      void SetValue(const T& value,
+                    const boost::posix_time::ptime& now)
+      {
+        hasValue_ = true;
+        value_ = value;
+        time_ = now;
+      }
 
-    void Touch(float value)
-    {
-      Touch(value, GetNow());
-    }
+      bool IsLargerOverPeriod(const T& value,
+                              int duration,
+                              const boost::posix_time::ptime& now) const
+      {
+        if (hasValue_)
+        {
+          return (value > value_ ||
+                  (now - time_).total_seconds() > duration /* old value has expired */);
+        }
+        else
+        {
+          return true;  // No value yet
+        }
+      }
 
-    void UpdateMax(float value,
-                   int duration)
-    {
-      if (hasValue_)
+      bool IsSmallerOverPeriod(const T& value,
+                               int duration,
+                               const boost::posix_time::ptime& now) const
+      {
+        if (hasValue_)
+        {
+          return (value < value_ ||
+                  (now - time_).total_seconds() > duration /* old value has expired */);
+        }
+        else
+        {
+          return true;  // No value yet
+        }
+      }
+
+    public:
+      explicit TimestampedValue() :
+        hasValue_(false),
+        value_(0)
+      {
+      }
+
+      void Update(const T& value,
+                  const MetricsUpdatePolicy& policy)
       {
         const boost::posix_time::ptime now = GetNow();
 
-        if (value > value_ ||
-            (now - time_).total_seconds() > duration)
+        switch (policy)
         {
-          Touch(value, now);
+          case MetricsUpdatePolicy_Directly:
+            SetValue(value, now);
+            break;
+          
+          case MetricsUpdatePolicy_MaxOver10Seconds:
+            if (IsLargerOverPeriod(value, 10, now))
+            {
+              SetValue(value, now);
+            }
+            break;
+
+          case MetricsUpdatePolicy_MaxOver1Minute:
+            if (IsLargerOverPeriod(value, 60, now))
+            {
+              SetValue(value, now);
+            }
+            break;
+
+          case MetricsUpdatePolicy_MinOver10Seconds:
+            if (IsSmallerOverPeriod(value, 10, now))
+            {
+              SetValue(value, now);
+            }
+            break;
+
+          case MetricsUpdatePolicy_MinOver1Minute:
+            if (IsSmallerOverPeriod(value, 60, now))
+            {
+              SetValue(value, now);
+            }
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_NotImplemented);
         }
       }
-      else
+
+      void Increment(const T& delta)
+      {
+        time_ = GetNow();
+
+        if (hasValue_)
+        {
+          value_ += delta;
+        }
+        else
+        {
+          value_ = delta;
+          hasValue_ = true;
+        }
+      }
+
+      bool HasValue() const
+      {
+        return hasValue_;
+      }
+
+      const boost::posix_time::ptime& GetTime() const
       {
-        Touch(value);
+        if (hasValue_)
+        {
+          return time_;
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_BadSequenceOfCalls);
+        }
       }
+
+      const T& GetValue() const
+      {
+        if (hasValue_)
+        {
+          return value_;
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_BadSequenceOfCalls);
+        }
+      }
+    };
+  }
+
+
+  class MetricsRegistry::Item : public boost::noncopyable
+  {
+  private:
+    MetricsUpdatePolicy   policy_;
+    
+  public:
+    explicit Item(MetricsUpdatePolicy policy) :
+      policy_(policy)
+    {
     }
     
-    void UpdateMin(float value,
-                   int duration)
-    {
-      if (hasValue_)
-      {
-        const boost::posix_time::ptime now = GetNow();
-        
-        if (value < value_ ||
-            (now - time_).total_seconds() > duration)
-        {
-          Touch(value, now);
-        }
-      }
-      else
-      {
-        Touch(value);
-      }
-    }
-
-  public:
-    explicit Item(MetricsType type) :
-      type_(type),
-      hasValue_(false),
-      value_(0)
+    virtual ~Item()
     {
     }
 
-    MetricsType GetType() const
+    MetricsUpdatePolicy GetPolicy() const
     {
-      return type_;
+      return policy_;
+    }
+
+    virtual void UpdateFloat(float value) = 0;
+
+    virtual void UpdateInteger(int64_t value) = 0;
+
+    virtual void IncrementInteger(int64_t delta) = 0;
+
+    virtual MetricsDataType GetDataType() const = 0;
+
+    virtual bool HasValue() const = 0;
+
+    virtual const boost::posix_time::ptime& GetTime() const = 0;
+    
+    virtual std::string FormatValue() const = 0;
+  };
+
+  
+  class MetricsRegistry::FloatItem : public Item
+  {
+  private:
+    TimestampedValue<float>  value_;
+
+  public:
+    explicit FloatItem(MetricsUpdatePolicy policy) :
+      Item(policy)
+    {
+    }
+    
+    virtual void UpdateFloat(float value) ORTHANC_OVERRIDE
+    {
+      value_.Update(value, GetPolicy());
+    }
+
+    virtual void UpdateInteger(int64_t value) ORTHANC_OVERRIDE
+    {
+      value_.Update(static_cast<float>(value), GetPolicy());
+    }
+
+    virtual void IncrementInteger(int64_t delta) ORTHANC_OVERRIDE
+    {
+      value_.Increment(static_cast<float>(delta));
+    }
+
+    virtual MetricsDataType GetDataType() const ORTHANC_OVERRIDE
+    {
+      return MetricsDataType_Float;
+    }
+
+    virtual bool HasValue() const ORTHANC_OVERRIDE
+    {
+      return value_.HasValue();
     }
 
-    void Update(float value)
+    virtual const boost::posix_time::ptime& GetTime() const ORTHANC_OVERRIDE
     {
-      switch (type_)
-      {
-        case MetricsType_Default:
-          Touch(value);
-          break;
-          
-        case MetricsType_MaxOver10Seconds:
-          UpdateMax(value, 10);
-          break;
+      return value_.GetTime();
+    }
+    
+    virtual std::string FormatValue() const ORTHANC_OVERRIDE
+    {
+      return boost::lexical_cast<std::string>(value_.GetValue());
+    }
+  };
 
-        case MetricsType_MaxOver1Minute:
-          UpdateMax(value, 60);
-          break;
-
-        case MetricsType_MinOver10Seconds:
-          UpdateMin(value, 10);
-          break;
+  
+  class MetricsRegistry::IntegerItem : public Item
+  {
+  private:
+    TimestampedValue<int64_t>  value_;
 
-        case MetricsType_MinOver1Minute:
-          UpdateMin(value, 60);
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_NotImplemented);
-      }
+  public:
+    explicit IntegerItem(MetricsUpdatePolicy policy) :
+      Item(policy)
+    {
+    }
+    
+    virtual void UpdateFloat(float value) ORTHANC_OVERRIDE
+    {
+      value_.Update(boost::math::llround(value), GetPolicy());
     }
 
-    bool HasValue() const
+    virtual void UpdateInteger(int64_t value) ORTHANC_OVERRIDE
     {
-      return hasValue_;
+      value_.Update(value, GetPolicy());
+    }
+
+    virtual void IncrementInteger(int64_t delta) ORTHANC_OVERRIDE
+    {
+      value_.Increment(delta);
     }
 
-    const boost::posix_time::ptime& GetTime() const
+    virtual MetricsDataType GetDataType() const ORTHANC_OVERRIDE
     {
-      if (hasValue_)
-      {
-        return time_;
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
+      return MetricsDataType_Integer;
     }
 
-    float GetValue() const
+    virtual bool HasValue() const ORTHANC_OVERRIDE
+    {
+      return value_.HasValue();
+    }
+
+    virtual const boost::posix_time::ptime& GetTime() const ORTHANC_OVERRIDE
     {
-      if (hasValue_)
-      {
-        return value_;
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
+      return value_.GetTime();
+    }
+    
+    virtual std::string FormatValue() const ORTHANC_OVERRIDE
+    {
+      return boost::lexical_cast<std::string>(value_.GetValue());
     }
   };
 
@@ -190,48 +339,53 @@
 
 
   void MetricsRegistry::Register(const std::string& name,
-                                 MetricsType type)
+                                 MetricsUpdatePolicy policy,
+                                 MetricsDataType type)
   {
     boost::mutex::scoped_lock lock(mutex_);
 
+    if (content_.find(name) != content_.end())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls, "Cannot register twice the same metrics: " + name);
+    }
+    else
+    {
+      GetItemInternal(name, policy, type);
+    }
+  }
+
+
+  MetricsRegistry::Item& MetricsRegistry::GetItemInternal(const std::string& name,
+                                                          MetricsUpdatePolicy policy,
+                                                          MetricsDataType type)
+  {
     Content::iterator found = content_.find(name);
 
     if (found == content_.end())
     {
-      content_[name] = new Item(type);
+      Item* item = NULL;
+      
+      switch (type)
+      {
+        case MetricsDataType_Float:
+          item = new FloatItem(policy);
+          break;
+
+        case MetricsDataType_Integer:
+          item = new IntegerItem(policy);
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+
+      content_[name] = item;
+      return *item;
     }
     else
     {
       assert(found->second != NULL);
-
-      // This metrics already exists: Only recreate it if there is a
-      // mismatch in the type of metrics
-      if (found->second->GetType() != type)
-      {
-        delete found->second;
-        found->second = new Item(type);
-      }
-    }    
-  }
-
-  void MetricsRegistry::SetValueInternal(const std::string& name,
-                                         float value,
-                                         MetricsType type)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    Content::iterator found = content_.find(name);
-
-    if (found == content_.end())
-    {
-      std::unique_ptr<Item> item(new Item(type));
-      item->Update(value);
-      content_[name] = item.release();
-    }
-    else
-    {
-      assert(found->second != NULL);
-      found->second->Update(value);
+      return *found->second;
     }
   }
 
@@ -241,29 +395,49 @@
   }
 
 
-  void MetricsRegistry::SetValue(const std::string &name,
-                                 float value,
-                                 MetricsType type)
+  void MetricsRegistry::SetFloatValue(const std::string& name,
+                                      float value,
+                                      MetricsUpdatePolicy policy)
   {
     // Inlining to avoid loosing time if metrics are disabled
     if (enabled_)
     {
-      SetValueInternal(name, value, type);
+      boost::mutex::scoped_lock lock(mutex_);
+      GetItemInternal(name, policy, MetricsDataType_Float).UpdateFloat(value);
+    }
+  }
+  
+
+  void MetricsRegistry::SetIntegerValue(const std::string &name,
+                                        int64_t value,
+                                        MetricsUpdatePolicy policy)
+  {
+    // Inlining to avoid loosing time if metrics are disabled
+    if (enabled_)
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      GetItemInternal(name, policy, MetricsDataType_Integer).UpdateInteger(value);
     }
   }
 
 
-  void MetricsRegistry::SetValue(const std::string &name, float value)
+  void MetricsRegistry::IncrementIntegerValue(const std::string &name,
+                                              int64_t delta)
   {
-    SetValue(name, value, MetricsType_Default);
+    // Inlining to avoid loosing time if metrics are disabled
+    if (enabled_)
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      GetItemInternal(name, MetricsUpdatePolicy_Directly, MetricsDataType_Integer).IncrementInteger(delta);
+    }
   }
 
 
-  MetricsType MetricsRegistry::GetMetricsType(const std::string& name)
+  MetricsUpdatePolicy MetricsRegistry::GetUpdatePolicy(const std::string& metrics)
   {
     boost::mutex::scoped_lock lock(mutex_);
 
-    Content::const_iterator found = content_.find(name);
+    Content::const_iterator found = content_.find(metrics);
 
     if (found == content_.end())
     {
@@ -272,7 +446,25 @@
     else
     {
       assert(found->second != NULL);
-      return found->second->GetType();
+      return found->second->GetPolicy();
+    }
+  }
+
+
+  MetricsDataType MetricsRegistry::GetDataType(const std::string& metrics)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Content::const_iterator found = content_.find(metrics);
+
+    if (found == content_.end())
+    {
+      throw OrthancException(ErrorCode_InexistentItem);
+    }
+    else
+    {
+      assert(found->second != NULL);
+      return found->second->GetDataType();
     }
   }
 
@@ -303,7 +495,7 @@
         boost::posix_time::time_duration diff = it->second->GetTime() - EPOCH;
 
         std::string line = (it->first + " " +
-                            boost::lexical_cast<std::string>(it->second->GetValue()) + " " + 
+                            it->second->FormatValue() + " " + 
                             boost::lexical_cast<std::string>(diff.total_milliseconds()) + "\n");
 
         buffer.AddChunk(line);
@@ -316,18 +508,18 @@
 
   MetricsRegistry::SharedMetrics::SharedMetrics(MetricsRegistry &registry,
                                                 const std::string &name,
-                                                MetricsType type) :
+                                                MetricsUpdatePolicy policy) :
     registry_(registry),
     name_(name),
     value_(0)
   {
   }
 
-  void MetricsRegistry::SharedMetrics::Add(float delta)
+  void MetricsRegistry::SharedMetrics::Add(int64_t delta)
   {
     boost::mutex::scoped_lock lock(mutex_);
     value_ += delta;
-    registry_.SetValue(name_, value_);
+    registry_.SetIntegerValue(name_, value_);
   }
 
 
@@ -361,7 +553,7 @@
                                 const std::string &name) :
     registry_(registry),
     name_(name),
-    type_(MetricsType_MaxOver10Seconds)
+    policy_(MetricsUpdatePolicy_MaxOver10Seconds)
   {
     Start();
   }
@@ -369,10 +561,10 @@
 
   MetricsRegistry::Timer::Timer(MetricsRegistry &registry,
                                 const std::string &name,
-                                MetricsType type) :
+                                MetricsUpdatePolicy policy) :
     registry_(registry),
     name_(name),
-    type_(type)
+    policy_(policy)
   {
     Start();
   }
@@ -383,8 +575,7 @@
     if (active_)
     {
       boost::posix_time::time_duration diff = GetNow() - start_;
-      registry_.SetValue(
-            name_, static_cast<float>(diff.total_milliseconds()), type_);
+      registry_.SetIntegerValue(name_, static_cast<int64_t>(diff.total_milliseconds()), policy_);
     }
   }
 }
--- a/OrthancFramework/Sources/MetricsRegistry.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/MetricsRegistry.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -35,22 +36,31 @@
 
 #include <boost/thread/mutex.hpp>
 #include <boost/date_time/posix_time/posix_time.hpp>
+#include <stdint.h>
 
 namespace Orthanc
 {
-  enum MetricsType
+  enum MetricsUpdatePolicy
   {
-    MetricsType_Default,
-    MetricsType_MaxOver10Seconds,
-    MetricsType_MaxOver1Minute,
-    MetricsType_MinOver10Seconds,
-    MetricsType_MinOver1Minute
+    MetricsUpdatePolicy_Directly,
+    MetricsUpdatePolicy_MaxOver10Seconds,
+    MetricsUpdatePolicy_MaxOver1Minute,
+    MetricsUpdatePolicy_MinOver10Seconds,
+    MetricsUpdatePolicy_MinOver1Minute
+  };
+  
+  enum MetricsDataType
+  {
+    MetricsDataType_Float,
+    MetricsDataType_Integer
   };
   
   class ORTHANC_PUBLIC MetricsRegistry : public boost::noncopyable
   {
   private:
     class Item;
+    class FloatItem;
+    class IntegerItem;
 
     typedef std::map<std::string, Item*>   Content;
 
@@ -58,9 +68,10 @@
     boost::mutex  mutex_;
     Content       content_;
 
-    void SetValueInternal(const std::string& name,
-                          float value,
-                          MetricsType type);
+    // The mutex must be locked
+    Item& GetItemInternal(const std::string& name,
+                          MetricsUpdatePolicy policy,
+                          MetricsDataType type);
 
   public:
     MetricsRegistry();
@@ -72,16 +83,35 @@
     void SetEnabled(bool enabled);
 
     void Register(const std::string& name,
-                  MetricsType type);
+                  MetricsUpdatePolicy policy,
+                  MetricsDataType type);
 
-    void SetValue(const std::string& name,
-                  float value,
-                  MetricsType type);
+    void SetFloatValue(const std::string& name,
+                       float value,
+                       MetricsUpdatePolicy policy /* only used if this is a new metrics */);
+    
+    void SetFloatValue(const std::string& name,
+                       float value)
+    {
+      SetFloatValue(name, value, MetricsUpdatePolicy_Directly);
+    }
     
-    void SetValue(const std::string& name,
-                  float value);
+    void SetIntegerValue(const std::string& name,
+                         int64_t value,
+                         MetricsUpdatePolicy policy /* only used if this is a new metrics */);
+    
+    void SetIntegerValue(const std::string& name,
+                         int64_t value)
+    {
+      SetIntegerValue(name, value, MetricsUpdatePolicy_Directly);
+    }
+    
+    void IncrementIntegerValue(const std::string& name,
+                               int64_t delta);
 
-    MetricsType GetMetricsType(const std::string& name);
+    MetricsUpdatePolicy GetUpdatePolicy(const std::string& metrics);
+
+    MetricsDataType GetDataType(const std::string& metrics);
 
     // https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format
     void ExportPrometheusText(std::string& s);
@@ -93,14 +123,14 @@
       boost::mutex      mutex_;
       MetricsRegistry&  registry_;
       std::string       name_;
-      float             value_;
+      int64_t           value_;
 
     public:
       SharedMetrics(MetricsRegistry& registry,
                     const std::string& name,
-                    MetricsType type);
+                    MetricsUpdatePolicy policy);
 
-      void Add(float delta);
+      void Add(int64_t delta);
     };
 
 
@@ -121,7 +151,7 @@
     private:
       MetricsRegistry&          registry_;
       std::string               name_;
-      MetricsType               type_;
+      MetricsUpdatePolicy       policy_;
       bool                      active_;
       boost::posix_time::ptime  start_;
 
@@ -133,7 +163,7 @@
 
       Timer(MetricsRegistry& registry,
             const std::string& name,
-            MetricsType type);
+            MetricsUpdatePolicy policy);
 
       ~Timer();
     };
--- a/OrthancFramework/Sources/MultiThreading/IRunnableBySteps.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/MultiThreading/IRunnableBySteps.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/MultiThreading/Mutex.h	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,70 @@
+/**
+ * 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
+
+#if !defined(__EMSCRIPTEN__)
+#  include <boost/thread/mutex.hpp>
+#endif
+
+namespace Orthanc
+{
+  // Wrapper class for compatibility with Emscripten
+
+#if defined(__EMSCRIPTEN__)
+
+  class ORTHANC_PUBLIC Mutex : public boost::noncopyable
+  {
+  public:
+    class ORTHANC_PUBLIC ScopedLock : public boost::noncopyable
+    {
+    public:
+      explicit ScopedLock(Mutex& mutex)
+      {
+      }
+    };
+  };
+
+#else
+
+  class ORTHANC_PUBLIC Mutex : public boost::noncopyable
+  {
+  private:
+    boost::mutex mutex_;
+
+  public:
+    class ORTHANC_PUBLIC ScopedLock : public boost::noncopyable
+    {
+    private:
+      boost::mutex::scoped_lock lock_;
+
+    public:
+      explicit ScopedLock(Mutex& mutex) :
+        lock_(mutex.mutex_)
+      {
+      }
+    };
+  };
+#endif
+}
--- a/OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -39,9 +40,12 @@
       const bool&           continue_;
       SharedMessageQueue&   queue_;
       boost::thread         thread_;
+      std::string           name_;
  
       static void WorkerThread(Worker* that)
       {
+        Logging::SetCurrentThreadName(that->name_);
+
         while (that->continue_)
         {
           try
@@ -81,9 +85,11 @@
 
     public:
       Worker(const bool& globalContinue,
-             SharedMessageQueue& queue) : 
+             SharedMessageQueue& queue,
+             const std::string& name) : 
         continue_(globalContinue),
-        queue_(queue)
+        queue_(queue),
+        name_(name)
       {
         thread_ = boost::thread(WorkerThread, this);
       }
@@ -105,7 +111,7 @@
 
 
 
-  RunnableWorkersPool::RunnableWorkersPool(size_t countWorkers) : pimpl_(new PImpl)
+  RunnableWorkersPool::RunnableWorkersPool(size_t countWorkers, const std::string& name) : pimpl_(new PImpl)
   {
     pimpl_->continue_ = true;
 
@@ -118,7 +124,8 @@
 
     for (size_t i = 0; i < countWorkers; i++)
     {
-      pimpl_->workers_[i] = new PImpl::Worker(pimpl_->continue_, pimpl_->queue_);
+      std::string workerName = name + boost::lexical_cast<std::string>(i);
+      pimpl_->workers_[i] = new PImpl::Worker(pimpl_->continue_, pimpl_->queue_, workerName);
     }
   }
 
--- a/OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -38,7 +39,7 @@
     void Stop();
 
   public:
-    explicit RunnableWorkersPool(size_t countWorkers);
+    explicit RunnableWorkersPool(size_t countWorkers, const std::string& name);
 
     ~RunnableWorkersPool();
 
--- a/OrthancFramework/Sources/MultiThreading/Semaphore.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/MultiThreading/Semaphore.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/MultiThreading/Semaphore.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/MultiThreading/Semaphore.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/MultiThreading/SharedMessageQueue.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/MultiThreading/SharedMessageQueue.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/MultiThreading/SharedMessageQueue.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/MultiThreading/SharedMessageQueue.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/OrthancException.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/OrthancException.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -31,7 +32,8 @@
 {
   OrthancException::OrthancException(const OrthancException& other) : 
     errorCode_(other.errorCode_),
-    httpStatus_(other.httpStatus_)
+    httpStatus_(other.httpStatus_),
+    logged_(false)
   {
     if (other.details_.get() != NULL)
     {
@@ -41,7 +43,8 @@
 
   OrthancException::OrthancException(ErrorCode errorCode) : 
     errorCode_(errorCode),
-    httpStatus_(ConvertErrorCodeToHttpStatus(errorCode))
+    httpStatus_(ConvertErrorCodeToHttpStatus(errorCode)),
+    logged_(false)
   {
   }
 
@@ -50,6 +53,7 @@
                                      bool log) :
     errorCode_(errorCode),
     httpStatus_(ConvertErrorCodeToHttpStatus(errorCode)),
+    logged_(log),
     details_(new std::string(details))
   {
 #if ORTHANC_ENABLE_LOGGING == 1
@@ -63,7 +67,8 @@
   OrthancException::OrthancException(ErrorCode errorCode,
                                      HttpStatus httpStatus) :
     errorCode_(errorCode),
-    httpStatus_(httpStatus)
+    httpStatus_(httpStatus),
+    logged_(false)
   {
   }
 
@@ -73,6 +78,7 @@
                                      bool log) :
     errorCode_(errorCode),
     httpStatus_(httpStatus),
+    logged_(log),
     details_(new std::string(details))
   {
 #if ORTHANC_ENABLE_LOGGING == 1
@@ -114,4 +120,10 @@
       return details_->c_str();
     }
   }
+
+  bool OrthancException::HasBeenLogged() const
+  {
+    return logged_;
+  }
+
 }
--- a/OrthancFramework/Sources/OrthancException.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/OrthancException.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -38,6 +39,7 @@
 
     ErrorCode  errorCode_;
     HttpStatus httpStatus_;
+    bool       logged_;    // has the exception already been logged ?  (to avoid double logs)
 
     // New in Orthanc 1.5.0
     std::unique_ptr<std::string>  details_;
@@ -68,5 +70,7 @@
     bool HasDetails() const;
 
     const char* GetDetails() const;
+
+    bool HasBeenLogged() const;
   };
 }
--- a/OrthancFramework/Sources/OrthancFramework.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/OrthancFramework.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/OrthancFramework.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/OrthancFramework.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Pkcs11.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Pkcs11.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Pkcs11.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Pkcs11.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/PrecompiledHeaders.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/PrecompiledHeaders.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/PrecompiledHeaders.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/PrecompiledHeaders.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/RestApi/RestApi.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApi.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -138,6 +139,7 @@
 
     protected:
       virtual bool HandleCall(RestApiCall& call,
+                              const std::string& path,
                               const std::set<std::string>& uriArgumentsNames) = 0;
   
     public:
@@ -157,7 +159,7 @@
         std::string path = Toolbox::FlattenUri(uri);
         if (hasTrailing)
         {
-          path += "/{...}";
+          path += "/{path}";
         }
 
         std::set<std::string> uriArgumentsNames;
@@ -173,8 +175,8 @@
 
         if (hasTrailing)
         {
-          uriArgumentsNames.insert("...");
-          uriArguments["..."] = "";
+          uriArgumentsNames.insert("path");
+          uriArguments["path"] = "";
         }
 
         if (resource.HasHandler(HttpMethod_Get))
@@ -182,7 +184,7 @@
           totalPathsCount_ ++;
           
           StringHttpOutput o1;
-          HttpOutput o2(o1, false);
+          HttpOutput o2(o1, false /* assume no keep-alive */, 0);
           RestApiOutput o3(o2, HttpMethod_Get);
           RestApiGetCall call(o3, restApi_, RequestOrigin_Documentation, "" /* remote IP */,
                               "" /* username */, HttpToolbox::Arguments() /* HTTP headers */,
@@ -194,7 +196,7 @@
           try
           {
             ok = (resource.Handle(call) &&
-                  HandleCall(call, uriArgumentsNames));
+                  HandleCall(call, path, uriArgumentsNames));
           }
           catch (OrthancException& e)
           {
@@ -220,7 +222,7 @@
           totalPathsCount_ ++;
           
           StringHttpOutput o1;
-          HttpOutput o2(o1, false);
+          HttpOutput o2(o1, false /* assume no keep-alive */, 0);
           RestApiOutput o3(o2, HttpMethod_Post);
           RestApiPostCall call(o3, restApi_, RequestOrigin_Documentation, "" /* remote IP */,
                                "" /* username */, HttpToolbox::Arguments() /* HTTP headers */,
@@ -232,7 +234,7 @@
           try
           {
             ok = (resource.Handle(call) &&
-                  HandleCall(call, uriArgumentsNames));
+                  HandleCall(call, path, uriArgumentsNames));
           }
           catch (OrthancException& e)
           {
@@ -258,7 +260,7 @@
           totalPathsCount_ ++;
           
           StringHttpOutput o1;
-          HttpOutput o2(o1, false);
+          HttpOutput o2(o1, false /* assume no keep-alive */, 0);
           RestApiOutput o3(o2, HttpMethod_Delete);
           RestApiDeleteCall call(o3, restApi_, RequestOrigin_Documentation, "" /* remote IP */,
                                  "" /* username */, HttpToolbox::Arguments() /* HTTP headers */,
@@ -269,7 +271,7 @@
           try
           {
             ok = (resource.Handle(call) &&
-                  HandleCall(call, uriArgumentsNames));
+                  HandleCall(call, path, uriArgumentsNames));
           }
           catch (OrthancException& e)
           {
@@ -295,7 +297,7 @@
           totalPathsCount_ ++;
           
           StringHttpOutput o1;
-          HttpOutput o2(o1, false);
+          HttpOutput o2(o1, false /* assume no keep-alive */, 0);
           RestApiOutput o3(o2, HttpMethod_Put);
           RestApiPutCall call(o3, restApi_, RequestOrigin_Documentation, "" /* remote IP */,
                               "" /* username */, HttpToolbox::Arguments() /* HTTP headers */,
@@ -307,7 +309,7 @@
           try
           {
             ok = (resource.Handle(call) &&
-                  HandleCall(call, uriArgumentsNames));
+                  HandleCall(call, path, uriArgumentsNames));
           }
           catch (OrthancException& e)
           {
@@ -366,10 +368,9 @@
 
     protected:
       virtual bool HandleCall(RestApiCall& call,
+                              const std::string& path,
                               const std::set<std::string>& uriArgumentsNames) ORTHANC_OVERRIDE
       {
-        const std::string path = Toolbox::FlattenUri(call.GetFullUri());
-
         Json::Value v;
         if (call.GetDocumentation().FormatOpenApi(v, uriArgumentsNames, path))
         {
@@ -684,9 +685,10 @@
 
     protected:
       virtual bool HandleCall(RestApiCall& call,
+                              const std::string& _path,
                               const std::set<std::string>& uriArgumentsNames) ORTHANC_OVERRIDE
       {
-        Path& path = paths_[ Toolbox::FlattenUri(call.GetFullUri()) ];
+        Path& path = paths_[ _path ];
 
         path.AddMethod(call.GetMethod(), call.GetDocumentation().GetTag(), call.GetDocumentation().IsDeprecated());
 
--- a/OrthancFramework/Sources/RestApi/RestApi.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApi.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/RestApi/RestApiCall.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiCall.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/RestApi/RestApiCall.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiCall.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/RestApi/RestApiDeleteCall.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiDeleteCall.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/RestApi/RestApiGetCall.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiGetCall.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/RestApi/RestApiGetCall.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiGetCall.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/RestApi/RestApiHierarchy.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiHierarchy.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/RestApi/RestApiHierarchy.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiHierarchy.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/RestApi/RestApiOutput.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiOutput.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/RestApi/RestApiOutput.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiOutput.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/RestApi/RestApiPath.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiPath.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/RestApi/RestApiPath.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiPath.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/RestApi/RestApiPostCall.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiPostCall.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/RestApi/RestApiPutCall.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiPutCall.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/SQLite/Connection.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/SQLite/Connection.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
  * Medical Physics Department, CHU of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
--- a/OrthancFramework/Sources/SQLite/Connection.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/SQLite/Connection.h	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
  * Medical Physics Department, CHU of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
@@ -46,7 +47,16 @@
 #include <string>
 #include <map>
 
-#define SQLITE_FROM_HERE ::Orthanc::SQLite::StatementId(__FILE__, __LINE__)
+#if !defined(__ORTHANC_FILE__)
+#  if defined(_MSC_VER)
+#    pragma message("Warning: Macro __ORTHANC_FILE__ is not defined, this will leak the full path of the source files in the binaries")
+#  else
+#    warning Warning: Macro __ORTHANC_FILE__ is not defined, this will leak the full path of the source files in the binaries
+#  endif
+#  define __ORTHANC_FILE__ __FILE__
+#endif
+
+#define SQLITE_FROM_HERE ::Orthanc::SQLite::StatementId(__ORTHANC_FILE__, __LINE__)
 
 namespace Orthanc
 {
--- a/OrthancFramework/Sources/SQLite/FunctionContext.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/SQLite/FunctionContext.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
  * Medical Physics Department, CHU of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 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
--- a/OrthancFramework/Sources/SQLite/FunctionContext.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/SQLite/FunctionContext.h	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
  * Medical Physics Department, CHU of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 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
--- a/OrthancFramework/Sources/SQLite/IScalarFunction.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/SQLite/IScalarFunction.h	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
  * Medical Physics Department, CHU of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 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
--- a/OrthancFramework/Sources/SQLite/ITransaction.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/SQLite/ITransaction.h	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
  * Medical Physics Department, CHU of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
--- a/OrthancFramework/Sources/SQLite/NonCopyable.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/SQLite/NonCopyable.h	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
  * Medical Physics Department, CHU of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 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
--- a/OrthancFramework/Sources/SQLite/OrthancSQLiteException.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/SQLite/OrthancSQLiteException.h	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
  * Medical Physics Department, CHU of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
--- a/OrthancFramework/Sources/SQLite/SQLiteTypes.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/SQLite/SQLiteTypes.h	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
  * Medical Physics Department, CHU of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 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
--- a/OrthancFramework/Sources/SQLite/Statement.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/SQLite/Statement.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
  * Medical Physics Department, CHU of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
--- a/OrthancFramework/Sources/SQLite/Statement.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/SQLite/Statement.h	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
  * Medical Physics Department, CHU of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
--- a/OrthancFramework/Sources/SQLite/StatementId.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/SQLite/StatementId.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
  * Medical Physics Department, CHU of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
--- a/OrthancFramework/Sources/SQLite/StatementId.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/SQLite/StatementId.h	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
  * Medical Physics Department, CHU of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
--- a/OrthancFramework/Sources/SQLite/StatementReference.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/SQLite/StatementReference.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
  * Medical Physics Department, CHU of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
--- a/OrthancFramework/Sources/SQLite/StatementReference.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/SQLite/StatementReference.h	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
  * Medical Physics Department, CHU of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
--- a/OrthancFramework/Sources/SQLite/Transaction.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/SQLite/Transaction.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
  * Medical Physics Department, CHU of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
--- a/OrthancFramework/Sources/SQLite/Transaction.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/SQLite/Transaction.h	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
  *
  * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
  * Medical Physics Department, CHU of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
--- a/OrthancFramework/Sources/SerializationToolbox.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/SerializationToolbox.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -72,6 +73,21 @@
   }
 
 
+  std::string SerializationToolbox::ReadString(const Json::Value& value,
+                                               const std::string& field,
+                                               const std::string& defaultValue)
+  {
+    if (value.isMember(field.c_str()))
+    {
+      return ReadString(value, field);
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
   int SerializationToolbox::ReadInteger(const Json::Value& value,
                                         const std::string& field)
   {
@@ -155,31 +171,52 @@
 
   
   void SerializationToolbox::ReadArrayOfStrings(std::vector<std::string>& target,
-                                                const Json::Value& value,
+                                                const Json::Value& valueObject,
                                                 const std::string& field)
   {
-    if (value.type() != Json::objectValue ||
-        !value.isMember(field.c_str()) ||
-        value[field.c_str()].type() != Json::arrayValue)
+    if (valueObject.type() != Json::objectValue ||
+        !valueObject.isMember(field.c_str()) ||
+        valueObject[field.c_str()].type() != Json::arrayValue)
     {
       throw OrthancException(ErrorCode_BadFileFormat,
-                             "List of strings expected in field: " + field);
+                            "List of strings expected in field: " + field);
     }
 
-    const Json::Value& arr = value[field.c_str()];
+    const Json::Value& arr = valueObject[field.c_str()];
 
-    target.resize(arr.size());
+    try
+    {
+      ReadArrayOfStrings(target, arr);
+    }
+    catch (OrthancException& ex)
+    {  // more detailed error
+      throw OrthancException(ErrorCode_BadFileFormat,
+                              "List of strings expected in field: " + field);
+    }
+  }
+
 
-    for (Json::Value::ArrayIndex i = 0; i < arr.size(); i++)
+  void SerializationToolbox::ReadArrayOfStrings(std::vector<std::string>& target,
+                                                const Json::Value& valueArray)
+  {
+    if (valueArray.type() != Json::arrayValue)
     {
-      if (arr[i].type() != Json::stringValue)
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "List of strings expected");
+    }
+
+    target.resize(valueArray.size());
+
+    for (Json::Value::ArrayIndex i = 0; i < valueArray.size(); i++)
+    {
+      if (valueArray[i].type() != Json::stringValue)
       {
         throw OrthancException(ErrorCode_BadFileFormat,
-                               "List of strings expected in field: " + field);
+                               "List of strings expected");
       }
       else
       {
-        target[i] = arr[i].asString();
+        target[i] = valueArray[i].asString();
       }
     }
   }
@@ -201,11 +238,25 @@
   
 
   void SerializationToolbox::ReadSetOfStrings(std::set<std::string>& target,
-                                              const Json::Value& value,
+                                              const Json::Value& valueObject,
                                               const std::string& field)
   {
     std::vector<std::string> tmp;
-    ReadArrayOfStrings(tmp, value, field);
+    ReadArrayOfStrings(tmp, valueObject, field);
+
+    target.clear();
+    for (size_t i = 0; i < tmp.size(); i++)
+    {
+      target.insert(tmp[i]);
+    }
+  }
+
+
+  void SerializationToolbox::ReadSetOfStrings(std::set<std::string>& target,
+                                              const Json::Value& valueArray)
+  {
+    std::vector<std::string> tmp;
+    ReadArrayOfStrings(tmp, valueArray);
 
     target.clear();
     for (size_t i = 0; i < tmp.size(); i++)
@@ -364,24 +415,38 @@
   }
 
 
-  void SerializationToolbox::WriteSetOfStrings(Json::Value& target,
+  void SerializationToolbox::WriteSetOfStrings(Json::Value& targetObject,
                                                const std::set<std::string>& values,
                                                const std::string& field)
   {
-    if (target.type() != Json::objectValue ||
-        target.isMember(field.c_str()))
+    if (targetObject.type() != Json::objectValue ||
+        targetObject.isMember(field.c_str()))
     {
       throw OrthancException(ErrorCode_BadFileFormat);
     }
 
-    Json::Value& value = target[field];
+    Json::Value& targetArray = targetObject[field];
+
+    targetArray = Json::arrayValue;
+
+    WriteSetOfStrings(targetArray, values);
+  }
+
 
-    value = Json::arrayValue;
+  void SerializationToolbox::WriteSetOfStrings(Json::Value& targetArray,
+                                               const std::set<std::string>& values)
+  {
+    if (targetArray.type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    targetArray.clear();
 
     for (std::set<std::string>::const_iterator it = values.begin();
          it != values.end(); ++it)
     {
-      value.append(*it);
+      targetArray.append(*it);
     }
   }
 
--- a/OrthancFramework/Sources/SerializationToolbox.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/SerializationToolbox.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -38,6 +39,10 @@
     static std::string ReadString(const Json::Value& value,
                                   const std::string& field);
 
+    static std::string ReadString(const Json::Value& value,
+                                  const std::string& field,
+                                  const std::string& defaultValue);
+
     static int ReadInteger(const Json::Value& value,
                            const std::string& field);
 
@@ -56,17 +61,23 @@
                             const std::string& field);
 
     static void ReadArrayOfStrings(std::vector<std::string>& target,
-                                   const Json::Value& value,
+                                   const Json::Value& valueObject,
                                    const std::string& field);
 
+    static void ReadArrayOfStrings(std::vector<std::string>& target,
+                                   const Json::Value& valueArray);
+
     static void ReadListOfStrings(std::list<std::string>& target,
                                   const Json::Value& value,
                                   const std::string& field);
 
     static void ReadSetOfStrings(std::set<std::string>& target,
-                                 const Json::Value& value,
+                                 const Json::Value& valueObject,
                                  const std::string& field);
 
+    static void ReadSetOfStrings(std::set<std::string>& target,
+                                 const Json::Value& valueArray);
+
     static void ReadSetOfTags(std::set<DicomTag>& target,
                               const Json::Value& value,
                               const std::string& field);
@@ -87,10 +98,13 @@
                                    const std::list<std::string>& values,
                                    const std::string& field);
 
-    static void WriteSetOfStrings(Json::Value& target,
+    static void WriteSetOfStrings(Json::Value& targetObject,
                                   const std::set<std::string>& values,
                                   const std::string& field);
 
+    static void WriteSetOfStrings(Json::Value& targetArray,
+                                  const std::set<std::string>& values);
+
     static void WriteSetOfTags(Json::Value& target,
                                const std::set<DicomTag>& tags,
                                const std::string& field);
--- a/OrthancFramework/Sources/SharedLibrary.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/SharedLibrary.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/SharedLibrary.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/SharedLibrary.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/StringMemoryBuffer.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/StringMemoryBuffer.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/StringMemoryBuffer.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/StringMemoryBuffer.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/SystemToolbox.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/SystemToolbox.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -520,7 +521,7 @@
   {
     // This is an adapted version of the patch proposed in issue #64
     // without an explicit call to "malloc()" to prevent memory leak
-    // https://bugs.orthanc-server.com/show_bug.cgi?id=64
+    // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=64
     // https://stackoverflow.com/q/31494901/881731
 
     const int mib[4] = { CTL_KERN, KERN_PROC_ARGS, getpid(), KERN_PROC_ARGV };
@@ -725,10 +726,56 @@
     }
   }
 
+  bool SystemToolbox::IsContentCompressible(MimeType mime)
+  {
+    switch (mime)
+    {
+      case MimeType_Css:
+      case MimeType_Html:
+      case MimeType_JavaScript:
+      case MimeType_Json:
+      case MimeType_Pam:
+      case MimeType_Pdf:
+      case MimeType_PlainText:
+      case MimeType_WebAssembly:
+      case MimeType_Xml:
+      case MimeType_PrometheusText:
+      case MimeType_DicomWebJson:
+      case MimeType_DicomWebXml:
+        return true;
+      default: // for all other (JPEG, DICOM, binary, ...)
+        return false;
+    }
+  }
+  
+  bool SystemToolbox::IsContentCompressible(const std::string& contentType)
+  {
+    if (contentType.empty())
+    {
+      return false;
+    }
+
+    if (contentType.find(MIME_JSON) != std::string::npos ||
+        contentType.find(MIME_XML) != std::string::npos ||
+        contentType.find(MIME_DICOM_WEB_JSON) != std::string::npos ||
+        contentType.find(MIME_DICOM_WEB_XML) != std::string::npos ||
+        contentType.find(MIME_PDF) != std::string::npos ||
+        contentType.find(MIME_CSS) != std::string::npos ||
+        contentType.find(MIME_HTML) != std::string::npos ||
+        contentType.find(MIME_JAVASCRIPT) != std::string::npos ||
+        contentType.find(MIME_PLAIN_TEXT) != std::string::npos ||
+        contentType.find(MIME_WEB_ASSEMBLY) != std::string::npos ||
+        contentType.find(MIME_XML_2) != std::string::npos)
+    {
+      return true;
+    }
+
+    return false;
+  }
 
   MimeType SystemToolbox::AutodetectMimeType(const std::string& path)
   {
-    std::string extension = boost::filesystem::extension(path);
+    std::string extension = boost::filesystem::path(path).extension().string();
     Toolbox::ToLowerCase(extension);
 
     // http://en.wikipedia.org/wiki/Mime_types
@@ -825,6 +872,18 @@
     {
       return MimeType_Zip;
     }
+    else if (extension == ".mtl")
+    {
+      return MimeType_Mtl;
+    }
+    else if (extension == ".obj")
+    {
+      return MimeType_Obj;
+    }
+    else if (extension == ".stl")
+    {
+      return MimeType_Stl;
+    }
 
     // Default type
     else
--- a/OrthancFramework/Sources/SystemToolbox.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/SystemToolbox.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -108,6 +109,10 @@
 
     static unsigned int GetHardwareConcurrency();
 
+    static bool IsContentCompressible(MimeType mime);
+
+    static bool IsContentCompressible(const std::string& contentType);
+
     static MimeType AutodetectMimeType(const std::string& path);
 
     static void GetEnvironmentVariables(std::map<std::string, std::string>& env);
--- a/OrthancFramework/Sources/TemporaryFile.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/TemporaryFile.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/TemporaryFile.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/TemporaryFile.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/Toolbox.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Toolbox.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -36,6 +37,10 @@
 #  error Cannot access the version of JsonCpp
 #endif
 
+#if !defined(ORTHANC_ENABLE_ICU)
+#  define ORTHANC_ENABLE_ICU 1
+#endif
+
 
 /**
  * We use deprecated "Json::Reader", "Json::StyledWriter" and
@@ -143,8 +148,14 @@
 
 
 #if defined(ORTHANC_STATIC_ICU)
+
+#  if (ORTHANC_STATIC_ICU == 1) && (ORTHANC_ENABLE_ICU == 1)
+#    if !defined(ORTHANC_FRAMEWORK_INCLUDE_RESOURCES) || (ORTHANC_FRAMEWORK_INCLUDE_RESOURCES == 1)
+#      include <OrthancFrameworkResources.h>
+#    endif
+#  endif
+
 #  if (ORTHANC_STATIC_ICU == 1 && ORTHANC_ENABLE_LOCALE == 1)
-#    include <OrthancFrameworkResources.h>
 #    include <unicode/udata.h>
 #    include <unicode/uloc.h>
 #    include "Compression/GzipCompressor.h"
@@ -414,6 +425,23 @@
     }
   }
 
+  std::string Toolbox::JoinUri(const std::string& base, const std::string& uri)
+  {
+    if (uri.size() > 0 && base.size() > 0)
+    {
+      if (base[base.size() - 1] == '/' && uri[0] == '/')
+      {
+        return base + uri.substr(1, uri.size() - 1);
+      }
+      else if (base[base.size() - 1] != '/' && uri[0] != '/')
+      {
+        return base + "/" + uri;
+      }
+    }
+
+    return base + uri;
+  }
+
 
 #if ORTHANC_ENABLE_MD5 == 1
   static char GetHexadecimalCharacter(uint8_t value)
@@ -469,6 +497,20 @@
       result[2 * i + 1] = GetHexadecimalCharacter(static_cast<uint8_t>(actualHash[i] % 16));
     }
   }
+
+  void Toolbox::ComputeMD5(std::string& result,
+                           const std::set<std::string>& data)
+  {
+    std::string s;
+
+    for (std::set<std::string>::const_iterator it = data.begin(); it != data.end(); ++it)
+    {
+      s += *it;
+    }
+
+    ComputeMD5(result, s);
+  }
+
 #endif
 
 
@@ -609,11 +651,15 @@
                                      bool hasCodeExtensions)
   {
 #if ORTHANC_STATIC_ICU == 1
+#  if ORTHANC_ENABLE_ICU == 0
+    throw OrthancException(ErrorCode_NotImplemented, "ICU is disabled for this target");
+#  else
     if (globalIcuData_.empty())
     {
       throw OrthancException(ErrorCode_BadSequenceOfCalls,
                              "Call Toolbox::InitializeGlobalLocale()");
     }
+#  endif
 #endif
 
     // The "::skip" flag makes boost skip invalid UTF-8
@@ -668,11 +714,15 @@
                                        Encoding targetEncoding)
   {
 #if ORTHANC_STATIC_ICU == 1
+#  if ORTHANC_ENABLE_ICU == 0
+    throw OrthancException(ErrorCode_NotImplemented, "ICU is disabled for this target");
+#  else
     if (globalIcuData_.empty())
     {
       throw OrthancException(ErrorCode_BadSequenceOfCalls,
                              "Call Toolbox::InitializeGlobalLocale()");
     }
+#  endif
 #endif
 
     // The "::skip" flag makes boost skip invalid UTF-8
@@ -751,7 +801,6 @@
     return result;
   }
 
-
   void Toolbox::ComputeSHA1(std::string& result,
                             const void* data,
                             size_t size)
@@ -764,11 +813,13 @@
     }
 
     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(unsigned int) == 4 && sizeof(digest) == (160 / 8));
+    assert(sizeof(boost::uuids::detail::sha1::digest_type) == 20);
     
-    sha1.get_digest(digest);
+    // 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(*(reinterpret_cast<boost::uuids::detail::sha1::digest_type*>(digest)));
 
     result.resize(8 * 5 + 4);
     sprintf(&result[0], "%08x-%08x-%08x-%08x-%08x",
@@ -1000,10 +1051,10 @@
     return result;
   }
 
-
-  void Toolbox::TokenizeString(std::vector<std::string>& result,
+  static void TokenizeStringInternal(std::vector<std::string>& result,
                                const std::string& value,
-                               char separator)
+                               char separator,
+                               bool includeEmptyStrings)
   {
     size_t countSeparators = 0;
     
@@ -1033,20 +1084,54 @@
       }
     }
 
-    result.push_back(currentItem);
+    if (includeEmptyStrings || !currentItem.empty())
+    {
+      result.push_back(currentItem);
+    }
+  }
+
+
+  void Toolbox::TokenizeString(std::vector<std::string>& result,
+                               const std::string& value,
+                               char separator)
+  {
+    TokenizeStringInternal(result, value, separator, true);
+  }
+
+
+  void Toolbox::SplitString(std::set<std::string>& result,
+                            const std::string& value,
+                            char separator)
+  {
+    result.clear();
+
+    std::vector<std::string> temp;
+    TokenizeStringInternal(temp, value, separator, false);
+    for (size_t i = 0; i < temp.size(); ++i)
+    {
+      result.insert(temp[i]);
+    }
+  }
+
+
+  void Toolbox::SplitString(std::vector<std::string>& result,
+                            const std::string& value,
+                            char separator)
+  {
+    TokenizeStringInternal(result, value, separator, false);
   }
 
 
   void Toolbox::JoinStrings(std::string& result,
-                            std::set<std::string>& source,
+                            const std::set<std::string>& source,
                             const char* separator)
   {
     result = boost::algorithm::join(source, separator);
   }
 
-  void JoinStrings(std::string& result,
-                   std::vector<std::string>& source,
-                   const char* separator)
+  void Toolbox::JoinStrings(std::string& result,
+                            const std::vector<std::string>& source,
+                            const char* separator)
   {
     result = boost::algorithm::join(source, separator);
   }
@@ -1508,7 +1593,7 @@
   
   static void InitializeIcu()
   {
-#if ORTHANC_STATIC_ICU == 1
+#if (ORTHANC_STATIC_ICU == 1) && (ORTHANC_ENABLE_ICU == 1)
     if (globalIcuData_.empty())
     {
       LOG(INFO) << "Setting up the ICU common data";
@@ -1676,10 +1761,14 @@
     bool error = (globalLocale_.get() == NULL);
 
 #if ORTHANC_STATIC_ICU == 1
+#  if ORTHANC_ENABLE_ICU == 0
+    throw OrthancException(ErrorCode_NotImplemented, "ICU is disabled for this target");
+#  else
     if (globalIcuData_.empty())
     {
       error = true;
     }
+#  endif
 #endif
     
     if (error)
@@ -1797,7 +1886,7 @@
 
   std::string Toolbox::GenerateUuid()
   {
-#ifdef WIN32
+#ifdef _WIN32
     UUID uuid;
     UuidCreate ( &uuid );
 
@@ -2426,6 +2515,302 @@
       value = value.substr(1, value.size() - 2);
     }
   }
+
+  Toolbox::ElapsedTimer::ElapsedTimer()
+  {
+    Restart();
+  }
+
+  void Toolbox::ElapsedTimer::Restart()
+  {
+    start_ = boost::posix_time::microsec_clock::universal_time();
+  }
+
+  uint64_t Toolbox::ElapsedTimer::GetElapsedMilliseconds()
+  {
+    return GetElapsedNanoseconds() / 1000000;
+  }
+  
+  uint64_t Toolbox::ElapsedTimer::GetElapsedMicroseconds()
+  {
+    return GetElapsedNanoseconds() / 1000;
+  }
+
+  uint64_t Toolbox::ElapsedTimer::GetElapsedNanoseconds()
+  {
+    boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time();
+    boost::posix_time::time_duration diff = now - start_;
+    return static_cast<uint64_t>(diff.total_nanoseconds());
+  }
+
+  std::string Toolbox::ElapsedTimer::GetHumanElapsedDuration()
+  {
+    return Toolbox::GetHumanDuration(GetElapsedNanoseconds());
+  }
+
+  // in "full" mode, returns " 26.45MB in 2.25s = 94.04Mbps"
+  // else, returns "94.04Mbps"
+  std::string Toolbox::ElapsedTimer::GetHumanTransferSpeed(bool full, uint64_t sizeInBytes)
+  {
+    return Toolbox::GetHumanTransferSpeed(full, sizeInBytes, GetElapsedNanoseconds());
+  }
+
+  Toolbox::ElapsedTimeLogger::ElapsedTimeLogger(const std::string& message)
+  : message_(message),
+    logged_(false)
+  {
+    Restart();
+  }
+
+  Toolbox::ElapsedTimeLogger::~ElapsedTimeLogger()
+  {
+    if (!logged_)
+    {
+      StopAndLog();
+    }
+  }
+
+  void Toolbox::ElapsedTimeLogger::Restart()
+  {
+    timer_.Restart();
+  }
+
+  void Toolbox::ElapsedTimeLogger::StopAndLog()
+  {
+    LOG(WARNING) << "ELAPSED TIMER: " << message_ << " (" << timer_.GetElapsedMicroseconds() << " us)";
+    logged_ = true;
+  }
+
+  std::string Toolbox::GetHumanFileSize(uint64_t sizeInBytes)
+  {
+    if (sizeInBytes < 1024)
+    {
+      std::ostringstream oss;
+      oss << sizeInBytes << "bytes";
+      return oss.str();
+    }
+    else
+    {
+      static const char* suffixes[] = {"KB", "MB", "GB", "TB"};
+      static const int suffixesCount = sizeof(suffixes) / sizeof(suffixes[0]);
+
+      int i = 0;
+      double size = static_cast<double>(sizeInBytes)/1024.0;
+
+      while (size >= 1024.0 && i < suffixesCount - 1) 
+      {
+        size /= 1024.0;
+        i++;
+      }
+
+      std::ostringstream oss;
+      oss << std::fixed << std::setprecision(2) << size << suffixes[i];
+      return oss.str();
+    }
+  }
+
+  std::string Toolbox::GetHumanDuration(uint64_t durationInNanoseconds)
+  {
+    if (durationInNanoseconds < 1024)
+    {
+      std::ostringstream oss;
+      oss << durationInNanoseconds << "ns";
+      return oss.str();
+    }
+    else
+    {
+      static const char* suffixes[] = {"ns", "us", "ms", "s"};
+      static const int suffixesCount = sizeof(suffixes) / sizeof(suffixes[0]);
+
+      int i = 0;
+      double duration = static_cast<double>(durationInNanoseconds);
+
+      while (duration >= 1000.0 && i < suffixesCount - 1) 
+      {
+        duration /= 1000.0;
+        i++;
+      }
+
+      std::ostringstream oss;
+      oss << std::fixed << std::setprecision(2) << duration <<  suffixes[i];
+      return oss.str();
+    }
+  }
+
+  std::string Toolbox::GetHumanTransferSpeed(bool full, uint64_t sizeInBytes, uint64_t durationInNanoseconds)
+  {
+    // in "full" mode, returns " 26.45MB in 2.25s = 94.04Mbps"    
+    // else, return "94.04Mbps"
+
+    if (full)
+    {
+      std::ostringstream oss;
+      oss << Toolbox::GetHumanFileSize(sizeInBytes) << " in " << Toolbox::GetHumanDuration(durationInNanoseconds) << " = " << GetHumanTransferSpeed(false, sizeInBytes, durationInNanoseconds);
+      return oss.str();
+    }
+
+    double throughputInBps = 8.0 * 1000000000.0 * static_cast<double>(sizeInBytes) / static_cast<double>(durationInNanoseconds);
+
+    if (throughputInBps < 1000.0)
+    {
+      std::ostringstream oss;
+      oss << throughputInBps << "bps";
+      return oss.str();
+    }
+    else
+    {
+      throughputInBps /= 1000.0;
+      static const char* suffixes[] = {"kbps", "Mbps", "Gbps"};
+      static const int suffixesCount = sizeof(suffixes) / sizeof(suffixes[0]);
+
+      int i = 0;
+
+      while (throughputInBps >= 1000.0 && i < suffixesCount - 1) 
+      {
+        throughputInBps /= 1000.0;
+        i++;
+      }
+
+      std::ostringstream oss;
+      oss << std::fixed << std::setprecision(2) << throughputInBps <<  suffixes[i];
+      return oss.str();
+    }
+  }
+
+
+  bool Toolbox::ParseVersion(unsigned int& major,
+                             unsigned int& minor,
+                             unsigned int& revision,
+                             const char* version)
+  {
+    if (version == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+#ifdef _MSC_VER
+#define ORTHANC_SCANF sscanf_s
+#else
+#define ORTHANC_SCANF sscanf
+#endif
+
+    int a, b, c;
+    if (ORTHANC_SCANF(version, "%4d.%4d.%4d", &a, &b, &c) == 3)
+    {
+      if (a >= 0 &&
+          b >= 0 &&
+          c >= 0)
+      {
+        major = static_cast<unsigned int>(a);
+        minor = static_cast<unsigned int>(b);
+        revision = static_cast<unsigned int>(c);
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+    else if (ORTHANC_SCANF(version, "%4d.%4d", &a, &b) == 2)
+    {
+      if (a >= 0 &&
+          b >= 0)
+      {
+        major = static_cast<unsigned int>(a);
+        minor = static_cast<unsigned int>(b);
+        revision = 0;
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+    else if (ORTHANC_SCANF(version, "%4d", &a) == 1 &&
+             a >= 0)
+    {
+      if (a >= 0)
+      {
+        major = static_cast<unsigned int>(a);
+        minor = 0;
+        revision = 0;
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  bool Toolbox::IsVersionAbove(const char* version,
+                               unsigned int major,
+                               unsigned int minor,
+                               unsigned int revision)
+  {
+    /**
+     * Note: Similar standalone functions are implemented in
+     * "OrthancCPlugin.h" and "OrthancPluginCppWrapper.cpp".
+     **/
+
+    unsigned int actualMajor, actualMinor, actualRevision;
+
+    if (version == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else if (!strcmp(version, "mainline"))
+    {
+      // Assume compatibility with the mainline
+      return true;
+    }
+    else if (ParseVersion(actualMajor, actualMinor, actualRevision, version))
+    {
+      if (actualMajor > major)
+      {
+        return true;
+      }
+
+      if (actualMajor < major)
+      {
+        return false;
+      }
+
+      // Check the minor version number
+      assert(actualMajor == major);
+
+      if (actualMinor > minor)
+      {
+        return true;
+      }
+
+      if (actualMinor < minor)
+      {
+        return false;
+      }
+
+      // Check the patch level version number
+      assert(actualMajor == major);
+
+      if (actualRevision >= revision)
+      {
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange, "Not a valid version: " + std::string(version));
+    }
+  }
 }
 
 
--- a/OrthancFramework/Sources/Toolbox.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/Toolbox.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -67,6 +68,8 @@
 #  include <pugixml.hpp>
 #endif
 
+#include <boost/date_time/posix_time/posix_time.hpp>
+
 
 namespace Orthanc
 {
@@ -119,6 +122,8 @@
     static std::string FlattenUri(const UriComponents& components,
                                   size_t fromLevel = 0);
 
+    static std::string JoinUri(const std::string& base, const std::string& uri);
+    
 #if ORTHANC_ENABLE_MD5 == 1
     static void ComputeMD5(std::string& result,
                            const std::string& data);
@@ -126,6 +131,9 @@
     static void ComputeMD5(std::string& result,
                            const void* data,
                            size_t size);
+
+    static void ComputeMD5(std::string& result,
+                           const std::set<std::string>& data);
 #endif
 
     static void ComputeSHA1(std::string& result,
@@ -181,16 +189,27 @@
 
     static std::string WildcardToRegularExpression(const std::string& s);
 
+    // TokenizeString result might contain empty strings (not SplitString)
     static void TokenizeString(std::vector<std::string>& result,
                                const std::string& source,
                                char separator);
 
+    // SplitString result won't contain empty strings (compared to TokenizeString)
+    static void SplitString(std::vector<std::string>& result,
+                            const std::string& source,
+                            char separator);
+
+    // SplitString result won't contain empty strings (compared to TokenizeString)
+    static void SplitString(std::set<std::string>& result,
+                            const std::string& source,
+                            char separator);
+
     static void JoinStrings(std::string& result,
-                            std::set<std::string>& source,
+                            const std::set<std::string>& source,
                             const char* separator);
 
     static void JoinStrings(std::string& result,
-                            std::vector<std::string>& source,
+                            const std::vector<std::string>& source,
                             const char* separator);
 
     // returns true if all element of 'needles' are found in 'haystack'
@@ -243,6 +262,22 @@
       }
     }
 
+    // returns true if all element of 'needles' are found in 'haystack'
+    template <typename T> static void GetIntersection(std::set<T>& target, const std::set<T>& a, const std::set<T>& b)
+    {
+      target.clear();
+
+      for (typename std::set<T>::const_iterator it = a.begin();
+            it != a.end(); ++it)
+      {
+        if (b.count(*it) > 0)
+        {
+          target.insert(*it);
+        }
+      }
+    }
+
+
 #if ORTHANC_ENABLE_PUGIXML == 1
     static void JsonToXml(std::string& target,
                           const Json::Value& source,
@@ -341,6 +376,56 @@
                                 const Json::Value& source);
 
     static void RemoveSurroundingQuotes(std::string& value);
+
+    class ORTHANC_PUBLIC ElapsedTimer
+    {
+      boost::posix_time::ptime  start_;
+    public:
+      explicit ElapsedTimer();
+
+      uint64_t GetElapsedMilliseconds();
+      uint64_t GetElapsedMicroseconds();
+      uint64_t GetElapsedNanoseconds();
+      
+      std::string GetHumanElapsedDuration();
+      std::string GetHumanTransferSpeed(bool full, uint64_t sizeInBytes);
+      
+      void Restart();
+    };
+
+    // This is a helper class to measure and log time spend e.g in a method.
+    // This should be used only during debugging and should likely not ever used in a release.
+    // By default, you should use it as a RAII but you may force Restart/StopAndLog manually if needed.
+    class ORTHANC_PUBLIC ElapsedTimeLogger
+    {
+    private:
+      ElapsedTimer      timer_;
+      const std::string message_;
+      bool logged_;
+
+    public:
+      explicit ElapsedTimeLogger(const std::string& message);
+      ~ElapsedTimeLogger();  
+
+      void Restart();
+      void StopAndLog();
+    };
+
+    static std::string GetHumanFileSize(uint64_t sizeInBytes);
+
+    static std::string GetHumanDuration(uint64_t durationInNanoseconds);
+
+    static std::string GetHumanTransferSpeed(bool full, uint64_t sizeInBytes, uint64_t durationInNanoseconds);
+
+    static bool ParseVersion(unsigned int& major,
+                             unsigned int& minor,
+                             unsigned int& revision,
+                             const char* version);
+
+    static bool IsVersionAbove(const char* version,
+                               unsigned int major,
+                               unsigned int minor,
+                               unsigned int revision);
   };
 }
 
--- a/OrthancFramework/Sources/WebServiceParameters.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/WebServiceParameters.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/Sources/WebServiceParameters.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/Sources/WebServiceParameters.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/UnitTestsSources/BitbucketCACertificates.h	Thu Sep 15 18:13:17 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,26 +0,0 @@
-#define BITBUCKET_CERTIFICATES  \
-"-----BEGIN CERTIFICATE-----\n"  \
-"MIIEFzCCAv+gAwIBAgIQB/LzXIeod6967+lHmTUlvTANBgkqhkiG9w0BAQwFADBh\n"  \
-"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n"  \
-"d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD\n"  \
-"QTAeFw0yMTA0MTQwMDAwMDBaFw0zMTA0MTMyMzU5NTlaMFYxCzAJBgNVBAYTAlVT\n"  \
-"MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxMDAuBgNVBAMTJ0RpZ2lDZXJ0IFRMUyBI\n"  \
-"eWJyaWQgRUNDIFNIQTM4NCAyMDIwIENBMTB2MBAGByqGSM49AgEGBSuBBAAiA2IA\n"  \
-"BMEbxppbmNmkKaDp1AS12+umsmxVwP/tmMZJLwYnUcu/cMEFesOxnYeJuq20ExfJ\n"  \
-"qLSDyLiQ0cx0NTY8g3KwtdD3ImnI8YDEe0CPz2iHJlw5ifFNkU3aiYvkA8ND5b8v\n"  \
-"c6OCAYIwggF+MBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFAq8CCkXjKU5\n"  \
-"bXoOzjPHLrPt+8N6MB8GA1UdIwQYMBaAFAPeUDVW0Uy7ZvCj4hsbw5eyPdFVMA4G\n"  \
-"A1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwdgYI\n"  \
-"KwYBBQUHAQEEajBoMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j\n"  \
-"b20wQAYIKwYBBQUHMAKGNGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdp\n"  \
-"Q2VydEdsb2JhbFJvb3RDQS5jcnQwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2Ny\n"  \
-"bDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdENBLmNybDA9BgNVHSAE\n"  \
-"NjA0MAsGCWCGSAGG/WwCATAHBgVngQwBATAIBgZngQwBAgEwCAYGZ4EMAQICMAgG\n"  \
-"BmeBDAECAzANBgkqhkiG9w0BAQwFAAOCAQEAR1mBf9QbH7Bx9phdGLqYR5iwfnYr\n"  \
-"6v8ai6wms0KNMeZK6BnQ79oU59cUkqGS8qcuLa/7Hfb7U7CKP/zYFgrpsC62pQsY\n"  \
-"kDUmotr2qLcy/JUjS8ZFucTP5Hzu5sn4kL1y45nDHQsFfGqXbbKrAjbYwrwsAZI/\n"  \
-"BKOLdRHHuSm8EdCGupK8JvllyDfNJvaGEwwEqonleLHBTnm8dqMLUeTF0J5q/hos\n"  \
-"Vq4GNiejcxwIfZMy0MJEGdqN9A57HSgDKwmKdsp33Id6rHtSJlWncg+d0ohP/rEh\n"  \
-"xRqhqjn1VtvChMQ1H3Dau0bwhr9kAMQ+959GG50jBbl9s08PqUU643QwmA==\n"  \
-"-----END CERTIFICATE-----\n"  \
-"\n" 
--- a/OrthancFramework/UnitTestsSources/CMakeLists.txt	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/UnitTestsSources/CMakeLists.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -109,6 +110,8 @@
   ${GOOGLE_TEST_SOURCES}
   )
 
+DefineSourceBasenameForTarget(UnitTests)
+
 target_link_libraries(UnitTests ${ORTHANC_FRAMEWORK_LIBRARIES})
 
 install(TARGETS UnitTests
--- a/OrthancFramework/UnitTestsSources/DicomMapTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/UnitTestsSources/DicomMapTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -42,6 +43,7 @@
 #include "../Sources/DicomParsing/DicomWebJsonVisitor.h"
 
 #include <boost/lexical_cast.hpp>
+#include <boost/tuple/tuple.hpp>
 
 using namespace Orthanc;
 
@@ -81,7 +83,8 @@
     ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_SOP_INSTANCE_UID));
 
     {
-      const std::set<DicomTag>& s = DicomMap::GetAllMainDicomTags();
+      std::set<DicomTag> s;
+      DicomMap::GetAllMainDicomTags(s);
       ASSERT_TRUE(s.end() != s.find(DICOM_TAG_PATIENT_ID));
       ASSERT_TRUE(s.end() != s.find(DICOM_TAG_STUDY_INSTANCE_UID));
       ASSERT_TRUE(s.end() != s.find(DICOM_TAG_ACCESSION_NUMBER));
@@ -90,26 +93,30 @@
     }
 
     {
-      const std::set<DicomTag>& s = DicomMap::GetMainDicomTags(ResourceType_Patient);
+      std::set<DicomTag> s;
+      DicomMap::GetMainDicomTags(s, ResourceType_Patient);
       ASSERT_TRUE(s.end() != s.find(DICOM_TAG_PATIENT_ID));
       ASSERT_TRUE(s.end() == s.find(DICOM_TAG_STUDY_INSTANCE_UID));
     }
 
     {
-      const std::set<DicomTag>& s = DicomMap::GetMainDicomTags(ResourceType_Study);
+      std::set<DicomTag> s;
+      DicomMap::GetMainDicomTags(s, ResourceType_Study);
       ASSERT_TRUE(s.end() != s.find(DICOM_TAG_STUDY_INSTANCE_UID));
       ASSERT_TRUE(s.end() != s.find(DICOM_TAG_ACCESSION_NUMBER));
       ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID));
     }
 
     {
-      const std::set<DicomTag>& s = DicomMap::GetMainDicomTags(ResourceType_Series);
+      std::set<DicomTag> s;
+      DicomMap::GetMainDicomTags(s, ResourceType_Series);
       ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SERIES_INSTANCE_UID));
       ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID));
     }
 
     {
-      const std::set<DicomTag>& s = DicomMap::GetMainDicomTags(ResourceType_Instance);
+      std::set<DicomTag> s;
+      DicomMap::GetMainDicomTags(s, ResourceType_Instance);
       ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID));
       ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID));
     }
@@ -120,12 +127,14 @@
     DicomMap::AddMainDicomTag(DICOM_TAG_BITS_ALLOCATED, ResourceType_Instance);
 
     {
-      const std::set<DicomTag>& s = DicomMap::GetMainDicomTags(ResourceType_Instance);
+      std::set<DicomTag> s;
+      DicomMap::GetMainDicomTags(s, ResourceType_Instance);
       ASSERT_TRUE(s.end() != s.find(DICOM_TAG_BITS_ALLOCATED));
       ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID));
     }
     {
-      const std::set<DicomTag>& s = DicomMap::GetMainDicomTags(ResourceType_Series);
+      std::set<DicomTag> s;
+      DicomMap::GetMainDicomTags(s, ResourceType_Series);
       ASSERT_TRUE(s.end() == s.find(DICOM_TAG_BITS_ALLOCATED));
     }
 
@@ -242,8 +251,10 @@
   // REFERENCE: DICOM PS3.3 2015c - Information Object Definitions
   // http://dicom.nema.org/medical/dicom/current/output/html/part03.html
 
+  std::set<DicomTag> main;
+  DicomMap::GetMainDicomTags(main, level);
+
   std::set<DicomTag> moduleTags;
-  const std::set<DicomTag>& main = DicomMap::GetMainDicomTags(level);
   DicomTag::AddTagsForModule(moduleTags, module);
   
   // The main dicom tags are a subset of the module
@@ -707,7 +718,7 @@
     DicomMap sequencesOnly;
     m.ExtractSequences(sequencesOnly);
 
-    ASSERT_EQ(1, sequencesOnly.GetSize());
+    ASSERT_EQ(1u, sequencesOnly.GetSize());
     ASSERT_TRUE(sequencesOnly.HasTag(0x0008, 0x1111));
     ASSERT_TRUE(sequencesOnly.GetValue(0x0008, 0x1111).GetSequenceContent()[0].isMember("0008,1150"));
 
@@ -715,7 +726,7 @@
     DicomMap sequencesCopy;
     sequencesCopy.SetValue(0x0008, 0x1111, sequencesOnly.GetValue(0x0008, 0x1111));
 
-    ASSERT_EQ(1, sequencesCopy.GetSize());
+    ASSERT_EQ(1u, sequencesCopy.GetSize());
     ASSERT_TRUE(sequencesCopy.HasTag(0x0008, 0x1111));
     ASSERT_TRUE(sequencesCopy.GetValue(0x0008, 0x1111).GetSequenceContent()[0].isMember("0008,1150"));
   }
@@ -918,7 +929,8 @@
   {
     ResourceType level = static_cast<ResourceType>(i);
 
-    const std::set<DicomTag>& tags = DicomMap::GetMainDicomTags(level);
+    std::set<DicomTag> tags;
+    DicomMap::GetMainDicomTags(tags, level);
 
     for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
     {
@@ -1056,6 +1068,113 @@
 }
 
 
+TEST(DicomMap, SetupFindTemplates)
+{
+  /**
+   * The templates for C-FIND must be common to all the Orthanc
+   * servers, and must not be altered by the "ExtraMainDicomTags"
+   * configuration option that was introduced in Orthanc 1.11.0.
+   **/
+  
+  {
+    DicomMap m;
+    m.SetValue(DICOM_TAG_ENCAPSULATED_DOCUMENT, "nope", false);
+    m.SetValue(DICOM_TAG_PATIENT_ID, "patient_id", false);
+    
+    DicomMap::SetupFindPatientTemplate(m);
+    std::set<DicomTag> tags;
+    m.GetTags(tags);
+
+    // This corresponds to the values of DEFAULT_PATIENT_MAIN_DICOM_TAGS
+    ASSERT_EQ(5u, tags.size());
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_PATIENT_ID, "nope", false));
+
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_OTHER_PATIENT_IDS, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_PATIENT_BIRTH_DATE, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_PATIENT_NAME, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_PATIENT_SEX, "nope", false));
+  }
+  
+  {
+    DicomMap m;
+    m.SetValue(DICOM_TAG_ENCAPSULATED_DOCUMENT, "nope", false);
+    m.SetValue(DICOM_TAG_PATIENT_ID, "patient_id", false);
+    
+    DicomMap::SetupFindStudyTemplate(m);
+    std::set<DicomTag> tags;
+    m.GetTags(tags);
+
+    // This corresponds to the values of DEFAULT_STUDY_MAIN_DICOM_TAGS
+    ASSERT_EQ(8u, tags.size());
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_PATIENT_ID, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_ACCESSION_NUMBER, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_STUDY_INSTANCE_UID, "nope", false));
+
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_REFERRING_PHYSICIAN_NAME, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_STUDY_DATE, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_STUDY_DESCRIPTION, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_STUDY_ID, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_STUDY_TIME, "nope", false));
+  }
+  
+  {
+    DicomMap m;
+    m.SetValue(DICOM_TAG_ENCAPSULATED_DOCUMENT, "nope", false);
+    m.SetValue(DICOM_TAG_PATIENT_ID, "patient_id", false);
+    
+    DicomMap::SetupFindSeriesTemplate(m);
+    std::set<DicomTag> tags;
+    m.GetTags(tags);
+
+    // This corresponds to the values of DEFAULT_SERIES_MAIN_DICOM_TAGS
+    ASSERT_EQ(13u, tags.size());
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_PATIENT_ID, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_ACCESSION_NUMBER, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_STUDY_INSTANCE_UID, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_SERIES_INSTANCE_UID, "nope", false));
+
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_BODY_PART_EXAMINED, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_MODALITY, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_OPERATOR_NAME, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_PERFORMED_PROCEDURE_STEP_DESCRIPTION, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_PROTOCOL_NAME, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_SERIES_DATE, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_SERIES_DESCRIPTION, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_SERIES_NUMBER, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_SERIES_TIME, "nope", false));
+  }
+  
+  {
+    DicomMap m;
+    m.SetValue(DICOM_TAG_ENCAPSULATED_DOCUMENT, "nope", false);
+    m.SetValue(DICOM_TAG_PATIENT_ID, "patient_id", false);
+    
+    DicomMap::SetupFindInstanceTemplate(m);
+    std::set<DicomTag> tags;
+    m.GetTags(tags);
+
+    // This corresponds to the values of DEFAULT_INSTANCE_MAIN_DICOM_TAGS
+    ASSERT_EQ(15u, tags.size());
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_PATIENT_ID, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_ACCESSION_NUMBER, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_STUDY_INSTANCE_UID, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_SERIES_INSTANCE_UID, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_SOP_INSTANCE_UID, "nope", false));
+
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_ACQUISITION_NUMBER, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_IMAGE_COMMENTS, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_IMAGE_INDEX, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_IMAGE_ORIENTATION_PATIENT, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_IMAGE_POSITION_PATIENT, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_INSTANCE_CREATION_DATE, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_INSTANCE_CREATION_TIME, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_INSTANCE_NUMBER, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_NUMBER_OF_FRAMES, "nope", false));
+    ASSERT_EQ("", m.GetStringValue(DICOM_TAG_TEMPORAL_POSITION_IDENTIFIER, "nope", false));
+  }
+}
+
+
 #if ORTHANC_SANDBOXED != 1
 
 #include "../Sources/SystemToolbox.h"
@@ -1171,50 +1290,61 @@
 {
   static const std::string PATH = "/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/";
 
-  typedef std::list< std::pair<std::string, uint64_t> >  Sources;
+  typedef boost::tuple<std::string, uint64_t, ValueRepresentation> Source;
+  typedef std::list<Source>  Sources;
+
+  // $ ~/Subversion/orthanc-tests/Tests/GetPixelDataVR.py ~/Subversion/orthanc-tests/Database/ColorTestMalaterre.dcm ~/Subversion/orthanc-tests/Database/ColorTestImageJ.dcm ~/Subversion/orthanc-tests/Database/Knee/T1/IM-0001-0001.dcm ~/Subversion/orthanc-tests/Database/TransferSyntaxes/*.dcm
 
   Sources sources;
-  sources.push_back(std::make_pair(PATH + "../ColorTestMalaterre.dcm", 0x03a0u));
-  sources.push_back(std::make_pair(PATH + "1.2.840.10008.1.2.1.dcm", 0x037cu));
-  sources.push_back(std::make_pair(PATH + "1.2.840.10008.1.2.2.dcm", 0x03e8u));  // Big Endian
-  sources.push_back(std::make_pair(PATH + "1.2.840.10008.1.2.4.50.dcm", 0x04acu));
-  sources.push_back(std::make_pair(PATH + "1.2.840.10008.1.2.4.51.dcm", 0x072cu));
-  sources.push_back(std::make_pair(PATH + "1.2.840.10008.1.2.4.57.dcm", 0x0620u));
-  sources.push_back(std::make_pair(PATH + "1.2.840.10008.1.2.4.70.dcm", 0x065au));
-  sources.push_back(std::make_pair(PATH + "1.2.840.10008.1.2.4.80.dcm", 0x0b46u));
-  sources.push_back(std::make_pair(PATH + "1.2.840.10008.1.2.4.81.dcm", 0x073eu));
-  sources.push_back(std::make_pair(PATH + "1.2.840.10008.1.2.4.90.dcm", 0x0b66u));
-  sources.push_back(std::make_pair(PATH + "1.2.840.10008.1.2.4.91.dcm", 0x19b8u));
-  sources.push_back(std::make_pair(PATH + "1.2.840.10008.1.2.5.dcm", 0x0b0au));
+  sources.push_back(Source(PATH + "../ColorTestMalaterre.dcm",   0x03a0u, ValueRepresentation_Unknown));  // This file has strange VR
+  sources.push_back(Source(PATH + "../ColorTestImageJ.dcm",      0x00924, ValueRepresentation_OtherByte));
+  sources.push_back(Source(PATH + "../Knee/T1/IM-0001-0001.dcm", 0x00c78, ValueRepresentation_OtherWord));
+  sources.push_back(Source(PATH + "1.2.840.10008.1.2.1.dcm",     0x037cu, ValueRepresentation_OtherByte));
+  sources.push_back(Source(PATH + "1.2.840.10008.1.2.2.dcm",     0x03e8u, ValueRepresentation_OtherByte));  // Big Endian
+  sources.push_back(Source(PATH + "1.2.840.10008.1.2.4.50.dcm",  0x04acu, ValueRepresentation_OtherByte));
+  sources.push_back(Source(PATH + "1.2.840.10008.1.2.4.51.dcm",  0x072cu, ValueRepresentation_OtherByte));
+  sources.push_back(Source(PATH + "1.2.840.10008.1.2.4.57.dcm",  0x0620u, ValueRepresentation_OtherByte));
+  sources.push_back(Source(PATH + "1.2.840.10008.1.2.4.70.dcm",  0x065au, ValueRepresentation_OtherByte));
+  sources.push_back(Source(PATH + "1.2.840.10008.1.2.4.80.dcm",  0x0b46u, ValueRepresentation_OtherByte));
+  sources.push_back(Source(PATH + "1.2.840.10008.1.2.4.81.dcm",  0x073eu, ValueRepresentation_OtherByte));
+  sources.push_back(Source(PATH + "1.2.840.10008.1.2.4.90.dcm",  0x0b66u, ValueRepresentation_OtherByte));
+  sources.push_back(Source(PATH + "1.2.840.10008.1.2.4.91.dcm",  0x19b8u, ValueRepresentation_OtherByte));
+  sources.push_back(Source(PATH + "1.2.840.10008.1.2.5.dcm",     0x0b0au, ValueRepresentation_OtherByte));
 
   {
     std::string dicom;
 
     uint64_t offset;
+    ValueRepresentation vr;
+
     // Not a DICOM image
     SystemToolbox::ReadFile(dicom, PATH + "1.2.840.10008.1.2.4.50.png", false);
-    ASSERT_FALSE(DicomStreamReader::LookupPixelDataOffset(offset, dicom));
+    ASSERT_FALSE(DicomStreamReader::LookupPixelDataOffset(offset, vr, dicom));
 
     // Image without valid DICOM preamble
     SystemToolbox::ReadFile(dicom, PATH + "1.2.840.10008.1.2.dcm", false);
-    ASSERT_FALSE(DicomStreamReader::LookupPixelDataOffset(offset, dicom));
+    ASSERT_FALSE(DicomStreamReader::LookupPixelDataOffset(offset, vr, dicom));
   }
   
   for (Sources::const_iterator it = sources.begin(); it != sources.end(); ++it)
   {
     std::string dicom;
-    SystemToolbox::ReadFile(dicom, it->first, false);
+    SystemToolbox::ReadFile(dicom, it->get<0>(), false);
 
     {
       uint64_t offset;
-      ASSERT_TRUE(DicomStreamReader::LookupPixelDataOffset(offset, dicom));
-      ASSERT_EQ(it->second, offset);
+      ValueRepresentation vr;
+      ASSERT_TRUE(DicomStreamReader::LookupPixelDataOffset(offset, vr, dicom));
+      ASSERT_EQ(it->get<1>(), offset);
+      ASSERT_EQ(it->get<2>(), vr);
     }
     
     {
       uint64_t offset;
-      ASSERT_TRUE(DicomStreamReader::LookupPixelDataOffset(offset, dicom.c_str(), dicom.size()));
-      ASSERT_EQ(it->second, offset);
+      ValueRepresentation vr;
+      ASSERT_TRUE(DicomStreamReader::LookupPixelDataOffset(offset, vr, dicom.c_str(), dicom.size()));
+      ASSERT_EQ(it->get<1>(), offset);
+      ASSERT_EQ(it->get<2>(), vr);
     }
     
     ParsedDicomFile a(dicom);
@@ -1238,7 +1368,7 @@
 
     r.Consume(visitor);
 
-    ASSERT_EQ(it->second, visitor.GetPixelDataOffset());
+    ASSERT_EQ(it->get<1>(), visitor.GetPixelDataOffset());
 
     // Truncate the original DICOM up to pixel data
     dicom.resize(visitor.GetPixelDataOffset());
--- a/OrthancFramework/UnitTestsSources/FileStorageTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -36,6 +37,7 @@
 #include "../Sources/Logging.h"
 #include "../Sources/OrthancException.h"
 #include "../Sources/Toolbox.h"
+#include "../Sources/SystemToolbox.h"
 
 #include <ctype.h>
 
@@ -87,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");
@@ -127,7 +171,7 @@
 {
   FilesystemStorage s("UnitTestsStorage");
   StorageCache cache;
-  StorageAccessor accessor(s, &cache);
+  StorageAccessor accessor(s, cache);
 
   std::string data = "Hello world";
   std::string uuid = Toolbox::GenerateUuid();
@@ -150,7 +194,7 @@
 {
   FilesystemStorage s("UnitTestsStorage");
   StorageCache cache;
-  StorageAccessor accessor(s, &cache);
+  StorageAccessor accessor(s, cache);
 
   std::string data = "Hello world";
   std::string uuid = Toolbox::GenerateUuid();
@@ -173,7 +217,7 @@
 {
   FilesystemStorage s("UnitTestsStorage");
   StorageCache cache;
-  StorageAccessor accessor(s, &cache);
+  StorageAccessor accessor(s, cache);
 
   std::string r;
   std::string compressedData = "Hello";
--- a/OrthancFramework/UnitTestsSources/FrameworkTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/UnitTestsSources/FrameworkTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -363,6 +364,10 @@
   ASSERT_STREQ("image/svg+xml", EnumerationToString(SystemToolbox::AutodetectMimeType(".svg")));
   ASSERT_STREQ("text/css", EnumerationToString(SystemToolbox::AutodetectMimeType(".css")));
   ASSERT_STREQ("text/html", EnumerationToString(SystemToolbox::AutodetectMimeType(".html")));
+
+  ASSERT_STREQ("model/obj", EnumerationToString(SystemToolbox::AutodetectMimeType(".obj")));
+  ASSERT_STREQ("model/mtl", EnumerationToString(SystemToolbox::AutodetectMimeType(".mtl")));
+  ASSERT_STREQ("model/stl", EnumerationToString(SystemToolbox::AutodetectMimeType(".stl")));
 }
 #endif
 
@@ -377,6 +382,21 @@
   ASSERT_EQ("8b1a9953c4611296a827abf8c47804d7", s);
   Toolbox::ComputeMD5(s, "");
   ASSERT_EQ("d41d8cd98f00b204e9800998ecf8427e", s);
+
+  Toolbox::ComputeMD5(s, "aaabbbccc");
+  ASSERT_EQ("d1aaf4767a3c10a473407a4e47b02da6", s);
+
+  std::set<std::string> set;
+
+  Toolbox::ComputeMD5(s, set);
+  ASSERT_EQ("d41d8cd98f00b204e9800998ecf8427e", s);  // empty set same as empty string
+
+  set.insert("bbb");
+  set.insert("ccc");
+  set.insert("aaa");
+
+  Toolbox::ComputeMD5(s, set);
+  ASSERT_EQ("d1aaf4767a3c10a473407a4e47b02da6", s); // set md5 same as string with the values sorted
 }
 
 TEST(Toolbox, ComputeSHA1)
@@ -702,6 +722,82 @@
   ASSERT_EQ("", t[3]);
 }
 
+TEST(Toolbox, SplitString)
+{
+  {
+    std::set<std::string> result;
+    Toolbox::SplitString(result, "", ';');
+    ASSERT_EQ(0u, result.size());
+  }
+
+  {
+    std::set<std::string> result;
+    Toolbox::SplitString(result, "a", ';');
+    ASSERT_EQ(1u, result.size());
+    ASSERT_TRUE(result.end() != result.find("a"));
+  }
+
+  {
+    std::set<std::string> result;
+    Toolbox::SplitString(result, "a;b", ';');
+    ASSERT_EQ(2u, result.size());
+    ASSERT_TRUE(result.end() != result.find("a"));
+    ASSERT_TRUE(result.end() != result.find("b"));
+  }
+
+  {
+    std::set<std::string> result;
+    Toolbox::SplitString(result, "a;b;", ';');
+    ASSERT_EQ(2u, result.size());
+    ASSERT_TRUE(result.end() != result.find("a"));
+    ASSERT_TRUE(result.end() != result.find("b"));
+  }
+
+  {
+    std::set<std::string> result;
+    Toolbox::SplitString(result, "a;a", ';');
+    ASSERT_EQ(1u, result.size());
+    ASSERT_TRUE(result.end() != result.find("a"));
+  }
+
+  {
+    std::vector<std::string> result;
+    Toolbox::SplitString(result, "", ';');
+    ASSERT_EQ(0u, result.size());
+  }
+
+  {
+    std::vector<std::string> result;
+    Toolbox::SplitString(result, "a", ';');
+    ASSERT_EQ(1u, result.size());
+    ASSERT_EQ("a", result[0]);
+  }
+
+  {
+    std::vector<std::string> result;
+    Toolbox::SplitString(result, "a;b", ';');
+    ASSERT_EQ(2u, result.size());
+    ASSERT_EQ("a", result[0]);
+    ASSERT_EQ("b", result[1]);
+  }
+
+  {
+    std::vector<std::string> result;
+    Toolbox::SplitString(result, "a;b;", ';');
+    ASSERT_EQ(2u, result.size());
+    ASSERT_EQ("a", result[0]);
+    ASSERT_EQ("b", result[1]);
+  }
+
+  {
+    std::vector<std::string> result;
+    Toolbox::TokenizeString(result, "a;a", ';');
+    ASSERT_EQ(2u, result.size());
+    ASSERT_EQ("a", result[0]);
+    ASSERT_EQ("a", result[1]);
+  }
+}
+
 TEST(Toolbox, Enumerations)
 {
   ASSERT_EQ(Encoding_Utf8, StringToEncoding(EnumerationToString(Encoding_Utf8)));
@@ -749,6 +845,7 @@
   ASSERT_EQ(DicomVersion_2008, StringToDicomVersion(EnumerationToString(DicomVersion_2008)));
   ASSERT_EQ(DicomVersion_2017c, StringToDicomVersion(EnumerationToString(DicomVersion_2017c)));
   ASSERT_EQ(DicomVersion_2021b, StringToDicomVersion(EnumerationToString(DicomVersion_2021b)));
+  ASSERT_EQ(DicomVersion_2023b, StringToDicomVersion(EnumerationToString(DicomVersion_2023b)));
 
   for (int i = static_cast<int>(ValueRepresentation_ApplicationEntity);
        i < static_cast<int>(ValueRepresentation_NotSupported); i += 1)
@@ -790,6 +887,9 @@
   ASSERT_EQ(MimeType_Xml, StringToMimeType(EnumerationToString(MimeType_Xml)));
   ASSERT_EQ(MimeType_DicomWebJson, StringToMimeType(EnumerationToString(MimeType_DicomWebJson)));
   ASSERT_EQ(MimeType_DicomWebXml, StringToMimeType(EnumerationToString(MimeType_DicomWebXml)));
+  ASSERT_EQ(MimeType_Mtl, StringToMimeType(EnumerationToString(MimeType_Mtl)));
+  ASSERT_EQ(MimeType_Obj, StringToMimeType(EnumerationToString(MimeType_Obj)));
+  ASSERT_EQ(MimeType_Stl, StringToMimeType(EnumerationToString(MimeType_Stl)));
   ASSERT_THROW(StringToMimeType("nope"), OrthancException);
 
   ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Patient));
@@ -1311,7 +1411,7 @@
   {
     MetricsRegistry m;
     m.SetEnabled(false);
-    m.SetValue("hello.world", 42.5f);
+    m.SetIntegerValue("hello.world", 42);
     
     std::string s;
     m.ExportPrometheusText(s);
@@ -1320,7 +1420,7 @@
 
   {
     MetricsRegistry m;
-    m.Register("hello.world", MetricsType_Default);
+    m.Register("hello.world", MetricsUpdatePolicy_Directly, MetricsDataType_Integer);
     
     std::string s;
     m.ExportPrometheusText(s);
@@ -1329,9 +1429,9 @@
 
   {
     MetricsRegistry m;
-    m.SetValue("hello.world", 42.5f);
-    ASSERT_EQ(MetricsType_Default, m.GetMetricsType("hello.world"));
-    ASSERT_THROW(m.GetMetricsType("nope"), OrthancException);
+    m.SetIntegerValue("hello.world", -42);
+    ASSERT_EQ(MetricsUpdatePolicy_Directly, m.GetUpdatePolicy("hello.world"));
+    ASSERT_THROW(m.GetUpdatePolicy("nope"), OrthancException);
     
     std::string s;
     m.ExportPrometheusText(s);
@@ -1339,33 +1439,33 @@
     std::vector<std::string> t;
     Toolbox::TokenizeString(t, s, '\n');
     ASSERT_EQ(2u, t.size());
-    ASSERT_EQ("hello.world 42.5 ", t[0].substr(0, 17));
+    ASSERT_EQ("hello.world -42 ", t[0].substr(0, 16));
     ASSERT_TRUE(t[1].empty());
   }
 
   {
     MetricsRegistry m;
-    m.Register("hello.max", MetricsType_MaxOver10Seconds);
-    m.SetValue("hello.max", 10);
-    m.SetValue("hello.max", 20);
-    m.SetValue("hello.max", -10);
-    m.SetValue("hello.max", 5);
+    m.Register("hello.max", MetricsUpdatePolicy_MaxOver10Seconds, MetricsDataType_Integer);
+    m.SetIntegerValue("hello.max", 10);
+    m.SetIntegerValue("hello.max", 20);
+    m.SetIntegerValue("hello.max", -10);
+    m.SetIntegerValue("hello.max", 5);
 
-    m.Register("hello.min", MetricsType_MinOver10Seconds);
-    m.SetValue("hello.min", 10);
-    m.SetValue("hello.min", 20);
-    m.SetValue("hello.min", -10);
-    m.SetValue("hello.min", 5);
+    m.Register("hello.min", MetricsUpdatePolicy_MinOver10Seconds, MetricsDataType_Integer);
+    m.SetIntegerValue("hello.min", 10);
+    m.SetIntegerValue("hello.min", 20);
+    m.SetIntegerValue("hello.min", -10);
+    m.SetIntegerValue("hello.min", 5);
     
-    m.Register("hello.default", MetricsType_Default);
-    m.SetValue("hello.default", 10);
-    m.SetValue("hello.default", 20);
-    m.SetValue("hello.default", -10);
-    m.SetValue("hello.default", 5);
+    m.Register("hello.directly", MetricsUpdatePolicy_Directly, MetricsDataType_Integer);
+    m.SetIntegerValue("hello.directly", 10);
+    m.SetIntegerValue("hello.directly", 20);
+    m.SetIntegerValue("hello.directly", -10);
+    m.SetIntegerValue("hello.directly", 5);
     
-    ASSERT_EQ(MetricsType_MaxOver10Seconds, m.GetMetricsType("hello.max"));
-    ASSERT_EQ(MetricsType_MinOver10Seconds, m.GetMetricsType("hello.min"));
-    ASSERT_EQ(MetricsType_Default, m.GetMetricsType("hello.default"));
+    ASSERT_EQ(MetricsUpdatePolicy_MaxOver10Seconds, m.GetUpdatePolicy("hello.max"));
+    ASSERT_EQ(MetricsUpdatePolicy_MinOver10Seconds, m.GetUpdatePolicy("hello.min"));
+    ASSERT_EQ(MetricsUpdatePolicy_Directly, m.GetUpdatePolicy("hello.directly"));
 
     std::string s;
     m.ExportPrometheusText(s);
@@ -1385,25 +1485,25 @@
 
     ASSERT_EQ("20", u["hello.max"]);
     ASSERT_EQ("-10", u["hello.min"]);
-    ASSERT_EQ("5", u["hello.default"]);
+    ASSERT_EQ("5", u["hello.directly"]);
   }
 
   {
     MetricsRegistry m;
 
-    m.SetValue("a", 10);
-    m.SetValue("b", 10, MetricsType_MinOver10Seconds);
+    m.SetIntegerValue("a", 10);
+    m.SetIntegerValue("b", 10, MetricsUpdatePolicy_MinOver10Seconds);
 
-    m.Register("c", MetricsType_MaxOver10Seconds);
-    m.SetValue("c", 10, MetricsType_MinOver10Seconds);
+    m.Register("c", MetricsUpdatePolicy_MaxOver10Seconds, MetricsDataType_Integer);
+    m.SetIntegerValue("c", 10, MetricsUpdatePolicy_MinOver10Seconds);
 
-    m.Register("d", MetricsType_MaxOver10Seconds);
-    m.Register("d", MetricsType_Default);
+    m.Register("d", MetricsUpdatePolicy_MaxOver10Seconds, MetricsDataType_Integer);
+    ASSERT_THROW(m.Register("d", MetricsUpdatePolicy_Directly, MetricsDataType_Integer), OrthancException);
 
-    ASSERT_EQ(MetricsType_Default, m.GetMetricsType("a"));
-    ASSERT_EQ(MetricsType_MinOver10Seconds, m.GetMetricsType("b"));
-    ASSERT_EQ(MetricsType_MaxOver10Seconds, m.GetMetricsType("c"));
-    ASSERT_EQ(MetricsType_Default, m.GetMetricsType("d"));
+    ASSERT_EQ(MetricsUpdatePolicy_Directly, m.GetUpdatePolicy("a"));
+    ASSERT_EQ(MetricsUpdatePolicy_MinOver10Seconds, m.GetUpdatePolicy("b"));
+    ASSERT_EQ(MetricsUpdatePolicy_MaxOver10Seconds, m.GetUpdatePolicy("c"));
+    ASSERT_EQ(MetricsUpdatePolicy_MaxOver10Seconds, m.GetUpdatePolicy("d"));
   }
 
   {
@@ -1411,11 +1511,47 @@
 
     {
       MetricsRegistry::Timer t1(m, "a");
-      MetricsRegistry::Timer t2(m, "b", MetricsType_MinOver10Seconds);
+      MetricsRegistry::Timer t2(m, "b", MetricsUpdatePolicy_MinOver10Seconds);
     }
 
-    ASSERT_EQ(MetricsType_MaxOver10Seconds, m.GetMetricsType("a"));
-    ASSERT_EQ(MetricsType_MinOver10Seconds, m.GetMetricsType("b"));
+    ASSERT_EQ(MetricsUpdatePolicy_MaxOver10Seconds, m.GetUpdatePolicy("a"));
+    ASSERT_EQ(MetricsUpdatePolicy_MinOver10Seconds, m.GetUpdatePolicy("b"));
+  }
+
+  {
+    MetricsRegistry m;
+    m.Register("c", MetricsUpdatePolicy_MaxOver10Seconds, MetricsDataType_Integer);
+    m.SetFloatValue("c", 100, MetricsUpdatePolicy_MinOver10Seconds);
+
+    ASSERT_EQ(MetricsUpdatePolicy_MaxOver10Seconds, m.GetUpdatePolicy("c"));
+    ASSERT_EQ(MetricsDataType_Integer, m.GetDataType("c"));
+  }
+
+  {
+    MetricsRegistry m;
+    m.Register("c", MetricsUpdatePolicy_MaxOver10Seconds, MetricsDataType_Float);
+    m.SetIntegerValue("c", 100, MetricsUpdatePolicy_MinOver10Seconds);
+
+    ASSERT_EQ(MetricsUpdatePolicy_MaxOver10Seconds, m.GetUpdatePolicy("c"));
+    ASSERT_EQ(MetricsDataType_Float, m.GetDataType("c"));
+  }
+
+  {
+    MetricsRegistry m;
+    m.SetIntegerValue("c", 100, MetricsUpdatePolicy_MinOver10Seconds);
+    m.SetFloatValue("c", 101, MetricsUpdatePolicy_MaxOver10Seconds);
+
+    ASSERT_EQ(MetricsUpdatePolicy_MinOver10Seconds, m.GetUpdatePolicy("c"));
+    ASSERT_EQ(MetricsDataType_Integer, m.GetDataType("c"));
+  }
+
+  {
+    MetricsRegistry m;
+    m.SetIntegerValue("c", 100);
+    m.SetFloatValue("c", 101, MetricsUpdatePolicy_MaxOver10Seconds);
+
+    ASSERT_EQ(MetricsUpdatePolicy_Directly, m.GetUpdatePolicy("c"));
+    ASSERT_EQ(MetricsDataType_Integer, m.GetDataType("c"));
   }
 }
 #endif
@@ -1467,3 +1603,53 @@
   }
 }
 #endif
+
+
+TEST(Toolbox, IsVersionAbove)
+{
+  unsigned int a, b, c;
+  ASSERT_FALSE(Toolbox::ParseVersion(a, b, c, "nope"));
+  ASSERT_FALSE(Toolbox::ParseVersion(a, b, c, "mainline"));
+  ASSERT_FALSE(Toolbox::ParseVersion(a, b, c, ""));
+  ASSERT_FALSE(Toolbox::ParseVersion(a, b, c, "-1"));
+  ASSERT_FALSE(Toolbox::ParseVersion(a, b, c, "1.-1"));
+  ASSERT_FALSE(Toolbox::ParseVersion(a, b, c, "1.1.-1"));
+
+  ASSERT_TRUE(Toolbox::ParseVersion(a, b, c, "14.17.20"));
+  ASSERT_EQ(14u, a);
+  ASSERT_EQ(17u, b);
+  ASSERT_EQ(20u, c);
+
+  ASSERT_TRUE(Toolbox::ParseVersion(a, b, c, "18.19"));
+  ASSERT_EQ(18u, a);
+  ASSERT_EQ(19u, b);
+  ASSERT_EQ(0u, c);
+
+  ASSERT_TRUE(Toolbox::ParseVersion(a, b, c, "78"));
+  ASSERT_EQ(78u, a);
+  ASSERT_EQ(0u, b);
+  ASSERT_EQ(0u, c);
+
+  ASSERT_TRUE(Toolbox::IsVersionAbove("mainline", 99, 99, 99));
+
+  ASSERT_TRUE(Toolbox::IsVersionAbove("18", 17, 99, 99));
+  ASSERT_TRUE(Toolbox::IsVersionAbove("18", 18, 0, 0));
+  ASSERT_FALSE(Toolbox::IsVersionAbove("18", 18, 0, 1));
+  ASSERT_FALSE(Toolbox::IsVersionAbove("18", 18, 1, 0));
+  ASSERT_FALSE(Toolbox::IsVersionAbove("18", 19, 0, 0));
+
+  ASSERT_TRUE(Toolbox::IsVersionAbove("18.19", 17, 99, 99));
+  ASSERT_TRUE(Toolbox::IsVersionAbove("18.19", 18, 18, 99));
+  ASSERT_TRUE(Toolbox::IsVersionAbove("18.19", 18, 19, 0));
+  ASSERT_FALSE(Toolbox::IsVersionAbove("18.19", 18, 19, 1));
+  ASSERT_FALSE(Toolbox::IsVersionAbove("18.19", 18, 20, 0));
+  ASSERT_FALSE(Toolbox::IsVersionAbove("18.19", 19, 0, 0));
+
+  ASSERT_TRUE(Toolbox::IsVersionAbove("18.19.20", 17, 99, 99));
+  ASSERT_TRUE(Toolbox::IsVersionAbove("18.19.20", 18, 18, 99));
+  ASSERT_TRUE(Toolbox::IsVersionAbove("18.19.20", 18, 19, 19));
+  ASSERT_TRUE(Toolbox::IsVersionAbove("18.19.20", 18, 19, 20));
+  ASSERT_FALSE(Toolbox::IsVersionAbove("18.19.20", 18, 19, 21));
+  ASSERT_FALSE(Toolbox::IsVersionAbove("18.19.20", 18, 20, 0));
+  ASSERT_FALSE(Toolbox::IsVersionAbove("18.19.20", 19, 0, 0));
+}
--- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -37,6 +38,7 @@
 #include <gtest/gtest.h>
 
 #include "../Sources/Compatibility.h"
+#include "../Sources/DicomFormat/DicomImageInformation.h"
 #include "../Sources/DicomFormat/DicomPath.h"
 #include "../Sources/DicomNetworking/DicomFindAnswers.h"
 #include "../Sources/DicomParsing/DicomModification.h"
@@ -314,6 +316,10 @@
   ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 192"));  ASSERT_EQ(Encoding_Utf8, e);
   ASSERT_TRUE(GetDicomEncoding(e, "GB18030"));     ASSERT_EQ(Encoding_Chinese, e);
   ASSERT_TRUE(GetDicomEncoding(e, "GBK"));         ASSERT_EQ(Encoding_Chinese, e);
+
+  // common spelling mistakes
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR_100"));  ASSERT_EQ(Encoding_Latin1, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_2022_IR_6"));  ASSERT_EQ(Encoding_Ascii, e);
 }
 
 
@@ -2767,7 +2773,7 @@
 
     {
       DicomModification modif;
-      modif.SetupAnonymization(DicomVersion_2021b);
+      modif.SetupAnonymization(DicomVersion_2023b);
       modif.Apply(*dicom1);
       modif.Apply(*dicom2);
     }
@@ -2794,7 +2800,7 @@
 
     {
       DicomModification modif;
-      modif.SetupAnonymization(DicomVersion_2021b);
+      modif.SetupAnonymization(DicomVersion_2023b);
       modif.Keep(DicomPath::Parse("ReferencedImageSequence[1].ReferencedSOPInstanceUID"));
       modif.Keep(DicomPath::Parse("RelatedSeriesSequence"));
       modif.Apply(*dicom);
@@ -3086,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)
 {
@@ -3217,6 +3238,313 @@
 }
 
 
+TEST(ParsedDicomFile, InjectEmptyPixelData)
+{
+  static const char* PIXEL_DATA = "7FE00010";
+
+  {
+    ParsedDicomFile dicom(true);
+
+    DicomWebJsonVisitor visitor;
+    dicom.Apply(visitor);
+
+    ASSERT_FALSE(visitor.GetResult().isMember(PIXEL_DATA));
+  }
+
+  {
+    ParsedDicomFile dicom(true);
+    dicom.InjectEmptyPixelData(ValueRepresentation_OtherByte);
+    dicom.InjectEmptyPixelData(ValueRepresentation_OtherWord); // Must be ignored
+
+    DicomWebJsonVisitor visitor;
+    dicom.Apply(visitor);
+
+    ASSERT_TRUE(visitor.GetResult().isMember(PIXEL_DATA));
+    ASSERT_EQ(2u, visitor.GetResult() [PIXEL_DATA].size());
+    ASSERT_EQ("", visitor.GetResult() [PIXEL_DATA]["InlineBinary"].asString());
+    ASSERT_EQ("OB", visitor.GetResult() [PIXEL_DATA]["vr"].asString());
+  }
+
+  {
+    ParsedDicomFile dicom(true);
+    dicom.InjectEmptyPixelData(ValueRepresentation_OtherWord);
+    dicom.InjectEmptyPixelData(ValueRepresentation_OtherByte); // Must be ignored
+
+    DicomWebJsonVisitor visitor;
+    dicom.Apply(visitor);
+
+    ASSERT_TRUE(visitor.GetResult().isMember(PIXEL_DATA));
+    ASSERT_EQ(2u, visitor.GetResult() [PIXEL_DATA].size());
+    ASSERT_EQ("", visitor.GetResult() [PIXEL_DATA]["InlineBinary"].asString());
+    ASSERT_EQ("OW", visitor.GetResult() [PIXEL_DATA]["vr"].asString());
+  }
+}
+
+
+TEST(ParsedDicomFile, RemoveFromPixelData)
+{
+  ParsedDicomFile dicom(true);
+  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DcmTag(0x7fe0, 0x0000), "").good());
+  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DcmTag(0x7fe0, 0x0009), "").good());
+  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertUint8Array(DcmTag(0x7fe0, 0x0010), NULL, 0).good());
+  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DcmTag(0x7fe0, 0x0011), "").good());
+  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DcmTag(0x7fe1, 0x0000), "").good());
+
+  {
+    DicomMap m;
+    dicom.ExtractDicomSummary(m, 0);
+
+    ASSERT_EQ(10u, m.GetSize());
+    ASSERT_TRUE(m.HasTag(DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID));
+    ASSERT_TRUE(m.HasTag(DICOM_TAG_SOP_INSTANCE_UID));
+    ASSERT_TRUE(m.HasTag(DICOM_TAG_PATIENT_ID));
+    ASSERT_TRUE(m.HasTag(DICOM_TAG_SERIES_INSTANCE_UID));
+    ASSERT_TRUE(m.HasTag(DICOM_TAG_STUDY_INSTANCE_UID));
+    ASSERT_TRUE(m.HasTag(0x7fe0, 0x0000));
+    ASSERT_TRUE(m.HasTag(0x7fe0, 0x0009));
+    ASSERT_TRUE(m.HasTag(DICOM_TAG_PIXEL_DATA));
+    ASSERT_TRUE(m.HasTag(0x7fe0, 0x0011));
+    ASSERT_TRUE(m.HasTag(0x7fe1, 0x0000));
+  }
+
+  dicom.RemoveFromPixelData();
+
+  {
+    DicomMap m;
+    dicom.ExtractDicomSummary(m, 0);
+
+    ASSERT_EQ(7u, m.GetSize());
+    ASSERT_TRUE(m.HasTag(DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID));
+    ASSERT_TRUE(m.HasTag(DICOM_TAG_SOP_INSTANCE_UID));
+    ASSERT_TRUE(m.HasTag(DICOM_TAG_PATIENT_ID));
+    ASSERT_TRUE(m.HasTag(DICOM_TAG_SERIES_INSTANCE_UID));
+    ASSERT_TRUE(m.HasTag(DICOM_TAG_STUDY_INSTANCE_UID));
+    ASSERT_TRUE(m.HasTag(0x7fe0, 0x0000));
+    ASSERT_TRUE(m.HasTag(0x7fe0, 0x0009));
+    ASSERT_FALSE(m.HasTag(DICOM_TAG_PIXEL_DATA));
+    ASSERT_FALSE(m.HasTag(0x7fe0, 0x0011));
+    ASSERT_FALSE(m.HasTag(0x7fe1, 0x0000));
+  }
+}
+
+
+TEST(ParsedDicomFile, GuessPixelDataValueRepresentation)
+{
+  typedef std::list< std::pair<E_TransferSyntax, DicomTransferSyntax> > Syntaxes;
+
+  // Create a list of the main non-retired transfer syntaxes, from:
+  // https://www.dicomlibrary.com/dicom/transfer-syntax/
+  Syntaxes compressedSyntaxes;
+  compressedSyntaxes.push_back(std::make_pair(EXS_DeflatedLittleEndianExplicit, DicomTransferSyntax_DeflatedLittleEndianExplicit));
+  compressedSyntaxes.push_back(std::make_pair(EXS_JPEGProcess1, DicomTransferSyntax_JPEGProcess1));
+  compressedSyntaxes.push_back(std::make_pair(EXS_JPEGProcess2_4, DicomTransferSyntax_JPEGProcess2_4));
+  compressedSyntaxes.push_back(std::make_pair(EXS_JPEGProcess14, DicomTransferSyntax_JPEGProcess14));
+  compressedSyntaxes.push_back(std::make_pair(EXS_JPEGProcess14SV1, DicomTransferSyntax_JPEGProcess14SV1));
+  compressedSyntaxes.push_back(std::make_pair(EXS_JPEGLSLossless, DicomTransferSyntax_JPEGLSLossless));
+  compressedSyntaxes.push_back(std::make_pair(EXS_JPEGLSLossy, DicomTransferSyntax_JPEGLSLossy));
+  compressedSyntaxes.push_back(std::make_pair(EXS_JPEG2000LosslessOnly, DicomTransferSyntax_JPEG2000LosslessOnly));
+  compressedSyntaxes.push_back(std::make_pair(EXS_JPEG2000, DicomTransferSyntax_JPEG2000));
+  compressedSyntaxes.push_back(std::make_pair(EXS_JPEG2000MulticomponentLosslessOnly, DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly));
+  compressedSyntaxes.push_back(std::make_pair(EXS_JPEG2000Multicomponent, DicomTransferSyntax_JPEG2000Multicomponent));
+  compressedSyntaxes.push_back(std::make_pair(EXS_JPIPReferenced, DicomTransferSyntax_JPIPReferenced));
+  compressedSyntaxes.push_back(std::make_pair(EXS_JPIPReferencedDeflate, DicomTransferSyntax_JPIPReferencedDeflate));
+  compressedSyntaxes.push_back(std::make_pair(EXS_RLELossless, DicomTransferSyntax_RLELossless));
+  compressedSyntaxes.push_back(std::make_pair(EXS_MPEG2MainProfileAtMainLevel, DicomTransferSyntax_MPEG2MainProfileAtMainLevel));
+  compressedSyntaxes.push_back(std::make_pair(EXS_MPEG4HighProfileLevel4_1, DicomTransferSyntax_MPEG4HighProfileLevel4_1));
+  compressedSyntaxes.push_back(std::make_pair(EXS_MPEG4BDcompatibleHighProfileLevel4_1, DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1));
+
+  for (unsigned int i = 0; i < 3; i++)
+  {
+    unsigned int bitsAllocated;
+    switch (i)
+    {
+      case 0: bitsAllocated = 1;   break;
+      case 1: bitsAllocated = 8;   break;
+      case 2: bitsAllocated = 16;  break;
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+      
+    for (Syntaxes::const_iterator it = compressedSyntaxes.begin(); it != compressedSyntaxes.end(); ++it)
+    {
+      // All the compressed transfer syntaxes must have "OB" pixel data
+      ASSERT_EQ(ValueRepresentation_OtherByte, DicomImageInformation::GuessPixelDataValueRepresentation(it->second, bitsAllocated));
+
+      {
+        DicomMap dicom;
+        dicom.SetValue(DICOM_TAG_BITS_ALLOCATED, boost::lexical_cast<std::string>(bitsAllocated), false);
+        ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation(it->second));
+      }
+
+      {
+        DicomMap dicom;
+        ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation(it->second));
+      }
+
+      {
+        ParsedDicomFile dicom(true);
+        ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertUint16(DCM_BitsAllocated, bitsAllocated).good());
+        ASSERT_TRUE(dicom.GetDcmtkObject().chooseRepresentation(it->first, NULL).good());
+        dicom.GetDcmtkObject().removeAllButCurrentRepresentations();
+        DicomTransferSyntax ts;
+        ASSERT_TRUE(dicom.LookupTransferSyntax(ts));
+        ASSERT_EQ(ts, it->second);
+        ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation());
+      }
+    }
+
+    {
+      // Little endian implicit is always OW
+      ASSERT_EQ(ValueRepresentation_OtherWord, DicomImageInformation::GuessPixelDataValueRepresentation(DicomTransferSyntax_LittleEndianImplicit, bitsAllocated));
+
+      {
+        DicomMap dicom;
+        dicom.SetValue(DICOM_TAG_BITS_ALLOCATED, boost::lexical_cast<std::string>(bitsAllocated), false);
+        ASSERT_EQ(ValueRepresentation_OtherWord, dicom.GuessPixelDataValueRepresentation(DicomTransferSyntax_LittleEndianImplicit));
+      }
+
+      {
+        DicomMap dicom;
+        ASSERT_EQ(ValueRepresentation_OtherWord, dicom.GuessPixelDataValueRepresentation(DicomTransferSyntax_LittleEndianImplicit));
+      }
+
+      {
+        ParsedDicomFile dicom(true);
+        ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertUint16(DCM_BitsAllocated, bitsAllocated).good());
+        ASSERT_TRUE(dicom.GetDcmtkObject().chooseRepresentation(EXS_LittleEndianImplicit, NULL).good());
+        dicom.GetDcmtkObject().removeAllButCurrentRepresentations();
+        ASSERT_EQ(ValueRepresentation_OtherWord, dicom.GuessPixelDataValueRepresentation());
+      }
+    }
+
+  }
+
+  // Explicit little and big endian with <= 8 bpp is OB
+
+  for (unsigned int i = 0; i < 2; i++)
+  {
+    unsigned int bitsAllocated;
+    switch (i)
+    {
+      case 0: bitsAllocated = 1;   break;
+      case 1: bitsAllocated = 8;   break;
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    ASSERT_EQ(ValueRepresentation_OtherByte, DicomImageInformation::GuessPixelDataValueRepresentation(DicomTransferSyntax_LittleEndianExplicit, bitsAllocated));
+    ASSERT_EQ(ValueRepresentation_OtherByte, DicomImageInformation::GuessPixelDataValueRepresentation(DicomTransferSyntax_BigEndianExplicit, bitsAllocated));
+
+    {
+      DicomMap dicom;
+      dicom.SetValue(DICOM_TAG_BITS_ALLOCATED, boost::lexical_cast<std::string>(bitsAllocated), false);
+      ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation(DicomTransferSyntax_LittleEndianExplicit));
+      ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation(DicomTransferSyntax_BigEndianExplicit));
+    }
+
+    {
+      DicomMap dicom;
+      ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation(DicomTransferSyntax_LittleEndianExplicit));
+      ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation(DicomTransferSyntax_BigEndianExplicit));
+    }
+    
+    {
+      ParsedDicomFile dicom(true);
+      ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertUint16(DCM_BitsAllocated, bitsAllocated).good());
+      ASSERT_TRUE(dicom.GetDcmtkObject().chooseRepresentation(EXS_LittleEndianExplicit, NULL).good());
+      dicom.GetDcmtkObject().removeAllButCurrentRepresentations();
+      ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation());
+    }
+
+    {
+      ParsedDicomFile dicom(true);
+      ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertUint16(DCM_BitsAllocated, bitsAllocated).good());
+      ASSERT_TRUE(dicom.GetDcmtkObject().chooseRepresentation(EXS_BigEndianExplicit, NULL).good());
+      dicom.GetDcmtkObject().removeAllButCurrentRepresentations();
+      ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation());
+    }
+  }
+
+  // Explicit little and big endian with > 8 bpp is OW
+  
+  ASSERT_EQ(ValueRepresentation_OtherWord, DicomImageInformation::GuessPixelDataValueRepresentation(DicomTransferSyntax_LittleEndianExplicit, 16));
+  ASSERT_EQ(ValueRepresentation_OtherWord, DicomImageInformation::GuessPixelDataValueRepresentation(DicomTransferSyntax_BigEndianExplicit, 16));
+
+  {
+    DicomMap dicom;
+    dicom.SetValue(DICOM_TAG_BITS_ALLOCATED, "16", false);
+    ASSERT_EQ(ValueRepresentation_OtherWord, dicom.GuessPixelDataValueRepresentation(DicomTransferSyntax_LittleEndianExplicit));
+    ASSERT_EQ(ValueRepresentation_OtherWord, dicom.GuessPixelDataValueRepresentation(DicomTransferSyntax_BigEndianExplicit));
+  }
+
+  {
+    ParsedDicomFile dicom(true);
+    ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertUint16(DCM_BitsAllocated, 16).good());
+    ASSERT_TRUE(dicom.GetDcmtkObject().chooseRepresentation(EXS_LittleEndianExplicit, NULL).good());
+    dicom.GetDcmtkObject().removeAllButCurrentRepresentations();
+    ASSERT_EQ(ValueRepresentation_OtherWord, dicom.GuessPixelDataValueRepresentation());
+  }
+
+  {
+    ParsedDicomFile dicom(true);
+    ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertUint16(DCM_BitsAllocated, 16).good());
+    ASSERT_TRUE(dicom.GetDcmtkObject().chooseRepresentation(EXS_BigEndianExplicit, NULL).good());
+    dicom.GetDcmtkObject().removeAllButCurrentRepresentations();
+    ASSERT_EQ(ValueRepresentation_OtherWord, dicom.GuessPixelDataValueRepresentation());
+  }
+}
+
+
+#if ORTHANC_SANDBOXED != 1
+TEST(ParsedDicomFile, DISABLED_InjectEmptyPixelData2)
+{
+  static const char* PIXEL_DATA = "7FE00010";
+
+  for (int i = 0; i <= DicomTransferSyntax_XML; i++)
+  {
+    DicomTransferSyntax a = (DicomTransferSyntax) i;
+
+    std::string path = (std::string(getenv("HOME")) +
+                        "/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);
+
+      ParsedDicomFile dicom(source);
+      std::unique_ptr<DcmElement> removal(dicom.GetDcmtkObject().getDataset()->remove(DCM_PixelData));
+
+      {
+        DicomWebJsonVisitor visitor;
+        dicom.Apply(visitor);
+        ASSERT_FALSE(visitor.GetResult().isMember(PIXEL_DATA));
+      }
+
+      {
+        DicomWebJsonVisitor visitor;
+        dicom.InjectEmptyPixelData(ValueRepresentation_OtherByte);
+        dicom.Apply(visitor);
+        ASSERT_TRUE(visitor.GetResult().isMember(PIXEL_DATA));
+        ASSERT_EQ("OB", visitor.GetResult() [PIXEL_DATA]["vr"].asString());
+      }
+
+      removal.reset(dicom.GetDcmtkObject().getDataset()->remove(DCM_PixelData));
+
+      {
+        DicomWebJsonVisitor visitor;
+        dicom.InjectEmptyPixelData(ValueRepresentation_OtherWord);
+        dicom.Apply(visitor);
+        ASSERT_TRUE(visitor.GetResult().isMember(PIXEL_DATA));
+        ASSERT_EQ("OW", visitor.GetResult() [PIXEL_DATA]["vr"].asString());
+      }
+    }
+  }
+}
+#endif
+
 
 
 #if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
@@ -3236,11 +3564,13 @@
   DcmtkTranscoder transcoder;
 
   for (int j = 0; j < 2; j++)
+  {
     for (int i = 0; i <= DicomTransferSyntax_XML; i++)
     {
       DicomTransferSyntax a = (DicomTransferSyntax) i;
 
-      std::string path = ("/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/" +
+      std::string path = (std::string(getenv("HOME")) +
+                          "/Subversion/orthanc-tests/Database/TransferSyntaxes/" +
                           std::string(GetTransferSyntaxUid(a)) + ".dcm");
       if (Orthanc::SystemToolbox::IsRegularFile(path))
       {
@@ -3268,6 +3598,7 @@
         }
       }
     }
+  }
 }
 
 
@@ -3277,7 +3608,8 @@
 
   {
     std::string source;
-    Orthanc::SystemToolbox::ReadFile(source, "/home/jodogne/Subversion/orthanc-tests/Database/KarstenHilbertRF.dcm");
+    Orthanc::SystemToolbox::ReadFile(source, std::string(getenv("HOME")) +
+                                     "/Subversion/orthanc-tests/Database/KarstenHilbertRF.dcm");
     toto.reset(FromDcmtkBridge::LoadFromMemoryBuffer(source.c_str(), source.size()));
   }
   
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/UnitTestsSources/GithubCACertificates.h	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,30 @@
+#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"  \
+"-----END CERTIFICATE-----\n"  \
+"\n" 
--- a/OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/UnitTestsSources/ImageTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/UnitTestsSources/ImageTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -33,10 +34,11 @@
 #include "../Sources/Images/ImageProcessing.h"
 #include "../Sources/Images/JpegReader.h"
 #include "../Sources/Images/JpegWriter.h"
+#include "../Sources/Images/PamReader.h"
+#include "../Sources/Images/PamWriter.h"
 #include "../Sources/Images/PngReader.h"
 #include "../Sources/Images/PngWriter.h"
-#include "../Sources/Images/PamReader.h"
-#include "../Sources/Images/PamWriter.h"
+#include "../Sources/OrthancException.h"
 #include "../Sources/Toolbox.h"
 
 #if ORTHANC_SANDBOXED != 1
@@ -96,14 +98,33 @@
     uint8_t *p = &image[0] + y * pitch;
     for (unsigned int x = 0; x < width; x++, p += 8)
     {
-      p[0] = (y % 8 == 0) ? 255 : 0;
-      p[1] = (y % 8 == 1) ? 255 : 0;
-      p[2] = (y % 8 == 2) ? 255 : 0;
-      p[3] = (y % 8 == 3) ? 255 : 0;
-      p[4] = (y % 8 == 4) ? 255 : 0;
-      p[5] = (y % 8 == 5) ? 255 : 0;
-      p[6] = (y % 8 == 6) ? 255 : 0;
-      p[7] = (y % 8 == 7) ? 255 : 0;
+      switch (Orthanc::Toolbox::DetectEndianness())
+      {
+        case Orthanc::Endianness_Little:
+          p[0] = (y % 8 == 0) ? 255 : 0;
+          p[1] = (y % 8 == 1) ? 255 : 0;
+          p[2] = (y % 8 == 2) ? 255 : 0;
+          p[3] = (y % 8 == 3) ? 255 : 0;
+          p[4] = (y % 8 == 4) ? 255 : 0;
+          p[5] = (y % 8 == 5) ? 255 : 0;
+          p[6] = (y % 8 == 6) ? 255 : 0;
+          p[7] = (y % 8 == 7) ? 255 : 0;
+          break;
+
+        case Orthanc::Endianness_Big:
+          p[0] = (y % 8 == 1) ? 255 : 0;
+          p[1] = (y % 8 == 0) ? 255 : 0;
+          p[2] = (y % 8 == 3) ? 255 : 0;
+          p[3] = (y % 8 == 2) ? 255 : 0;
+          p[4] = (y % 8 == 5) ? 255 : 0;
+          p[5] = (y % 8 == 4) ? 255 : 0;
+          p[6] = (y % 8 == 7) ? 255 : 0;
+          p[7] = (y % 8 == 6) ? 255 : 0;
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
     }
   }
 
--- a/OrthancFramework/UnitTestsSources/JobsTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/UnitTestsSources/JobsTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -131,6 +132,11 @@
     {
       return false;
     }
+
+    virtual bool DeleteOutput(const std::string& key) ORTHANC_OVERRIDE
+    {
+      return false;
+    }
   };
 
 
--- a/OrthancFramework/UnitTestsSources/JpegLosslessTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/UnitTestsSources/JpegLosslessTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/UnitTestsSources/LoggingTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/UnitTestsSources/LoggingTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -69,7 +70,7 @@
                               const std::string& logLine)
 {
   const char* regexStr = "[A-Z][0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{6} "
-    "[a-zA-Z\\.\\-_]+:[0-9]+\\] (.*)" EOLSTRING "$";
+    ".{16} [a-zA-Z\\.\\-_]+:[0-9]+\\] (.*)" EOLSTRING "$";
 
   boost::regex regexObj(regexStr);
 
--- a/OrthancFramework/UnitTestsSources/LuaTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/UnitTestsSources/LuaTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/UnitTestsSources/MemoryCacheTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/UnitTestsSources/MemoryCacheTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -33,6 +34,7 @@
 #include "../Sources/Cache/SharedArchive.h"
 #include "../Sources/IDynamicObject.h"
 #include "../Sources/Logging.h"
+#include "../Sources/SystemToolbox.h"
 
 #include <memory>
 #include <algorithm>
@@ -319,44 +321,260 @@
   Orthanc::MemoryStringCache c;
   ASSERT_THROW(c.SetMaximumSize(0), Orthanc::OrthancException);
   
-  c.SetMaximumSize(2);
+  c.SetMaximumSize(3);
 
   std::string v;
-  ASSERT_FALSE(c.Fetch(v, "hello"));
+  {
+    Orthanc::MemoryStringCache::Accessor a(c);
+    ASSERT_FALSE(a.Fetch(v, "key1"));
+  }
 
-  c.Add("hello", "a");
-  ASSERT_TRUE(c.Fetch(v, "hello"));   ASSERT_EQ("a", v);
-  ASSERT_FALSE(c.Fetch(v, "hello2"));
-  ASSERT_FALSE(c.Fetch(v, "hello3"));
+  {
+    Orthanc::MemoryStringCache::Accessor a(c);
+    ASSERT_FALSE(a.Fetch(v, "key1"));
+    a.Add("key1", "a");
+    ASSERT_TRUE(a.Fetch(v, "key1"));
+    ASSERT_EQ("a", v);
 
-  c.Add("hello2", "b");
-  ASSERT_TRUE(c.Fetch(v, "hello"));   ASSERT_EQ("a", v);
-  ASSERT_TRUE(c.Fetch(v, "hello2"));  ASSERT_EQ("b", v);
-  ASSERT_FALSE(c.Fetch(v, "hello3"));
+    ASSERT_FALSE(a.Fetch(v, "key2"));
+    ASSERT_FALSE(a.Fetch(v, "key3"));
+
+    a.Add("key2", "b");
+    ASSERT_TRUE(a.Fetch(v, "key1"));
+    ASSERT_EQ("a", v);
+    ASSERT_TRUE(a.Fetch(v, "key2"));
+    ASSERT_EQ("b", v);
 
-  c.Add("hello3", "too large value");
-  ASSERT_TRUE(c.Fetch(v, "hello"));   ASSERT_EQ("a", v);
-  ASSERT_TRUE(c.Fetch(v, "hello2"));  ASSERT_EQ("b", v);
-  ASSERT_FALSE(c.Fetch(v, "hello3"));
-  
-  c.Add("hello3", "c");
-  ASSERT_FALSE(c.Fetch(v, "hello"));  // Recycled
-  ASSERT_TRUE(c.Fetch(v, "hello2"));  ASSERT_EQ("b", v);
-  ASSERT_TRUE(c.Fetch(v, "hello3"));  ASSERT_EQ("c", v);
+    a.Add("key3", "too-large-value");
+    ASSERT_TRUE(a.Fetch(v, "key1"));
+    ASSERT_EQ("a", v);
+    ASSERT_TRUE(a.Fetch(v, "key2"));
+    ASSERT_EQ("b", v);
+    ASSERT_FALSE(a.Fetch(v, "key3"));
+
+    a.Add("key3", "c");
+    ASSERT_TRUE(a.Fetch(v, "key2"));
+    ASSERT_EQ("b", v);
+    ASSERT_TRUE(a.Fetch(v, "key1"));
+    ASSERT_EQ("a", v);
+    ASSERT_TRUE(a.Fetch(v, "key3"));
+    ASSERT_EQ("c", v);
+
+    // adding a fourth value should remove the oldest accessed value (key2)
+    a.Add("key4", "d");
+    ASSERT_FALSE(a.Fetch(v, "key2"));
+    ASSERT_TRUE(a.Fetch(v, "key1"));
+    ASSERT_EQ("a", v);
+    ASSERT_TRUE(a.Fetch(v, "key3"));
+    ASSERT_EQ("c", v);
+    ASSERT_TRUE(a.Fetch(v, "key4"));
+    ASSERT_EQ("d", v);
+
+  }
 }
 
-
 TEST(MemoryStringCache, Invalidate)
 {
   Orthanc::MemoryStringCache c;
-  c.Add("hello", "a");
-  c.Add("hello2", "b");
+  Orthanc::MemoryStringCache::Accessor a(c);
+
+  a.Add("hello", "a");
+  a.Add("hello2", "b");
 
   std::string v;
-  ASSERT_TRUE(c.Fetch(v, "hello"));   ASSERT_EQ("a", v);
-  ASSERT_TRUE(c.Fetch(v, "hello2"));  ASSERT_EQ("b", v);
+  ASSERT_TRUE(a.Fetch(v, "hello"));   
+  ASSERT_EQ("a", v);
+  ASSERT_TRUE(a.Fetch(v, "hello2"));  
+  ASSERT_EQ("b", v);
 
   c.Invalidate("hello");
-  ASSERT_FALSE(c.Fetch(v, "hello"));
-  ASSERT_TRUE(c.Fetch(v, "hello2"));  ASSERT_EQ("b", v);
+  ASSERT_FALSE(a.Fetch(v, "hello"));
+  ASSERT_TRUE(a.Fetch(v, "hello2"));  
+  ASSERT_EQ("b", v);
+}
+
+
+static int ThreadingScenarioHappyStep = 0;
+static Orthanc::MemoryStringCache ThreadingScenarioHappyCache;
+
+void ThreadingScenarioHappyThread1()
+{
+  // the first thread to call Fetch (will be in charge of adding)
+  Orthanc::MemoryStringCache::Accessor a(ThreadingScenarioHappyCache);
+  std::string v;
+
+  LOG(INFO) << "Thread1 will fetch";
+  if (!a.Fetch(v, "key1"))
+  {
+    LOG(INFO) << "Thread1 has fetch";
+    ThreadingScenarioHappyStep = 1;
+    
+    // wait for the other thread to fetch too
+    while (ThreadingScenarioHappyStep < 2)
+    {
+      Orthanc::SystemToolbox::USleep(10000);
+    }
+    LOG(INFO) << "Thread1 will add after a short sleep";
+    Orthanc::SystemToolbox::USleep(100000);
+    LOG(INFO) << "Thread1 will add";
+
+    a.Add("key1", "value1");
+
+    LOG(INFO) << "Thread1 has added";
+  }
+}
+
+void ThreadingScenarioHappyThread2()
+{
+  Orthanc::MemoryStringCache::Accessor a(ThreadingScenarioHappyCache);
+  std::string v;
+
+  // nobody has added key2 -> go
+  if (!a.Fetch(v, "key2"))
+  {
+    a.Add("key2", "value2");
+  }
+
+  // wait until thread 1 has completed its "Fetch" but not added yet
+  while (ThreadingScenarioHappyStep < 1)
+  {
+    Orthanc::SystemToolbox::USleep(10000);
+  }
+
+  ThreadingScenarioHappyStep = 2;
+  LOG(INFO) << "Thread2 will fetch";
+  // this should wait until thread 1 has added
+  if (!a.Fetch(v, "key1"))
+  {
+    ASSERT_FALSE(true); // this thread should not add since thread1 should have done it
+  }
+  LOG(INFO) << "Thread2 has fetched the value";
+  ASSERT_EQ("value1", v);
+}
+
+
+TEST(MemoryStringCache, ThreadingScenarioHappy)
+{
+  boost::thread thread1 = boost::thread(ThreadingScenarioHappyThread1);
+  boost::thread thread2 = boost::thread(ThreadingScenarioHappyThread2);
+
+  thread1.join();
+  thread2.join();
 }
+
+
+static int ThreadingScenarioFailureStep = 0;
+static Orthanc::MemoryStringCache ThreadingScenarioFailureCache;
+
+void ThreadingScenarioFailureThread1()
+{
+  // the first thread to call Fetch (will be in charge of adding)
+  Orthanc::MemoryStringCache::Accessor a(ThreadingScenarioFailureCache);
+  std::string v;
+
+  LOG(INFO) << "Thread1 will fetch";
+  if (!a.Fetch(v, "key1"))
+  {
+    LOG(INFO) << "Thread1 has fetch";
+    ThreadingScenarioFailureStep = 1;
+    
+    // wait for the other thread to fetch too
+    while (ThreadingScenarioFailureStep < 2)
+    {
+      Orthanc::SystemToolbox::USleep(10000);
+    }
+    LOG(INFO) << "Thread1 will add after a short sleep";
+    Orthanc::SystemToolbox::USleep(100000);
+    LOG(INFO) << "Thread1 fails to add because of an error";
+  }
+}
+
+void ThreadingScenarioFailureThread2()
+{
+  Orthanc::MemoryStringCache::Accessor a(ThreadingScenarioFailureCache);
+  std::string v;
+
+  // wait until thread 1 has completed its "Fetch" but not added yet
+  while (ThreadingScenarioFailureStep < 1)
+  {
+    Orthanc::SystemToolbox::USleep(10000);
+  }
+
+  ThreadingScenarioFailureStep = 2;
+  LOG(INFO) << "Thread2 will fetch and wait for thread1 to add";
+  // this should wait until thread 1 has added
+  if (!a.Fetch(v, "key1"))
+  {
+    LOG(INFO) << "Thread2 has been awaken and will add since Thread1 has failed to add";
+    a.Add("key1", "value1");
+  }
+  LOG(INFO) << "Thread2 has added the value";
+}
+
+
+TEST(MemoryStringCache, ThreadingScenarioFailure)
+{
+  boost::thread thread1 = boost::thread(ThreadingScenarioFailureThread1);
+  boost::thread thread2 = boost::thread(ThreadingScenarioFailureThread2);
+
+  thread1.join();
+  thread2.join();
+}
+
+
+static int ThreadingScenarioInvalidateStep = 0;
+static Orthanc::MemoryStringCache ThreadingScenarioInvalidateCache;
+
+void ThreadingScenarioInvalidateThread1()
+{
+  // the first thread to call Fetch (will be in charge of adding)
+  Orthanc::MemoryStringCache::Accessor a(ThreadingScenarioInvalidateCache);
+  std::string v;
+
+  LOG(INFO) << "Thread1 will fetch";
+  if (!a.Fetch(v, "key1"))
+  {
+    LOG(INFO) << "Thread1 has fetch";
+    ThreadingScenarioInvalidateStep = 1;
+    
+    // wait for the other thread to fetch too
+    while (ThreadingScenarioInvalidateStep < 2)
+    {
+      Orthanc::SystemToolbox::USleep(10000);
+    }
+    LOG(INFO) << "Thread1 will invalidate after a short sleep";
+    Orthanc::SystemToolbox::USleep(100000);
+    LOG(INFO) << "Thread1 is invalidating";
+    ThreadingScenarioInvalidateCache.Invalidate("key1");
+  }
+}
+
+void ThreadingScenarioInvalidateThread2()
+{
+  Orthanc::MemoryStringCache::Accessor a(ThreadingScenarioInvalidateCache);
+  std::string v;
+
+  // wait until thread 1 has completed its "Fetch" but not added yet
+  while (ThreadingScenarioInvalidateStep < 1)
+  {
+    Orthanc::SystemToolbox::USleep(10000);
+  }
+
+  ThreadingScenarioInvalidateStep = 2;
+  LOG(INFO) << "Thread2 will fetch and wait for thread1 to add";
+  // this should wait until thread 1 has added
+  if (!a.Fetch(v, "key1"))
+  {
+    LOG(INFO) << "Thread2 has been awaken because thread1 has invalidated the key";
+  }
+}
+
+
+TEST(MemoryStringCache, ThreadingScenarioInvalidate)
+{
+  boost::thread thread1 = boost::thread(ThreadingScenarioInvalidateThread1);
+  boost::thread thread2 = boost::thread(ThreadingScenarioInvalidateThread2);
+
+  thread1.join();
+  thread2.join();
+}
--- a/OrthancFramework/UnitTestsSources/RestApiTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/UnitTestsSources/RestApiTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -95,29 +96,22 @@
 /**
    The HTTPS CA certificates for BitBucket were extracted as follows:
    
-   (1) We retrieve the certification chain of BitBucket:
-
-   # echo | openssl s_client -showcerts -connect www.bitbucket.org:443
+   (1) We retrieve the URI of the root CA of BitBucket:
 
-   (2) We see that the certification authority (CA) is
-   "www.digicert.com", and the root certificate is "DigiCert High
-   Assurance EV Root CA". As a consequence, we navigate to DigiCert to
-   find the URL to this CA certificate:
+   # echo | openssl s_client -servername raw.githubusercontent.com -connect raw.githubusercontent.com:443 2>/dev/null | openssl x509 -text | grep "CA Issuers"
 
-   firefox https://www.digicert.com/digicert-root-certificates.htm
-
-   (3) Once we get the URL to the CA certificate, we convert it to a C
+   (2) Once we get the URL to the CA certificate, we convert it to a C
    macro that can be used by libcurl:
 
    # cd UnitTestsSources
-   # ../Resources/RetrieveCACertificates.py BITBUCKET_CERTIFICATES https://cacerts.digicert.com/DigiCertTLSHybridECCSHA3842020CA1-1.crt > BitbucketCACertificates.h
+   # python2 ../Resources/RetrieveCACertificates.py GITHUB_CERTIFICATES http://cacerts.digicert.com/DigiCertGlobalG2TLSRSASHA2562020CA1-1.crt > GithubCACertificates.h
 **/
 
-#include "BitbucketCACertificates.h"
+#include "GithubCACertificates.h"
 
 TEST(HttpClient, Ssl)
 {
-  SystemToolbox::WriteFile(BITBUCKET_CERTIFICATES, "UnitTestsResults/bitbucket.cert");
+  SystemToolbox::WriteFile(GITHUB_CERTIFICATES, "UnitTestsResults/github.cert");
 
   /*{
     std::string s;
@@ -128,12 +122,12 @@
   HttpClient c;
   //c.SetVerbose(true);
   c.SetHttpsVerifyPeers(true);
-  c.SetHttpsCACertificates("UnitTestsResults/bitbucket.cert");
+  c.SetHttpsCACertificates("UnitTestsResults/github.cert");
 
   // Test file modified on 2020-04-20, in order to use a git
   // repository on BitBucket instead of a Mercurial repository
   // (because Mercurial support disappears on 2020-05-31)
-  c.SetUrl("https://bitbucket.org/osimis/orthanc-setup-samples/raw/master/docker/serve-folders/orthanc/serve-folders.json");
+  c.SetUrl("https://raw.githubusercontent.com/orthanc-server/orthanc-setup-samples/refs/heads/master/docker/serve-folders/orthanc/serve-folders.json");
 
   Json::Value v;
   c.Apply(v);
@@ -144,7 +138,7 @@
 {
   HttpClient c;
   c.SetHttpsVerifyPeers(false);
-  c.SetUrl("https://bitbucket.org/osimis/orthanc-setup-samples/raw/master/docker/serve-folders/orthanc/serve-folders.json");
+  c.SetUrl("https://raw.githubusercontent.com/orthanc-server/orthanc-setup-samples/refs/heads/master/docker/serve-folders/orthanc/serve-folders.json");
 
   Json::Value v;
   c.Apply(v);
@@ -391,6 +385,7 @@
   private:
     std::string type_;
     std::string subtype_;
+    HttpContentNegociation::Dictionary parameters_;
 
   public:
     AcceptHandler()
@@ -400,7 +395,8 @@
 
     void Reset()
     {
-      Handle("nope", "nope");
+      HttpContentNegociation::Dictionary parameters;
+      Handle("nope", "nope", parameters);
     }
 
     const std::string& GetType() const
@@ -413,11 +409,18 @@
       return subtype_;
     }
 
+    HttpContentNegociation::Dictionary& GetParameters()
+    {
+      return parameters_;
+    }
+
     virtual void Handle(const std::string& type,
-                        const std::string& subtype) ORTHANC_OVERRIDE
+                        const std::string& subtype,
+                        const HttpContentNegociation::Dictionary& parameters) ORTHANC_OVERRIDE
     {
       type_ = type;
       subtype_ = subtype;
+      parameters_ = parameters;
     }
   };
 }
@@ -437,22 +440,29 @@
     ASSERT_TRUE(d.Apply("audio/*; q=0.2, audio/basic"));
     ASSERT_EQ("audio", h.GetType());
     ASSERT_EQ("basic", h.GetSubType());
+    ASSERT_EQ(0u, h.GetParameters().size());
 
-    ASSERT_TRUE(d.Apply("audio/*; q=0.2, audio/nope"));
+    ASSERT_TRUE(d.Apply("audio/*; q=0.2 ;  type =   test   ;  hello  , audio/nope"));
     ASSERT_EQ("audio", h.GetType());
     ASSERT_EQ("mp3", h.GetSubType());
+    ASSERT_EQ(3u, h.GetParameters().size());
+    ASSERT_EQ("0.2", h.GetParameters() ["q"]);
+    ASSERT_EQ("test", h.GetParameters() ["type"]);
+    ASSERT_EQ("", h.GetParameters() ["hello"]);
     
     ASSERT_FALSE(d.Apply("application/*; q=0.2, application/pdf"));
     
-    ASSERT_TRUE(d.Apply("*/*; application/*; q=0.2, application/pdf"));
+    ASSERT_TRUE(d.Apply("*/*; hello=world, application/*; q=0.2, application/pdf"));
     ASSERT_EQ("audio", h.GetType());
+    ASSERT_EQ(1u, h.GetParameters().size());
+    ASSERT_EQ("world", h.GetParameters() ["hello"]);
   }
 
   // "This would be interpreted as "text/html and text/x-c are the
   // preferred media types, but if they do not exist, then send the
   // text/x-dvi entity, and if that does not exist, send the
   // text/plain entity.""
-  const std::string T1 = "text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c";
+  const std::string T1 = "text/plain; q=0.5, text/html ;  hello  = \"world\"   , text/x-dvi; q=0.8, text/x-c";
   
   {
     HttpContentNegociation d;
@@ -462,6 +472,8 @@
     ASSERT_TRUE(d.Apply(T1));
     ASSERT_EQ("text", h.GetType());
     ASSERT_EQ("html", h.GetSubType());
+    ASSERT_EQ(1u, h.GetParameters().size());
+    ASSERT_EQ("world", h.GetParameters() ["hello"]);
   }
   
   {
@@ -472,6 +484,7 @@
     ASSERT_TRUE(d.Apply(T1));
     ASSERT_EQ("text", h.GetType());
     ASSERT_EQ("x-c", h.GetSubType());
+    ASSERT_EQ(0u, h.GetParameters().size());
   }
   
   {
@@ -483,6 +496,15 @@
     ASSERT_TRUE(d.Apply(T1));
     ASSERT_EQ("text", h.GetType());
     ASSERT_TRUE(h.GetSubType() == "x-c" || h.GetSubType() == "html");
+    if (h.GetSubType() == "x-c")
+    {
+      ASSERT_EQ(0u, h.GetParameters().size());
+    }
+    else
+    {
+      ASSERT_EQ(1u, h.GetParameters().size());
+      ASSERT_EQ("world", h.GetParameters() ["hello"]);
+    }
   }
   
   {
@@ -492,6 +514,8 @@
     ASSERT_TRUE(d.Apply(T1));
     ASSERT_EQ("text", h.GetType());
     ASSERT_EQ("x-dvi", h.GetSubType());
+    ASSERT_EQ(1u, h.GetParameters().size());
+    ASSERT_EQ("0.8", h.GetParameters() ["q"]);
   }
   
   {
@@ -500,6 +524,51 @@
     ASSERT_TRUE(d.Apply(T1));
     ASSERT_EQ("text", h.GetType());
     ASSERT_EQ("plain", h.GetSubType());
+    ASSERT_EQ(1u, h.GetParameters().size());
+    ASSERT_EQ("0.5", h.GetParameters() ["q"]);
+  }
+
+  // Below are the tests from issue 216:
+  // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=216
+
+  {
+    HttpContentNegociation d;
+    d.Register("application/dicom+json", h);
+    ASSERT_TRUE(d.Apply("image/webp, */*;q=0.8, text/html, application/xhtml+xml, application/xml;q=0.9"));
+    ASSERT_EQ("application", h.GetType());
+    ASSERT_EQ("dicom+json", h.GetSubType());
+    ASSERT_EQ(1u, h.GetParameters().size());
+    ASSERT_EQ("0.8", h.GetParameters() ["q"]);
+  }
+
+  {
+    HttpContentNegociation d;
+    d.Register("application/dicom+json", h);
+    ASSERT_TRUE(d.Apply("image/webp, */*; q  = \"0.8\"  , text/html, application/xhtml+xml, application/xml;q=0.9"));
+    ASSERT_EQ("application", h.GetType());
+    ASSERT_EQ("dicom+json", h.GetSubType());
+    ASSERT_EQ(1u, h.GetParameters().size());
+    ASSERT_EQ("0.8", h.GetParameters() ["q"]);
+  }
+
+  {
+    HttpContentNegociation d;
+    d.Register("application/dicom+json", h);
+    ASSERT_TRUE(d.Apply("text/html, application/xhtml+xml, application/xml, image/webp, */*;q=0.8"));
+    ASSERT_EQ("application", h.GetType());
+    ASSERT_EQ("dicom+json", h.GetSubType());
+    ASSERT_EQ(1u, h.GetParameters().size());
+    ASSERT_EQ("0.8", h.GetParameters() ["q"]);
+  }
+
+  {
+    HttpContentNegociation d;
+    d.Register("application/dicom+json", h);
+    ASSERT_TRUE(d.Apply("text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2"));
+    ASSERT_EQ("application", h.GetType());
+    ASSERT_EQ("dicom+json", h.GetSubType());
+    ASSERT_EQ(1u, h.GetParameters().size());
+    ASSERT_EQ(".2", h.GetParameters() ["q"]);
   }
 }
 
@@ -1045,8 +1114,8 @@
 
 TEST(MultipartStreamReader, Issue190)
 {
-  // https://bugs.orthanc-server.com/show_bug.cgi?id=190
-  // https://hg.orthanc-server.com/orthanc-dicomweb/rev/6dc2f79b5579
+  // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=190
+  // https://orthanc.uclouvain.be/hg/orthanc-dicomweb/rev/6dc2f79b5579
 
   std::map<std::string, std::string> headers;
   headers["content-type"] = "multipart/related; type=application/dicom; boundary=0f3cf5c0-70e0-41ef-baef-c6f9f65ec3e1";
@@ -1255,7 +1324,7 @@
 
 TEST(HttpClient, DISABLED_Issue156_Slow)
 {
-  // https://bugs.orthanc-server.com/show_bug.cgi?id=156
+  // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=156
   
   TotoServer handler;
   HttpServer server;
--- a/OrthancFramework/UnitTestsSources/SQLiteChromiumTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/UnitTestsSources/SQLiteChromiumTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/UnitTestsSources/SQLiteTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/UnitTestsSources/SQLiteTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/UnitTestsSources/SharedLibraryUnitTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/UnitTestsSources/SharedLibraryUnitTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/UnitTestsSources/StreamTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/UnitTestsSources/StreamTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancFramework/UnitTestsSources/ToolboxTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/UnitTestsSources/ToolboxTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -280,6 +281,48 @@
   }
 }
 
+TEST(Toolbox, GetSetIntersection)
+{
+  {
+    std::set<int> target;
+    std::set<int> a;
+    std::set<int> b;
+
+    Toolbox::GetIntersection(target, a, b);
+    ASSERT_EQ(0u, target.size());
+  }
+
+  {
+    std::set<int> target;
+    std::set<int> a;
+    std::set<int> b;
+
+    a.insert(1);
+    b.insert(1);
+
+    Toolbox::GetIntersection(target, a, b);
+    ASSERT_EQ(1u, target.size());
+    ASSERT_EQ(1u, target.count(1));
+  }
+
+  {
+    std::set<int> target;
+    std::set<int> a;
+    std::set<int> b;
+
+    a.insert(1);
+    a.insert(2);
+    b.insert(2);
+
+    Toolbox::GetIntersection(target, a, b);
+    ASSERT_EQ(1u, target.size());
+    ASSERT_EQ(0u, target.count(1));
+    ASSERT_EQ(1u, target.count(2));
+  }
+
+}
+
+
 TEST(Toolbox, JoinStrings)
 {
   {
@@ -322,3 +365,46 @@
     ASSERT_EQ("1\\2", result);
   }
 }
+
+TEST(Toolbox, JoinUri)
+{
+  ASSERT_EQ("https://test.org/path", Toolbox::JoinUri("https://test.org", "path"));
+  ASSERT_EQ("https://test.org/path", Toolbox::JoinUri("https://test.org/", "path"));
+  ASSERT_EQ("https://test.org/path", Toolbox::JoinUri("https://test.org", "/path"));
+  ASSERT_EQ("https://test.org/path", Toolbox::JoinUri("https://test.org/", "/path"));
+
+  ASSERT_EQ("http://test.org:8042", Toolbox::JoinUri("http://test.org:8042", ""));
+  ASSERT_EQ("http://test.org:8042/", Toolbox::JoinUri("http://test.org:8042/", ""));
+}
+
+TEST(Toolbox, GetHumanFileSize)
+{
+  ASSERT_EQ("234bytes", Toolbox::GetHumanFileSize(234));
+  ASSERT_EQ("2.29KB", Toolbox::GetHumanFileSize(2345));
+  ASSERT_EQ("22.91KB", Toolbox::GetHumanFileSize(23456));
+  ASSERT_EQ("229.07KB", Toolbox::GetHumanFileSize(234567));
+  ASSERT_EQ("2.24MB", Toolbox::GetHumanFileSize(2345678));
+  ASSERT_EQ("22.37MB", Toolbox::GetHumanFileSize(23456789));
+  ASSERT_EQ("223.70MB", Toolbox::GetHumanFileSize(234567890));
+  ASSERT_EQ("2.18GB", Toolbox::GetHumanFileSize(2345678901));
+  ASSERT_EQ("21.33TB", Toolbox::GetHumanFileSize(23456789012345));
+}
+
+TEST(Toolbox, GetHumanDuration)
+{
+  ASSERT_EQ("234ns", Toolbox::GetHumanDuration(234));
+  ASSERT_EQ("2.35us", Toolbox::GetHumanDuration(2345));
+  ASSERT_EQ("23.46us", Toolbox::GetHumanDuration(23456));
+  ASSERT_EQ("234.57us", Toolbox::GetHumanDuration(234567));
+  ASSERT_EQ("2.35ms", Toolbox::GetHumanDuration(2345678));
+  ASSERT_EQ("2.35s", Toolbox::GetHumanDuration(2345678901));
+  ASSERT_EQ("23456.79s", Toolbox::GetHumanDuration(23456789012345));
+}
+
+TEST(Toolbox, GetHumanTransferSpeed)
+{
+  ASSERT_EQ("8.00Mbps", Toolbox::GetHumanTransferSpeed(false, 1000, 1000000));
+  ASSERT_EQ("8.59Gbps", Toolbox::GetHumanTransferSpeed(false, 1024*1024*1024, 1000000000));
+  ASSERT_EQ("1.00GB in 1.00s = 8.59Gbps", Toolbox::GetHumanTransferSpeed(true, 1024*1024*1024, 1000000000));
+  ASSERT_EQ("976.56KB in 1.00s = 8.00Mbps", Toolbox::GetHumanTransferSpeed(true, 1000*1000, 1000000000));
+}
\ No newline at end of file
--- a/OrthancFramework/UnitTestsSources/ZipTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancFramework/UnitTestsSources/ZipTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/CMakeLists.txt	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/CMakeLists.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -62,6 +63,7 @@
 SET(BUILD_HOUSEKEEPER ON CACHE BOOL "Whether to build the Housekeeper plugin")
 SET(BUILD_DELAYED_DELETION ON CACHE BOOL "Whether to build the DelayedDeletion plugin")
 SET(BUILD_ADVANCED_STORAGE ON CACHE BOOL "Whether to build the AdvancedStorage plugin")
+SET(BUILD_MULTITENANT_DICOM ON CACHE BOOL "Whether to build the MultitenantDicom plugin")
 SET(ENABLE_PLUGINS ON CACHE BOOL "Enable plugins")
 SET(UNIT_TESTS_WITH_HTTP_CONNEXIONS ON CACHE BOOL "Allow unit tests to make HTTP requests")
 
@@ -70,6 +72,11 @@
 ## Configuration of the Orthanc framework
 #####################################################################
 
+if (ENABLE_PLUGINS)
+  set(ENABLE_PROTOBUF ON)
+  set(ENABLE_PROTOBUF_COMPILER ON)
+endif()
+
 include(${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/CMake/VisualStudioPrecompiledHeaders.cmake)
 include(${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake)
 
@@ -82,6 +89,7 @@
 #####################################################################
 
 set(ORTHANC_SERVER_SOURCES
+  ${CMAKE_SOURCE_DIR}/Sources/Database/BaseDatabaseWrapper.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/DatabaseLookup.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/ICreateInstance.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/IGetChildrenMetadata.cpp
@@ -137,6 +145,7 @@
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/ResourceModificationJob.cpp
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/SplitStudyJob.cpp
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/StorageCommitmentScpJob.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp  
   ${CMAKE_SOURCE_DIR}/Sources/ServerToolbox.cpp
   ${CMAKE_SOURCE_DIR}/Sources/SliceOrdering.cpp
   ${CMAKE_SOURCE_DIR}/Sources/StorageCommitmentReports.cpp
@@ -223,18 +232,16 @@
 #####################################################################
 
 set(ORTHANC_EMBEDDED_FILES
-  CONFIGURATION_SAMPLE         ${CMAKE_SOURCE_DIR}/Resources/Configuration.json
-  DICOM_CONFORMANCE_STATEMENT  ${CMAKE_SOURCE_DIR}/Resources/DicomConformanceStatement.txt
-  FONT_UBUNTU_MONO_BOLD_16     ${CMAKE_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json
-  LUA_TOOLBOX                  ${CMAKE_SOURCE_DIR}/Resources/Toolbox.lua
-  PREPARE_DATABASE             ${CMAKE_SOURCE_DIR}/Sources/Database/PrepareDatabase.sql
-  UPGRADE_DATABASE_3_TO_4      ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade3To4.sql
-  UPGRADE_DATABASE_4_TO_5      ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade4To5.sql
-  INSTALL_REVISION_AND_CUSTOM_DATA
-  ${CMAKE_SOURCE_DIR}/Sources/Database/InstallRevisionAndCustomData.sql
-
-  INSTALL_TRACK_ATTACHMENTS_SIZE
-  ${CMAKE_SOURCE_DIR}/Sources/Database/InstallTrackAttachmentsSize.sql
+  CONFIGURATION_SAMPLE              ${CMAKE_SOURCE_DIR}/Resources/Configuration.json
+  DICOM_CONFORMANCE_STATEMENT       ${CMAKE_SOURCE_DIR}/Resources/DicomConformanceStatement.txt
+  FONT_UBUNTU_MONO_BOLD_16          ${CMAKE_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json
+  LUA_TOOLBOX                       ${CMAKE_SOURCE_DIR}/Resources/Toolbox.lua
+  PREPARE_DATABASE                  ${CMAKE_SOURCE_DIR}/Sources/Database/PrepareDatabase.sql
+  UPGRADE_DATABASE_3_TO_4           ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade3To4.sql
+  UPGRADE_DATABASE_4_TO_5           ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade4To5.sql
+  INSTALL_TRACK_ATTACHMENTS_SIZE    ${CMAKE_SOURCE_DIR}/Sources/Database/InstallTrackAttachmentsSize.sql
+  INSTALL_LABELS_TABLE              ${CMAKE_SOURCE_DIR}/Sources/Database/InstallLabelsTable.sql
+  INSTALL_REVISION_AND_CUSTOM_DATA  ${CMAKE_SOURCE_DIR}/Sources/Database/InstallRevisionAndCustomData.sql  
   )
 
 if (STANDALONE_BUILD)
@@ -284,6 +291,7 @@
 #####################################################################
 
 check_symbol_exists(mallopt "malloc.h" HAVE_MALLOPT)
+check_symbol_exists(malloc_trim "malloc.h" HAVE_MALLOC_TRIM)
 
 if (HAVE_MALLOPT)
   add_definitions(-DHAVE_MALLOPT=1)
@@ -291,6 +299,11 @@
   add_definitions(-DHAVE_MALLOPT=0)
 endif()
 
+if (HAVE_MALLOC_TRIM)
+  add_definitions(-DHAVE_MALLOC_TRIM=1)
+else()
+  add_definitions(-DHAVE_MALLOC_TRIM=0)
+endif()
 
 if (STATIC_BUILD)
   add_definitions(-DORTHANC_STATIC=1)
@@ -316,13 +329,9 @@
 add_definitions(
   -DORTHANC_BUILD_UNIT_TESTS=1
   -DORTHANC_BUILDING_SERVER_LIBRARY=1
-  
+
   # Macros for the plugins
   -DHAS_ORTHANC_EXCEPTION=0
-  -DMODALITY_WORKLISTS_VERSION="${ORTHANC_VERSION}"
-  -DSERVE_FOLDERS_VERSION="${ORTHANC_VERSION}"
-  -DHOUSEKEEPER_VERSION="${ORTHANC_VERSION}"
-  -DADVANCED_STORAGE_VERSION="${ORTHANC_VERSION}"
   )
 
 
@@ -334,7 +343,7 @@
 
 if (MSVC)
   add_definitions(-DORTHANC_USE_PRECOMPILED_HEADERS=1)
-
+  
   set(TMP
     ${ORTHANC_CORE_SOURCES_INTERNAL}
     ${ORTHANC_DICOM_SOURCES_INTERNAL}
@@ -359,8 +368,13 @@
 ## Build the core of Orthanc
 #####################################################################
 
+add_custom_target(AutogeneratedTarget
+  DEPENDS
+  ${AUTOGENERATED_SOURCES}
+  )
+
 # "CoreLibrary" contains all the third-party dependencies and the
-# content of the "Core" folder
+# content of the "OrthancFramework" folder
 add_library(CoreLibrary
   STATIC
   ${ORTHANC_CORE_PCH}
@@ -369,6 +383,10 @@
   ${AUTOGENERATED_SOURCES}
   )
 
+DefineSourceBasenameForTarget(CoreLibrary)
+
+add_dependencies(CoreLibrary AutogeneratedTarget)
+
 if (LIBICU_LIBRARIES)
   target_link_libraries(CoreLibrary ${LIBICU_LIBRARIES})
 endif()
@@ -378,20 +396,51 @@
 ## Build the Orthanc server
 #####################################################################
 
+if (ENABLE_PLUGINS)
+  add_custom_command(
+    COMMAND
+    ${PROTOC_EXECUTABLE} ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancDatabasePlugin.proto --cpp_out=${AUTOGENERATED_DIR} -I${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc
+    COMMAND
+    ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/Resources/PreventProtobufDirectoryLeaks.py ${AUTOGENERATED_DIR}/OrthancDatabasePlugin.pb.cc
+    DEPENDS
+    ProtobufCompiler
+    ${CMAKE_SOURCE_DIR}/Resources/PreventProtobufDirectoryLeaks.py
+    ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancDatabasePlugin.proto
+    OUTPUT
+    ${AUTOGENERATED_DIR}/OrthancDatabasePlugin.pb.cc
+    ${AUTOGENERATED_DIR}/OrthancDatabasePlugin.pb.h
+    )
+  
+  add_custom_target(OrthancDatabaseProtobuf
+    DEPENDS
+    ${AUTOGENERATED_DIR}/OrthancDatabasePlugin.pb.h
+    )
+
+  list(APPEND ORTHANC_SERVER_SOURCES
+    ${AUTOGENERATED_DIR}/OrthancDatabasePlugin.pb.cc
+    )
+else()
+  add_custom_target(OrthancDatabaseProtobuf)
+endif()
+
 add_library(ServerLibrary
   STATIC
   ${ORTHANC_SERVER_PCH}
   ${ORTHANC_SERVER_SOURCES}
   )
 
+DefineSourceBasenameForTarget(ServerLibrary)
+
 # Ensure autogenerated code is built before building ServerLibrary
-add_dependencies(ServerLibrary CoreLibrary)
+add_dependencies(ServerLibrary CoreLibrary OrthancDatabaseProtobuf)
 
 add_executable(Orthanc
   ${CMAKE_SOURCE_DIR}/Sources/main.cpp
   ${ORTHANC_RESOURCES}
   )
 
+DefineSourceBasenameForTarget(Orthanc)
+
 target_link_libraries(Orthanc ServerLibrary CoreLibrary ${DCMTK_LIBRARIES})
 
 if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
@@ -422,6 +471,8 @@
   ${BOOST_EXTENDED_SOURCES}
   )
 
+DefineSourceBasenameForTarget(UnitTests)
+
 target_link_libraries(UnitTests
   ServerLibrary
   CoreLibrary
@@ -431,28 +482,52 @@
 
 
 #####################################################################
-## Build a static library to share code between the plugins
+## Static library to share third-party libraries between the plugins
 #####################################################################
 
 if (ENABLE_PLUGINS AND
-    (BUILD_SERVE_FOLDERS OR BUILD_MODALITY_WORKLISTS OR BUILD_HOUSEKEEPER OR BUILD_ADVANCED_STORAGE))
-  add_library(ThirdPartyPlugins STATIC
+    (BUILD_SERVE_FOLDERS OR BUILD_MODALITY_WORKLISTS OR BUILD_HOUSEKEEPER OR
+      BUILD_DELAYED_DELETION OR BUILD_MULTITENANT_DICOM OR BUILD_ADVANCED_STORAGE))
+  set(PLUGINS_DEPENDENCIES_SOURCES
     ${BOOST_SOURCES}
     ${JSONCPP_SOURCES}
     ${LIBICONV_SOURCES}
     ${LIBICU_SOURCES}
+    ${PUGIXML_SOURCES}
+    ${UUID_SOURCES}
+    ${ZLIB_SOURCES}
+    
+    ${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/ThirdParty/base64/base64.cpp
+    ${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/ThirdParty/md5/md5.c
     Plugins/Samples/Common/OrthancPluginCppWrapper.cpp
     )
 
-  if (LIBICU_LIBRARIES)
-    target_link_libraries(ThirdPartyPlugins ${LIBICU_LIBRARIES})
+  if (BUILD_DELAYED_DELETION)
+    list(APPEND PLUGINS_DEPENDENCIES_SOURCES
+      ${SQLITE_SOURCES}
+      )
   endif()
+  
+  if (BUILD_MULTITENANT_DICOM)
+    list(APPEND PLUGINS_DEPENDENCIES_SOURCES
+      ${DCMTK_SOURCES}
+      ${OPENSSL_SOURCES}
+      ${LIBJPEG_SOURCES}
+      ${LIBPNG_SOURCES}
+      )
+  endif()
+  
+  add_library(PluginsDependencies STATIC
+    ${PLUGINS_DEPENDENCIES_SOURCES}
+    )
+
+  DefineSourceBasenameForTarget(PluginsDependencies)
 
   # Add the "-fPIC" option as this static library must be embedded
   # inside shared libraries (important on UNIX)
-  set_property(
-    TARGET ThirdPartyPlugins
-    PROPERTY POSITION_INDEPENDENT_CODE ON
+  set_target_properties(
+    PluginsDependencies
+    PROPERTIES POSITION_INDEPENDENT_CODE ON
     )
 endif()
 
@@ -478,12 +553,19 @@
     list(APPEND SERVE_FOLDERS_RESOURCES ${AUTOGENERATED_DIR}/ServeFolders.rc)
   endif()  
 
+  set_source_files_properties(
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/ServeFolders/Plugin.cpp
+    PROPERTIES COMPILE_DEFINITIONS "SERVE_FOLDERS_VERSION=\"${ORTHANC_VERSION}\""
+    )
+
   add_library(ServeFolders SHARED 
     ${CMAKE_SOURCE_DIR}/Plugins/Samples/ServeFolders/Plugin.cpp
     ${SERVE_FOLDERS_RESOURCES}
     )
 
-  target_link_libraries(ServeFolders ThirdPartyPlugins)
+  DefineSourceBasenameForTarget(ServeFolders)
+
+  target_link_libraries(ServeFolders PluginsDependencies)
 
   set_target_properties(
     ServeFolders PROPERTIES 
@@ -521,12 +603,19 @@
     list(APPEND MODALITY_WORKLISTS_RESOURCES ${AUTOGENERATED_DIR}/ModalityWorklists.rc)
   endif()
 
+  set_source_files_properties(
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/ModalityWorklists/Plugin.cpp
+    PROPERTIES COMPILE_DEFINITIONS "MODALITY_WORKLISTS_VERSION=\"${ORTHANC_VERSION}\""
+    )
+
   add_library(ModalityWorklists SHARED 
     ${CMAKE_SOURCE_DIR}/Plugins/Samples/ModalityWorklists/Plugin.cpp
     ${MODALITY_WORKLISTS_RESOURCES}
     )
 
-  target_link_libraries(ModalityWorklists ThirdPartyPlugins)
+  DefineSourceBasenameForTarget(ModalityWorklists)
+
+  target_link_libraries(ModalityWorklists PluginsDependencies)
 
   set_target_properties(
     ModalityWorklists PROPERTIES 
@@ -542,80 +631,67 @@
 endif()
 
 
-if (ENABLE_PLUGINS AND (BUILD_DELAYED_DELETION OR BUILD_CONNECTIVITY_CHECKS))
-  include(ExternalProject)
-
-endif()
-
 
 #####################################################################
 ## Build the "ConnectivityChecks" plugin
 #####################################################################
 
 if (ENABLE_PLUGINS AND BUILD_CONNECTIVITY_CHECKS)
-
-  set(ConnectivityChecksFlags)
-
-  if (CMAKE_TOOLCHAIN_FILE)
-    # Take absolute path to the toolchain
-    get_filename_component(TMP ${CMAKE_TOOLCHAIN_FILE} REALPATH BASE ${CMAKE_SOURCE_DIR})
-    list(APPEND ConnectivityChecksFlags -DCMAKE_TOOLCHAIN_FILE=${TMP})
-  endif()
-
-  if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
-    list(APPEND ConnectivityChecksFlags
-      -DLSB_CC=${CMAKE_LSB_CC}
-      -DLSB_CXX=${CMAKE_LSB_CXX}
+  if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+    execute_process(
+      COMMAND 
+      ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/WindowsResources.py
+      ${ORTHANC_VERSION} ConnectivityChecks ConnectivityChecks.dll "Orthanc plugin to show connectivity status"
+      ERROR_VARIABLE Failure
+      OUTPUT_FILE ${AUTOGENERATED_DIR}/ConnectivityChecks.rc
       )
+    
+    if (Failure)
+      message(FATAL_ERROR "Error while computing the version information: ${Failure}")
+    endif()
+    
+    list(APPEND CONNECTIVITY_CHECKS_RESOURCES ${AUTOGENERATED_DIR}/ConnectivityChecks.rc)
   endif()
 
-  externalproject_add(ConnectivityChecks
-    SOURCE_DIR "${CMAKE_SOURCE_DIR}/Plugins/Samples/ConnectivityChecks"
-
-    # We explicitly provide a build directory, in order to avoid paths
-    # that are too long on our Visual Studio 2008 CIS
-    BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/ConnectivityChecks-build"
-
-    # this helps triggering build when changing the external project
-    BUILD_ALWAYS 1
-
-    CMAKE_ARGS
-    -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
-    -DCMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_BINARY_DIR}
-    -DPLUGIN_VERSION=${ORTHANC_VERSION}
-    -DSTATIC_BUILD=${STATIC_BUILD}
-    -DALLOW_DOWNLOADS=${ALLOW_DOWNLOADS}
-    -DUSE_SYSTEM_BOOST=${USE_SYSTEM_BOOST}
-    -DUSE_LEGACY_JSONCPP=${USE_LEGACY_JSONCPP}
-    -DUSE_LEGACY_BOOST=${USE_LEGACY_BOOST}
-    ${ConnectivityChecksFlags}
-
-    -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
-    -DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS}
-    -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
-    -DCMAKE_C_FLAGS=${CMAKE_C_FLAGS}
-    -DCMAKE_OSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET}
-    -DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES}
+  include(${CMAKE_SOURCE_DIR}/Plugins/Samples/ConnectivityChecks/JavaScriptLibraries.cmake)
+  
+  EmbedResources(
+    --target=ConnectivityChecksResources
+    --framework-path=${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources
+    WEB_RESOURCES  ${CMAKE_SOURCE_DIR}/Plugins/Samples/ConnectivityChecks/WebResources
+    LIBRARIES      ${CONNECTIVITY_CHECKS_JAVASCRIPT_DIR}
     )
 
-  if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
-    if (MSVC)
-      set(Prefix "")
-    else()
-      set(Prefix "lib")  # MinGW
-    endif()
+  set_source_files_properties(
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/ConnectivityChecks/Plugin.cpp
+    PROPERTIES COMPILE_DEFINITIONS "ORTHANC_PLUGIN_NAME=\"connectivity-checks\";ORTHANC_PLUGIN_VERSION=\"${ORTHANC_VERSION}\""
+    )
+  
+  # The "OrthancFrameworkDependencies.cpp" file is used to bypass the
+  # precompiled headers if compiling with Visual Studio
+  add_library(ConnectivityChecks SHARED 
+    ${AUTOGENERATED_DIR}/ConnectivityChecksResources.cpp
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/ConnectivityChecks/Plugin.cpp
+    
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/ConnectivityChecks/OrthancFrameworkDependencies.cpp
+    ${CONNECTIVITY_CHECKS_RESOURCES}
+    )
 
-    install(FILES
-      ${CMAKE_CURRENT_BINARY_DIR}/${Prefix}ConnectivityChecks.dll
-      DESTINATION "lib")
-  else()
-    list(GET CMAKE_FIND_LIBRARY_PREFIXES 0 Prefix)
-    list(GET CMAKE_FIND_LIBRARY_SUFFIXES 0 Suffix)
-    install(FILES
-      ${CMAKE_CURRENT_BINARY_DIR}/${Prefix}ConnectivityChecks${Suffix}
-      ${CMAKE_CURRENT_BINARY_DIR}/${Prefix}ConnectivityChecks${Suffix}.${ORTHANC_VERSION}
-      DESTINATION "share/orthanc/plugins")
-  endif()
+  DefineSourceBasenameForTarget(ConnectivityChecks)
+
+  target_link_libraries(ConnectivityChecks PluginsDependencies)
+  
+  set_target_properties(
+    ConnectivityChecks PROPERTIES
+    VERSION ${ORTHANC_VERSION}
+    SOVERSION ${ORTHANC_VERSION}
+    )
+  
+  install(
+    TARGETS ConnectivityChecks
+    RUNTIME DESTINATION lib    # Destination for Windows
+    LIBRARY DESTINATION share/orthanc/plugins    # Destination for Linux
+    )
 endif()
 
 
@@ -624,69 +700,52 @@
 #####################################################################
 
 if (ENABLE_PLUGINS AND BUILD_DELAYED_DELETION)
-
-  set(DelayedDeletionFlags)
-
-  if (CMAKE_TOOLCHAIN_FILE)
-    # Take absolute path to the toolchain
-    get_filename_component(TMP ${CMAKE_TOOLCHAIN_FILE} REALPATH BASE ${CMAKE_SOURCE_DIR})
-    list(APPEND DelayedDeletionFlags -DCMAKE_TOOLCHAIN_FILE=${TMP})
-  endif()
-
-  if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
-    list(APPEND DelayedDeletionFlags
-      -DLSB_CC=${CMAKE_LSB_CC}
-      -DLSB_CXX=${CMAKE_LSB_CXX}
+  if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+    execute_process(
+      COMMAND 
+      ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/WindowsResources.py
+      ${ORTHANC_VERSION} DelayedDeletion DelayedDeletion.dll "Orthanc plugin to delay deletion of files"
+      ERROR_VARIABLE Failure
+      OUTPUT_FILE ${AUTOGENERATED_DIR}/DelayedDeletion.rc
       )
+    
+    if (Failure)
+      message(FATAL_ERROR "Error while computing the version information: ${Failure}")
+    endif()
+    
+    list(APPEND DELAYED_DELETION_RESOURCES ${AUTOGENERATED_DIR}/DelayedDeletion.rc)
   endif()
 
-  externalproject_add(DelayedDeletion
-    SOURCE_DIR "${CMAKE_SOURCE_DIR}/Plugins/Samples/DelayedDeletion"
-
-    # We explicitly provide a build directory, in order to avoid paths
-    # that are too long on our Visual Studio 2008 CIS
-    BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/DelayedDeletion-build"
-
-    # this helps triggering build when changing the external project
-    BUILD_ALWAYS 1
-
-    CMAKE_ARGS
-    -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
-    -DCMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_BINARY_DIR}
-    -DPLUGIN_VERSION=${ORTHANC_VERSION}
-    -DSTATIC_BUILD=${STATIC_BUILD}
-    -DALLOW_DOWNLOADS=${ALLOW_DOWNLOADS}
-    -DUSE_SYSTEM_BOOST=${USE_SYSTEM_BOOST}
-    -DUSE_LEGACY_JSONCPP=${USE_LEGACY_JSONCPP}
-    -DUSE_LEGACY_BOOST=${USE_LEGACY_BOOST}
-    ${DelayedDeletionFlags}
-
-    -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
-    -DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS}
-    -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
-    -DCMAKE_C_FLAGS=${CMAKE_C_FLAGS}
-    -DCMAKE_OSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET}
-    -DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES}
+  set_source_files_properties(
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/DelayedDeletion/Plugin.cpp
+    PROPERTIES COMPILE_DEFINITIONS "ORTHANC_PLUGIN_NAME=\"delayed-deletion\";ORTHANC_PLUGIN_VERSION=\"${ORTHANC_VERSION}\""
     )
 
-  if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
-    if (MSVC)
-      set(Prefix "")
-    else()
-      set(Prefix "lib")  # MinGW
-    endif()
+  # The "OrthancFrameworkDependencies.cpp" file is used to bypass the
+  # precompiled headers if compiling with Visual Studio
+  add_library(DelayedDeletion SHARED 
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/DelayedDeletion/PendingDeletionsDatabase.cpp
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/DelayedDeletion/Plugin.cpp
+    
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/DelayedDeletion/OrthancFrameworkDependencies.cpp
+    ${DELAYED_DELETION_RESOURCES}
+    )
+  
+  DefineSourceBasenameForTarget(DelayedDeletion)
 
-    install(FILES
-      ${CMAKE_CURRENT_BINARY_DIR}/${Prefix}DelayedDeletion.dll
-      DESTINATION "lib")
-  else()
-    list(GET CMAKE_FIND_LIBRARY_PREFIXES 0 Prefix)
-    list(GET CMAKE_FIND_LIBRARY_SUFFIXES 0 Suffix)
-    install(FILES
-      ${CMAKE_CURRENT_BINARY_DIR}/${Prefix}DelayedDeletion${Suffix}
-      ${CMAKE_CURRENT_BINARY_DIR}/${Prefix}DelayedDeletion${Suffix}.${ORTHANC_VERSION}
-      DESTINATION "share/orthanc/plugins")
-  endif()
+  target_link_libraries(DelayedDeletion PluginsDependencies)
+  
+  set_target_properties(
+    DelayedDeletion PROPERTIES
+    VERSION ${ORTHANC_VERSION}
+    SOVERSION ${ORTHANC_VERSION}
+    )
+  
+  install(
+    TARGETS DelayedDeletion
+    RUNTIME DESTINATION lib    # Destination for Windows
+    LIBRARY DESTINATION share/orthanc/plugins    # Destination for Linux
+    )
 endif()
 
 
@@ -711,14 +770,19 @@
     list(APPEND HOUSEKEEPER_RESOURCES ${AUTOGENERATED_DIR}/Housekeeper.rc)
   endif()
   
+  set_source_files_properties(
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/Housekeeper/Plugin.cpp
+    PROPERTIES COMPILE_DEFINITIONS "HOUSEKEEPER_VERSION=\"${ORTHANC_VERSION}\""
+    )
+
   add_library(Housekeeper SHARED 
     ${CMAKE_SOURCE_DIR}/Plugins/Samples/Housekeeper/Plugin.cpp
     ${HOUSEKEEPER_RESOURCES}
     )
   
-  target_link_libraries(Housekeeper 
-    ThirdPartyPlugins
-    )
+  DefineSourceBasenameForTarget(Housekeeper)
+
+  target_link_libraries(Housekeeper PluginsDependencies)
   
   set_target_properties(
     Housekeeper PROPERTIES 
@@ -739,69 +803,120 @@
 #####################################################################
 
 if (ENABLE_PLUGINS AND BUILD_ADVANCED_STORAGE)
-
-  set(AdvancedStorageFlags)
+  if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+    execute_process(
+      COMMAND 
+      ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/WindowsResources.py
+      ${ORTHANC_VERSION} AdvancedStorage AdvancedStorage.dll "Orthanc plugin to provide advanced file storage"
+      ERROR_VARIABLE Failure
+      OUTPUT_FILE ${AUTOGENERATED_DIR}/AdvancedStorage.rc
+      )
 
-  if (CMAKE_TOOLCHAIN_FILE)
-    # Take absolute path to the toolchain
-    get_filename_component(TMP ${CMAKE_TOOLCHAIN_FILE} REALPATH BASE ${CMAKE_SOURCE_DIR})
-    list(APPEND AdvancedStorageFlags -DCMAKE_TOOLCHAIN_FILE=${TMP})
-  endif()
+    if (Failure)
+      message(FATAL_ERROR "Error while computing the version information: ${Failure}")
+    endif()
 
-  if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
-    list(APPEND AdvancedStorageFlags
-      -DLSB_CC=${CMAKE_LSB_CC}
-      -DLSB_CXX=${CMAKE_LSB_CXX}
-      )
+    list(APPEND ADVANCED_STORAGE_RESOURCES ${AUTOGENERATED_DIR}/AdvancedStorage.rc)
   endif()
 
-  externalproject_add(AdvancedStorage
-    SOURCE_DIR "${CMAKE_SOURCE_DIR}/Plugins/Samples/AdvancedStorage"
-
-    # We explicitly provide a build directory, in order to avoid paths
-    # that are too long on our Visual Studio 2008 CIS
-    BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/AdvancedStorage-build"
-
-    # this helps triggering build when changing the external project
-    BUILD_ALWAYS 1
+  set_source_files_properties(
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/AdvancedStorage/Plugin.cpp
+    PROPERTIES COMPILE_DEFINITIONS "ADVANCED_STORAGE_VERSION=\"${ORTHANC_VERSION}\""
+    )
 
-    CMAKE_ARGS
-    -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
-    -DCMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_BINARY_DIR}
-    -DPLUGIN_VERSION=${ORTHANC_VERSION}
-    -DSTATIC_BUILD=${STATIC_BUILD}
-    -DALLOW_DOWNLOADS=${ALLOW_DOWNLOADS}
-    -DUSE_SYSTEM_BOOST=${USE_SYSTEM_BOOST}
-    -DUSE_LEGACY_JSONCPP=${USE_LEGACY_JSONCPP}
-    -DUSE_LEGACY_BOOST=${USE_LEGACY_BOOST}
-    ${AdvancedStorageFlags}
-
-    -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
-    -DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS}
-    -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
-    -DCMAKE_C_FLAGS=${CMAKE_C_FLAGS}
-    -DCMAKE_OSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET}
-    -DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES}
+  add_library(AdvancedStorage SHARED 
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/AdvancedStorage/Plugin.cpp
+    ${ORTHANC}
+    ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/OrthancException.cpp
+    ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/SystemToolbox.cpp
+    ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/Toolbox.cpp
+    ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/Logging.cpp
+    ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/ChunkedBuffer.cpp
+    ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/Enumerations.cpp
     )
 
+  DefineSourceBasenameForTarget(AdvancedStorage)
+
+  target_link_libraries(AdvancedStorage PluginsDependencies)
+  
+  set_target_properties(
+    AdvancedStorage PROPERTIES 
+    VERSION ${ORTHANC_VERSION} 
+    SOVERSION ${ORTHANC_VERSION}
+    )
+  
+  install(
+    TARGETS AdvancedStorage
+    RUNTIME DESTINATION lib    # Destination for Windows
+    LIBRARY DESTINATION share/orthanc/plugins    # Destination for Linux
+    )
+endif()
+
+#####################################################################
+## Build the "MultitenantDicom" plugin
+#####################################################################
+
+if (ENABLE_PLUGINS AND BUILD_MULTITENANT_DICOM)
   if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
-    if (MSVC)
-      set(Prefix "")
-    else()
-      set(Prefix "lib")  # MinGW
+    execute_process(
+      COMMAND 
+      ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/WindowsResources.py
+      ${ORTHANC_VERSION} MultitenantDicom MultitenantDicom.dll "Orthanc plugin to provide a multitenant DICOM server"
+      ERROR_VARIABLE Failure
+      OUTPUT_FILE ${AUTOGENERATED_DIR}/MultitenantDicom.rc
+      )
+    
+    if (Failure)
+      message(FATAL_ERROR "Error while computing the version information: ${Failure}")
     endif()
+    
+    list(APPEND MULTITENANT_DICOM_RESOURCES ${AUTOGENERATED_DIR}/MultitenantDicom.rc)
+  endif()
 
-    install(FILES
-      ${CMAKE_CURRENT_BINARY_DIR}/${Prefix}AdvancedStorage.dll
-      DESTINATION "lib")
-  else()
-    list(GET CMAKE_FIND_LIBRARY_PREFIXES 0 Prefix)
-    list(GET CMAKE_FIND_LIBRARY_SUFFIXES 0 Suffix)
-    install(FILES
-      ${CMAKE_CURRENT_BINARY_DIR}/${Prefix}AdvancedStorage${Suffix}
-      ${CMAKE_CURRENT_BINARY_DIR}/${Prefix}AdvancedStorage${Suffix}.${ORTHANC_VERSION}
-      DESTINATION "share/orthanc/plugins")
-  endif()
+  EmbedResources(
+    --target=MultitenantDicomResources
+    --namespace=Orthanc.FrameworkResources
+    --framework-path=${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources
+    ${LIBICU_RESOURCES}
+    ${DCMTK_DICTIONARIES}
+    )
+
+  set_source_files_properties(
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/Plugin.cpp
+    PROPERTIES COMPILE_DEFINITIONS "ORTHANC_PLUGIN_VERSION=\"${ORTHANC_VERSION}\""
+    )
+
+  # The "OrthancFrameworkDependencies.cpp" file is used to bypass the
+  # precompiled headers if compiling with Visual Studio
+  add_library(MultitenantDicom SHARED 
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/DicomFilter.cpp
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/FindRequestHandler.cpp
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/MoveRequestHandler.cpp
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.cpp
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/Plugin.cpp
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/PluginToolbox.cpp
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/StoreRequestHandler.cpp    
+
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/OrthancFrameworkDependencies.cpp
+    ${AUTOGENERATED_DIR}/MultitenantDicomResources.cpp
+    ${MULTITENANT_DICOM_RESOURCES}
+    )
+  
+  DefineSourceBasenameForTarget(MultitenantDicom)
+
+  target_link_libraries(MultitenantDicom PluginsDependencies ${DCMTK_LIBRARIES})
+  
+  set_target_properties(
+    MultitenantDicom PROPERTIES
+    VERSION ${ORTHANC_VERSION}
+    SOVERSION ${ORTHANC_VERSION}
+    )
+  
+  install(
+    TARGETS MultitenantDicom
+    RUNTIME DESTINATION lib    # Destination for Windows
+    LIBRARY DESTINATION share/orthanc/plugins    # Destination for Linux
+    )
 endif()
 
 
@@ -834,6 +949,7 @@
   endif()
 
   add_executable(OrthancRecoverCompressedFile ${RECOVER_COMPRESSED_SOURCES})
+  DefineSourceBasenameForTarget(OrthancRecoverCompressedFile)
 
   target_link_libraries(OrthancRecoverCompressedFile CoreLibrary)
 
@@ -890,8 +1006,9 @@
 if (ENABLE_PLUGINS)
   install(
     FILES
-    ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancCPlugin.h 
-    ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancCDatabasePlugin.h 
+    ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancCPlugin.h
+    ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancCDatabasePlugin.h
+    ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancDatabasePlugin.proto
     DESTINATION include/orthanc
     )
 endif()
--- a/OrthancServer/OrthancExplorer/explorer.css	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/OrthancExplorer/explorer.css	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -63,3 +64,22 @@
 .switch-container .ui-slider-switch {
     width: 100%;
 }
+
+.label {
+    display: inline-block;
+    background-color: gray;
+    margin: 5px;
+    padding: 5px;
+    border-radius: 10px;
+}
+
+.label button {
+    background-color: transparent;
+    border: 0px;
+    cursor: pointer;
+    border-radius: 10px;
+}
+
+.label button:hover {
+    background-color: lightgray;
+}
--- a/OrthancServer/OrthancExplorer/explorer.html	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/OrthancExplorer/explorer.html	Tue Sep 24 11:39:52 2024 +0200
@@ -81,6 +81,11 @@
           <input type="text" name="lookup-study-description" id="lookup-study-description" value="" />
         </div>
 
+        <div data-role="fieldcontain" id="lookup-study-labels-div">
+          <label for="lookup-study-labels">Labels:</label>
+          <input type="text" name="lookup-study-description" id="lookup-study-labels" value="" />
+        </div>
+
         <div data-role="fieldcontain">
           <label for="lookup-study-date">Study Date:</label>
           <select name="lookup-study-date" id="lookup-study-date">
@@ -262,6 +267,7 @@
               </li>
               <li data-icon="gear"><a href="#" id="patient-archive">Download ZIP</a></li>
               <li data-icon="gear"><a href="#" id="patient-media">Download DICOMDIR</a></li>
+              <li data-icon="gear"><a href="#" id="patient-archive-link">Copy link to ZIP</a></li>
             </ul>
           </div>
         </div>
@@ -316,6 +322,7 @@
               </li>
               <li data-icon="gear"><a href="#" id="study-archive">Download ZIP</a></li>
               <li data-icon="gear"><a href="#" id="study-media">Download DICOMDIR</a></li>
+              <li data-icon="gear"><a href="#" id="study-archive-link">Copy link to ZIP</a></li>
             </ul>
           </div>
         </div>
@@ -372,6 +379,7 @@
               <li data-icon="search"><a href="#" id="series-preview">Preview this series</a></li>
               <li data-icon="gear"><a href="#" id="series-archive">Download ZIP</a></li>
               <li data-icon="gear"><a href="#" id="series-media">Download DICOMDIR</a></li>
+              <li data-icon="gear"><a href="#" id="series-archive-link">Copy link to ZIP</a></li>
             </ul>
           </div>
         </div>
@@ -427,6 +435,7 @@
               </li>
               <li data-icon="arrow-d"><a href="#" id="instance-download-dicom">Download the DICOM file</a></li>
               <li data-icon="arrow-d"><a href="#" id="instance-download-json">Download the JSON file</a></li>
+              <li data-icon="gear"><a href="#" id="instance-download-link">Copy link to the DICOM file</a></li>
               <li data-icon="search"><a href="#" id="instance-preview">Preview the instance</a></li>
             </ul>
           </div>
@@ -527,6 +536,7 @@
               <input type="checkbox" name="DR" id="qr-dr" class="custom" /> <label for="qr-dr">DR</label>
               <input type="checkbox" name="DX" id="qr-dx" class="custom" /> <label for="qr-dx">DX</label>
               <input type="checkbox" name="MG" id="qr-mg" class="custom" /> <label for="qr-mg">MG</label>
+              <input type="checkbox" name="XC" id="qr-xc" class="custom" /> <label for="qr-xc">XC</label>
             </fieldset>
           </div>
         </div>
--- a/OrthancServer/OrthancExplorer/explorer.js	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/OrthancExplorer/explorer.js	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -65,6 +66,12 @@
 var MODIFIED_FROM = 'ModifiedFrom';
 
 
+function IsAlphanumeric(s)
+{
+  return s.match(/^[0-9a-zA-Z]+$/);
+}
+
+
 function DeepCopy(obj)
 {
   return jQuery.extend(true, {}, obj);
@@ -479,6 +486,14 @@
       } else {
         $('.warning-insecure').hide();
       }
+
+      // New in Orthanc 1.12.0
+      if ('HasLabels' in s &&
+          s.HasLabels) {
+        $('#lookup-study-labels-div').show();
+      } else {
+        $('#lookup-study-labels-div').hide();
+      }
     }
   });
 });
@@ -550,6 +565,10 @@
       else if (input.id == 'lookup-study-date-specific') {
         // Ignore
       }
+      else if (input.id == 'lookup-study-labels') {
+        // New in Orthanc 1.12.0
+        lookup['Labels'] = input.value.split(' ');
+      }
       else {
         console.error('Unknown lookup field: ' + input.id);
       }
@@ -696,14 +715,99 @@
     $('.' + 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');
   });
+}
 
+
+
+function RefreshLabels(nodeLabels, resourceLevel, resourceId)
+{
+  GetResource('/' + resourceLevel + '/' + resourceId + '/labels', function(labels) {
+    nodeLabels.empty();
+    
+    if (labels.length > 0) {
+      nodeLabels.css('display', 'block');
+
+      for (var i = 0; i < labels.length; i++) {
+        var removeButton = $('<button>').text('X').attr('title', 'Remove label "' + labels[i] + '"');
+
+        removeButton.click({
+          label : labels[i],
+          nodeLabels : nodeLabels
+        }, function(s) {
+          $.ajax({
+            url: '../' + resourceLevel + '/' + resourceId + '/labels/' + s.data.label,
+            dataType: 'json',
+            type: 'DELETE',
+            success: function(ss) {
+              RefreshLabels(nodeLabels, resourceLevel, resourceId);
+            }
+          });
+        });
+        
+        nodeLabels.append($('<span>').text(labels[i] + ' ').addClass('label').append(removeButton));
+      }
+    } else {
+      nodeLabels.css('display', 'none');
+    }
+  });
 }
 
+
+function ConfigureLabels(target, system, resourceLevel, resourceId)
+{
+  if (system.HasLabels === true) {
+    var nodeLabels = $('<li>').append($('<div>'));
+    var addLabelButton = $('<a>').text('Add label');
+
+    RefreshLabels(nodeLabels, resourceLevel, resourceId);
+
+    addLabelButton.click(function(s) {
+      $('#dialog').simpledialog2({
+        mode: 'button',
+        animate: false,
+        headerText: 'Add label',
+        headerClose: true,
+        buttonPrommpt: 'Enter the new label',
+        buttonInput: true,
+        width: '100%',
+        buttons : {
+          'OK': {
+            click: function () {
+              var label = $.mobile.sdLastInput;
+              if (label.length > 0) {
+                if (IsAlphanumeric(label)) {
+                  $.ajax({
+                    url: '../' + resourceLevel + '/' + resourceId + '/labels/' + label,
+                    dataType: 'json',
+                    type: 'PUT',
+                    data: '',
+                    success: function(ss) {
+                      RefreshLabels(nodeLabels, resourceLevel, resourceId);
+                    }
+                  });
+                } else {
+                  alert('Error: Labels can only contain alphanumeric characters');
+                }
+              }
+            }
+          },
+        }
+      });
+    });
+
+    target
+      .append('<li data-role="list-divider">Labels</li>')
+      .append(nodeLabels)
+      .append($('<li>').attr('data-icon', 'plus').append(addLabelButton));
+  }
+}
+
+
 function RefreshPatient()
 {
   var pageData, target, v;
@@ -711,53 +815,57 @@
   if ($.mobile.pageData) {
     pageData = DeepCopy($.mobile.pageData);
 
-    GetResource('/patients/' + pageData.uuid + '?full', function(patient) {
-      GetResource('/patients/' + pageData.uuid + '/studies?full', function(studies) {
-        SortOnDicomTag(studies, STUDY_DATE, false, true);
+    GetResource('/system', function(system) {
+      GetResource('/patients/' + pageData.uuid + '?full', function(patient) {
+        GetResource('/patients/' + pageData.uuid + '/studies?full', function(studies) {
+          SortOnDicomTag(studies, STUDY_DATE, false, true);
 
-        $('#patient-info li').remove();
-        $('#patient-info')
-          .append('<li data-role="list-divider">Patient</li>')
-          .append(FormatPatient(patient))
-          .listview('refresh');
+          $('#patient-info li').remove();
+
+          var info = $('#patient-info')
+              .append('<li data-role="list-divider">Patient</li>')
+              .append(FormatPatient(patient));
+          ConfigureLabels(info, system, 'patients', patient.ID);
+          info.listview('refresh');
 
-        target = $('#list-studies');
-        $('li', target).remove();
-        
-        for (var i = 0; i < studies.length; i++) {
-          if (i == 0 ||
-              GetMainDicomTag(studies[i].MainDicomTags, STUDY_DATE) !=
-              GetMainDicomTag(studies[i - 1].MainDicomTags, STUDY_DATE))
-          {
-            target.append($('<li>')
-                          .attr('data-role', 'list-divider')
-                          .text(FormatDicomDate(GetMainDicomTag(studies[i].MainDicomTags, STUDY_DATE))));
+          target = $('#list-studies');
+          $('li', target).remove();
+          
+          for (var i = 0; i < studies.length; i++) {
+            if (i == 0 ||
+                GetMainDicomTag(studies[i].MainDicomTags, STUDY_DATE) !=
+                GetMainDicomTag(studies[i - 1].MainDicomTags, STUDY_DATE))
+            {
+              target.append($('<li>')
+                            .attr('data-role', 'list-divider')
+                            .text(FormatDicomDate(GetMainDicomTag(studies[i].MainDicomTags, STUDY_DATE))));
+            }
+
+            target.append(FormatStudy(studies[i], '#study?uuid=' + studies[i].ID));
           }
 
-          target.append(FormatStudy(studies[i], '#study?uuid=' + studies[i].ID));
-        }
+          SetupAnonymizedOrModifiedFrom('#patient-anonymized-from', patient, 'patient', ANONYMIZED_FROM);
+          SetupAnonymizedOrModifiedFrom('#patient-modified-from', patient, 'patient', MODIFIED_FROM);
+          SetupAttachments('#patient-access', 'patient-attachment', pageData.uuid, 'patients');
 
-        SetupAnonymizedOrModifiedFrom('#patient-anonymized-from', patient, 'patient', ANONYMIZED_FROM);
-        SetupAnonymizedOrModifiedFrom('#patient-modified-from', patient, 'patient', MODIFIED_FROM);
-        SetupAttachments('#patient-access', 'patient-attachment', pageData.uuid, 'patients');
-
-        target.listview('refresh');
+          target.listview('refresh');
 
-        // Check whether this patient is protected
-        $.ajax({
-          url: '../patients/' + pageData.uuid + '/protected',
-          type: 'GET',
-          dataType: 'text',
-          async: false,
-          cache: false,
-          success: function (s) {
-            v = (s == '1') ? 'on' : 'off';
-            $('#protection').val(v).slider('refresh');
-          }
+          // Check whether this patient is protected
+          $.ajax({
+            url: '../patients/' + pageData.uuid + '/protected',
+            type: 'GET',
+            dataType: 'text',
+            async: false,
+            cache: false,
+            success: function (s) {
+              v = (s == '1') ? 'on' : 'off';
+              $('#protection').val(v).slider('refresh');
+            }
+          });
+
+          currentPage = 'patient';
+          currentUuid = pageData.uuid;
         });
-
-        currentPage = 'patient';
-        currentUuid = pageData.uuid;
       });
     });
   }
@@ -771,43 +879,47 @@
   if ($.mobile.pageData) {
     pageData = DeepCopy($.mobile.pageData);
 
-    GetResource('/studies/' + pageData.uuid + '?full', function(study) {
-      GetResource('/patients/' + study.ParentPatient + '?full', function(patient) {
-        GetResource('/studies/' + pageData.uuid + '/series?full', function(series) {
-          SortOnDicomTag(series, SERIES_DATE, false, true);
+    GetResource('/system', function(system) {
+      GetResource('/studies/' + pageData.uuid + '?full', function(study) {
+        GetResource('/patients/' + study.ParentPatient + '?full', function(patient) {
+          GetResource('/studies/' + pageData.uuid + '/series?full', function(series) {
+            SortOnDicomTag(series, SERIES_DATE, false, true);
+            
+            $('#study .patient-link').attr('href', '#patient?uuid=' + patient.ID);
+            $('#study-info li').remove();
 
-          $('#study .patient-link').attr('href', '#patient?uuid=' + patient.ID);
-          $('#study-info li').remove();
-          $('#study-info')
-            .append('<li data-role="list-divider">Patient</li>')
-            .append(FormatPatient(patient, '#patient?uuid=' + patient.ID, true))
-            .append('<li data-role="list-divider">Study</li>')
-            .append(FormatStudy(study))
-            .listview('refresh');
+            var info = $('#study-info')
+                .append('<li data-role="list-divider">Patient</li>')
+                .append(FormatPatient(patient, '#patient?uuid=' + patient.ID, true))
+                .append('<li data-role="list-divider">Study</li>')
+                .append(FormatStudy(study));
+            ConfigureLabels(info, system, 'studies', study.ID);
+            info.listview('refresh');
 
-          SetupAnonymizedOrModifiedFrom('#study-anonymized-from', study, 'study', ANONYMIZED_FROM);
-          SetupAnonymizedOrModifiedFrom('#study-modified-from', study, 'study', MODIFIED_FROM);
-          SetupAttachments('#study-access', 'study-attachment', pageData.uuid, 'studies');
+            SetupAnonymizedOrModifiedFrom('#study-anonymized-from', study, 'study', ANONYMIZED_FROM);
+            SetupAnonymizedOrModifiedFrom('#study-modified-from', study, 'study', MODIFIED_FROM);
+            SetupAttachments('#study-access', 'study-attachment', pageData.uuid, 'studies');
 
-          target = $('#list-series');
-          $('li', target).remove();
-          for (var i = 0; i < series.length; i++) {
-            if (i == 0 ||
-                GetMainDicomTag(series[i].MainDicomTags, SERIES_DATE) !=
-                GetMainDicomTag(series[i - 1].MainDicomTags, SERIES_DATE))
-            {
-              target.append($('<li>')
-                            .attr('data-role', 'list-divider')
-                            .text(FormatDicomDate(GetMainDicomTag(series[i].MainDicomTags, SERIES_DATE))));
+            target = $('#list-series');
+            $('li', target).remove();
+            for (var i = 0; i < series.length; i++) {
+              if (i == 0 ||
+                  GetMainDicomTag(series[i].MainDicomTags, SERIES_DATE) !=
+                  GetMainDicomTag(series[i - 1].MainDicomTags, SERIES_DATE))
+              {
+                target.append($('<li>')
+                              .attr('data-role', 'list-divider')
+                              .text(FormatDicomDate(GetMainDicomTag(series[i].MainDicomTags, SERIES_DATE))));
+              }
+              
+              target.append(FormatSeries(series[i], '#series?uuid=' + series[i].ID));
             }
-            
-            target.append(FormatSeries(series[i], '#series?uuid=' + series[i].ID));
-          }
-          target.listview('refresh');
+            target.listview('refresh');
 
 
-          currentPage = 'study';
-          currentUuid = pageData.uuid;
+            currentPage = 'study';
+            currentUuid = pageData.uuid;
+          });
         });
       });
     });
@@ -822,38 +934,41 @@
   if ($.mobile.pageData) {
     pageData = DeepCopy($.mobile.pageData);
 
-    GetResource('/series/' + pageData.uuid + '?full', function(series) {
-      GetResource('/studies/' + series.ParentStudy + '?full', function(study) {
-        GetResource('/patients/' + study.ParentPatient + '?full', function(patient) {
-          GetResource('/series/' + pageData.uuid + '/instances?full', function(instances) {
-            Sort(instances, function(x) { return x.IndexInSeries; }, true, false);
+    GetResource('/system', function(system) {
+      GetResource('/series/' + pageData.uuid + '?full', function(series) {
+        GetResource('/studies/' + series.ParentStudy + '?full', function(study) {
+          GetResource('/patients/' + study.ParentPatient + '?full', function(patient) {
+            GetResource('/series/' + pageData.uuid + '/instances?full', function(instances) {
+              Sort(instances, function(x) { return x.IndexInSeries; }, true, false);
 
-            $('#series .patient-link').attr('href', '#patient?uuid=' + patient.ID);
-            $('#series .study-link').attr('href', '#study?uuid=' + study.ID);
+              $('#series .patient-link').attr('href', '#patient?uuid=' + patient.ID);
+              $('#series .study-link').attr('href', '#study?uuid=' + study.ID);
 
-            $('#series-info li').remove();
-            $('#series-info')
-              .append('<li data-role="list-divider">Patient</li>')
-              .append(FormatPatient(patient, '#patient?uuid=' + patient.ID, true))
-              .append('<li data-role="list-divider">Study</li>')
-              .append(FormatStudy(study, '#study?uuid=' + study.ID, true))
-              .append('<li data-role="list-divider">Series</li>')
-              .append(FormatSeries(series))
-              .listview('refresh');
+              $('#series-info li').remove();
+              var info = $('#series-info')
+                  .append('<li data-role="list-divider">Patient</li>')
+                  .append(FormatPatient(patient, '#patient?uuid=' + patient.ID, true))
+                  .append('<li data-role="list-divider">Study</li>')
+                  .append(FormatStudy(study, '#study?uuid=' + study.ID, true))
+                  .append('<li data-role="list-divider">Series</li>')
+                  .append(FormatSeries(series));
+              ConfigureLabels(info, system, 'series', series.ID);
+              info.listview('refresh');
 
-            SetupAnonymizedOrModifiedFrom('#series-anonymized-from', series, 'series', ANONYMIZED_FROM);
-            SetupAnonymizedOrModifiedFrom('#series-modified-from', series, 'series', MODIFIED_FROM);
-            SetupAttachments('#series-access', 'series-attachment', pageData.uuid, 'series');
+              SetupAnonymizedOrModifiedFrom('#series-anonymized-from', series, 'series', ANONYMIZED_FROM);
+              SetupAnonymizedOrModifiedFrom('#series-modified-from', series, 'series', MODIFIED_FROM);
+              SetupAttachments('#series-access', 'series-attachment', pageData.uuid, 'series');
 
-            target = $('#list-instances');
-            $('li', target).remove();
-            for (var i = 0; i < instances.length; i++) {
-              target.append(FormatInstance(instances[i], '#instance?uuid=' + instances[i].ID));
-            }
-            target.listview('refresh');
+              target = $('#list-instances');
+              $('li', target).remove();
+              for (var i = 0; i < instances.length; i++) {
+                target.append(FormatInstance(instances[i], '#instance?uuid=' + instances[i].ID));
+              }
+              target.listview('refresh');
 
-            currentPage = 'series';
-            currentUuid = pageData.uuid;
+              currentPage = 'series';
+              currentUuid = pageData.uuid;
+            });
           });
         });
       });
@@ -935,51 +1050,54 @@
   if ($.mobile.pageData) {
     pageData = DeepCopy($.mobile.pageData);
 
-    GetResource('/instances/' + pageData.uuid + '?full', function(instance) {
-      GetResource('/series/' + instance.ParentSeries + '?full', function(series) {
-        GetResource('/studies/' + series.ParentStudy + '?full', function(study) {
-          GetResource('/patients/' + study.ParentPatient + '?full', function(patient) {
+    GetResource('/system', function(system) {
+      GetResource('/instances/' + pageData.uuid + '?full', function(instance) {
+        GetResource('/series/' + instance.ParentSeries + '?full', function(series) {
+          GetResource('/studies/' + series.ParentStudy + '?full', function(study) {
+            GetResource('/patients/' + study.ParentPatient + '?full', function(patient) {
 
-            $('#instance .patient-link').attr('href', '#patient?uuid=' + patient.ID);
-            $('#instance .study-link').attr('href', '#study?uuid=' + study.ID);
-            $('#instance .series-link').attr('href', '#series?uuid=' + series.ID);
-            
-            $('#instance-info li').remove();
-            $('#instance-info')
-              .append('<li data-role="list-divider">Patient</li>')
-              .append(FormatPatient(patient, '#patient?uuid=' + patient.ID, true))
-              .append('<li data-role="list-divider">Study</li>')
-              .append(FormatStudy(study, '#study?uuid=' + study.ID, true))
-              .append('<li data-role="list-divider">Series</li>')
-              .append(FormatSeries(series, '#series?uuid=' + series.ID, true))
-              .append('<li data-role="list-divider">Instance</li>')
-              .append(FormatInstance(instance))
-              .listview('refresh');
+              $('#instance .patient-link').attr('href', '#patient?uuid=' + patient.ID);
+              $('#instance .study-link').attr('href', '#study?uuid=' + study.ID);
+              $('#instance .series-link').attr('href', '#series?uuid=' + series.ID);
+              
+              $('#instance-info li').remove();
+              var info = $('#instance-info')
+                  .append('<li data-role="list-divider">Patient</li>')
+                  .append(FormatPatient(patient, '#patient?uuid=' + patient.ID, true))
+                  .append('<li data-role="list-divider">Study</li>')
+                  .append(FormatStudy(study, '#study?uuid=' + study.ID, true))
+                  .append('<li data-role="list-divider">Series</li>')
+                  .append(FormatSeries(series, '#series?uuid=' + series.ID, true))
+                  .append('<li data-role="list-divider">Instance</li>')
+                  .append(FormatInstance(instance));
+              ConfigureLabels(info, system, 'instances', instance.ID);
+              info.listview('refresh');
 
-            GetResource('/instances/' + instance.ID + '/tags', function(s) {
-              $('#dicom-tree').tree('loadData', ConvertForTree(s));
-            });
+              GetResource('/instances/' + instance.ID + '/tags', function(s) {
+                $('#dicom-tree').tree('loadData', ConvertForTree(s));
+              });
 
-            GetResource('/instances/' + instance.ID + '/header', function(s) {
-              $('#dicom-metaheader').tree('loadData', ConvertForTree(s));
-            });
+              GetResource('/instances/' + instance.ID + '/header', function(s) {
+                $('#dicom-metaheader').tree('loadData', ConvertForTree(s));
+              });
 
-            $('#transfer-syntax').hide();
-            GetResource('/instances/' + instance.ID + '/metadata?expand', function(s) {
-              transferSyntax = s['TransferSyntax'];
-              if (transferSyntax !== undefined) {
-                $('#transfer-syntax').show();
-                $('#transfer-syntax-text').text(transferSyntax);
-              }
+              $('#transfer-syntax').hide();
+              GetResource('/instances/' + instance.ID + '/metadata?expand', function(s) {
+                transferSyntax = s['TransferSyntax'];
+                if (transferSyntax !== undefined) {
+                  $('#transfer-syntax').show();
+                  $('#transfer-syntax-text').text(transferSyntax);
+                }
+              });
+
+              SetupAnonymizedOrModifiedFrom('#instance-anonymized-from', instance, 'instance', ANONYMIZED_FROM);
+              SetupAnonymizedOrModifiedFrom('#instance-modified-from', instance, 'instance', MODIFIED_FROM);
+
+              SetupAttachments('#instance-access', 'instance-attachment', pageData.uuid, 'instances');
+
+              currentPage = 'instance';
+              currentUuid = pageData.uuid;
             });
-
-            SetupAnonymizedOrModifiedFrom('#instance-anonymized-from', instance, 'instance', ANONYMIZED_FROM);
-            SetupAnonymizedOrModifiedFrom('#instance-modified-from', instance, 'instance', MODIFIED_FROM);
-
-            SetupAttachments('#instance-access', 'instance-attachment', pageData.uuid, 'instances');
-
-            currentPage = 'instance';
-            currentUuid = pageData.uuid;
           });
         });
       });
@@ -1246,7 +1364,7 @@
                 href: '#',
                 rel: 'close',
                 text: name
-              })
+              });
               liElement.append(aElement);
     
               items.append(liElement);
@@ -1365,6 +1483,36 @@
   window.location.href = '../series/' + $.mobile.pageData.uuid + '/media';
 });
 
+
+$('#patient-archive-link').live('click', function(e) {
+  e.preventDefault();
+  var url = new URL('../patients/' + $.mobile.pageData.uuid + '/archive', window.location.href);
+  navigator.clipboard.writeText(url.href);
+  $(e.target).closest('li').buttonMarkup({ icon: 'check' });
+});
+
+$('#study-archive-link').live('click', function(e) {
+  e.preventDefault();
+  var url = new URL('../studies/' + $.mobile.pageData.uuid + '/archive', window.location.href);
+  navigator.clipboard.writeText(url.href);
+  $(e.target).closest('li').buttonMarkup({ icon: 'check' });
+});
+
+$('#series-archive-link').live('click', function(e) {
+  e.preventDefault();
+  var url = new URL('../series/' + $.mobile.pageData.uuid + '/archive', window.location.href);
+  navigator.clipboard.writeText(url.href);
+  $(e.target).closest('li').buttonMarkup({ icon: 'check' });
+});
+
+$('#instance-download-link').live('click', function(e) {
+  e.preventDefault();
+  var url = new URL('../instances/' + $.mobile.pageData.uuid + '/file', window.location.href);
+  navigator.clipboard.writeText(url.href);
+  $(e.target).closest('li').buttonMarkup({ icon: 'check' });
+});
+
+
 $('.patient-attachment').live('click', function(e) {
   e.preventDefault();  //stop the browser from following
   window.location.href = '../patients/' + $.mobile.pageData.uuid + '/attachments/' + e.target.id + '/data';
--- a/OrthancServer/OrthancExplorer/file-upload.js	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/OrthancExplorer/file-upload.js	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/OrthancExplorer/query-retrieve.js	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/OrthancExplorer/query-retrieve.js	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Plugins/Engine/IPluginServiceProvider.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Engine/IPluginServiceProvider.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -43,7 +44,7 @@
 namespace Orthanc
 {
   class OrthancPluginDatabase::Transaction :
-    public IDatabaseWrapper::ITransaction,
+    public BaseDatabaseWrapper::BaseTransaction,
     public Compatibility::ICreateInstance,
     public Compatibility::IGetChildrenMetadata,
     public Compatibility::ILookupResources,
@@ -565,10 +566,17 @@
     
     virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
                                       std::list<std::string>* instancesId,
-                                      const std::vector<DatabaseConstraint>& lookup,
+                                      const DatabaseConstraints& lookup,
                                       ResourceType queryLevel,
-                                      size_t limit) ORTHANC_OVERRIDE
+                                      const std::set<std::string>& labels,
+                                      LabelsConstraint labelsConstraint,
+                                      uint32_t limit) ORTHANC_OVERRIDE
     {
+      if (!labels.empty())
+      {
+        throw OrthancException(ErrorCode_InternalError);  // "HasLabelsSupport()" has returned "false"
+      }
+      
       if (that_.extensions_.lookupResources == NULL)
       {
         // Fallback to compatibility mode
@@ -580,20 +588,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)));
       }
@@ -802,8 +810,8 @@
 
     virtual void GetAllPublicIds(std::list<std::string>& target,
                                  ResourceType resourceType,
-                                 size_t since,
-                                 size_t limit) ORTHANC_OVERRIDE
+                                 int64_t since,
+                                 uint32_t limit) ORTHANC_OVERRIDE
     {
       if (that_.extensions_.getAllPublicIdsWithLimit != NULL)
       {
@@ -827,7 +835,7 @@
         std::list<std::string> tmp;
         GetAllPublicIds(tmp, resourceType);
     
-        if (tmp.size() <= since)
+        if (tmp.size() <= static_cast<size_t>(since))
         {
           // Not enough results => empty answer
           return;
@@ -849,14 +857,14 @@
     virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
                             bool& done /*out*/,
                             int64_t since,
-                            uint32_t maxResults) ORTHANC_OVERRIDE
+                            uint32_t limit) ORTHANC_OVERRIDE
     {
       ResetAnswers();
       answerChanges_ = &target;
       answerDone_ = &done;
       done = false;
 
-      CheckSuccess(that_.backend_.getChanges(that_.GetContext(), that_.payload_, since, maxResults));
+      CheckSuccess(that_.backend_.getChanges(that_.GetContext(), that_.payload_, since, limit));
     }
 
 
@@ -899,14 +907,14 @@
     virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
                                       bool& done /*out*/,
                                       int64_t since,
-                                      uint32_t maxResults) ORTHANC_OVERRIDE
+                                      uint32_t limit) ORTHANC_OVERRIDE
     {
       ResetAnswers();
       answerExportedResources_ = &target;
       answerDone_ = &done;
       done = false;
 
-      CheckSuccess(that_.backend_.getExportedResources(that_.GetContext(), that_.payload_, since, maxResults));
+      CheckSuccess(that_.backend_.getExportedResources(that_.GetContext(), that_.payload_, since, limit));
     }
 
 
@@ -1023,14 +1031,6 @@
     }
 
 
-    virtual bool IsExistingResource(int64_t internalId) ORTHANC_OVERRIDE
-    {
-      int32_t existing;
-      CheckSuccess(that_.backend_.isExistingResource(&existing, that_.payload_, internalId));
-      return (existing != 0);
-    }
-
-
     virtual bool IsProtectedPatient(int64_t internalId) ORTHANC_OVERRIDE
     {
       int32_t isProtected;
@@ -1065,15 +1065,18 @@
     }
 
 
-    virtual void LogChange(int64_t internalId,
-                           const ServerIndexChange& change) ORTHANC_OVERRIDE
+    virtual void LogChange(ChangeType changeType,
+                           ResourceType resourceType,
+                           int64_t internalId,
+                           const std::string& publicId,
+                           const std::string& date) ORTHANC_OVERRIDE
     {
       OrthancPluginChange tmp;
-      tmp.seq = change.GetSeq();
-      tmp.changeType = static_cast<int32_t>(change.GetChangeType());
-      tmp.resourceType = Plugins::Convert(change.GetResourceType());
-      tmp.publicId = change.GetPublicId().c_str();
-      tmp.date = change.GetDate().c_str();
+      tmp.seq = -1;  // Unused (it is attributed by the database engine)
+      tmp.changeType = static_cast<int32_t>(changeType);
+      tmp.resourceType = Plugins::Convert(resourceType);
+      tmp.publicId = publicId.c_str();
+      tmp.date = date.c_str();
 
       CheckSuccess(that_.backend_.logChange(that_.payload_, &tmp));
     }
@@ -1372,12 +1375,12 @@
                it = content.GetListTags().begin(); it != content.GetListTags().end(); ++it)
         {
           OrthancPluginResourcesContentTags tmp;
-          tmp.resource = it->resourceId_;
-          tmp.group = it->tag_.GetGroup();
-          tmp.element = it->tag_.GetElement();
-          tmp.value = it->value_.c_str();
+          tmp.resource = it->GetResourceId();
+          tmp.group = it->GetTag().GetGroup();
+          tmp.element = it->GetTag().GetElement();
+          tmp.value = it->GetValue().c_str();
 
-          if (it->isIdentifier_)
+          if (it->IsIdentifier())
           {
             identifierTags.push_back(tmp);
           }
@@ -1391,9 +1394,9 @@
                it = content.GetListMetadata().begin(); it != content.GetListMetadata().end(); ++it)
         {
           OrthancPluginResourcesContentMetadata tmp;
-          tmp.resource = it->resourceId_;
-          tmp.metadata = it->metadata_;
-          tmp.value = it->value_.c_str();
+          tmp.resource = it->GetResourceId();
+          tmp.metadata = it->GetType();
+          tmp.value = it->GetValue().c_str();
           metadata.push_back(tmp);
         }
 
@@ -1420,6 +1423,33 @@
         CheckSuccess(that_.extensions_.tagMostRecentPatient(that_.payload_, patient));
       }
     }
+
+
+    virtual void AddLabel(int64_t resource,
+                          const std::string& label) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+
+    virtual void RemoveLabel(int64_t resource,
+                             const std::string& label) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+
+    virtual void ListLabels(std::set<std::string>& target,
+                            int64_t resource) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+    
+
+    virtual void ListAllLabels(std::set<std::string>& target) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
   };
 
 
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -25,7 +26,7 @@
 #if ORTHANC_ENABLE_PLUGINS == 1
 
 #include "../../../OrthancFramework/Sources/SharedLibrary.h"
-#include "../../Sources/Database/IDatabaseWrapper.h"
+#include "../../Sources/Database/BaseDatabaseWrapper.h"
 #include "../Include/orthanc/OrthancCDatabasePlugin.h"
 #include "PluginsErrorDictionary.h"
 
@@ -45,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 IDatabaseWrapper
+  class OrthancPluginDatabase : public BaseDatabaseWrapper
   {
   private:
     class Transaction;
@@ -65,6 +66,7 @@
     Transaction*                    activeTransaction_;
     bool                            fastGetTotalSize_;
     uint64_t                        currentDiskSize_;
+    IDatabaseWrapper::Capabilities  dbCapabilities_;
 
     OrthancPluginDatabaseContext* GetContext()
     {
@@ -94,11 +96,6 @@
     {
     }
 
-    virtual bool HasFlushToDisk() const ORTHANC_OVERRIDE
-    {
-      return false;
-    }
-
     virtual IDatabaseWrapper::ITransaction* StartTransaction(TransactionType type,
                                                              IDatabaseListener& listener)
       ORTHANC_OVERRIDE;
@@ -108,14 +105,9 @@
     virtual void Upgrade(unsigned int targetVersion,
                          IStorageArea& storageArea) ORTHANC_OVERRIDE;    
 
-    virtual bool HasRevisionsSupport() const ORTHANC_OVERRIDE
+    virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE
     {
-      return false;  // No support for revisions in old API
-    }
-
-    virtual bool HasAttachmentCustomDataSupport() const ORTHANC_OVERRIDE
-    {
-      return false;  // No support for custom data in old API
+      return dbCapabilities_;
     }
 
     void AnswerReceived(const _OrthancPluginDatabaseAnswer& answer);
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -45,7 +46,7 @@
 
 namespace Orthanc
 {
-  class OrthancPluginDatabaseV3::Transaction : public IDatabaseWrapper::ITransaction
+  class OrthancPluginDatabaseV3::Transaction : public BaseDatabaseWrapper::BaseTransaction
   {
   private:
     OrthancPluginDatabaseV3&           that_;
@@ -280,7 +281,6 @@
       }
     }
     
-
     virtual void Rollback() ORTHANC_OVERRIDE
     {
       CheckSuccess(that_.backend_.rollback(transaction_));
@@ -390,8 +390,8 @@
     
     virtual void GetAllPublicIds(std::list<std::string>& target,
                                  ResourceType resourceType,
-                                 size_t since,
-                                 size_t limit) ORTHANC_OVERRIDE
+                                 int64_t since,
+                                 uint32_t limit) ORTHANC_OVERRIDE
     {
       CheckSuccess(that_.backend_.getAllPublicIdsWithLimit(
                      transaction_, Plugins::Convert(resourceType),
@@ -405,10 +405,10 @@
     virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
                             bool& done /*out*/,
                             int64_t since,
-                            uint32_t maxResults) ORTHANC_OVERRIDE
+                            uint32_t limit) ORTHANC_OVERRIDE
     {
       uint8_t tmpDone = true;
-      CheckSuccess(that_.backend_.getChanges(transaction_, &tmpDone, since, maxResults));
+      CheckSuccess(that_.backend_.getChanges(transaction_, &tmpDone, since, limit));
       CheckNoEvent();
 
       done = (tmpDone != 0);
@@ -456,10 +456,10 @@
     virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
                                       bool& done /*out*/,
                                       int64_t since,
-                                      uint32_t maxResults) ORTHANC_OVERRIDE
+                                      uint32_t limit) ORTHANC_OVERRIDE
     {
       uint8_t tmpDone = true;
-      CheckSuccess(that_.backend_.getExportedResources(transaction_, &tmpDone, since, maxResults));
+      CheckSuccess(that_.backend_.getExportedResources(transaction_, &tmpDone, since, limit));
       CheckNoEvent();
 
       done = (tmpDone != 0);
@@ -596,15 +596,6 @@
     }
 
     
-    virtual bool IsExistingResource(int64_t internalId) ORTHANC_OVERRIDE
-    {
-      uint8_t b;
-      CheckSuccess(that_.backend_.isExistingResource(transaction_, &b, internalId));
-      CheckNoEvent();
-      return (b != 0);
-    }
-
-    
     virtual bool IsProtectedPatient(int64_t internalId) ORTHANC_OVERRIDE
     {
       uint8_t b;
@@ -633,12 +624,15 @@
     }
 
     
-    virtual void LogChange(int64_t internalId,
-                           const ServerIndexChange& change) ORTHANC_OVERRIDE
+    virtual void LogChange(ChangeType changeType,
+                           ResourceType resourceType,
+                           int64_t internalId,
+                           const std::string& /* publicId - unused */,
+                           const std::string& date) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.logChange(transaction_, static_cast<int32_t>(change.GetChangeType()),
-                                            internalId, Plugins::Convert(change.GetResourceType()),
-                                            change.GetDate().c_str()));
+      CheckSuccess(that_.backend_.logChange(transaction_, static_cast<int32_t>(changeType),
+                                            internalId, Plugins::Convert(resourceType),
+                                            date.c_str()));
       CheckNoEvent();
     }
 
@@ -806,23 +800,30 @@
     
     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 DatabaseConstraints& lookup,
                                       ResourceType queryLevel,
-                                      size_t limit) ORTHANC_OVERRIDE
+                                      const std::set<std::string>& labels,
+                                      LabelsConstraint labelsConstraint,
+                                      uint32_t limit) ORTHANC_OVERRIDE
     {
+      if (!labels.empty())
+      {
+        throw OrthancException(ErrorCode_InternalError);  // "HasLabelsSupport()" has returned "false"
+      }
+      
       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();
@@ -912,12 +913,12 @@
              it = content.GetListTags().begin(); it != content.GetListTags().end(); ++it)
       {
         OrthancPluginResourcesContentTags tmp;
-        tmp.resource = it->resourceId_;
-        tmp.group = it->tag_.GetGroup();
-        tmp.element = it->tag_.GetElement();
-        tmp.value = it->value_.c_str();
+        tmp.resource = it->GetResourceId();
+        tmp.group = it->GetTag().GetGroup();
+        tmp.element = it->GetTag().GetElement();
+        tmp.value = it->GetValue().c_str();
 
-        if (it->isIdentifier_)
+        if (it->IsIdentifier())
         {
           identifierTags.push_back(tmp);
         }
@@ -931,9 +932,9 @@
              it = content.GetListMetadata().begin(); it != content.GetListMetadata().end(); ++it)
       {
         OrthancPluginResourcesContentMetadata tmp;
-        tmp.resource = it->resourceId_;
-        tmp.metadata = it->metadata_;
-        tmp.value = it->value_.c_str();
+        tmp.resource = it->GetResourceId();
+        tmp.metadata = it->GetType();
+        tmp.value = it->GetValue().c_str();
         metadata.push_back(tmp);
       }
 
@@ -1035,6 +1036,33 @@
         return false;
       }
     }
+
+
+    virtual void AddLabel(int64_t resource,
+                          const std::string& label) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+
+    virtual void RemoveLabel(int64_t resource,
+                             const std::string& label) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+
+    virtual void ListLabels(std::set<std::string>& target,
+                            int64_t resource) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+    
+
+    virtual void ListAllLabels(std::set<std::string>& target) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
   };
 
   
@@ -1058,6 +1086,7 @@
     errorDictionary_(errorDictionary),
     database_(database),
     serverIdentifier_(serverIdentifier)
+
   {
     CLOG(INFO, PLUGINS) << "Identifier of this Orthanc server for the global properties "
                         << "of the custom database: \"" << serverIdentifier << "\"";
@@ -1164,6 +1193,11 @@
   void OrthancPluginDatabaseV3::Open()
   {
     CheckSuccess(backend_.open(database_));
+
+    // update the db capabilities
+    uint8_t hasRevisions;
+    CheckSuccess(backend_.hasRevisionsSupport(database_, &hasRevisions));
+    dbCapabilities_.SetRevisionsSupport(hasRevisions != 0);
   }
 
 
@@ -1224,12 +1258,4 @@
     }
   }
 
-  
-  bool OrthancPluginDatabaseV3::HasRevisionsSupport() const
-  {
-    // WARNING: This method requires "Open()" to have been called
-    uint8_t hasRevisions;
-    CheckSuccess(backend_.hasRevisionsSupport(database_, &hasRevisions));
-    return (hasRevisions != 0);
-  }
 }
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -25,13 +26,13 @@
 #if ORTHANC_ENABLE_PLUGINS == 1
 
 #include "../../../OrthancFramework/Sources/SharedLibrary.h"
-#include "../../Sources/Database/IDatabaseWrapper.h"
+#include "../../Sources/Database/BaseDatabaseWrapper.h"
 #include "../Include/orthanc/OrthancCDatabasePlugin.h"
 #include "PluginsErrorDictionary.h"
 
 namespace Orthanc
 {
-  class OrthancPluginDatabaseV3 : public IDatabaseWrapper
+  class OrthancPluginDatabaseV3 : public BaseDatabaseWrapper
   {
   private:
     class Transaction;
@@ -41,6 +42,7 @@
     OrthancPluginDatabaseBackendV3  backend_;
     void*                           database_;
     std::string                     serverIdentifier_;
+    IDatabaseWrapper::Capabilities  dbCapabilities_;
 
     void CheckSuccess(OrthancPluginErrorCode code) const;
 
@@ -67,11 +69,6 @@
     {
     }
 
-    virtual bool HasFlushToDisk() const ORTHANC_OVERRIDE
-    {
-      return false;
-    }
-
     virtual IDatabaseWrapper::ITransaction* StartTransaction(TransactionType type,
                                                              IDatabaseListener& listener)
       ORTHANC_OVERRIDE;
@@ -81,13 +78,10 @@
     virtual void Upgrade(unsigned int targetVersion,
                          IStorageArea& storageArea) ORTHANC_OVERRIDE;    
 
-    virtual bool HasRevisionsSupport() const ORTHANC_OVERRIDE;
-
-    virtual bool HasAttachmentCustomDataSupport() const ORTHANC_OVERRIDE
+    virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE
     {
-      return false; // introduced in V4
+      return dbCapabilities_;
     }
-
   };
 }
 
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -27,282 +28,303 @@
 #  error The plugin support is disabled
 #endif
 
+#include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
 #include "../../../OrthancFramework/Sources/Logging.h"
 #include "../../../OrthancFramework/Sources/OrthancException.h"
 #include "../../Sources/Database/ResourcesContent.h"
 #include "../../Sources/Database/VoidDatabaseListener.h"
+#include "../../Sources/ServerToolbox.h"
 #include "PluginsEnumerations.h"
 
+#include "OrthancDatabasePlugin.pb.h"  // Auto-generated file
+
 #include <cassert>
 
 
-#define CHECK_FUNCTION_EXISTS(backend, func)                            \
-  if (backend.func == NULL)                                             \
-  {                                                                     \
-    throw OrthancException(                                             \
-      ErrorCode_DatabasePlugin, "Missing primitive: " #func "()");      \
+namespace Orthanc
+{
+  static void CheckSuccess(PluginsErrorDictionary& errorDictionary,
+                           OrthancPluginErrorCode code)
+  {
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      errorDictionary.LogError(code, true);
+      throw OrthancException(static_cast<ErrorCode>(code));
+    }
+  }
+
+
+  static ResourceType Convert(DatabasePluginMessages::ResourceType type)
+  {
+    switch (type)
+    {
+      case DatabasePluginMessages::RESOURCE_PATIENT:
+        return ResourceType_Patient;
+
+      case DatabasePluginMessages::RESOURCE_STUDY:
+        return ResourceType_Study;
+
+      case DatabasePluginMessages::RESOURCE_SERIES:
+        return ResourceType_Series;
+
+      case DatabasePluginMessages::RESOURCE_INSTANCE:
+        return ResourceType_Instance;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+    
+  static DatabasePluginMessages::ResourceType Convert(ResourceType type)
+  {
+    switch (type)
+    {
+      case ResourceType_Patient:
+        return DatabasePluginMessages::RESOURCE_PATIENT;
+
+      case ResourceType_Study:
+        return DatabasePluginMessages::RESOURCE_STUDY;
+
+      case ResourceType_Series:
+        return DatabasePluginMessages::RESOURCE_SERIES;
+
+      case ResourceType_Instance:
+        return DatabasePluginMessages::RESOURCE_INSTANCE;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+    
+  static FileInfo Convert(const DatabasePluginMessages::FileInfo& source)
+  {
+    return FileInfo(source.uuid(),
+                    static_cast<FileContentType>(source.content_type()),
+                    source.uncompressed_size(),
+                    source.uncompressed_hash(),
+                    static_cast<CompressionType>(source.compression_type()),
+                    source.compressed_size(),
+                    source.compressed_hash(),
+                    source.custom_data());
   }
 
-namespace Orthanc
-{
+
+  static ServerIndexChange Convert(const DatabasePluginMessages::ServerIndexChange& source)
+  {
+    return ServerIndexChange(source.seq(),
+                             static_cast<ChangeType>(source.change_type()),
+                             Convert(source.resource_type()),
+                             source.public_id(),
+                             source.date());
+  }
+
+
+  static ExportedResource Convert(const DatabasePluginMessages::ExportedResource& source)
+  {
+    return ExportedResource(source.seq(),
+                            Convert(source.resource_type()),
+                            source.public_id(),
+                            source.modality(),
+                            source.date(),
+                            source.patient_id(),
+                            source.study_instance_uid(),
+                            source.series_instance_uid(),
+                            source.sop_instance_uid());
+  }
+
+
+  static void Execute(DatabasePluginMessages::Response& response,
+                      const OrthancPluginDatabaseV4& database,
+                      const DatabasePluginMessages::Request& request)
+  {
+    std::string requestSerialized;
+    request.SerializeToString(&requestSerialized);
+
+    OrthancPluginMemoryBuffer64 responseSerialized;
+    CheckSuccess(database.GetErrorDictionary(), database.GetDefinition().operations(
+                   &responseSerialized, database.GetDefinition().backend,
+                   requestSerialized.empty() ? NULL : requestSerialized.c_str(),
+                   requestSerialized.size()));
+
+    bool success = response.ParseFromArray(responseSerialized.data, responseSerialized.size);
+
+    if (responseSerialized.size > 0)
+    {
+      free(responseSerialized.data);
+    }
+
+    if (!success)
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin, "Cannot unserialize protobuf originating from the database plugin");
+    }
+  }
+  
+
+  static void ExecuteDatabase(DatabasePluginMessages::DatabaseResponse& response,
+                              const OrthancPluginDatabaseV4& database,
+                              DatabasePluginMessages::DatabaseOperation operation,
+                              const DatabasePluginMessages::DatabaseRequest& request)
+  {
+    DatabasePluginMessages::Request fullRequest;
+    fullRequest.set_type(DatabasePluginMessages::REQUEST_DATABASE);
+    fullRequest.mutable_database_request()->CopyFrom(request);
+    fullRequest.mutable_database_request()->set_operation(operation);
+
+    DatabasePluginMessages::Response fullResponse;
+    Execute(fullResponse, database, fullRequest);
+    
+    response.CopyFrom(fullResponse.database_response());
+  }
+
+  
   class OrthancPluginDatabaseV4::Transaction : public IDatabaseWrapper::ITransaction
   {
   private:
-    OrthancPluginDatabaseV4&           that_;
-    IDatabaseListener&                 listener_;
-    OrthancPluginDatabaseTransaction*  transaction_;
+    OrthancPluginDatabaseV4&  database_;
+    IDatabaseListener&        listener_;
+    void*                     transaction_;
+    
+    void ExecuteTransaction(DatabasePluginMessages::TransactionResponse& response,
+                            DatabasePluginMessages::TransactionOperation operation,
+                            const DatabasePluginMessages::TransactionRequest& request)
+    {
+      DatabasePluginMessages::Request fullRequest;
+      fullRequest.set_type(DatabasePluginMessages::REQUEST_TRANSACTION);
+      fullRequest.mutable_transaction_request()->CopyFrom(request);
+      fullRequest.mutable_transaction_request()->set_transaction(reinterpret_cast<intptr_t>(transaction_));
+      fullRequest.mutable_transaction_request()->set_operation(operation);
 
+      DatabasePluginMessages::Response fullResponse;
+      Execute(fullResponse, database_, fullRequest);
     
-    void CheckSuccess(OrthancPluginErrorCode code) const
-    {
-      that_.CheckSuccess(code);
+      response.CopyFrom(fullResponse.transaction_response());
     }
     
-
-    static FileInfo Convert(const OrthancPluginAttachment& attachment)
-    {
-      std::string customData;
-      return FileInfo(attachment.uuid,
-                      static_cast<FileContentType>(attachment.contentType),
-                      attachment.uncompressedSize,
-                      attachment.uncompressedHash,
-                      static_cast<CompressionType>(attachment.compressionType),
-                      attachment.compressedSize,
-                      attachment.compressedHash,
-                      customData);
-    }
-
-    static FileInfo Convert(const OrthancPluginAttachment2& attachment)
+    
+    void ExecuteTransaction(DatabasePluginMessages::TransactionResponse& response,
+                            DatabasePluginMessages::TransactionOperation operation)
     {
-      return FileInfo(attachment.uuid,
-                      static_cast<FileContentType>(attachment.contentType),
-                      attachment.uncompressedSize,
-                      attachment.uncompressedHash,
-                      static_cast<CompressionType>(attachment.compressionType),
-                      attachment.compressedSize,
-                      attachment.compressedHash,
-                      attachment.customData);
+      DatabasePluginMessages::TransactionRequest request;    // Ignored
+      ExecuteTransaction(response, operation, request);
     }
-
-    void ReadStringAnswers(std::list<std::string>& target)
+    
+    
+    void ExecuteTransaction(DatabasePluginMessages::TransactionOperation operation,
+                            const DatabasePluginMessages::TransactionRequest& request)
     {
-      uint32_t count;
-      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
-
-      target.clear();
-      for (uint32_t i = 0; i < count; i++)
-      {
-        const char* value = NULL;
-        CheckSuccess(that_.backend_.readAnswerString(transaction_, &value, i));
-        if (value == NULL)
-        {
-          throw OrthancException(ErrorCode_DatabasePlugin);
-        }
-        else
-        {
-          target.push_back(value);
-        }
-      }
+      DatabasePluginMessages::TransactionResponse response;  // Ignored
+      ExecuteTransaction(response, operation, request);
+    }
+    
+    
+    void ExecuteTransaction(DatabasePluginMessages::TransactionOperation operation)
+    {
+      DatabasePluginMessages::TransactionResponse response;  // Ignored
+      DatabasePluginMessages::TransactionRequest request;    // Ignored
+      ExecuteTransaction(response, operation, request);
     }
 
 
-    bool ReadSingleStringAnswer(std::string& target)
+    void ListLabelsInternal(std::set<std::string>& target,
+                            bool isSingleResource,
+                            int64_t resource)
     {
-      uint32_t count;
-      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
-
-      if (count == 0)
-      {
-        return false;
-      }
-      else if (count == 1)
+      if (database_.GetDatabaseCapabilities().HasLabelsSupport())
       {
-        const char* value = NULL;
-        CheckSuccess(that_.backend_.readAnswerString(transaction_, &value, 0));
-        if (value == NULL)
+        DatabasePluginMessages::TransactionRequest request;
+        request.mutable_list_labels()->set_single_resource(isSingleResource);
+        request.mutable_list_labels()->set_id(resource);
+
+        DatabasePluginMessages::TransactionResponse response;
+        ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LIST_LABELS, request);
+
+        target.clear();
+        for (int i = 0; i < response.list_labels().labels().size(); i++)
         {
-          throw OrthancException(ErrorCode_DatabasePlugin);
-        }
-        else
-        {
-          target.assign(value);
-          return true;
+          target.insert(response.list_labels().labels(i));
         }
       }
       else
       {
-        throw OrthancException(ErrorCode_DatabasePlugin);
-      }
-    }
-
-
-    bool ReadSingleInt64Answer(int64_t& target)
-    {
-      uint32_t count;
-      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
-
-      if (count == 0)
-      {
-        return false;
-      }
-      else if (count == 1)
-      {
-        CheckSuccess(that_.backend_.readAnswerInt64(transaction_, &target, 0));
-        return true;
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_DatabasePlugin);
-      }
-    }
-
-    
-    ExportedResource ReadAnswerExportedResource(uint32_t answerIndex)
-    {
-      OrthancPluginExportedResource exported;
-      CheckSuccess(that_.backend_.readAnswerExportedResource(transaction_, &exported, answerIndex));
-
-      if (exported.publicId == NULL ||
-          exported.modality == NULL ||
-          exported.date == NULL ||
-          exported.patientId == NULL ||
-          exported.studyInstanceUid == NULL ||
-          exported.seriesInstanceUid == NULL ||
-          exported.sopInstanceUid == NULL)
-      {
-        throw OrthancException(ErrorCode_DatabasePlugin);
-      }
-      else
-      {
-        return ExportedResource(exported.seq,
-                                Plugins::Convert(exported.resourceType),
-                                exported.publicId,
-                                exported.modality,
-                                exported.date,
-                                exported.patientId,
-                                exported.studyInstanceUid,
-                                exported.seriesInstanceUid,
-                                exported.sopInstanceUid);
+        // This method shouldn't have been called
+        throw OrthancException(ErrorCode_InternalError);
       }
     }
     
 
-    ServerIndexChange ReadAnswerChange(uint32_t answerIndex)
+  public:
+    Transaction(OrthancPluginDatabaseV4& database,
+                IDatabaseListener& listener,
+                TransactionType type) :
+      database_(database),
+      listener_(listener),
+      transaction_(NULL)
     {
-      OrthancPluginChange change;
-      CheckSuccess(that_.backend_.readAnswerChange(transaction_, &change, answerIndex));
-
-      if (change.publicId == NULL ||
-          change.date == NULL)
-      {
-        throw OrthancException(ErrorCode_DatabasePlugin);
-      }
-      else
-      {
-        return ServerIndexChange(change.seq,
-                                 static_cast<ChangeType>(change.changeType),
-                                 Plugins::Convert(change.resourceType),
-                                 change.publicId,
-                                 change.date);
-      }
-    }
-
-
-    void CheckNoEvent()
-    {
-      uint32_t count;
-      CheckSuccess(that_.backend_.readEventsCount(transaction_, &count));
-      if (count != 0)
-      {
-        throw OrthancException(ErrorCode_DatabasePlugin);
-      }
-    }
-
-
-    void ProcessEvents(bool isDeletingAttachment)
-    {
-      uint32_t count;
-      CheckSuccess(that_.backend_.readEventsCount(transaction_, &count));
-
-      for (uint32_t i = 0; i < count; i++)
-      {
-        OrthancPluginDatabaseEvent2 event;
-        CheckSuccess(that_.backend_.readEvent2(transaction_, &event, i));
+      DatabasePluginMessages::DatabaseRequest request;
 
-        switch (event.type)
-        {
-          case OrthancPluginDatabaseEventType_DeletedAttachment:
-            listener_.SignalAttachmentDeleted(Convert(event.content.attachment));
-            break;
-            
-          case OrthancPluginDatabaseEventType_DeletedResource:
-            if (isDeletingAttachment)
-            {
-              // This event should only be triggered by "DeleteResource()"
-              throw OrthancException(ErrorCode_DatabasePlugin);
-            }
-            else
-            {
-              listener_.SignalResourceDeleted(Plugins::Convert(event.content.resource.level), event.content.resource.publicId);
-            }            
-            break;
-            
-          case OrthancPluginDatabaseEventType_RemainingAncestor:
-            if (isDeletingAttachment)
-            {
-              // This event should only triggered by "DeleteResource()"
-              throw OrthancException(ErrorCode_DatabasePlugin);
-            }
-            else
-            {
-              listener_.SignalRemainingAncestor(Plugins::Convert(event.content.resource.level), event.content.resource.publicId);
-            }
-            break;
+      switch (type)
+      {
+        case TransactionType_ReadOnly:
+          request.mutable_start_transaction()->set_type(DatabasePluginMessages::TRANSACTION_READ_ONLY);
+          break;
+
+        case TransactionType_ReadWrite:
+          request.mutable_start_transaction()->set_type(DatabasePluginMessages::TRANSACTION_READ_WRITE);
+          break;
 
-          default:
-            break;  // Unhandled event
-        }
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
       }
-    }
-
 
-  public:
-    Transaction(OrthancPluginDatabaseV4& that,
-                IDatabaseListener& listener,
-                OrthancPluginDatabaseTransactionType type) :
-      that_(that),
-      listener_(listener)
-    {
-      CheckSuccess(that.backend_.startTransaction(that.database_, &transaction_, type));
+      DatabasePluginMessages::DatabaseResponse response;
+      ExecuteDatabase(response, database, DatabasePluginMessages::OPERATION_START_TRANSACTION, request);
+
+      transaction_ = reinterpret_cast<void*>(response.start_transaction().transaction());
+
       if (transaction_ == NULL)
       {
-        throw OrthancException(ErrorCode_DatabasePlugin);
+        throw OrthancException(ErrorCode_NullPointer);
       }
     }
 
     
     virtual ~Transaction()
     {
-      OrthancPluginErrorCode code = that_.backend_.destructTransaction(transaction_);
-      if (code != OrthancPluginErrorCode_Success)
+      try
       {
-        // Don't throw exception in destructors
-        that_.errorDictionary_.LogError(code, true);
+        DatabasePluginMessages::DatabaseRequest request;
+        request.mutable_finalize_transaction()->set_transaction(reinterpret_cast<intptr_t>(transaction_));
+
+        DatabasePluginMessages::DatabaseResponse response;
+        ExecuteDatabase(response, database_, DatabasePluginMessages::OPERATION_FINALIZE_TRANSACTION, request);
       }
+      catch (OrthancException& e)
+      {
+        // Destructors must not throw exceptions
+        LOG(ERROR) << "Cannot finalize the database engine: " << e.What();
+      }
+    }
+
+    void* GetTransactionObject()
+    {
+      return transaction_;
     }
     
 
     virtual void Rollback() ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.rollback(transaction_));
-      CheckNoEvent();
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_ROLLBACK);
     }
     
 
     virtual void Commit(int64_t fileSizeDelta) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.commit(transaction_, fileSizeDelta));
-      CheckNoEvent();
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_commit()->set_file_size_delta(fileSizeDelta);
+
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_COMMIT, request);
     }
 
     
@@ -310,81 +332,106 @@
                                const FileInfo& attachment,
                                int64_t revision) ORTHANC_OVERRIDE
     {
-      OrthancPluginAttachment2 tmp;
-      tmp.uuid = attachment.GetUuid().c_str();
-      tmp.contentType = static_cast<int32_t>(attachment.GetContentType());
-      tmp.uncompressedSize = attachment.GetUncompressedSize();
-      tmp.uncompressedHash = attachment.GetUncompressedMD5().c_str();
-      tmp.compressionType = static_cast<int32_t>(attachment.GetCompressionType());
-      tmp.compressedSize = attachment.GetCompressedSize();
-      tmp.compressedHash = attachment.GetCompressedMD5().c_str();
-      tmp.customData = attachment.GetCustomData().c_str();
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_add_attachment()->set_id(id);
+      request.mutable_add_attachment()->mutable_attachment()->set_uuid(attachment.GetUuid());
+      request.mutable_add_attachment()->mutable_attachment()->set_content_type(attachment.GetContentType());
+      request.mutable_add_attachment()->mutable_attachment()->set_uncompressed_size(attachment.GetUncompressedSize());
+      request.mutable_add_attachment()->mutable_attachment()->set_uncompressed_hash(attachment.GetUncompressedMD5());
+      request.mutable_add_attachment()->mutable_attachment()->set_compression_type(attachment.GetCompressionType());
+      request.mutable_add_attachment()->mutable_attachment()->set_compressed_size(attachment.GetCompressedSize());
+      request.mutable_add_attachment()->mutable_attachment()->set_compressed_hash(attachment.GetCompressedMD5());        
+      request.mutable_add_attachment()->set_revision(revision);
 
-      CheckSuccess(that_.backend_.addAttachment2(transaction_, id, &tmp, revision));
-      CheckNoEvent();
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_ADD_ATTACHMENT, request);
     }
 
 
     virtual void ClearChanges() ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.clearChanges(transaction_));
-      CheckNoEvent();
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_CLEAR_CHANGES);
     }
 
     
     virtual void ClearExportedResources() ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.clearExportedResources(transaction_));
-      CheckNoEvent();
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_CLEAR_EXPORTED_RESOURCES);
     }
 
-    
+
     virtual void DeleteAttachment(int64_t id,
                                   FileContentType attachment) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.deleteAttachment(transaction_, id, static_cast<int32_t>(attachment)));
-      ProcessEvents(true);
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_delete_attachment()->set_id(id);
+      request.mutable_delete_attachment()->set_type(attachment);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_DELETE_ATTACHMENT, request);
+
+      listener_.SignalAttachmentDeleted(Convert(response.delete_attachment().deleted_attachment()));
     }
 
     
     virtual void DeleteMetadata(int64_t id,
                                 MetadataType type) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.deleteMetadata(transaction_, id, static_cast<int32_t>(type)));
-      CheckNoEvent();
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_delete_metadata()->set_id(id);
+      request.mutable_delete_metadata()->set_type(type);
+
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_DELETE_METADATA, request);
     }
 
     
     virtual void DeleteResource(int64_t id) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.deleteResource(transaction_, id));
-      ProcessEvents(false);
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_delete_resource()->set_id(id);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_DELETE_RESOURCE, request);
+
+      for (int i = 0; i < response.delete_resource().deleted_attachments().size(); i++)
+      {
+        listener_.SignalAttachmentDeleted(Convert(response.delete_resource().deleted_attachments(i)));
+      }
+
+      for (int i = 0; i < response.delete_resource().deleted_resources().size(); i++)
+      {
+        listener_.SignalResourceDeleted(Convert(response.delete_resource().deleted_resources(i).level()),
+                                        response.delete_resource().deleted_resources(i).public_id());
+      }
+
+      if (response.delete_resource().is_remaining_ancestor())
+      {
+        listener_.SignalRemainingAncestor(Convert(response.delete_resource().remaining_ancestor().level()),
+                                          response.delete_resource().remaining_ancestor().public_id());
+      }
     }
 
     
     virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
                                 int64_t id) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.getAllMetadata(transaction_, id));
-      CheckNoEvent();
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_get_all_metadata()->set_id(id);
 
-      uint32_t count;
-      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
-      
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_ALL_METADATA, request);
+
       target.clear();
-      for (uint32_t i = 0; i < count; i++)
+      for (int i = 0; i < response.get_all_metadata().metadata().size(); i++)
       {
-        int32_t metadata;
-        const char* value = NULL;
-        CheckSuccess(that_.backend_.readAnswerMetadata(transaction_, &metadata, &value, i));
-
-        if (value == NULL)
+        MetadataType key = static_cast<MetadataType>(response.get_all_metadata().metadata(i).type());
+          
+        if (target.find(key) == target.end())
         {
-          throw OrthancException(ErrorCode_DatabasePlugin);
+          target[key] = response.get_all_metadata().metadata(i).value();
         }
         else
         {
-          target[static_cast<MetadataType>(metadata)] = value;
+          throw OrthancException(ErrorCode_DatabasePlugin);
         }
       }
     }
@@ -393,45 +440,59 @@
     virtual void GetAllPublicIds(std::list<std::string>& target,
                                  ResourceType resourceType) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.getAllPublicIds(transaction_, Plugins::Convert(resourceType)));
-      CheckNoEvent();
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_get_all_public_ids()->set_resource_type(Convert(resourceType));
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_ALL_PUBLIC_IDS, request);
 
-      ReadStringAnswers(target);
+      target.clear();
+      for (int i = 0; i < response.get_all_public_ids().ids().size(); i++)
+      {
+        target.push_back(response.get_all_public_ids().ids(i));
+      }
     }
 
     
     virtual void GetAllPublicIds(std::list<std::string>& target,
                                  ResourceType resourceType,
-                                 size_t since,
-                                 size_t limit) ORTHANC_OVERRIDE
+                                 int64_t since,
+                                 uint32_t limit) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.getAllPublicIdsWithLimit(
-                     transaction_, Plugins::Convert(resourceType),
-                     static_cast<uint64_t>(since), static_cast<uint64_t>(limit)));
-      CheckNoEvent();
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_get_all_public_ids_with_limits()->set_resource_type(Convert(resourceType));
+      request.mutable_get_all_public_ids_with_limits()->set_since(since);
+      request.mutable_get_all_public_ids_with_limits()->set_limit(limit);
 
-      ReadStringAnswers(target);
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_ALL_PUBLIC_IDS_WITH_LIMITS, request);
+
+      target.clear();
+      for (int i = 0; i < response.get_all_public_ids_with_limits().ids().size(); i++)
+      {
+        target.push_back(response.get_all_public_ids_with_limits().ids(i));
+      }
     }
 
     
     virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
                             bool& done /*out*/,
                             int64_t since,
-                            uint32_t maxResults) ORTHANC_OVERRIDE
+                            uint32_t limit) ORTHANC_OVERRIDE
     {
-      uint8_t tmpDone = true;
-      CheckSuccess(that_.backend_.getChanges(transaction_, &tmpDone, since, maxResults));
-      CheckNoEvent();
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_get_changes()->set_since(since);
+      request.mutable_get_changes()->set_limit(limit);
 
-      done = (tmpDone != 0);
-      
-      uint32_t count;
-      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
-      
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_CHANGES, request);
+
+      done = response.get_changes().done();
+        
       target.clear();
-      for (uint32_t i = 0; i < count; i++)
+      for (int i = 0; i < response.get_changes().changes().size(); i++)
       {
-        target.push_back(ReadAnswerChange(i));
+        target.push_back(Convert(response.get_changes().changes(i)));
       }
     }
 
@@ -439,18 +500,16 @@
     virtual void GetChildrenInternalId(std::list<int64_t>& target,
                                        int64_t id) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.getChildrenInternalId(transaction_, id));
-      CheckNoEvent();
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_get_children_internal_id()->set_id(id);
 
-      uint32_t count;
-      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
-      
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_CHILDREN_INTERNAL_ID, request);
+
       target.clear();
-      for (uint32_t i = 0; i < count; i++)
+      for (int i = 0; i < response.get_children_internal_id().ids().size(); i++)
       {
-        int64_t value;
-        CheckSuccess(that_.backend_.readAnswerInt64(transaction_, &value, i));
-        target.push_back(value);
+        target.push_back(response.get_children_internal_id().ids(i));
       }
     }
 
@@ -458,71 +517,64 @@
     virtual void GetChildrenPublicId(std::list<std::string>& target,
                                      int64_t id) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.getChildrenPublicId(transaction_, id));
-      CheckNoEvent();
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_get_children_public_id()->set_id(id);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_CHILDREN_PUBLIC_ID, request);
 
-      ReadStringAnswers(target);
+      target.clear();
+      for (int i = 0; i < response.get_children_public_id().ids().size(); i++)
+      {
+        target.push_back(response.get_children_public_id().ids(i));
+      }
     }
 
     
     virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
                                       bool& done /*out*/,
                                       int64_t since,
-                                      uint32_t maxResults) ORTHANC_OVERRIDE
+                                      uint32_t limit) ORTHANC_OVERRIDE
     {
-      uint8_t tmpDone = true;
-      CheckSuccess(that_.backend_.getExportedResources(transaction_, &tmpDone, since, maxResults));
-      CheckNoEvent();
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_get_exported_resources()->set_since(since);
+      request.mutable_get_exported_resources()->set_limit(limit);
 
-      done = (tmpDone != 0);
-      
-      uint32_t count;
-      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
-      
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_EXPORTED_RESOURCES, request);
+
+      done = response.get_exported_resources().done();
+        
       target.clear();
-      for (uint32_t i = 0; i < count; i++)
+      for (int i = 0; i < response.get_exported_resources().resources().size(); i++)
       {
-        target.push_back(ReadAnswerExportedResource(i));
+        target.push_back(Convert(response.get_exported_resources().resources(i)));
       }
     }
 
-
+    
     virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.getLastChange(transaction_));
-      CheckNoEvent();
-
-      uint32_t count;
-      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_LAST_CHANGE);
 
       target.clear();
-      if (count == 1)
+      if (response.get_last_change().found())
       {
-        target.push_back(ReadAnswerChange(0));
-      }
-      else if (count > 1)
-      {
-        throw OrthancException(ErrorCode_DatabasePlugin);
+        target.push_back(Convert(response.get_last_change().change()));
       }
     }
 
-
+    
     virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.getLastExportedResource(transaction_));
-      CheckNoEvent();
-
-      uint32_t count;
-      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_LAST_EXPORTED_RESOURCE);
 
       target.clear();
-      if (count == 1)
+      if (response.get_last_exported_resource().found())
       {
-        target.push_back(ReadAnswerExportedResource(0));
-      }
-      else if (count > 1)
-      {
-        throw OrthancException(ErrorCode_DatabasePlugin);
+        target.push_back(Convert(response.get_last_exported_resource().resource()));
       }
     }
 
@@ -530,26 +582,25 @@
     virtual void GetMainDicomTags(DicomMap& target,
                                   int64_t id) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.getMainDicomTags(transaction_, id));
-      CheckNoEvent();
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_get_main_dicom_tags()->set_id(id);
 
-      uint32_t count;
-      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_MAIN_DICOM_TAGS, request);
 
       target.Clear();
-      for (uint32_t i = 0; i < count; i++)
+
+      for (int i = 0; i < response.get_main_dicom_tags().tags().size(); i++)
       {
-        uint16_t group, element;
-        const char* value = NULL;
-        CheckSuccess(that_.backend_.readAnswerDicomTag(transaction_, &group, &element, &value, i));
-
-        if (value == NULL)
+        const DatabasePluginMessages::GetMainDicomTags_Response_Tag& tag = response.get_main_dicom_tags().tags(i);
+        if (tag.group() > 0xffffu ||
+            tag.element() > 0xffffu)
         {
-          throw OrthancException(ErrorCode_DatabasePlugin);
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
         }
         else
         {
-          target.SetValue(group, element, std::string(value), false);
+          target.SetValue(tag.group(), tag.element(), tag.value(), false);
         }
       }
     }
@@ -557,115 +608,121 @@
     
     virtual std::string GetPublicId(int64_t resourceId) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.getPublicId(transaction_, resourceId));
-      CheckNoEvent();
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_get_public_id()->set_id(resourceId);
 
-      std::string s;
-      if (ReadSingleStringAnswer(s))
-      {
-        return s;
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_InexistentItem);
-      }
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_PUBLIC_ID, request);
+      return response.get_public_id().id();
     }
 
     
     virtual uint64_t GetResourcesCount(ResourceType resourceType) ORTHANC_OVERRIDE
     {
-      uint64_t value;
-      CheckSuccess(that_.backend_.getResourcesCount(transaction_, &value, Plugins::Convert(resourceType)));
-      CheckNoEvent();
-      return value;
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_get_resources_count()->set_type(Convert(resourceType));
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_RESOURCES_COUNT, request);
+      return response.get_resources_count().count();
     }
 
     
     virtual ResourceType GetResourceType(int64_t resourceId) ORTHANC_OVERRIDE
     {
-      OrthancPluginResourceType type;
-      CheckSuccess(that_.backend_.getResourceType(transaction_, &type, resourceId));
-      CheckNoEvent();
-      return Plugins::Convert(type);
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_get_resource_type()->set_id(resourceId);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_RESOURCE_TYPE, request);
+      return Convert(response.get_resource_type().type());
     }
 
     
     virtual uint64_t GetTotalCompressedSize() ORTHANC_OVERRIDE
     {
-      uint64_t s;
-      CheckSuccess(that_.backend_.getTotalCompressedSize(transaction_, &s));
-      CheckNoEvent();
-      return s;
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_TOTAL_COMPRESSED_SIZE);
+      return response.get_total_compressed_size().size();
     }
 
     
     virtual uint64_t GetTotalUncompressedSize() ORTHANC_OVERRIDE
     {
-      uint64_t s;
-      CheckSuccess(that_.backend_.getTotalUncompressedSize(transaction_, &s));
-      CheckNoEvent();
-      return s;
-    }
-
-    
-    virtual bool IsExistingResource(int64_t internalId) ORTHANC_OVERRIDE
-    {
-      uint8_t b;
-      CheckSuccess(that_.backend_.isExistingResource(transaction_, &b, internalId));
-      CheckNoEvent();
-      return (b != 0);
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_TOTAL_UNCOMPRESSED_SIZE);
+      return response.get_total_uncompressed_size().size();
     }
 
     
     virtual bool IsProtectedPatient(int64_t internalId) ORTHANC_OVERRIDE
     {
-      uint8_t b;
-      CheckSuccess(that_.backend_.isProtectedPatient(transaction_, &b, internalId));
-      CheckNoEvent();
-      return (b != 0);
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_is_protected_patient()->set_patient_id(internalId);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_IS_PROTECTED_PATIENT, request);
+      return response.is_protected_patient().protected_patient();
     }
 
     
     virtual void ListAvailableAttachments(std::set<FileContentType>& target,
                                           int64_t id) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.listAvailableAttachments(transaction_, id));
-      CheckNoEvent();
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_list_available_attachments()->set_id(id);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LIST_AVAILABLE_ATTACHMENTS, request);
 
-      uint32_t count;
-      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
-      
       target.clear();
-      for (uint32_t i = 0; i < count; i++)
+      for (int i = 0; i < response.list_available_attachments().attachments().size(); i++)
       {
-        int32_t value;
-        CheckSuccess(that_.backend_.readAnswerInt32(transaction_, &value, i));
-        target.insert(static_cast<FileContentType>(value));
+        FileContentType attachment = static_cast<FileContentType>(response.list_available_attachments().attachments(i));
+
+        if (target.find(attachment) == target.end())
+        {
+          target.insert(attachment);
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
       }
     }
 
     
-    virtual void LogChange(int64_t internalId,
-                           const ServerIndexChange& change) ORTHANC_OVERRIDE
+    virtual void LogChange(ChangeType changeType,
+                           ResourceType resourceType,
+                           int64_t internalId,
+                           const std::string& /* publicId - unused */,
+                           const std::string& date) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.logChange(transaction_, static_cast<int32_t>(change.GetChangeType()),
-                                            internalId, Plugins::Convert(change.GetResourceType()),
-                                            change.GetDate().c_str()));
-      CheckNoEvent();
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_log_change()->set_change_type(changeType);
+      request.mutable_log_change()->set_resource_type(Convert(resourceType));
+      request.mutable_log_change()->set_resource_id(internalId);
+      request.mutable_log_change()->set_date(date);
+
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_LOG_CHANGE, request);
     }
 
     
     virtual void LogExportedResource(const ExportedResource& resource) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.logExportedResource(transaction_, Plugins::Convert(resource.GetResourceType()),
-                                                      resource.GetPublicId().c_str(),
-                                                      resource.GetModality().c_str(),
-                                                      resource.GetDate().c_str(),
-                                                      resource.GetPatientId().c_str(),
-                                                      resource.GetStudyInstanceUid().c_str(),
-                                                      resource.GetSeriesInstanceUid().c_str(),
-                                                      resource.GetSopInstanceUid().c_str()));
-      CheckNoEvent();
+      // TODO: "seq" is ignored, could be simplified in "ExportedResource"
+      
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_log_exported_resource()->set_resource_type(Convert(resource.GetResourceType()));
+      request.mutable_log_exported_resource()->set_public_id(resource.GetPublicId());
+      request.mutable_log_exported_resource()->set_modality(resource.GetModality());
+      request.mutable_log_exported_resource()->set_date(resource.GetDate());
+      request.mutable_log_exported_resource()->set_patient_id(resource.GetPatientId());
+      request.mutable_log_exported_resource()->set_study_instance_uid(resource.GetStudyInstanceUid());
+      request.mutable_log_exported_resource()->set_series_instance_uid(resource.GetSeriesInstanceUid());
+      request.mutable_log_exported_resource()->set_sop_instance_uid(resource.GetSopInstanceUid());
+
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_LOG_EXPORTED_RESOURCE, request);
     }
 
     
@@ -674,26 +731,22 @@
                                   int64_t id,
                                   FileContentType contentType) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.lookupAttachment(transaction_, &revision, id, static_cast<int32_t>(contentType)));
-      CheckNoEvent();
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_lookup_attachment()->set_id(id);
+      request.mutable_lookup_attachment()->set_content_type(contentType);
 
-      uint32_t count;
-      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LOOKUP_ATTACHMENT, request);
 
-      if (count == 0)
+      if (response.lookup_attachment().found())
       {
-        return false;
-      }
-      else if (count == 1)
-      {
-        OrthancPluginAttachment2 tmp;
-        CheckSuccess(that_.backend_.readAnswerAttachment2(transaction_, &tmp, 0));
-        attachment = Convert(tmp);
+        attachment = Convert(response.lookup_attachment().attachment());
+        revision = response.lookup_attachment().revision();
         return true;
       }
       else
       {
-        throw OrthancException(ErrorCode_DatabasePlugin);
+        return false;
       }
     }
 
@@ -702,32 +755,101 @@
                                       GlobalProperty property,
                                       bool shared) ORTHANC_OVERRIDE
     {
-      const char* id = (shared ? "" : that_.serverIdentifier_.c_str());
-      
-      CheckSuccess(that_.backend_.lookupGlobalProperty(transaction_, id, static_cast<int32_t>(property)));
-      CheckNoEvent();
-      return ReadSingleStringAnswer(target);      
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_lookup_global_property()->set_server_id(shared ? "" : database_.GetServerIdentifier());
+      request.mutable_lookup_global_property()->set_property(property);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LOOKUP_GLOBAL_PROPERTY, request);
+
+      if (response.lookup_global_property().found())
+      {
+        target = response.lookup_global_property().value();
+        return true;
+      }
+      else
+      {
+        return false;
+      }
     }
 
-    
+
+    virtual int64_t IncrementGlobalProperty(GlobalProperty property,
+                                            int64_t increment,
+                                            bool shared) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_increment_global_property()->set_server_id(shared ? "" : database_.GetServerIdentifier());
+      request.mutable_increment_global_property()->set_property(property);
+      request.mutable_increment_global_property()->set_increment(increment);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_INCREMENT_GLOBAL_PROPERTY, request);
+
+      return response.increment_global_property().new_value();
+    }
+
+    virtual void UpdateAndGetStatistics(int64_t& patientsCount,
+                                        int64_t& studiesCount,
+                                        int64_t& seriesCount,
+                                        int64_t& instancesCount,
+                                        int64_t& compressedSize,
+                                        int64_t& uncompressedSize) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_UPDATE_AND_GET_STATISTICS);
+
+      patientsCount = response.update_and_get_statistics().patients_count();
+      studiesCount = response.update_and_get_statistics().studies_count();
+      seriesCount = response.update_and_get_statistics().series_count();
+      instancesCount = response.update_and_get_statistics().instances_count();
+      compressedSize = response.update_and_get_statistics().total_compressed_size();
+      uncompressedSize = response.update_and_get_statistics().total_uncompressed_size();
+    }
+
     virtual bool LookupMetadata(std::string& target,
                                 int64_t& revision,
                                 int64_t id,
                                 MetadataType type) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.lookupMetadata(transaction_, &revision, id, static_cast<int32_t>(type)));
-      CheckNoEvent();
-      return ReadSingleStringAnswer(target);      
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_lookup_metadata()->set_id(id);
+      request.mutable_lookup_metadata()->set_metadata_type(type);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LOOKUP_METADATA, request);
+
+      if (response.lookup_metadata().found())
+      {
+        target = response.lookup_metadata().value();
+        revision = response.lookup_metadata().revision();
+        return true;
+      }
+      else
+      {
+        return false;
+      }
     }
 
     
     virtual bool LookupParent(int64_t& parentId,
                               int64_t resourceId) ORTHANC_OVERRIDE
     {
-      uint8_t existing;
-      CheckSuccess(that_.backend_.lookupParent(transaction_, &existing, &parentId, resourceId));
-      CheckNoEvent();
-      return (existing != 0);
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_lookup_parent()->set_id(resourceId);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LOOKUP_PARENT, request);
+
+      if (response.lookup_parent().found())
+      {
+        parentId = response.lookup_parent().parent();
+        return true;
+      }
+      else
+      {
+        return false;
+      }
     }
 
     
@@ -735,39 +857,60 @@
                                 ResourceType& type,
                                 const std::string& publicId) ORTHANC_OVERRIDE
     {
-      uint8_t existing;
-      OrthancPluginResourceType t;
-      CheckSuccess(that_.backend_.lookupResource(transaction_, &existing, &id, &t, publicId.c_str()));
-      CheckNoEvent();
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_lookup_resource()->set_public_id(publicId);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LOOKUP_RESOURCE, request);
 
-      if (existing == 0)
+      if (response.lookup_resource().found())
       {
-        return false;
+        id = response.lookup_resource().internal_id();
+        type = Convert(response.lookup_resource().type());
+        return true;
       }
       else
       {
-        type = Plugins::Convert(t);
-        return true;
+        return false;
       }
     }
 
     
     virtual bool SelectPatientToRecycle(int64_t& internalId) ORTHANC_OVERRIDE
     {
-      uint8_t available;      
-      CheckSuccess(that_.backend_.selectPatientToRecycle(transaction_, &available, &internalId));
-      CheckNoEvent();
-      return (available != 0);
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_SELECT_PATIENT_TO_RECYCLE);
+
+      if (response.select_patient_to_recycle().found())
+      {
+        internalId = response.select_patient_to_recycle().patient_id();
+        return true;
+      }
+      else
+      {
+        return false;
+      }
     }
 
     
     virtual bool SelectPatientToRecycle(int64_t& internalId,
                                         int64_t patientIdToAvoid) ORTHANC_OVERRIDE
     {
-      uint8_t available;      
-      CheckSuccess(that_.backend_.selectPatientToRecycle2(transaction_, &available, &internalId, patientIdToAvoid));
-      CheckNoEvent();
-      return (available != 0);
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_select_patient_to_recycle_with_avoid()->set_patient_id_to_avoid(patientIdToAvoid);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_SELECT_PATIENT_TO_RECYCLE_WITH_AVOID, request);
+
+      if (response.select_patient_to_recycle_with_avoid().found())
+      {
+        internalId = response.select_patient_to_recycle_with_avoid().patient_id();
+        return true;
+      }
+      else
+      {
+        return false;
+      }
     }
 
     
@@ -775,17 +918,21 @@
                                    bool shared,
                                    const std::string& value) ORTHANC_OVERRIDE
     {
-      const char* id = (shared ? "" : that_.serverIdentifier_.c_str());
-      
-      CheckSuccess(that_.backend_.setGlobalProperty(transaction_, id, static_cast<int32_t>(property), value.c_str()));
-      CheckNoEvent();
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_set_global_property()->set_server_id(shared ? "" : database_.GetServerIdentifier());
+      request.mutable_set_global_property()->set_property(property);
+      request.mutable_set_global_property()->set_value(value);
+
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_SET_GLOBAL_PROPERTY, request);
     }
 
     
     virtual void ClearMainDicomTags(int64_t id) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.clearMainDicomTags(transaction_, id));
-      CheckNoEvent();
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_clear_main_dicom_tags()->set_id(id);
+
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_CLEAR_MAIN_DICOM_TAGS, request);
     }
 
     
@@ -794,82 +941,147 @@
                              const std::string& value,
                              int64_t revision) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.setMetadata(transaction_, id, static_cast<int32_t>(type), value.c_str(), revision));
-      CheckNoEvent();
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_set_metadata()->set_id(id);
+      request.mutable_set_metadata()->set_metadata_type(type);
+      request.mutable_set_metadata()->set_value(value);
+      request.mutable_set_metadata()->set_revision(revision);
+
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_SET_METADATA, request);
     }
 
     
     virtual void SetProtectedPatient(int64_t internalId, 
                                      bool isProtected) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.setProtectedPatient(transaction_, internalId, (isProtected ? 1 : 0)));
-      CheckNoEvent();
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_set_protected_patient()->set_patient_id(internalId);
+      request.mutable_set_protected_patient()->set_protected_patient(isProtected);
+
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_SET_PROTECTED_PATIENT, request);
     }
 
 
     virtual bool IsDiskSizeAbove(uint64_t threshold) ORTHANC_OVERRIDE
     {
-      uint8_t tmp;
-      CheckSuccess(that_.backend_.isDiskSizeAbove(transaction_, &tmp, threshold));
-      CheckNoEvent();
-      return (tmp != 0);
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_is_disk_size_above()->set_threshold(threshold);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_IS_DISK_SIZE_ABOVE, request);
+
+      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 DatabaseConstraints& lookup,
                                       ResourceType queryLevel,
-                                      size_t limit) ORTHANC_OVERRIDE
+                                      const std::set<std::string>& labels,
+                                      LabelsConstraint labelsConstraint,
+                                      uint32_t limit) ORTHANC_OVERRIDE
     {
-      std::vector<OrthancPluginDatabaseConstraint> constraints;
-      std::vector< std::vector<const char*> > constraintsValues;
-
-      constraints.resize(lookup.size());
-      constraintsValues.resize(lookup.size());
-
-      for (size_t i = 0; i < lookup.size(); i++)
+      if (!database_.GetDatabaseCapabilities().HasLabelsSupport() &&
+          !labels.empty())
       {
-        lookup[i].EncodeForPlugins(constraints[i], constraintsValues[i]);
+        throw OrthancException(ErrorCode_InternalError);
       }
 
-      CheckSuccess(that_.backend_.lookupResources(transaction_, lookup.size(),
-                                                  (lookup.empty() ? NULL : &constraints[0]),
-                                                  Plugins::Convert(queryLevel),
-                                                  limit, (instancesId == NULL ? 0 : 1)));
-      CheckNoEvent();
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_lookup_resources()->set_query_level(Convert(queryLevel));
+      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.GetSize());
+      
+      for (size_t i = 0; i < lookup.GetSize(); i++)
+      {
+        const DatabaseConstraint& source = lookup.GetConstraint(i);
+
+        DatabasePluginMessages::DatabaseConstraint* target = request.mutable_lookup_resources()->add_lookup();
+        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));
+        }
 
-      uint32_t count;
-      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
+        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);
+        }
+      }
+
+      for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
+      {
+        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);
+      }
       
-      resourcesId.clear();
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LOOKUP_RESOURCES, request);
 
+      for (int i = 0; i < response.lookup_resources().resources_ids().size(); i++)
+      {
+        resourcesId.push_back(response.lookup_resources().resources_ids(i));
+      }
+      
       if (instancesId != NULL)
       {
-        instancesId->clear();
-      }
-      
-      for (uint32_t i = 0; i < count; i++)
-      {
-        OrthancPluginMatchingResource resource;
-        CheckSuccess(that_.backend_.readAnswerMatchingResource(transaction_, &resource, i));
-
-        if (resource.resourceId == NULL)
+        if (response.lookup_resources().resources_ids().size() != response.lookup_resources().instances_ids().size())
         {
           throw OrthancException(ErrorCode_DatabasePlugin);
         }
-        
-        resourcesId.push_back(resource.resourceId);
-
-        if (instancesId != NULL)
+        else
         {
-          if (resource.someInstanceId == NULL)
+          for (int i = 0; i < response.lookup_resources().instances_ids().size(); i++)
           {
-            throw OrthancException(ErrorCode_DatabasePlugin);
-          }
-          else
-          {
-            instancesId->push_back(resource.someInstanceId);
+            instancesId->push_back(response.lookup_resources().instances_ids(i));
           }
         }
       }
@@ -883,83 +1095,61 @@
                                 const std::string& series,
                                 const std::string& instance) ORTHANC_OVERRIDE
     {
-      OrthancPluginCreateInstanceResult output;
-      memset(&output, 0, sizeof(output));
-
-      CheckSuccess(that_.backend_.createInstance(transaction_, &output, patient.c_str(),
-                                                 study.c_str(), series.c_str(), instance.c_str()));
-      CheckNoEvent();
-
-      instanceId = output.instanceId;
+      // TODO: "CreateInstanceResult" => constructor and getters
       
-      if (output.isNewInstance)
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_create_instance()->set_patient(patient);
+      request.mutable_create_instance()->set_study(study);
+      request.mutable_create_instance()->set_series(series);
+      request.mutable_create_instance()->set_instance(instance);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_CREATE_INSTANCE, request);
+
+      instanceId = response.create_instance().instance_id();
+
+      if (response.create_instance().is_new_instance())
       {
-        result.isNewPatient_ = output.isNewPatient;
-        result.isNewStudy_ = output.isNewStudy;
-        result.isNewSeries_ = output.isNewSeries;
-        result.patientId_ = output.patientId;
-        result.studyId_ = output.studyId;
-        result.seriesId_ = output.seriesId;
+        result.isNewPatient_ = response.create_instance().is_new_patient();
+        result.isNewStudy_ = response.create_instance().is_new_study();
+        result.isNewSeries_ = response.create_instance().is_new_series();
+        result.patientId_ = response.create_instance().patient_id();
+        result.studyId_ = response.create_instance().study_id();
+        result.seriesId_ = response.create_instance().series_id();
         return true;
       }
       else
       {
         return false;
       }
-
     }
 
     
     virtual void SetResourcesContent(const ResourcesContent& content) ORTHANC_OVERRIDE
     {
-      std::vector<OrthancPluginResourcesContentTags> identifierTags;
-      std::vector<OrthancPluginResourcesContentTags> mainDicomTags;
-      std::vector<OrthancPluginResourcesContentMetadata> metadata;
+      DatabasePluginMessages::TransactionRequest request;
 
-      identifierTags.reserve(content.GetListTags().size());
-      mainDicomTags.reserve(content.GetListTags().size());
-      metadata.reserve(content.GetListMetadata().size());
-
-      for (ResourcesContent::ListTags::const_iterator
-             it = content.GetListTags().begin(); it != content.GetListTags().end(); ++it)
+      request.mutable_set_resources_content()->mutable_tags()->Reserve(content.GetListTags().size());
+      for (ResourcesContent::ListTags::const_iterator it = content.GetListTags().begin(); it != content.GetListTags().end(); ++it)
       {
-        OrthancPluginResourcesContentTags tmp;
-        tmp.resource = it->resourceId_;
-        tmp.group = it->tag_.GetGroup();
-        tmp.element = it->tag_.GetElement();
-        tmp.value = it->value_.c_str();
-
-        if (it->isIdentifier_)
-        {
-          identifierTags.push_back(tmp);
-        }
-        else
-        {
-          mainDicomTags.push_back(tmp);
-        }
+        DatabasePluginMessages::SetResourcesContent_Request_Tag* tag = request.mutable_set_resources_content()->add_tags();
+        tag->set_resource_id(it->GetResourceId());
+        tag->set_is_identifier(it->IsIdentifier());
+        tag->set_group(it->GetTag().GetGroup());
+        tag->set_element(it->GetTag().GetElement());
+        tag->set_value(it->GetValue());
+      }
+      
+      request.mutable_set_resources_content()->mutable_metadata()->Reserve(content.GetListMetadata().size());
+      for (ResourcesContent::ListMetadata::const_iterator it = content.GetListMetadata().begin(); it != content.GetListMetadata().end(); ++it)
+      {
+        DatabasePluginMessages::SetResourcesContent_Request_Metadata* metadata = request.mutable_set_resources_content()->add_metadata();
+        metadata->set_resource_id(it->GetResourceId());
+        metadata->set_metadata(it->GetType());
+        metadata->set_value(it->GetValue());
       }
 
-      for (ResourcesContent::ListMetadata::const_iterator
-             it = content.GetListMetadata().begin(); it != content.GetListMetadata().end(); ++it)
-      {
-        OrthancPluginResourcesContentMetadata tmp;
-        tmp.resource = it->resourceId_;
-        tmp.metadata = it->metadata_;
-        tmp.value = it->value_.c_str();
-        metadata.push_back(tmp);
-      }
-
-      assert(identifierTags.size() + mainDicomTags.size() == content.GetListTags().size() &&
-             metadata.size() == content.GetListMetadata().size());
-       
-      CheckSuccess(that_.backend_.setResourcesContent(transaction_,
-                                                      identifierTags.size(),
-                                                      (identifierTags.empty() ? NULL : &identifierTags[0]),
-                                                      mainDicomTags.size(),
-                                                      (mainDicomTags.empty() ? NULL : &mainDicomTags[0]),
-                                                      metadata.size(),
-                                                      (metadata.empty() ? NULL : &metadata[0])));
-      CheckNoEvent();
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_SET_RESOURCES_CONTENT, request);
     }
 
     
@@ -967,18 +1157,25 @@
                                      int64_t resourceId,
                                      MetadataType metadata) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.getChildrenMetadata(transaction_, resourceId, static_cast<int32_t>(metadata)));
-      CheckNoEvent();
-      ReadStringAnswers(target);
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_get_children_metadata()->set_id(resourceId);
+      request.mutable_get_children_metadata()->set_metadata(metadata);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_CHILDREN_METADATA, request);
+
+      for (int i = 0; i < response.get_children_metadata().values().size(); i++)
+      {
+        target.push_back(response.get_children_metadata().values(i));
+      }
     }
 
     
     virtual int64_t GetLastChangeIndex() ORTHANC_OVERRIDE
     {
-      int64_t tmp;
-      CheckSuccess(that_.backend_.getLastChangeIndex(transaction_, &tmp));
-      CheckNoEvent();
-      return tmp;
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_LAST_CHANGE_INDEX);
+      return response.get_last_change_index().result();
     }
 
     
@@ -987,57 +1184,41 @@
                                          std::string& parentPublicId,
                                          const std::string& publicId) ORTHANC_OVERRIDE
     {
-      uint8_t isExisting;
-      OrthancPluginResourceType tmpType;
-      CheckSuccess(that_.backend_.lookupResourceAndParent(transaction_, &isExisting, &id, &tmpType, publicId.c_str()));
-      CheckNoEvent();
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_lookup_resource_and_parent()->set_public_id(publicId);
 
-      if (isExisting)
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LOOKUP_RESOURCE_AND_PARENT, request);
+
+      if (response.lookup_resource_and_parent().found())
       {
-        type = Plugins::Convert(tmpType);
-        
-        uint32_t count;
-        CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
-
-        if (count > 1)
-        {
-          throw OrthancException(ErrorCode_DatabasePlugin);
-        }
+        id = response.lookup_resource_and_parent().id();
+        type = Convert(response.lookup_resource_and_parent().type());
 
         switch (type)
         {
           case ResourceType_Patient:
-            // A patient has no parent
-            if (count == 1)
+            if (!response.lookup_resource_and_parent().parent_public_id().empty())
             {
               throw OrthancException(ErrorCode_DatabasePlugin);
             }
             break;
-
+            
           case ResourceType_Study:
           case ResourceType_Series:
           case ResourceType_Instance:
-            if (count == 0)
+            if (response.lookup_resource_and_parent().parent_public_id().empty())
             {
               throw OrthancException(ErrorCode_DatabasePlugin);
             }
             else
             {
-              const char* value = NULL;
-              CheckSuccess(that_.backend_.readAnswerString(transaction_, &value, 0));
-              if (value == NULL)
-              {
-                throw OrthancException(ErrorCode_DatabasePlugin);
-              }
-              else
-              {
-                parentPublicId.assign(value);
-              }              
+              parentPublicId = response.lookup_resource_and_parent().parent_public_id();
             }
             break;
-
+            
           default:
-            throw OrthancException(ErrorCode_DatabasePlugin);
+            throw OrthancException(ErrorCode_ParameterOutOfRange);
         }
         
         return true;
@@ -1047,210 +1228,269 @@
         return false;
       }
     }
-  };
+
+    
+    virtual void AddLabel(int64_t resource,
+                          const std::string& label) ORTHANC_OVERRIDE
+    {
+      if (database_.GetDatabaseCapabilities().HasLabelsSupport())
+      {
+        DatabasePluginMessages::TransactionRequest request;
+        request.mutable_add_label()->set_id(resource);
+        request.mutable_add_label()->set_label(label);
+
+        ExecuteTransaction(DatabasePluginMessages::OPERATION_ADD_LABEL, request);
+      }
+      else
+      {
+        // This method shouldn't have been called
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
 
-  
-  void OrthancPluginDatabaseV4::CheckSuccess(OrthancPluginErrorCode code) const
-  {
-    if (code != OrthancPluginErrorCode_Success)
+    virtual void RemoveLabel(int64_t resource,
+                             const std::string& label) ORTHANC_OVERRIDE
     {
-      errorDictionary_.LogError(code, true);
-      throw OrthancException(static_cast<ErrorCode>(code));
+      if (database_.GetDatabaseCapabilities().HasLabelsSupport())
+      {
+        DatabasePluginMessages::TransactionRequest request;
+        request.mutable_remove_label()->set_id(resource);
+        request.mutable_remove_label()->set_label(label);
+
+        ExecuteTransaction(DatabasePluginMessages::OPERATION_REMOVE_LABEL, request);
+      }
+      else
+      {
+        // This method shouldn't have been called
+        throw OrthancException(ErrorCode_InternalError);
+      }
     }
-  }
+
+
+    virtual void ListLabels(std::set<std::string>& target,
+                            int64_t resource) ORTHANC_OVERRIDE
+    {
+      ListLabelsInternal(target, true, resource);
+    }
+
+    
+    virtual void ListAllLabels(std::set<std::string>& target) ORTHANC_OVERRIDE
+    {
+      ListLabelsInternal(target, false, -1);
+    }
+  };
 
 
   OrthancPluginDatabaseV4::OrthancPluginDatabaseV4(SharedLibrary& library,
                                                    PluginsErrorDictionary&  errorDictionary,
-                                                   const OrthancPluginDatabaseBackendV4* backend,
-                                                   size_t backendSize,
-                                                   void* database,
+                                                   const _OrthancPluginRegisterDatabaseBackendV4& database,
                                                    const std::string& serverIdentifier) :
     library_(library),
     errorDictionary_(errorDictionary),
-    database_(database),
-    serverIdentifier_(serverIdentifier)
+    definition_(database),
+    serverIdentifier_(serverIdentifier),
+    open_(false),
+    databaseVersion_(0)
   {
     CLOG(INFO, PLUGINS) << "Identifier of this Orthanc server for the global properties "
                         << "of the custom database: \"" << serverIdentifier << "\"";
-    
-    if (backendSize >= sizeof(backend_))
+
+    if (definition_.backend == NULL ||
+        definition_.operations == NULL ||
+        definition_.finalize == NULL)
     {
-      memcpy(&backend_, backend, sizeof(backend_));
-    }
-    else
-    {
-      // Not all the primitives are implemented by the plugin
-      memset(&backend_, 0, sizeof(backend_));
-      memcpy(&backend_, backend, backendSize);
+      throw OrthancException(ErrorCode_NullPointer);
     }
-
-    // Sanity checks
-    CHECK_FUNCTION_EXISTS(backend_, readAnswersCount);
-    CHECK_FUNCTION_EXISTS(backend_, readAnswerAttachment2);
-    CHECK_FUNCTION_EXISTS(backend_, readAnswerChange);
-    CHECK_FUNCTION_EXISTS(backend_, readAnswerDicomTag);
-    CHECK_FUNCTION_EXISTS(backend_, readAnswerExportedResource);
-    CHECK_FUNCTION_EXISTS(backend_, readAnswerInt32);
-    CHECK_FUNCTION_EXISTS(backend_, readAnswerInt64);
-    CHECK_FUNCTION_EXISTS(backend_, readAnswerMatchingResource);
-    CHECK_FUNCTION_EXISTS(backend_, readAnswerMetadata);
-    CHECK_FUNCTION_EXISTS(backend_, readAnswerString);
-    
-    CHECK_FUNCTION_EXISTS(backend_, readEventsCount);
-    CHECK_FUNCTION_EXISTS(backend_, readEvent2);
-
-    CHECK_FUNCTION_EXISTS(backend_, open);
-    CHECK_FUNCTION_EXISTS(backend_, close);
-    CHECK_FUNCTION_EXISTS(backend_, destructDatabase);
-    CHECK_FUNCTION_EXISTS(backend_, getDatabaseVersion);
-    CHECK_FUNCTION_EXISTS(backend_, upgradeDatabase);
-    CHECK_FUNCTION_EXISTS(backend_, startTransaction);
-    CHECK_FUNCTION_EXISTS(backend_, destructTransaction);
-    CHECK_FUNCTION_EXISTS(backend_, hasRevisionsSupport);
-    CHECK_FUNCTION_EXISTS(backend_, hasAttachmentCustomDataSupport);   // new in v4
-
-    CHECK_FUNCTION_EXISTS(backend_, rollback);
-    CHECK_FUNCTION_EXISTS(backend_, commit);
-    
-    CHECK_FUNCTION_EXISTS(backend_, addAttachment2);
-    CHECK_FUNCTION_EXISTS(backend_, clearChanges);
-    CHECK_FUNCTION_EXISTS(backend_, clearExportedResources);
-    CHECK_FUNCTION_EXISTS(backend_, clearMainDicomTags);
-    CHECK_FUNCTION_EXISTS(backend_, createInstance);
-    CHECK_FUNCTION_EXISTS(backend_, deleteAttachment);
-    CHECK_FUNCTION_EXISTS(backend_, deleteMetadata);
-    CHECK_FUNCTION_EXISTS(backend_, deleteResource);
-    CHECK_FUNCTION_EXISTS(backend_, getAllMetadata);
-    CHECK_FUNCTION_EXISTS(backend_, getAllPublicIds);
-    CHECK_FUNCTION_EXISTS(backend_, getAllPublicIdsWithLimit);
-    CHECK_FUNCTION_EXISTS(backend_, getChanges);
-    CHECK_FUNCTION_EXISTS(backend_, getChildrenInternalId);
-    CHECK_FUNCTION_EXISTS(backend_, getChildrenMetadata);
-    CHECK_FUNCTION_EXISTS(backend_, getChildrenPublicId);
-    CHECK_FUNCTION_EXISTS(backend_, getExportedResources);
-    CHECK_FUNCTION_EXISTS(backend_, getLastChange);
-    CHECK_FUNCTION_EXISTS(backend_, getLastChangeIndex);
-    CHECK_FUNCTION_EXISTS(backend_, getLastExportedResource);
-    CHECK_FUNCTION_EXISTS(backend_, getMainDicomTags);
-    CHECK_FUNCTION_EXISTS(backend_, getPublicId);
-    CHECK_FUNCTION_EXISTS(backend_, getResourceType);
-    CHECK_FUNCTION_EXISTS(backend_, getResourcesCount);
-    CHECK_FUNCTION_EXISTS(backend_, getTotalCompressedSize);
-    CHECK_FUNCTION_EXISTS(backend_, getTotalUncompressedSize);
-    CHECK_FUNCTION_EXISTS(backend_, isDiskSizeAbove);
-    CHECK_FUNCTION_EXISTS(backend_, isExistingResource);
-    CHECK_FUNCTION_EXISTS(backend_, isProtectedPatient);
-    CHECK_FUNCTION_EXISTS(backend_, listAvailableAttachments);
-    CHECK_FUNCTION_EXISTS(backend_, logChange);
-    CHECK_FUNCTION_EXISTS(backend_, logExportedResource);
-    CHECK_FUNCTION_EXISTS(backend_, lookupAttachment);
-    CHECK_FUNCTION_EXISTS(backend_, lookupGlobalProperty);
-    CHECK_FUNCTION_EXISTS(backend_, lookupMetadata);
-    CHECK_FUNCTION_EXISTS(backend_, lookupParent);
-    CHECK_FUNCTION_EXISTS(backend_, lookupResource);
-    CHECK_FUNCTION_EXISTS(backend_, lookupResourceAndParent);
-    CHECK_FUNCTION_EXISTS(backend_, lookupResources);
-    CHECK_FUNCTION_EXISTS(backend_, selectPatientToRecycle);
-    CHECK_FUNCTION_EXISTS(backend_, selectPatientToRecycle2);
-    CHECK_FUNCTION_EXISTS(backend_, setGlobalProperty);
-    CHECK_FUNCTION_EXISTS(backend_, setMetadata);
-    CHECK_FUNCTION_EXISTS(backend_, setProtectedPatient);
-    CHECK_FUNCTION_EXISTS(backend_, setResourcesContent);
   }
 
   
   OrthancPluginDatabaseV4::~OrthancPluginDatabaseV4()
   {
-    if (database_ != NULL)
+    definition_.finalize(definition_.backend);
+  }
+
+
+  static void AddIdentifierTags(DatabasePluginMessages::Open::Request& request,
+                                ResourceType level)
+  {
+    const DicomTag* tags = NULL;
+    size_t size;
+
+    ServerToolbox::LoadIdentifiers(tags, size, level);
+
+    if (tags == NULL ||
+        size == 0)
     {
-      OrthancPluginErrorCode code = backend_.destructDatabase(database_);
-      if (code != OrthancPluginErrorCode_Success)
-      {
-        // Don't throw exception in destructors
-        errorDictionary_.LogError(code, true);
-      }
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    for (size_t i = 0; i < size; i++)
+    {
+      DatabasePluginMessages::Open_Request_IdentifierTag* tag = request.add_identifier_tags();
+      tag->set_level(Convert(level));
+      tag->set_group(tags[i].GetGroup());
+      tag->set_element(tags[i].GetElement());
+      tag->set_name(FromDcmtkBridge::GetTagName(tags[i], ""));
     }
   }
 
   
   void OrthancPluginDatabaseV4::Open()
   {
-    CheckSuccess(backend_.open(database_));
+    if (open_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    
+    {
+      DatabasePluginMessages::DatabaseRequest request;
+      AddIdentifierTags(*request.mutable_open(), ResourceType_Patient);
+      AddIdentifierTags(*request.mutable_open(), ResourceType_Study);
+      AddIdentifierTags(*request.mutable_open(), ResourceType_Series);
+      AddIdentifierTags(*request.mutable_open(), ResourceType_Instance);
+
+      DatabasePluginMessages::DatabaseResponse response;
+      ExecuteDatabase(response, *this, DatabasePluginMessages::OPERATION_OPEN, request);
+    }
+
+    {
+      DatabasePluginMessages::DatabaseRequest request;
+      DatabasePluginMessages::DatabaseResponse response;
+      ExecuteDatabase(response, *this, DatabasePluginMessages::OPERATION_GET_SYSTEM_INFORMATION, request);
+      
+      const ::Orthanc::DatabasePluginMessages::GetSystemInformation_Response& systemInfo = response.get_system_information();
+      databaseVersion_ = systemInfo.database_version();
+      dbCapabilities_.SetFlushToDisk(systemInfo.supports_flush_to_disk());
+      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_.SetMeasureLatency(systemInfo.has_measure_latency());
+    }
+
+    open_ = true;
   }
 
 
   void OrthancPluginDatabaseV4::Close()
   {
-    CheckSuccess(backend_.close(database_));
+    if (!open_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      DatabasePluginMessages::DatabaseRequest request;
+      DatabasePluginMessages::DatabaseResponse response;
+      ExecuteDatabase(response, *this, DatabasePluginMessages::OPERATION_CLOSE, request);
+    }
+  }
+  
+
+
+  void OrthancPluginDatabaseV4::FlushToDisk()
+  {
+    if (!open_ ||
+        !GetDatabaseCapabilities().HasFlushToDisk())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      DatabasePluginMessages::DatabaseRequest request;
+      DatabasePluginMessages::DatabaseResponse response;
+      ExecuteDatabase(response, *this, DatabasePluginMessages::OPERATION_FLUSH_TO_DISK, request);
+    }
   }
   
 
   IDatabaseWrapper::ITransaction* OrthancPluginDatabaseV4::StartTransaction(TransactionType type,
                                                                             IDatabaseListener& listener)
   {
-    switch (type)
+    if (!open_)
     {
-      case TransactionType_ReadOnly:
-        return new Transaction(*this, listener, OrthancPluginDatabaseTransactionType_ReadOnly);
-
-      case TransactionType_ReadWrite:
-        return new Transaction(*this, listener, OrthancPluginDatabaseTransactionType_ReadWrite);
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return new Transaction(*this, listener, type);
     }
   }
 
   
   unsigned int OrthancPluginDatabaseV4::GetDatabaseVersion()
   {
-    uint32_t version = 0;
-    CheckSuccess(backend_.getDatabaseVersion(database_, &version));
-    return version;
+    if (!open_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return databaseVersion_;
+    }
   }
 
   
   void OrthancPluginDatabaseV4::Upgrade(unsigned int targetVersion,
                                         IStorageArea& storageArea)
   {
-    VoidDatabaseListener listener;
-    
-    if (backend_.upgradeDatabase != NULL)
+    if (!open_)
     {
-      Transaction transaction(*this, listener, OrthancPluginDatabaseTransactionType_ReadWrite);
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      VoidDatabaseListener listener;
+      Transaction transaction(*this, listener, TransactionType_ReadWrite);
 
-      OrthancPluginErrorCode code = backend_.upgradeDatabase(
-        database_, reinterpret_cast<OrthancPluginStorageArea*>(&storageArea),
-        static_cast<uint32_t>(targetVersion));
+      try
+      {
+        DatabasePluginMessages::DatabaseRequest request;
+        request.mutable_upgrade()->set_target_version(targetVersion);
+        request.mutable_upgrade()->set_storage_area(reinterpret_cast<intptr_t>(&storageArea));
+        request.mutable_upgrade()->set_transaction(reinterpret_cast<intptr_t>(transaction.GetTransactionObject()));
+        
+        DatabasePluginMessages::DatabaseResponse response;
 
-      if (code == OrthancPluginErrorCode_Success)
-      {
+        ExecuteDatabase(response, *this, DatabasePluginMessages::OPERATION_UPGRADE, request);
         transaction.Commit(0);
       }
-      else
+      catch (OrthancException& e)
       {
         transaction.Rollback();
-        errorDictionary_.LogError(code, true);
-        throw OrthancException(static_cast<ErrorCode>(code));
+        throw;
       }
     }
   }
 
-  
-  bool OrthancPluginDatabaseV4::HasRevisionsSupport() const
+
+  uint64_t OrthancPluginDatabaseV4::MeasureLatency()
   {
-    // WARNING: This method requires "Open()" to have been called
-    uint8_t hasRevisions;
-    CheckSuccess(backend_.hasRevisionsSupport(database_, &hasRevisions));
-    return (hasRevisions != 0);
+    if (!open_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      DatabasePluginMessages::DatabaseRequest request;
+      DatabasePluginMessages::DatabaseResponse response;
+
+      ExecuteDatabase(response, *this, DatabasePluginMessages::OPERATION_MEASURE_LATENCY, request);
+      return response.measure_latency().latency_us();
+    }
   }
 
-  bool OrthancPluginDatabaseV4::HasAttachmentCustomDataSupport() const
+
+  const IDatabaseWrapper::Capabilities OrthancPluginDatabaseV4::GetDatabaseCapabilities() const
   {
-    // WARNING: This method requires "Open()" to have been called
-    uint8_t hasAttachmentCustomDataSupport;
-    CheckSuccess(backend_.hasAttachmentCustomDataSupport(database_, &hasAttachmentCustomDataSupport));
-    return (hasAttachmentCustomDataSupport != 0);
+    if (!open_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return dbCapabilities_;
+    }
   }
 }
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -26,7 +27,7 @@
 
 #include "../../../OrthancFramework/Sources/SharedLibrary.h"
 #include "../../Sources/Database/IDatabaseWrapper.h"
-#include "../Include/orthanc/OrthancCDatabasePlugin.h"
+#include "../Include/orthanc/OrthancCPlugin.h"
 #include "PluginsErrorDictionary.h"
 
 namespace Orthanc
@@ -36,24 +37,39 @@
   private:
     class Transaction;
 
-    SharedLibrary&                  library_;
-    PluginsErrorDictionary&         errorDictionary_;
-    OrthancPluginDatabaseBackendV4  backend_;
-    void*                           database_;
-    std::string                     serverIdentifier_;
+    SharedLibrary&                          library_;
+    PluginsErrorDictionary&                 errorDictionary_;
+    _OrthancPluginRegisterDatabaseBackendV4 definition_;
+    std::string                             serverIdentifier_;
+    bool                                    open_;
+    unsigned int                            databaseVersion_;
+    IDatabaseWrapper::Capabilities          dbCapabilities_;
 
     void CheckSuccess(OrthancPluginErrorCode code) const;
 
   public:
     OrthancPluginDatabaseV4(SharedLibrary& library,
-                            PluginsErrorDictionary&  errorDictionary,
-                            const OrthancPluginDatabaseBackendV4* backend,
-                            size_t backendSize,
-                            void* database,
+                            PluginsErrorDictionary& errorDictionary,
+                            const _OrthancPluginRegisterDatabaseBackendV4& database,
                             const std::string& serverIdentifier);
 
     virtual ~OrthancPluginDatabaseV4();
 
+    const _OrthancPluginRegisterDatabaseBackendV4& GetDefinition() const
+    {
+      return definition_;
+    }
+
+    PluginsErrorDictionary& GetErrorDictionary() const
+    {
+      return errorDictionary_;
+    }
+
+    const std::string& GetServerIdentifier() const
+    {
+      return serverIdentifier_;
+    }
+    
     virtual void Open() ORTHANC_OVERRIDE;
 
     virtual void Close() ORTHANC_OVERRIDE;
@@ -63,14 +79,7 @@
       return library_;
     }
 
-    virtual void FlushToDisk() ORTHANC_OVERRIDE
-    {
-    }
-
-    virtual bool HasFlushToDisk() const ORTHANC_OVERRIDE
-    {
-      return false;
-    }
+    virtual void FlushToDisk() ORTHANC_OVERRIDE;
 
     virtual IDatabaseWrapper::ITransaction* StartTransaction(TransactionType type,
                                                              IDatabaseListener& listener)
@@ -81,10 +90,9 @@
     virtual void Upgrade(unsigned int targetVersion,
                          IStorageArea& storageArea) ORTHANC_OVERRIDE;    
 
-    virtual bool HasRevisionsSupport() const ORTHANC_OVERRIDE;
+    virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE;
 
-    virtual bool HasAttachmentCustomDataSupport() const ORTHANC_OVERRIDE;
-
+    virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE;
   };
 }
 
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -68,6 +69,7 @@
 #include "PluginsEnumerations.h"
 #include "PluginsJob.h"
 
+#include <boost/math/special_functions/round.hpp>
 #include <boost/regex.hpp>
 #include <dcmtk/dcmdata/dcdict.h>
 #include <dcmtk/dcmdata/dcdicent.h>
@@ -78,6 +80,125 @@
 
 namespace Orthanc
 {
+  class OrthancPlugins::IDicomInstance : public boost::noncopyable
+  {
+  public:
+    virtual ~IDicomInstance()
+    {
+    }
+
+    virtual bool CanBeFreed() const = 0;
+
+    virtual const DicomInstanceToStore& GetInstance() const = 0;
+  };
+
+
+  class OrthancPlugins::DicomInstanceFromCallback : public IDicomInstance
+  {
+  private:
+    const DicomInstanceToStore&  instance_;
+
+  public:
+    explicit DicomInstanceFromCallback(const DicomInstanceToStore& instance) :
+      instance_(instance)
+    {
+    }
+
+    virtual bool CanBeFreed() const ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+
+    virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE
+    {
+      return instance_;
+    };
+  };
+
+
+  class OrthancPlugins::DicomInstanceFromBuffer : public IDicomInstance
+  {
+  private:
+    std::string                            buffer_;
+    std::unique_ptr<DicomInstanceToStore>  instance_;
+
+    void Setup(const void* buffer,
+               size_t size)
+    {
+      buffer_.assign(reinterpret_cast<const char*>(buffer), size);
+
+      instance_.reset(DicomInstanceToStore::CreateFromBuffer(buffer_));
+      instance_->SetOrigin(DicomInstanceOrigin::FromPlugins());
+    }
+
+  public:
+    DicomInstanceFromBuffer(const void* buffer,
+                            size_t size)
+    {
+      Setup(buffer, size);
+    }
+
+    explicit DicomInstanceFromBuffer(const std::string& buffer)
+    {
+      Setup(buffer.empty() ? NULL : buffer.c_str(), buffer.size());
+    }
+
+    virtual bool CanBeFreed() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+    virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE
+    {
+      return *instance_;
+    };
+  };
+
+
+  class OrthancPlugins::DicomInstanceFromParsed : public IDicomInstance
+  {
+  private:
+    std::unique_ptr<ParsedDicomFile>       parsed_;
+    std::unique_ptr<DicomInstanceToStore>  instance_;
+
+    void Setup(ParsedDicomFile* parsed)
+    {
+      parsed_.reset(parsed);
+      
+      if (parsed_.get() == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+      else
+      {
+        instance_.reset(DicomInstanceToStore::CreateFromParsedDicomFile(*parsed_));
+        instance_->SetOrigin(DicomInstanceOrigin::FromPlugins());
+      }
+    }
+
+  public:
+    explicit DicomInstanceFromParsed(IDicomTranscoder::DicomImage& transcoded)
+    {
+      Setup(transcoded.ReleaseAsParsedDicomFile());
+    }
+
+    explicit DicomInstanceFromParsed(ParsedDicomFile* parsed /* takes ownership */)
+    {
+      Setup(parsed);
+    }
+
+    virtual bool CanBeFreed() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+    virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE
+    {
+      return *instance_;
+    };
+  };
+
+
   class OrthancPlugins::WebDavCollection : public IWebDavBucket
   {
   private:
@@ -415,99 +536,6 @@
     }
   };
   
-  class OrthancPlugins::IDicomInstance : public boost::noncopyable
-  {
-  public:
-    virtual ~IDicomInstance()
-    {
-    }
-
-    virtual bool CanBeFreed() const = 0;
-
-    virtual const DicomInstanceToStore& GetInstance() const = 0;
-  };
-
-
-  class OrthancPlugins::DicomInstanceFromCallback : public IDicomInstance
-  {
-  private:
-    const DicomInstanceToStore&  instance_;
-
-  public:
-    explicit DicomInstanceFromCallback(const DicomInstanceToStore& instance) :
-      instance_(instance)
-    {
-    }
-
-    virtual bool CanBeFreed() const ORTHANC_OVERRIDE
-    {
-      return false;
-    }
-
-    virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE
-    {
-      return instance_;
-    };
-  };
-
-
-  class OrthancPlugins::DicomInstanceFromBuffer : public IDicomInstance
-  {
-  private:
-    std::string                            buffer_;
-    std::unique_ptr<DicomInstanceToStore>  instance_;
-
-  public:
-    DicomInstanceFromBuffer(const void* buffer,
-                            size_t size)
-    {
-      buffer_.assign(reinterpret_cast<const char*>(buffer), size);
-
-      instance_.reset(DicomInstanceToStore::CreateFromBuffer(buffer_));
-      instance_->SetOrigin(DicomInstanceOrigin::FromPlugins());
-    }
-
-    virtual bool CanBeFreed() const ORTHANC_OVERRIDE
-    {
-      return true;
-    }
-
-    virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE
-    {
-      return *instance_;
-    };
-  };
-
-
-  class OrthancPlugins::DicomInstanceFromTranscoded : public IDicomInstance
-  {
-  private:
-    std::unique_ptr<ParsedDicomFile>       parsed_;
-    std::unique_ptr<DicomInstanceToStore>  instance_;
-
-  public:
-    explicit DicomInstanceFromTranscoded(IDicomTranscoder::DicomImage& transcoded) :
-      parsed_(transcoded.ReleaseAsParsedDicomFile())
-    {
-      if (parsed_.get() == NULL)
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-      
-      instance_.reset(DicomInstanceToStore::CreateFromParsedDicomFile(*parsed_));
-      instance_->SetOrigin(DicomInstanceOrigin::FromPlugins());
-    }
-
-    virtual bool CanBeFreed() const ORTHANC_OVERRIDE
-    {
-      return true;
-    }
-
-    virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE
-    {
-      return *instance_;
-    };
-  };
 
   static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target,
                                  const void* data,
@@ -1433,7 +1461,10 @@
   class OrthancPlugins::PImpl
   {
   private:
-    boost::mutex   contextMutex_;
+    boost::mutex              contextMutex_;
+    boost::condition_variable contextCond_;
+    size_t                    contextRefCount_;
+
     ServerContext* context_;
     
   public:
@@ -1801,21 +1832,38 @@
     };
 
 
-    class ServerContextLock
+    // This class ensures that the Context remains valid while being used.
+    // But it does not prevent multiple users to use the context at the same time.
+    // (new behavior in 1.12.2.  In previous version, only one user could use the "locked" context)
+    class ServerContextReference
     {
     private:
-      boost::mutex::scoped_lock  lock_;
       ServerContext* context_;
+      boost::mutex&  mutex_;
+      boost::condition_variable& cond_;
+      size_t& refCount_;
 
     public:
-      explicit ServerContextLock(PImpl& that) : 
-        lock_(that.contextMutex_),
-        context_(that.context_)
+      explicit ServerContextReference(PImpl& that) : 
+        context_(that.context_),
+        mutex_(that.contextMutex_),
+        cond_(that.contextCond_),
+        refCount_(that.contextRefCount_)
       {
         if (context_ == NULL)
         {
           throw OrthancException(ErrorCode_DatabaseNotInitialized);
         }
+
+        boost::mutex::scoped_lock lock(mutex_);
+        refCount_++;
+      }
+
+      ~ServerContextReference()
+      {
+        boost::mutex::scoped_lock lock(mutex_);
+        refCount_--;
+        cond_.notify_one();
       }
 
       ServerContext& GetContext()
@@ -1828,7 +1876,13 @@
 
     void SetServerContext(ServerContext* context)
     {
+      // update only the context while nobody is using it
       boost::mutex::scoped_lock lock(contextMutex_);
+
+      while (contextRefCount_ > 0)
+      {
+        contextCond_.wait(lock);
+      }
       context_ = context;
     }
 
@@ -1889,7 +1943,7 @@
     Properties properties_;
     int argc_;
     char** argv_;
-    std::unique_ptr<OrthancPluginDatabase>  database_;
+    std::unique_ptr<OrthancPluginDatabase>    database_;
     std::unique_ptr<OrthancPluginDatabaseV3>  databaseV3_;  // New in Orthanc 1.9.2
     std::unique_ptr<OrthancPluginDatabaseV4>  databaseV4_;  // New in Orthanc 1.12.0
     PluginsErrorDictionary  dictionary_;
@@ -1897,6 +1951,7 @@
     unsigned int maxDatabaseRetries_;   // New in Orthanc 1.9.2
 
     explicit PImpl(const std::string& databaseServerIdentifier) : 
+      contextRefCount_(0),
       context_(NULL), 
       findCallback_(NULL),
       worklistCallback_(NULL),
@@ -1943,7 +1998,7 @@
       {
         static const char* LUA_CALLBACK = "IncomingWorklistRequestFilter";
 
-        PImpl::ServerContextLock lock(*that_.pimpl_);
+        PImpl::ServerContextReference lock(*that_.pimpl_);
         LuaScripting::Lock lua(lock.GetContext().GetLuaScripting());
 
         if (!lua.GetLua().IsExistingFunction(LUA_CALLBACK))
@@ -2381,7 +2436,8 @@
   OrthancPlugins::OrthancPlugins(const std::string& databaseServerIdentifier)
   {
     /* Sanity check of the compiler */
-    if (sizeof(int32_t) != sizeof(OrthancPluginErrorCode) ||
+    if (sizeof(int32_t) != sizeof(int) ||  // Ensure binary compatibility with Orthanc SDK <= 1.12.1
+        sizeof(int32_t) != sizeof(OrthancPluginErrorCode) ||
         sizeof(int32_t) != sizeof(OrthancPluginHttpMethod) ||
         sizeof(int32_t) != sizeof(_OrthancPluginService) ||
         sizeof(int32_t) != sizeof(_OrthancPluginProperty) ||
@@ -2404,6 +2460,9 @@
         sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode) ||
         sizeof(int32_t) != sizeof(OrthancPluginStorageCommitmentFailureReason) ||
         sizeof(int32_t) != sizeof(OrthancPluginReceivedInstanceAction) ||
+        sizeof(int32_t) != sizeof(OrthancPluginLoadDicomInstanceMode) ||
+        sizeof(int32_t) != sizeof(OrthancPluginLogLevel) ||
+        sizeof(int32_t) != sizeof(OrthancPluginLogCategory) ||
         static_cast<int>(OrthancPluginDicomToJsonFlags_IncludeBinary) != static_cast<int>(DicomToJsonFlags_IncludeBinary) ||
         static_cast<int>(OrthancPluginDicomToJsonFlags_IncludePrivateTags) != static_cast<int>(DicomToJsonFlags_IncludePrivateTags) ||
         static_cast<int>(OrthancPluginDicomToJsonFlags_IncludeUnknownTags) != static_cast<int>(DicomToJsonFlags_IncludeUnknownTags) ||
@@ -2952,6 +3011,25 @@
   }
 
 
+  void OrthancPlugins::SignalJobEvent(const JobEvent& event)
+  {
+    // job events are actually considered as changes inside plugins -> translate
+    switch (event.GetEventType())
+    {
+      case JobEventType_Submitted:
+        SignalChangeInternal(OrthancPluginChangeType_JobSubmitted, OrthancPluginResourceType_None, event.GetJobId().c_str());
+        break;
+      case JobEventType_Success:
+        SignalChangeInternal(OrthancPluginChangeType_JobSuccess, OrthancPluginResourceType_None, event.GetJobId().c_str());
+        break;
+      case JobEventType_Failure:
+        SignalChangeInternal(OrthancPluginChangeType_JobFailure, OrthancPluginResourceType_None, event.GetJobId().c_str());
+        break;
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
 
   void OrthancPlugins::RegisterRestCallback(const void* parameters,
                                             bool mutualExclusion)
@@ -2998,6 +3076,8 @@
 
   void OrthancPlugins::RegisterOnChangeCallback(const void* parameters)
   {
+    boost::recursive_mutex::scoped_lock lock(pimpl_->changeCallbackMutex_);
+
     const _OrthancPluginOnChangeCallback& p = 
       *reinterpret_cast<const _OrthancPluginOnChangeCallback*>(parameters);
 
@@ -3357,13 +3437,34 @@
     std::string dicom;
 
     {
-      PImpl::ServerContextLock lock(*pimpl_);
+      PImpl::ServerContextReference lock(*pimpl_);
       lock.GetContext().ReadDicom(dicom, p.instanceId);
     }
 
     CopyToMemoryBuffer(*p.target, dicom);
   }
 
+  static void ThrowOnHttpError(HttpStatus httpStatus)
+  {
+    int intHttpStatus = static_cast<int>(httpStatus);
+    if (intHttpStatus >= 200 && intHttpStatus <= 300)
+    {
+      return; // not an error
+    }
+    else if (intHttpStatus == 401 || intHttpStatus == 403)
+    {
+      throw OrthancException(ErrorCode_Unauthorized);
+    }
+    else if (intHttpStatus == 404)
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+  }
+
 
   void OrthancPlugins::RestApiGet(const void* parameters,
                                   bool afterPlugins)
@@ -3377,21 +3478,16 @@
     IHttpHandler* handler;
 
     {
-      PImpl::ServerContextLock lock(*pimpl_);
+      PImpl::ServerContextReference lock(*pimpl_);
       handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins);
     }
 
     std::map<std::string, std::string> httpHeaders;
 
     std::string result;
-    if (IHttpHandler::SimpleGet(result, NULL, *handler, RequestOrigin_Plugins, p.uri, httpHeaders) == HttpStatus_200_Ok)
-    {
-      CopyToMemoryBuffer(*p.target, result);
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
+
+    ThrowOnHttpError(IHttpHandler::SimpleGet(result, NULL, *handler, RequestOrigin_Plugins, p.uri, httpHeaders));
+    CopyToMemoryBuffer(*p.target, result);
   }
 
 
@@ -3415,19 +3511,14 @@
     IHttpHandler* handler;
 
     {
-      PImpl::ServerContextLock lock(*pimpl_);
+      PImpl::ServerContextReference lock(*pimpl_);
       handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!p.afterPlugins);
     }
       
     std::string result;
-    if (IHttpHandler::SimpleGet(result, NULL, *handler, RequestOrigin_Plugins, p.uri, headers) == HttpStatus_200_Ok)
-    {
-      CopyToMemoryBuffer(*p.target, result);
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
+
+    ThrowOnHttpError(IHttpHandler::SimpleGet(result, NULL, *handler, RequestOrigin_Plugins, p.uri, headers));
+    CopyToMemoryBuffer(*p.target, result);
   }
 
 
@@ -3444,25 +3535,20 @@
     IHttpHandler* handler;
 
     {
-      PImpl::ServerContextLock lock(*pimpl_);
+      PImpl::ServerContextReference lock(*pimpl_);
       handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins);
     }
       
     std::map<std::string, std::string> httpHeaders;
 
     std::string result;
-    if (isPost ? 
+    
+    ThrowOnHttpError((isPost ? 
         IHttpHandler::SimplePost(result, NULL, *handler, RequestOrigin_Plugins, p.uri,
-                                 p.body, p.bodySize, httpHeaders) == HttpStatus_200_Ok :
+                                 p.body, p.bodySize, httpHeaders) :
         IHttpHandler::SimplePut(result, NULL, *handler, RequestOrigin_Plugins, p.uri,
-                                p.body, p.bodySize, httpHeaders) == HttpStatus_200_Ok)
-    {
-      CopyToMemoryBuffer(*p.target, result);
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
+                                p.body, p.bodySize, httpHeaders)));
+    CopyToMemoryBuffer(*p.target, result);
   }
 
 
@@ -3476,16 +3562,13 @@
     IHttpHandler* handler;
 
     {
-      PImpl::ServerContextLock lock(*pimpl_);
+      PImpl::ServerContextReference lock(*pimpl_);
       handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins);
     }
       
     std::map<std::string, std::string> httpHeaders;
 
-    if (IHttpHandler::SimpleDelete(NULL, *handler, RequestOrigin_Plugins, uri, httpHeaders) != HttpStatus_200_Ok)
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
+    ThrowOnHttpError(IHttpHandler::SimpleDelete(NULL, *handler, RequestOrigin_Plugins, uri, httpHeaders));
   }
 
 
@@ -3538,7 +3621,7 @@
     std::vector<std::string> result;
 
     {
-      PImpl::ServerContextLock lock(*pimpl_);
+      PImpl::ServerContextReference lock(*pimpl_);
       lock.GetContext().GetIndex().LookupIdentifierExact(result, level, tag, p.argument);
     }
 
@@ -3833,7 +3916,7 @@
 
         std::unique_ptr<ImageAccessor> decoded;
         {
-          PImpl::ServerContextLock lock(*pimpl_);
+          PImpl::ServerContextReference lock(*pimpl_);
           decoded.reset(lock.GetContext().DecodeDicomFrame(instance, p.frameIndex));
         }
         
@@ -3916,7 +3999,7 @@
 
       case OrthancPluginImageFormat_Dicom:
       {
-        PImpl::ServerContextLock lock(*pimpl_);
+        PImpl::ServerContextReference lock(*pimpl_);
         image.reset(lock.GetContext().DecodeDicomFrame(p.data, p.size, 0));
         break;
       }
@@ -4241,7 +4324,7 @@
     IHttpHandler* handler;
 
     {
-      PImpl::ServerContextLock lock(*pimpl_);
+      PImpl::ServerContextReference lock(*pimpl_);
       handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!p.afterPlugins);
     }
     
@@ -4463,7 +4546,7 @@
       std::string content;
 
       {
-        PImpl::ServerContextLock lock(*pimpl_);
+        PImpl::ServerContextReference lock(*pimpl_);
         lock.GetContext().ReadDicom(content, p.instanceId);
       }
 
@@ -4499,7 +4582,7 @@
     {
       // Fix issue 168 (Plugins can't read private tags from the
       // configuration file)
-      // https://bugs.orthanc-server.com/show_bug.cgi?id=168
+      // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=168
       std::string privateCreator;
 
       if (privateCreatorC == NULL)
@@ -4592,7 +4675,7 @@
 
       case _OrthancPluginService_DecodeDicomImage:
       {
-        PImpl::ServerContextLock lock(*pimpl_);
+        PImpl::ServerContextReference lock(*pimpl_);
         result.reset(lock.GetContext().DecodeDicomFrame(p.constBuffer, p.bufferSize, p.frameIndex));
         break;
       }
@@ -4632,7 +4715,115 @@
     
     reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->SendMultipartItem(p.answer, p.answerSize, headers);
   }
-      
+
+
+  void OrthancPlugins::ApplyLoadDicomInstance(const _OrthancPluginLoadDicomInstance& params)
+  {
+    std::unique_ptr<IDicomInstance> target;
+    
+    switch (params.mode)
+    {
+      case OrthancPluginLoadDicomInstanceMode_WholeDicom:
+      {
+        std::string buffer;
+
+        {
+          PImpl::ServerContextReference lock(*pimpl_);
+          lock.GetContext().ReadDicom(buffer, params.instanceId);
+        }
+
+        target.reset(new DicomInstanceFromBuffer(buffer));
+        break;
+      }
+        
+      case OrthancPluginLoadDicomInstanceMode_UntilPixelData:
+      case OrthancPluginLoadDicomInstanceMode_EmptyPixelData:
+      {
+        std::unique_ptr<ParsedDicomFile> parsed;
+
+        {
+          std::string buffer;
+        
+          {
+            PImpl::ServerContextReference lock(*pimpl_);
+            if (!lock.GetContext().ReadDicomUntilPixelData(buffer, params.instanceId))
+            {
+              lock.GetContext().ReadDicom(buffer, params.instanceId);
+            }
+          }
+
+          parsed.reset(new ParsedDicomFile(buffer));
+        }
+
+        parsed->RemoveFromPixelData();
+
+        if (params.mode == OrthancPluginLoadDicomInstanceMode_EmptyPixelData)
+        {
+          bool hasPixelData = false;
+          ValueRepresentation pixelDataVR = parsed->GuessPixelDataValueRepresentation();
+
+          {
+            PImpl::ServerContextReference lock(*pimpl_);
+
+            std::string s;
+            int64_t revision;  // unused
+            if (lock.GetContext().GetIndex().LookupMetadata(
+                  s, revision, params.instanceId,
+                  ResourceType_Instance, MetadataType_Instance_PixelDataVR))
+            {
+              hasPixelData = true;
+              if (s == "OB")
+              {
+                pixelDataVR = ValueRepresentation_OtherByte;
+              }
+              else if (s == "OW")
+              {
+                pixelDataVR = ValueRepresentation_OtherWord;
+              }
+              else
+              {
+                LOG(WARNING) << "Corrupted PixelDataVR metadata associated with instance "
+                             << params.instanceId << ": " << s;
+              }
+            }
+            else if (lock.GetContext().GetIndex().LookupMetadata(
+                       s, revision, params.instanceId,
+                       ResourceType_Instance, MetadataType_Instance_PixelDataOffset))
+            {
+              // This file was stored by an older version of Orthanc,
+              // "PixelDataVR" is not available, so use the guess
+              hasPixelData = true;
+            }
+            else
+            {
+              hasPixelData = false;
+            }
+          }
+
+          if (hasPixelData)
+          {
+            parsed->InjectEmptyPixelData(pixelDataVR);
+          }
+        }
+
+        target.reset(new DicomInstanceFromParsed(parsed.release()));
+        break;
+      }
+        
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    if (target.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      *params.target = reinterpret_cast<OrthancPluginDicomInstance*>(target.release());
+    } 
+  }
+  
 
   void OrthancPlugins::DatabaseAnswer(const void* parameters)
   {
@@ -4695,7 +4886,7 @@
     {
       // Fix issue 168 (Plugins can't read private tags from the
       // configuration file)
-      // https://bugs.orthanc-server.com/show_bug.cgi?id=168
+      // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=168
       std::string privateCreator;
       {
         OrthancConfiguration::ReaderLock lock;
@@ -4903,7 +5094,7 @@
         {
           // TODO - Plugins can only access global properties of their
           // own Orthanc server (no access to the shared global properties)
-          PImpl::ServerContextLock lock(*pimpl_);
+          PImpl::ServerContextReference lock(*pimpl_);
           lock.GetContext().GetIndex().SetGlobalProperty(static_cast<GlobalProperty>(p.property),
                                                          false /* not shared */, p.value);
           return true;
@@ -4920,7 +5111,7 @@
         {
           // TODO - Plugins can only access global properties of their
           // own Orthanc server (no access to the shared global properties)
-          PImpl::ServerContextLock lock(*pimpl_);
+          PImpl::ServerContextReference lock(*pimpl_);
           result = lock.GetContext().GetIndex().GetGlobalProperty(static_cast<GlobalProperty>(p.property),
                                                                   false /* not shared */, p.value);
         }
@@ -5362,6 +5553,14 @@
         return true;
       }
 
+      case _OrthancPluginService_CreateJob2:
+      {
+        const _OrthancPluginCreateJob2& p =
+          *reinterpret_cast<const _OrthancPluginCreateJob2*>(parameters);
+        *(p.target) = reinterpret_cast<OrthancPluginJob*>(new PluginsJob(p));
+        return true;
+      }
+
       case _OrthancPluginService_FreeJob:
       {
         const _OrthancPluginFreeJob& p =
@@ -5382,7 +5581,7 @@
 
         std::string uuid;
 
-        PImpl::ServerContextLock lock(*pimpl_);
+        PImpl::ServerContextReference lock(*pimpl_);
         lock.GetContext().GetJobsEngine().GetRegistry().Submit
           (uuid, reinterpret_cast<PluginsJob*>(p.job), p.priority);
         
@@ -5404,24 +5603,22 @@
         const _OrthancPluginSetMetricsValue& p =
           *reinterpret_cast<const _OrthancPluginSetMetricsValue*>(parameters);
 
-        MetricsType type;
-        switch (p.type)
-        {
-          case OrthancPluginMetricsType_Default:
-            type = MetricsType_Default;
-            break;
-
-          case OrthancPluginMetricsType_Timer:
-            type = MetricsType_MaxOver10Seconds;
-            break;
-
-          default:
-            throw OrthancException(ErrorCode_ParameterOutOfRange);
-        }
-        
-        {
-          PImpl::ServerContextLock lock(*pimpl_);
-          lock.GetContext().GetMetricsRegistry().SetValue(p.name, p.value, type);
+        {
+          PImpl::ServerContextReference lock(*pimpl_);
+          lock.GetContext().GetMetricsRegistry().SetFloatValue(p.name, p.value, Plugins::Convert(p.type));
+        }
+
+        return true;
+      }
+
+      case _OrthancPluginService_SetMetricsIntegerValue:
+      {
+        const _OrthancPluginSetMetricsIntegerValue& p =
+          *reinterpret_cast<const _OrthancPluginSetMetricsIntegerValue*>(parameters);
+
+        {
+          PImpl::ServerContextReference lock(*pimpl_);
+          lock.GetContext().GetMetricsRegistry().SetIntegerValue(p.name, p.value, Plugins::Convert(p.type));
         }
 
         return true;
@@ -5512,7 +5709,7 @@
           bool success;
           
           {
-            PImpl::ServerContextLock lock(*pimpl_);
+            PImpl::ServerContextReference lock(*pimpl_);
             success = lock.GetContext().Transcode(
               transcoded, source, syntaxes, true /* allow new sop */);
           }
@@ -5520,7 +5717,7 @@
           if (success)
           {
             *(p.target) = reinterpret_cast<OrthancPluginDicomInstance*>(
-              new DicomInstanceFromTranscoded(transcoded));
+              new DicomInstanceFromParsed(transcoded));
             return true;
           }
           else
@@ -5582,6 +5779,20 @@
         RegisterIncomingHttpRequestFilter2(parameters);
         return true;
 
+      case _OrthancPluginService_LoadDicomInstance:
+      {
+        const _OrthancPluginLoadDicomInstance& p =
+          *reinterpret_cast<const _OrthancPluginLoadDicomInstance*>(parameters);
+        ApplyLoadDicomInstance(p);
+        return true;
+      }
+
+      case _OrthancPluginService_SetCurrentThreadName:
+      {
+        Logging::SetCurrentThreadName(std::string(reinterpret_cast<const char*>(parameters)));
+        return true;
+      }
+
       default:
         return false;
     }
@@ -5741,7 +5952,7 @@
 
       case _OrthancPluginService_RegisterDatabaseBackend:
       {
-        LOG(WARNING) << "Performance warning: Plugin has registered a custom database back-end with an old API";
+        LOG(WARNING) << "Performance warning: Plugin has registered a custom database back-end with an old API (version 1)";
         LOG(WARNING) << "The database backend has *no* support for revisions of metadata and attachments";
 
         const _OrthancPluginRegisterDatabaseBackend& p =
@@ -5766,7 +5977,7 @@
 
       case _OrthancPluginService_RegisterDatabaseBackendV2:
       {
-        LOG(WARNING) << "Performance warning: Plugin has registered a custom database back-end with an old API";
+        LOG(WARNING) << "Performance warning: Plugin has registered a custom database back-end with an old API (version 2)";
         LOG(WARNING) << "The database backend has *no* support for revisions of metadata and attachments";
 
         const _OrthancPluginRegisterDatabaseBackendV2& p =
@@ -5792,7 +6003,7 @@
 
       case _OrthancPluginService_RegisterDatabaseBackendV3:
       {
-        CLOG(INFO, PLUGINS) << "Plugin has registered a custom database back-end (v3)";
+        LOG(WARNING) << "Performance warning: Plugin has registered a custom database back-end with an old API (version 3)";
 
         const _OrthancPluginRegisterDatabaseBackendV3& p =
           *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackendV3*>(parameters);
@@ -5813,7 +6024,6 @@
         return true;
       }
 
-
       case _OrthancPluginService_RegisterDatabaseBackendV4:
       {
         CLOG(INFO, PLUGINS) << "Plugin has registered a custom database back-end (v4)";
@@ -5825,8 +6035,7 @@
             pimpl_->databaseV3_.get() == NULL &&
             pimpl_->databaseV4_.get() == NULL)
         {
-          pimpl_->databaseV4_.reset(new OrthancPluginDatabaseV4(plugin, GetErrorDictionary(), p.backend,
-                                                                p.backendSize, p.database, pimpl_->databaseServerIdentifier_));
+          pimpl_->databaseV4_.reset(new OrthancPluginDatabaseV4(plugin, GetErrorDictionary(), p, pimpl_->databaseServerIdentifier_));
           pimpl_->maxDatabaseRetries_ = p.maxDatabaseRetries;
         }
         else
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -88,7 +89,7 @@
     class HttpServerChunkedReader;
     class IDicomInstance;
     class DicomInstanceFromBuffer;
-    class DicomInstanceFromTranscoded;
+    class DicomInstanceFromParsed;
     class WebDavCollection;
 
 public:
@@ -220,6 +221,8 @@
 
     void ApplySendMultipartItem2(const void* parameters);
 
+    void ApplyLoadDicomInstance(const _OrthancPluginLoadDicomInstance& parameters);
+
     void ComputeHash(_OrthancPluginService service,
                      const void* parameters);
 
@@ -270,7 +273,9 @@
                                const void* parameters) ORTHANC_OVERRIDE;
 
     virtual void SignalChange(const ServerIndexChange& change) ORTHANC_OVERRIDE;
-    
+
+    virtual void SignalJobEvent(const JobEvent& event) ORTHANC_OVERRIDE;
+
     virtual void SignalStoredInstance(const std::string& instanceId,
                                       const DicomInstanceToStore& instance,
                                       const Json::Value& simplifiedTags) ORTHANC_OVERRIDE;
@@ -320,21 +325,6 @@
       SignalChangeInternal(OrthancPluginChangeType_OrthancStopped, OrthancPluginResourceType_None, NULL);
     }
 
-    void SignalJobSubmitted(const std::string& jobId)
-    {
-      SignalChangeInternal(OrthancPluginChangeType_JobSubmitted, OrthancPluginResourceType_None, jobId.c_str());
-    }
-
-    void SignalJobSuccess(const std::string& jobId)
-    {
-      SignalChangeInternal(OrthancPluginChangeType_JobSuccess, OrthancPluginResourceType_None, jobId.c_str());
-    }
-
-    void SignalJobFailure(const std::string& jobId)
-    {
-      SignalChangeInternal(OrthancPluginChangeType_JobFailure, OrthancPluginResourceType_None, jobId.c_str());
-    }
-
     void SignalUpdatedPeers()
     {
       SignalChangeInternal(OrthancPluginChangeType_UpdatedPeers, OrthancPluginResourceType_None, NULL);
--- a/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -578,5 +579,21 @@
           throw OrthancException(ErrorCode_ParameterOutOfRange);
       }
     }
+
+
+    MetricsUpdatePolicy Convert(OrthancPluginMetricsType type)
+    {
+      switch (type)
+      {
+        case OrthancPluginMetricsType_Default:
+          return MetricsUpdatePolicy_Directly;
+
+        case OrthancPluginMetricsType_Timer:
+          return MetricsUpdatePolicy_MaxOver10Seconds;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
   }
 }
--- a/OrthancServer/Plugins/Engine/PluginsEnumerations.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -31,6 +32,7 @@
  * "orthanc-databases" project.
  **/
 
+#include "../../../OrthancFramework/Sources/MetricsRegistry.h"
 #include "../../Sources/Search/DatabaseConstraint.h"
 #include "../../Sources/ServerEnumerations.h"
 #include "../Include/orthanc/OrthancCPlugin.h"
@@ -71,6 +73,8 @@
     JobStepCode Convert(OrthancPluginJobStepStatus step);
 
     StorageCommitmentFailureReason Convert(OrthancPluginStorageCommitmentFailureReason reason);
+
+    MetricsUpdatePolicy Convert(OrthancPluginMetricsType type);
   }
 }
 
--- a/OrthancServer/Plugins/Engine/PluginsErrorDictionary.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsErrorDictionary.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Plugins/Engine/PluginsErrorDictionary.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsErrorDictionary.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Plugins/Engine/PluginsJob.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsJob.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -36,8 +37,7 @@
 
 namespace Orthanc
 {
-  PluginsJob::PluginsJob(const _OrthancPluginCreateJob& parameters) :
-    parameters_(parameters)
+  void PluginsJob::Setup()
   {
     if (parameters_.job == NULL)
     {
@@ -48,17 +48,47 @@
         parameters_.finalize == NULL ||
         parameters_.type == NULL ||
         parameters_.getProgress == NULL ||
-        parameters_.getContent == NULL ||
-        parameters_.getSerialized == NULL ||
+        (parameters_.getContent == NULL && deprecatedGetContent_ == NULL) ||
+        (parameters_.getSerialized == NULL && deprecatedGetSerialized_ == NULL) ||
         parameters_.step == NULL ||
         parameters_.stop == NULL ||
         parameters_.reset == NULL)
     {
-      parameters_.finalize(parameters.job);
+      parameters_.finalize(parameters_.job);
       throw OrthancException(ErrorCode_NullPointer);
     }
 
-    type_.assign(parameters.type);
+    type_.assign(parameters_.type);
+  }
+  
+  PluginsJob::PluginsJob(const _OrthancPluginCreateJob2& parameters) :
+    parameters_(parameters),
+    deprecatedGetContent_(NULL),
+    deprecatedGetSerialized_(NULL)
+  {
+    Setup();
+  }
+
+  PluginsJob::PluginsJob(const _OrthancPluginCreateJob& parameters)
+  {
+    LOG(WARNING) << "Your plugin is using the deprecated OrthancPluginCreateJob() function";
+
+    memset(&parameters_, 0, sizeof(parameters_));
+    parameters_.target = parameters.target;
+    parameters_.job = parameters.job;
+    parameters_.finalize = parameters.finalize;
+    parameters_.type = parameters.type;
+    parameters_.getProgress = parameters.getProgress;
+    parameters_.getContent = NULL;
+    parameters_.getSerialized = NULL;
+    parameters_.step = parameters.step;
+    parameters_.stop = parameters.stop;
+    parameters_.reset = parameters.reset;
+
+    deprecatedGetContent_ = parameters.getContent;
+    deprecatedGetSerialized_ = parameters.getSerialized;
+    
+    Setup();
   }
 
   PluginsJob::~PluginsJob()
@@ -122,53 +152,148 @@
     return parameters_.getProgress(parameters_.job);
   }
 
+
+  namespace
+  {
+    class MemoryBufferRaii : public boost::noncopyable
+    {
+    private:
+      OrthancPluginMemoryBuffer  buffer_;
+
+    public:
+      MemoryBufferRaii()
+      {
+        buffer_.size = 0;
+        buffer_.data = NULL;
+      }
+
+      ~MemoryBufferRaii()
+      {
+        if (buffer_.size != 0)
+        {
+          free(buffer_.data);
+        }
+      }
+
+      OrthancPluginMemoryBuffer* GetObject()
+      {
+        return &buffer_;
+      }
+
+      void ToJsonObject(Json::Value& target) const
+      {
+        if ((buffer_.data == NULL && buffer_.size != 0) ||
+            (buffer_.data != NULL && buffer_.size == 0) ||
+            !Toolbox::ReadJson(target, buffer_.data, buffer_.size) ||
+            target.type() != Json::objectValue)
+        {
+          throw OrthancException(ErrorCode_Plugin,
+                                 "A job plugin must provide a JSON object as its public content and as its serialization");
+        }
+      }
+    };
+  }
+  
   void PluginsJob::GetPublicContent(Json::Value& value)
   {
-    const char* content = parameters_.getContent(parameters_.job);
+    if (parameters_.getContent != NULL)
+    {
+      MemoryBufferRaii target;
+
+      OrthancPluginErrorCode code = parameters_.getContent(target.GetObject(), parameters_.job);
 
-    if (content == NULL)
-    {
-      value = Json::objectValue;
+      if (code != OrthancPluginErrorCode_Success)
+      {
+        throw OrthancException(static_cast<ErrorCode>(code));
+      }
+      else
+      {
+        target.ToJsonObject(value);
+      }
     }
     else
     {
-      if (!Toolbox::ReadJson(value, content) ||
-          value.type() != Json::objectValue)
+      // This was the source code in Orthanc <= 1.11.2
+      const char* content = deprecatedGetContent_(parameters_.job);
+
+      if (content == NULL)
       {
-        throw OrthancException(ErrorCode_Plugin,
-                               "A job plugin must provide a JSON object as its public content");
+        value = Json::objectValue;
+      }
+      else
+      {
+        if (!Toolbox::ReadJson(value, content) ||
+            value.type() != Json::objectValue)
+        {
+          throw OrthancException(ErrorCode_Plugin,
+                                 "A job plugin must provide a JSON object as its public content");
+        }
       }
     }
   }
 
   bool PluginsJob::Serialize(Json::Value& value)
   {
-    const char* serialized = parameters_.getSerialized(parameters_.job);
+    if (parameters_.getSerialized != NULL)
+    {
+      MemoryBufferRaii target;
+
+      int32_t code = parameters_.getContent(target.GetObject(), parameters_.job);
 
-    if (serialized == NULL)
-    {
-      return false;
+      if (code < 0)
+      {
+        throw OrthancException(ErrorCode_Plugin, "Error during the serialization of a job");
+      }
+      else if (code == 0)
+      {
+        return false;  // Serialization is not implemented
+      }
+      else
+      {
+        target.ToJsonObject(value);
+
+        static const char* KEY_TYPE = "Type";
+      
+        if (value.isMember(KEY_TYPE))
+        {
+          throw OrthancException(ErrorCode_Plugin,
+                                 "The \"Type\" field is for reserved use for serialized job");
+        }
+
+        value[KEY_TYPE] = type_;
+        return true;
+      }
     }
     else
     {
-      if (!Toolbox::ReadJson(value, serialized) ||
-          value.type() != Json::objectValue)
+      // This was the source code in Orthanc <= 1.11.2
+      const char* serialized = deprecatedGetSerialized_(parameters_.job);
+
+      if (serialized == NULL)
       {
-        throw OrthancException(ErrorCode_Plugin,
-                               "A job plugin must provide a JSON object as its serialized content");
+        return false;
       }
+      else
+      {
+        if (!Toolbox::ReadJson(value, serialized) ||
+            value.type() != Json::objectValue)
+        {
+          throw OrthancException(ErrorCode_Plugin,
+                                 "A job plugin must provide a JSON object as its serialized content");
+        }
 
 
-      static const char* KEY_TYPE = "Type";
+        static const char* KEY_TYPE = "Type";
       
-      if (value.isMember(KEY_TYPE))
-      {
-        throw OrthancException(ErrorCode_Plugin,
-                               "The \"Type\" field is for reserved use for serialized job");
+        if (value.isMember(KEY_TYPE))
+        {
+          throw OrthancException(ErrorCode_Plugin,
+                                 "The \"Type\" field is for reserved use for serialized job");
+        }
+
+        value[KEY_TYPE] = type_;
+        return true;
       }
-
-      value[KEY_TYPE] = type_;
-      return true;
     }
   }
 }
--- a/OrthancServer/Plugins/Engine/PluginsJob.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsJob.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -33,12 +34,18 @@
   class PluginsJob : public IJob
   {
   private:
-    _OrthancPluginCreateJob  parameters_;
-    std::string              type_;
+    _OrthancPluginCreateJob2       parameters_;
+    std::string                    type_;
+    OrthancPluginJobGetContent     deprecatedGetContent_;
+    OrthancPluginJobGetSerialized  deprecatedGetSerialized_;
+
+    void Setup();
 
   public:
     explicit PluginsJob(const _OrthancPluginCreateJob& parameters);
 
+    explicit PluginsJob(const _OrthancPluginCreateJob2& parameters);
+
     virtual ~PluginsJob();
 
     virtual void Start() ORTHANC_OVERRIDE
@@ -70,6 +77,12 @@
       // TODO
       return false;
     }
+
+    virtual bool DeleteOutput(const std::string& key) ORTHANC_OVERRIDE
+    {
+      // TODO
+      return false;
+    }
   };
 }
 
--- a/OrthancServer/Plugins/Engine/PluginsManager.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsManager.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -36,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"
@@ -161,6 +162,18 @@
         CLOG(INFO, PLUGINS) << reinterpret_cast<const char*>(params);
         return OrthancPluginErrorCode_Success;
 
+      case _OrthancPluginService_LogMessage:
+      {
+        const _OrthancPluginLogMessage& m = *reinterpret_cast<const _OrthancPluginLogMessage*>(params);
+        // We can convert directly from OrthancPluginLogLevel to LogLevel (and category) because the enum values must be identical 
+        // for Orthanc::Logging to work both in the core and in the plugins
+        Orthanc::Logging::LogLevel level = static_cast<Orthanc::Logging::LogLevel>(m.level);
+        Orthanc::Logging::LogCategory category = static_cast<Orthanc::Logging::LogCategory>(m.category);
+          
+        LOG_FROM_PLUGIN(level, category, m.plugin, m.file, m.line) << m.message;
+        return OrthancPluginErrorCode_Success;
+      }
+
       default:
         break;
     }
@@ -183,7 +196,10 @@
         // This service provider has failed
         if (e.GetErrorCode() != ErrorCode_UnknownResource)  // This error code is valid in plugins
         {
-          LOG(ERROR) << "Exception while invoking plugin service " << service << ": " << e.What();
+          if (!e.HasBeenLogged())
+          {
+            LOG(ERROR) << "Exception while invoking plugin service " << service << ": " << e.What();
+          }
         }
 
         return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
@@ -292,7 +308,7 @@
       }
       else
       {
-        std::string extension = boost::filesystem::extension(it->path());
+        std::string extension = it->path().extension().string();
         Toolbox::ToLowerCase(extension);
 
         if (extension == PLUGIN_EXTENSION)
--- a/OrthancServer/Plugins/Engine/PluginsManager.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsManager.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Tue Sep 24 11:39:52 2024 +0200
@@ -6,8 +6,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -1402,371 +1403,340 @@
   }
 
 
-  typedef struct
-  {
-    OrthancPluginDatabaseEventType type;
+  // typedef struct
+  // {
+  //   OrthancPluginDatabaseEventType type;
 
-    union
-    {
-      struct
-      {
-        /* For ""DeletedResource" and "RemainingAncestor" */
-        OrthancPluginResourceType  level;
-        const char*                publicId;
-      } resource;
+  //   union
+  //   {
+  //     struct
+  //     {
+  //       /* For ""DeletedResource" and "RemainingAncestor" */
+  //       OrthancPluginResourceType  level;
+  //       const char*                publicId;
+  //     } resource;
 
-      /* For "DeletedAttachment" */
-      OrthancPluginAttachment2  attachment;
+  //     /* For "DeletedAttachment" */
+  //     OrthancPluginAttachment2  attachment;
       
-    } content;
+  //   } content;
     
-  } OrthancPluginDatabaseEvent2;
+  // } OrthancPluginDatabaseEvent2;
 
 
-  typedef struct
-  {
-    /**
-     * Functions to read the answers inside a transaction
-     **/
+  // typedef struct
+  // {
+  //   /**
+  //    * Functions to read the answers inside a transaction
+  //    **/
     
-    OrthancPluginErrorCode (*readAnswersCount) (OrthancPluginDatabaseTransaction* transaction,
-                                                uint32_t* target /* out */);
+  //   OrthancPluginErrorCode (*readAnswersCount) (OrthancPluginDatabaseTransaction* transaction,
+  //                                               uint32_t* target /* out */);
 
-    OrthancPluginErrorCode (*readAnswerAttachment2) (OrthancPluginDatabaseTransaction* transaction,
-                                                     OrthancPluginAttachment2* target /* out */,          // new in v4
-                                                     uint32_t index);
+  //   OrthancPluginErrorCode (*readAnswerAttachment2) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                    OrthancPluginAttachment2* target /* out */,          // new in v4
+  //                                                    uint32_t index);
 
-    OrthancPluginErrorCode (*readAnswerChange) (OrthancPluginDatabaseTransaction* transaction,
-                                                OrthancPluginChange* target /* out */,
-                                                uint32_t index);
+  //   OrthancPluginErrorCode (*readAnswerChange) (OrthancPluginDatabaseTransaction* transaction,
+  //                                               OrthancPluginChange* target /* out */,
+  //                                               uint32_t index);
 
-    OrthancPluginErrorCode (*readAnswerDicomTag) (OrthancPluginDatabaseTransaction* transaction,
-                                                  uint16_t* group,
-                                                  uint16_t* element,
-                                                  const char** value,
-                                                  uint32_t index);
+  //   OrthancPluginErrorCode (*readAnswerDicomTag) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                 uint16_t* group,
+  //                                                 uint16_t* element,
+  //                                                 const char** value,
+  //                                                 uint32_t index);
 
-    OrthancPluginErrorCode (*readAnswerExportedResource) (OrthancPluginDatabaseTransaction* transaction,
-                                                          OrthancPluginExportedResource* target /* out */,
-                                                          uint32_t index);
+  //   OrthancPluginErrorCode (*readAnswerExportedResource) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                         OrthancPluginExportedResource* target /* out */,
+  //                                                         uint32_t index);
 
-    OrthancPluginErrorCode (*readAnswerInt32) (OrthancPluginDatabaseTransaction* transaction,
-                                               int32_t* target /* out */,
-                                               uint32_t index);
+  //   OrthancPluginErrorCode (*readAnswerInt32) (OrthancPluginDatabaseTransaction* transaction,
+  //                                              int32_t* target /* out */,
+  //                                              uint32_t index);
 
-    OrthancPluginErrorCode (*readAnswerInt64) (OrthancPluginDatabaseTransaction* transaction,
-                                               int64_t* target /* out */,
-                                               uint32_t index);
+  //   OrthancPluginErrorCode (*readAnswerInt64) (OrthancPluginDatabaseTransaction* transaction,
+  //                                              int64_t* target /* out */,
+  //                                              uint32_t index);
 
-    OrthancPluginErrorCode (*readAnswerMatchingResource) (OrthancPluginDatabaseTransaction* transaction,
-                                                          OrthancPluginMatchingResource* target /* out */,
-                                                          uint32_t index);
+  //   OrthancPluginErrorCode (*readAnswerMatchingResource) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                         OrthancPluginMatchingResource* target /* out */,
+  //                                                         uint32_t index);
     
-    OrthancPluginErrorCode (*readAnswerMetadata) (OrthancPluginDatabaseTransaction* transaction,
-                                                  int32_t* metadata /* out */,
-                                                  const char** value /* out */,
-                                                  uint32_t index);
+  //   OrthancPluginErrorCode (*readAnswerMetadata) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                 int32_t* metadata /* out */,
+  //                                                 const char** value /* out */,
+  //                                                 uint32_t index);
 
-    OrthancPluginErrorCode (*readAnswerString) (OrthancPluginDatabaseTransaction* transaction,
-                                                const char** target /* out */,
-                                                uint32_t index);
+  //   OrthancPluginErrorCode (*readAnswerString) (OrthancPluginDatabaseTransaction* transaction,
+  //                                               const char** target /* out */,
+  //                                               uint32_t index);
     
-    OrthancPluginErrorCode (*readEventsCount) (OrthancPluginDatabaseTransaction* transaction,
-                                               uint32_t* target /* out */);
+  //   OrthancPluginErrorCode (*readEventsCount) (OrthancPluginDatabaseTransaction* transaction,
+  //                                              uint32_t* target /* out */);
 
-    OrthancPluginErrorCode (*readEvent2) (OrthancPluginDatabaseTransaction* transaction,
-                                          OrthancPluginDatabaseEvent2* event /* out */,                // new in v4
-                                          uint32_t index);
+  //   OrthancPluginErrorCode (*readEvent2) (OrthancPluginDatabaseTransaction* transaction,
+  //                                         OrthancPluginDatabaseEvent2* event /* out */,                // new in v4
+  //                                         uint32_t index);
 
     
     
-    /**
-     * Functions to access the global database object
-     * (cf. "IDatabaseWrapper" class in Orthanc)
-     **/
+  //   /**
+  //    * Functions to access the global database object
+  //    * (cf. "IDatabaseWrapper" class in Orthanc)
+  //    **/
 
-    OrthancPluginErrorCode (*open) (void* database);
+  //   OrthancPluginErrorCode (*open) (void* database);
 
-    OrthancPluginErrorCode (*close) (void* database);
+  //   OrthancPluginErrorCode (*close) (void* database);
 
-    OrthancPluginErrorCode (*destructDatabase) (void* database);
+  //   OrthancPluginErrorCode (*destructDatabase) (void* database);
 
-    OrthancPluginErrorCode (*getDatabaseVersion) (void* database,
-                                                  uint32_t* target /* out */);
+  //   OrthancPluginErrorCode (*getDatabaseVersion) (void* database,
+  //                                                 uint32_t* target /* out */);
 
-    OrthancPluginErrorCode (*hasRevisionsSupport) (void* database,
-                                                   uint8_t* target /* out */);
+  //   OrthancPluginErrorCode (*hasRevisionsSupport) (void* database,
+  //                                                  uint8_t* target /* out */);
 
-    OrthancPluginErrorCode (*hasAttachmentCustomDataSupport) (void* database,                           // new in v4
-                                                              uint8_t* target /* out */);
+  //   OrthancPluginErrorCode (*hasAttachmentCustomDataSupport) (void* database,                           // new in v4
+  //                                                             uint8_t* target /* out */);
 
-    OrthancPluginErrorCode (*upgradeDatabase) (void* database,
-                                               OrthancPluginStorageArea* storageArea,
-                                               uint32_t targetVersion);
+  //   OrthancPluginErrorCode (*upgradeDatabase) (void* database,
+  //                                              OrthancPluginStorageArea* storageArea,
+  //                                              uint32_t targetVersion);
 
-    OrthancPluginErrorCode (*startTransaction) (void* database,
-                                                OrthancPluginDatabaseTransaction** target /* out */,
-                                                OrthancPluginDatabaseTransactionType type);
+  //   OrthancPluginErrorCode (*startTransaction) (void* database,
+  //                                               OrthancPluginDatabaseTransaction** target /* out */,
+  //                                               OrthancPluginDatabaseTransactionType type);
 
-    OrthancPluginErrorCode (*destructTransaction) (OrthancPluginDatabaseTransaction* transaction);
+  //   OrthancPluginErrorCode (*destructTransaction) (OrthancPluginDatabaseTransaction* transaction);
 
 
-    /**
-     * Functions to run operations within a database transaction
-     * (cf. "IDatabaseWrapper::ITransaction" class in Orthanc)
-     **/
+  //   /**
+  //    * Functions to run operations within a database transaction
+  //    * (cf. "IDatabaseWrapper::ITransaction" class in Orthanc)
+  //    **/
 
-    OrthancPluginErrorCode (*rollback) (OrthancPluginDatabaseTransaction* transaction);
+  //   OrthancPluginErrorCode (*rollback) (OrthancPluginDatabaseTransaction* transaction);
     
-    OrthancPluginErrorCode (*commit) (OrthancPluginDatabaseTransaction* transaction,
-                                      int64_t fileSizeDelta);
+  //   OrthancPluginErrorCode (*commit) (OrthancPluginDatabaseTransaction* transaction,
+  //                                     int64_t fileSizeDelta);
 
-    /* A call to "addAttachment()" guarantees that this attachment is not already existing ("INSERT") */
-    OrthancPluginErrorCode (*addAttachment2) (OrthancPluginDatabaseTransaction* transaction,
-                                             int64_t id,
-                                             const OrthancPluginAttachment2* attachment,                      // new in v4
-                                             int64_t revision);
+  //   /* A call to "addAttachment()" guarantees that this attachment is not already existing ("INSERT") */
+  //   OrthancPluginErrorCode (*addAttachment2) (OrthancPluginDatabaseTransaction* transaction,
+  //                                            int64_t id,
+  //                                            const OrthancPluginAttachment2* attachment,                      // new in v4
+  //                                            int64_t revision);
 
-    OrthancPluginErrorCode (*clearChanges) (OrthancPluginDatabaseTransaction* transaction);
+  //   OrthancPluginErrorCode (*clearChanges) (OrthancPluginDatabaseTransaction* transaction);
     
-    OrthancPluginErrorCode (*clearExportedResources) (OrthancPluginDatabaseTransaction* transaction);
+  //   OrthancPluginErrorCode (*clearExportedResources) (OrthancPluginDatabaseTransaction* transaction);
     
-    OrthancPluginErrorCode (*clearMainDicomTags) (OrthancPluginDatabaseTransaction* transaction,
-                                                  int64_t resourceId);
+  //   OrthancPluginErrorCode (*clearMainDicomTags) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                 int64_t resourceId);
 
-    OrthancPluginErrorCode (*createInstance) (OrthancPluginDatabaseTransaction* transaction,
-                                              OrthancPluginCreateInstanceResult* target /* out */,
-                                              const char* hashPatient,
-                                              const char* hashStudy,
-                                              const char* hashSeries,
-                                              const char* hashInstance);
+  //   OrthancPluginErrorCode (*createInstance) (OrthancPluginDatabaseTransaction* transaction,
+  //                                             OrthancPluginCreateInstanceResult* target /* out */,
+  //                                             const char* hashPatient,
+  //                                             const char* hashStudy,
+  //                                             const char* hashSeries,
+  //                                             const char* hashInstance);
 
-    OrthancPluginErrorCode (*deleteAttachment) (OrthancPluginDatabaseTransaction* transaction,
-                                                int64_t id,
-                                                int32_t contentType);
+  //   OrthancPluginErrorCode (*deleteAttachment) (OrthancPluginDatabaseTransaction* transaction,
+  //                                               int64_t id,
+  //                                               int32_t contentType);
     
-    OrthancPluginErrorCode (*deleteMetadata) (OrthancPluginDatabaseTransaction* transaction,
-                                              int64_t id,
-                                              int32_t metadataType);
+  //   OrthancPluginErrorCode (*deleteMetadata) (OrthancPluginDatabaseTransaction* transaction,
+  //                                             int64_t id,
+  //                                             int32_t metadataType);
 
-    OrthancPluginErrorCode (*deleteResource) (OrthancPluginDatabaseTransaction* transaction,
-                                              int64_t id);
+  //   OrthancPluginErrorCode (*deleteResource) (OrthancPluginDatabaseTransaction* transaction,
+  //                                             int64_t id);
 
-    /* Answers are read using "readAnswerMetadata()" */
-    OrthancPluginErrorCode (*getAllMetadata) (OrthancPluginDatabaseTransaction* transaction,
-                                              int64_t id);
+  //   /* Answers are read using "readAnswerMetadata()" */
+  //   OrthancPluginErrorCode (*getAllMetadata) (OrthancPluginDatabaseTransaction* transaction,
+  //                                             int64_t id);
     
-    /* Answers are read using "readAnswerString()" */
-    OrthancPluginErrorCode (*getAllPublicIds) (OrthancPluginDatabaseTransaction* transaction,
-                                               OrthancPluginResourceType resourceType);
+  //   /* Answers are read using "readAnswerString()" */
+  //   OrthancPluginErrorCode (*getAllPublicIds) (OrthancPluginDatabaseTransaction* transaction,
+  //                                              OrthancPluginResourceType resourceType);
     
-    /* Answers are read using "readAnswerString()" */
-    OrthancPluginErrorCode (*getAllPublicIdsWithLimit) (OrthancPluginDatabaseTransaction* transaction,
-                                                        OrthancPluginResourceType resourceType,
-                                                        uint64_t since,
-                                                        uint64_t limit);
+  //   /* Answers are read using "readAnswerString()" */
+  //   OrthancPluginErrorCode (*getAllPublicIdsWithLimit) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                       OrthancPluginResourceType resourceType,
+  //                                                       uint64_t since,
+  //                                                       uint64_t limit);
 
-    /* Answers are read using "readAnswerChange()" */
-    OrthancPluginErrorCode (*getChanges) (OrthancPluginDatabaseTransaction* transaction,
-                                          uint8_t* targetDone /* out */,
-                                          int64_t since,
-                                          uint32_t maxResults);
+  //   /* Answers are read using "readAnswerChange()" */
+  //   OrthancPluginErrorCode (*getChanges) (OrthancPluginDatabaseTransaction* transaction,
+  //                                         uint8_t* targetDone /* out */,
+  //                                         int64_t since,
+  //                                         uint32_t maxResults);
     
-    /* Answers are read using "readAnswerInt64()" */
-    OrthancPluginErrorCode (*getChildrenInternalId) (OrthancPluginDatabaseTransaction* transaction,
-                                                     int64_t id);
+  //   /* Answers are read using "readAnswerInt64()" */
+  //   OrthancPluginErrorCode (*getChildrenInternalId) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                    int64_t id);
     
-    /* Answers are read using "readAnswerString()" */
-    OrthancPluginErrorCode  (*getChildrenMetadata) (OrthancPluginDatabaseTransaction* transaction,
-                                                    int64_t resourceId,
-                                                    int32_t metadata);
+  //   /* Answers are read using "readAnswerString()" */
+  //   OrthancPluginErrorCode  (*getChildrenMetadata) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                   int64_t resourceId,
+  //                                                   int32_t metadata);
 
-    /* Answers are read using "readAnswerString()" */
-    OrthancPluginErrorCode (*getChildrenPublicId) (OrthancPluginDatabaseTransaction* transaction,
-                                                   int64_t id);
+  //   /* Answers are read using "readAnswerString()" */
+  //   OrthancPluginErrorCode (*getChildrenPublicId) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                  int64_t id);
 
-    /* Answers are read using "readAnswerExportedResource()" */
-    OrthancPluginErrorCode (*getExportedResources) (OrthancPluginDatabaseTransaction* transaction,
-                                                    uint8_t* targetDone /* out */,
-                                                    int64_t since,
-                                                    uint32_t maxResults);
+  //   /* Answers are read using "readAnswerExportedResource()" */
+  //   OrthancPluginErrorCode (*getExportedResources) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                   uint8_t* targetDone /* out */,
+  //                                                   int64_t since,
+  //                                                   uint32_t maxResults);
     
-    /* Answer is read using "readAnswerChange()" */
-    OrthancPluginErrorCode (*getLastChange) (OrthancPluginDatabaseTransaction* transaction);
+  //   /* Answer is read using "readAnswerChange()" */
+  //   OrthancPluginErrorCode (*getLastChange) (OrthancPluginDatabaseTransaction* transaction);
     
-    OrthancPluginErrorCode (*getLastChangeIndex) (OrthancPluginDatabaseTransaction* transaction,
-                                                  int64_t* target /* out */);
+  //   OrthancPluginErrorCode (*getLastChangeIndex) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                 int64_t* target /* out */);
     
-    /* Answer is read using "readAnswerExportedResource()" */
-    OrthancPluginErrorCode (*getLastExportedResource) (OrthancPluginDatabaseTransaction* transaction);
+  //   /* Answer is read using "readAnswerExportedResource()" */
+  //   OrthancPluginErrorCode (*getLastExportedResource) (OrthancPluginDatabaseTransaction* transaction);
     
-    /* Answers are read using "readAnswerDicomTag()" */
-    OrthancPluginErrorCode (*getMainDicomTags) (OrthancPluginDatabaseTransaction* transaction,
-                                                int64_t id);
+  //   /* Answers are read using "readAnswerDicomTag()" */
+  //   OrthancPluginErrorCode (*getMainDicomTags) (OrthancPluginDatabaseTransaction* transaction,
+  //                                               int64_t id);
     
-    /* Answer is read using "readAnswerString()" */
-    OrthancPluginErrorCode (*getPublicId) (OrthancPluginDatabaseTransaction* transaction,
-                                           int64_t internalId);
+  //   /* Answer is read using "readAnswerString()" */
+  //   OrthancPluginErrorCode (*getPublicId) (OrthancPluginDatabaseTransaction* transaction,
+  //                                          int64_t internalId);
     
-    OrthancPluginErrorCode (*getResourcesCount) (OrthancPluginDatabaseTransaction* transaction,
-                                                 uint64_t* target /* out */,
-                                                 OrthancPluginResourceType resourceType);
+  //   OrthancPluginErrorCode (*getResourcesCount) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                uint64_t* target /* out */,
+  //                                                OrthancPluginResourceType resourceType);
     
-    OrthancPluginErrorCode (*getResourceType) (OrthancPluginDatabaseTransaction* transaction,
-                                               OrthancPluginResourceType* target /* out */,
-                                               uint64_t resourceId);
+  //   OrthancPluginErrorCode (*getResourceType) (OrthancPluginDatabaseTransaction* transaction,
+  //                                              OrthancPluginResourceType* target /* out */,
+  //                                              uint64_t resourceId);
     
-    OrthancPluginErrorCode (*getTotalCompressedSize) (OrthancPluginDatabaseTransaction* transaction,
-                                                      uint64_t* target /* out */);
+  //   OrthancPluginErrorCode (*getTotalCompressedSize) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                     uint64_t* target /* out */);
     
-    OrthancPluginErrorCode (*getTotalUncompressedSize) (OrthancPluginDatabaseTransaction* transaction,
-                                                        uint64_t* target /* out */);
+  //   OrthancPluginErrorCode (*getTotalUncompressedSize) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                       uint64_t* target /* out */);
     
-    OrthancPluginErrorCode (*isDiskSizeAbove) (OrthancPluginDatabaseTransaction* transaction,
-                                               uint8_t* target /* out */,
-                                               uint64_t threshold);
+  //   OrthancPluginErrorCode (*isDiskSizeAbove) (OrthancPluginDatabaseTransaction* transaction,
+  //                                              uint8_t* target /* out */,
+  //                                              uint64_t threshold);
     
-    OrthancPluginErrorCode (*isExistingResource) (OrthancPluginDatabaseTransaction* transaction,
-                                                  uint8_t* target /* out */,
-                                                  int64_t resourceId);
+  //   OrthancPluginErrorCode (*isExistingResource) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                 uint8_t* target /* out */,
+  //                                                 int64_t resourceId);
     
-    OrthancPluginErrorCode (*isProtectedPatient) (OrthancPluginDatabaseTransaction* transaction,
-                                                  uint8_t* target /* out */,
-                                                  int64_t resourceId);
+  //   OrthancPluginErrorCode (*isProtectedPatient) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                 uint8_t* target /* out */,
+  //                                                 int64_t resourceId);
     
-    /* Answers are read using "readAnswerInt32()" */
-    OrthancPluginErrorCode (*listAvailableAttachments) (OrthancPluginDatabaseTransaction* transaction,
-                                                        int64_t internalId);
+  //   /* Answers are read using "readAnswerInt32()" */
+  //   OrthancPluginErrorCode (*listAvailableAttachments) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                       int64_t internalId);
 
-    OrthancPluginErrorCode (*logChange) (OrthancPluginDatabaseTransaction* transaction,
-                                         int32_t changeType,
-                                         int64_t resourceId,
-                                         OrthancPluginResourceType resourceType,
-                                         const char* date);
+  //   OrthancPluginErrorCode (*logChange) (OrthancPluginDatabaseTransaction* transaction,
+  //                                        int32_t changeType,
+  //                                        int64_t resourceId,
+  //                                        OrthancPluginResourceType resourceType,
+  //                                        const char* date);
 
-    OrthancPluginErrorCode (*logExportedResource) (OrthancPluginDatabaseTransaction* transaction,
-                                                   OrthancPluginResourceType resourceType,
-                                                   const char* publicId,
-                                                   const char* modality,
-                                                   const char* date,
-                                                   const char* patientId,
-                                                   const char* studyInstanceUid,
-                                                   const char* seriesInstanceUid,
-                                                   const char* sopInstanceUid);
+  //   OrthancPluginErrorCode (*logExportedResource) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                  OrthancPluginResourceType resourceType,
+  //                                                  const char* publicId,
+  //                                                  const char* modality,
+  //                                                  const char* date,
+  //                                                  const char* patientId,
+  //                                                  const char* studyInstanceUid,
+  //                                                  const char* seriesInstanceUid,
+  //                                                  const char* sopInstanceUid);
 
-    /* Answer is read using "readAnswerAttachment()" */
-    OrthancPluginErrorCode (*lookupAttachment) (OrthancPluginDatabaseTransaction* transaction,
-                                                int64_t* revision /* out */,
-                                                int64_t resourceId,
-                                                int32_t contentType);
+  //   /* Answer is read using "readAnswerAttachment()" */
+  //   OrthancPluginErrorCode (*lookupAttachment) (OrthancPluginDatabaseTransaction* transaction,
+  //                                               int64_t* revision /* out */,
+  //                                               int64_t resourceId,
+  //                                               int32_t contentType);
 
-    /* Answer is read using "readAnswerString()" */
-    OrthancPluginErrorCode (*lookupGlobalProperty) (OrthancPluginDatabaseTransaction* transaction,
-                                                    const char* serverIdentifier,
-                                                    int32_t property);
+  //   /* Answer is read using "readAnswerString()" */
+  //   OrthancPluginErrorCode (*lookupGlobalProperty) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                   const char* serverIdentifier,
+  //                                                   int32_t property);
     
-    /* Answer is read using "readAnswerString()" */
-    OrthancPluginErrorCode (*lookupMetadata) (OrthancPluginDatabaseTransaction* transaction,
-                                              int64_t* revision /* out */,
-                                              int64_t id,
-                                              int32_t metadata);
+  //   /* Answer is read using "readAnswerString()" */
+  //   OrthancPluginErrorCode (*lookupMetadata) (OrthancPluginDatabaseTransaction* transaction,
+  //                                             int64_t* revision /* out */,
+  //                                             int64_t id,
+  //                                             int32_t metadata);
     
-    OrthancPluginErrorCode (*lookupParent) (OrthancPluginDatabaseTransaction* transaction,
-                                            uint8_t* isExisting /* out */,
-                                            int64_t* parentId /* out */,
-                                            int64_t id);
+  //   OrthancPluginErrorCode (*lookupParent) (OrthancPluginDatabaseTransaction* transaction,
+  //                                           uint8_t* isExisting /* out */,
+  //                                           int64_t* parentId /* out */,
+  //                                           int64_t id);
     
-    OrthancPluginErrorCode (*lookupResource) (OrthancPluginDatabaseTransaction* transaction,
-                                              uint8_t* isExisting /* out */,
-                                              int64_t* id /* out */,
-                                              OrthancPluginResourceType* type /* out */,
-                                              const char* publicId);
+  //   OrthancPluginErrorCode (*lookupResource) (OrthancPluginDatabaseTransaction* transaction,
+  //                                             uint8_t* isExisting /* out */,
+  //                                             int64_t* id /* out */,
+  //                                             OrthancPluginResourceType* type /* out */,
+  //                                             const char* publicId);
     
-    /* Answers are read using "readAnswerMatchingResource()" */
-    OrthancPluginErrorCode  (*lookupResources) (OrthancPluginDatabaseTransaction* transaction,
-                                                uint32_t constraintsCount,
-                                                const OrthancPluginDatabaseConstraint* constraints,
-                                                OrthancPluginResourceType queryLevel,
-                                                uint32_t limit,
-                                                uint8_t requestSomeInstanceId);
+  //   /* Answers are read using "readAnswerMatchingResource()" */
+  //   OrthancPluginErrorCode  (*lookupResources) (OrthancPluginDatabaseTransaction* transaction,
+  //                                               uint32_t constraintsCount,
+  //                                               const OrthancPluginDatabaseConstraint* constraints,
+  //                                               OrthancPluginResourceType queryLevel,
+  //                                               uint32_t limit,
+  //                                               uint8_t requestSomeInstanceId);
 
-    /* The public ID of the parent resource is read using "readAnswerString()" */
-    OrthancPluginErrorCode (*lookupResourceAndParent) (OrthancPluginDatabaseTransaction* transaction,
-                                                       uint8_t* isExisting /* out */,
-                                                       int64_t* id /* out */,
-                                                       OrthancPluginResourceType* type /* out */,
-                                                       const char* publicId);
+  //   /* The public ID of the parent resource is read using "readAnswerString()" */
+  //   OrthancPluginErrorCode (*lookupResourceAndParent) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                      uint8_t* isExisting /* out */,
+  //                                                      int64_t* id /* out */,
+  //                                                      OrthancPluginResourceType* type /* out */,
+  //                                                      const char* publicId);
 
-    OrthancPluginErrorCode (*selectPatientToRecycle) (OrthancPluginDatabaseTransaction* transaction,
-                                                      uint8_t* patientAvailable /* out */,
-                                                      int64_t* patientId /* out */);
+  //   OrthancPluginErrorCode (*selectPatientToRecycle) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                     uint8_t* patientAvailable /* out */,
+  //                                                     int64_t* patientId /* out */);
     
-    OrthancPluginErrorCode (*selectPatientToRecycle2) (OrthancPluginDatabaseTransaction* transaction,
-                                                       uint8_t* patientAvailable /* out */,
-                                                       int64_t* patientId /* out */,
-                                                       int64_t patientIdToAvoid);
+  //   OrthancPluginErrorCode (*selectPatientToRecycle2) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                      uint8_t* patientAvailable /* out */,
+  //                                                      int64_t* patientId /* out */,
+  //                                                      int64_t patientIdToAvoid);
 
-    OrthancPluginErrorCode (*setGlobalProperty) (OrthancPluginDatabaseTransaction* transaction,
-                                                 const char* serverIdentifier,
-                                                 int32_t property,
-                                                 const char* value);
+  //   OrthancPluginErrorCode (*setGlobalProperty) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                const char* serverIdentifier,
+  //                                                int32_t property,
+  //                                                const char* value);
 
-    /* In "setMetadata()", the metadata might already be existing ("INSERT OR REPLACE")  */
-    OrthancPluginErrorCode (*setMetadata) (OrthancPluginDatabaseTransaction* transaction,
-                                           int64_t id,
-                                           int32_t metadata,
-                                           const char* value,
-                                           int64_t revision);
+  //   /* In "setMetadata()", the metadata might already be existing ("INSERT OR REPLACE")  */
+  //   OrthancPluginErrorCode (*setMetadata) (OrthancPluginDatabaseTransaction* transaction,
+  //                                          int64_t id,
+  //                                          int32_t metadata,
+  //                                          const char* value,
+  //                                          int64_t revision);
     
-    OrthancPluginErrorCode (*setProtectedPatient) (OrthancPluginDatabaseTransaction* transaction,
-                                                   int64_t id,
-                                                   uint8_t isProtected);
+  //   OrthancPluginErrorCode (*setProtectedPatient) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                  int64_t id,
+  //                                                  uint8_t isProtected);
 
-    OrthancPluginErrorCode  (*setResourcesContent) (OrthancPluginDatabaseTransaction* transaction,
-                                                    uint32_t countIdentifierTags,
-                                                    const OrthancPluginResourcesContentTags* identifierTags,
-                                                    uint32_t countMainDicomTags,
-                                                    const OrthancPluginResourcesContentTags* mainDicomTags,
-                                                    uint32_t countMetadata,
-                                                    const OrthancPluginResourcesContentMetadata* metadata);
+  //   OrthancPluginErrorCode  (*setResourcesContent) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                   uint32_t countIdentifierTags,
+  //                                                   const OrthancPluginResourcesContentTags* identifierTags,
+  //                                                   uint32_t countMainDicomTags,
+  //                                                   const OrthancPluginResourcesContentTags* mainDicomTags,
+  //                                                   uint32_t countMetadata,
+  //                                                   const OrthancPluginResourcesContentMetadata* metadata);
     
 
-  } OrthancPluginDatabaseBackendV4;
+  // } OrthancPluginDatabaseBackendV4;
 
 /*<! @endcond */
   
 
-  typedef struct
-  {
-    const OrthancPluginDatabaseBackendV4*  backend;
-    uint32_t                               backendSize;
-    uint32_t                               maxDatabaseRetries;
-    void*                                  database;
-  } _OrthancPluginRegisterDatabaseBackendV4;
-
-
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterDatabaseBackendV4(
-    OrthancPluginContext*                  context,
-    const OrthancPluginDatabaseBackendV4*  backend,
-    uint32_t                               backendSize,
-    uint32_t                               maxDatabaseRetries,  /* To handle "OrthancPluginErrorCode_DatabaseCannotSerialize" */
-    void*                                  database)
-  {
-    _OrthancPluginRegisterDatabaseBackendV4 params;
-
-    if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType))
-    {
-      return OrthancPluginErrorCode_Plugin;
-    }
-
-    memset(&params, 0, sizeof(params));
-    params.backend = backend;
-    params.backendSize = sizeof(OrthancPluginDatabaseBackendV4);
-    params.maxDatabaseRetries = maxDatabaseRetries;
-    params.database = database;
-
-    return context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackendV4, &params);
-  }
 
 #ifdef  __cplusplus
 }
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Tue Sep 24 11:39:52 2024 +0200
@@ -17,7 +17,7 @@
  *    - Possibly register its callback for received DICOM instances using ::OrthancPluginRegisterOnStoredInstanceCallback().
  *    - Possibly register its callback for changes to the DICOM store using ::OrthancPluginRegisterOnChangeCallback().
  *    - Possibly register a custom storage area using ::OrthancPluginRegisterStorageArea2().
- *    - Possibly register a custom database back-end area using OrthancPluginRegisterDatabaseBackendV3().
+ *    - Possibly register a custom database back-end area using OrthancPluginRegisterDatabaseBackendV4().
  *    - Possibly register a handler for C-Find SCP using OrthancPluginRegisterFindCallback().
  *    - Possibly register a handler for C-Find SCP against DICOM worklists using OrthancPluginRegisterWorklistCallback().
  *    - Possibly register a handler for C-Move SCP using OrthancPluginRegisterMoveCallback().
@@ -85,8 +85,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -110,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")))
@@ -120,7 +121,7 @@
 
 #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     1
 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     12
-#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  0
+#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  4
 
 
 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
@@ -167,6 +168,20 @@
 #endif
 
 
+#ifndef ORTHANC_PLUGIN_DEPRECATED
+#  if defined(_MSC_VER)
+#    define ORTHANC_PLUGIN_DEPRECATED __declspec(deprecated)
+#  elif __GNUC__ >= 4
+#    define ORTHANC_PLUGIN_DEPRECATED __attribute__ ((deprecated))
+#  elif defined(__clang__)
+#    define ORTHANC_PLUGIN_DEPRECATED __attribute__ ((deprecated))
+#  else
+#    pragma message("WARNING: You need to implement ORTHANC_PLUGINS_DEPRECATED for this compiler")
+#    define ORTHANC_PLUGIN_DEPRECATED
+#  endif
+#endif
+
+
 
 /********************************************************************
  ** Inclusion of standard libraries.
@@ -175,7 +190,7 @@
 /**
  * For Microsoft Visual Studio, a compatibility "stdint.h" can be
  * downloaded at the following URL:
- * https://hg.orthanc-server.com/orthanc/raw-file/tip/Resources/ThirdParty/VisualStudio/stdint.h
+ * https://orthanc.uclouvain.be/hg/orthanc/raw-file/default/OrthancFramework/Resources/ThirdParty/VisualStudio/stdint.h
  **/
 #include <stdint.h>
 
@@ -245,6 +260,8 @@
     OrthancPluginErrorCode_DatabaseCannotSerialize = 42    /*!< Database could not serialize access due to concurrent update, the transaction should be retried */,
     OrthancPluginErrorCode_Revision = 43    /*!< A bad revision number was provided, which might indicate conflict between multiple writers */,
     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_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 */,
@@ -447,6 +464,10 @@
     _OrthancPluginService_CreateMemoryBuffer64 = 40, /* New in Orthanc 1.9.0 */
     _OrthancPluginService_CreateDicom2 = 41,         /* New in Orthanc 1.9.0 */
     _OrthancPluginService_GetDatabaseServerIdentifier = 42,         /* New in Orthanc 1.11.1 */
+    _OrthancPluginService_SetMetricsIntegerValue = 43,              /* New in Orthanc 1.12.1 */
+    _OrthancPluginService_SetCurrentThreadName = 44,                /* New in Orthanc 1.12.2 */
+    _OrthancPluginService_LogMessage = 45,                          /* New in Orthanc 1.12.4 */
+
 
     /* Registration of callbacks */
     _OrthancPluginService_RegisterRestCallback = 1000,
@@ -527,6 +548,7 @@
     _OrthancPluginService_GetInstanceAdvancedJson = 4017,  /* New in Orthanc 1.7.0 */
     _OrthancPluginService_GetInstanceDicomWebJson = 4018,  /* New in Orthanc 1.7.0 */
     _OrthancPluginService_GetInstanceDicomWebXml = 4019,   /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_LoadDicomInstance = 4020,        /* New in Orthanc 1.12.1 */
     
     /* Services for plugins implementing a database back-end */
     _OrthancPluginService_RegisterDatabaseBackend = 5000,    /* New in Orthanc 0.8.6 */
@@ -580,10 +602,11 @@
     _OrthancPluginService_GetPeerUserProperty = 8007,
 
     /* Primitives for handling jobs (new in 1.4.2) */
-    _OrthancPluginService_CreateJob = 9000,
+    _OrthancPluginService_CreateJob = 9000,   /* Deprecated since SDK 1.11.3 */
     _OrthancPluginService_FreeJob = 9001,
     _OrthancPluginService_SubmitJob = 9002,
     _OrthancPluginService_RegisterJobsUnserializer = 9003,
+    _OrthancPluginService_CreateJob2 = 9004,  /* New in SDK 1.11.3 */
     
     _OrthancPluginService_INTERNAL = 0x7fffffff
   } _OrthancPluginService;
@@ -843,7 +866,7 @@
    **/
   typedef enum
   {
-    OrthancPluginDicomToJsonFlags_None                  = 0,
+    OrthancPluginDicomToJsonFlags_None                  = 0,         /*!< Default formatting */
     OrthancPluginDicomToJsonFlags_IncludeBinary         = (1 << 0),  /*!< Include the binary tags */
     OrthancPluginDicomToJsonFlags_IncludePrivateTags    = (1 << 1),  /*!< Include the private tags */
     OrthancPluginDicomToJsonFlags_IncludeUnknownTags    = (1 << 2),  /*!< Include the tags unknown by the dictionary */
@@ -858,13 +881,13 @@
 
 
   /**
-   * Flags to the creation of a DICOM file.
+   * Flags for the creation of a DICOM file.
    * @ingroup Toolbox
    * @see OrthancPluginCreateDicom()
    **/
   typedef enum
   {
-    OrthancPluginCreateDicomFlags_None                  = 0,
+    OrthancPluginCreateDicomFlags_None                  = 0,         /*!< Default mode */
     OrthancPluginCreateDicomFlags_DecodeDataUriScheme   = (1 << 0),  /*!< Decode fields encoded using data URI scheme */
     OrthancPluginCreateDicomFlags_GenerateIdentifiers   = (1 << 1),  /*!< Automatically generate DICOM identifiers */
 
@@ -981,32 +1004,46 @@
    **/
   typedef enum
   {
+    /**
+     * Success: The DICOM instance is properly stored in the SCP
+     **/
     OrthancPluginStorageCommitmentFailureReason_Success = 0,
-    /*!< Success: The DICOM instance is properly stored in the SCP */
-
+
+    /**
+     * 0110H: A general failure in processing the operation was encountered
+     **/
     OrthancPluginStorageCommitmentFailureReason_ProcessingFailure = 1,
-    /*!< 0110H: A general failure in processing the operation was encountered */
-
+
+    /**
+     * 0112H: One or more of the elements in the Referenced SOP
+     * Instance Sequence was not available
+     **/
     OrthancPluginStorageCommitmentFailureReason_NoSuchObjectInstance = 2,
-    /*!< 0112H: One or more of the elements in the Referenced SOP
-      Instance Sequence was not available */
-
+
+    /**
+     * 0213H: The SCP does not currently have enough resources to
+     * store the requested SOP Instance(s)
+     **/
     OrthancPluginStorageCommitmentFailureReason_ResourceLimitation = 3,
-    /*!< 0213H: The SCP does not currently have enough resources to
-      store the requested SOP Instance(s) */
-
+
+    /**
+     * 0122H: Storage Commitment has been requested for a SOP Instance
+     * with a SOP Class that is not supported by the SCP
+     **/
     OrthancPluginStorageCommitmentFailureReason_ReferencedSOPClassNotSupported = 4,
-    /*!< 0122H: Storage Commitment has been requested for a SOP
-      Instance with a SOP Class that is not supported by the SCP */
-
+
+    /**
+     * 0119H: The SOP Class of an element in the Referenced SOP
+     * Instance Sequence did not correspond to the SOP class
+     * registered for this SOP Instance at the SCP
+     **/
     OrthancPluginStorageCommitmentFailureReason_ClassInstanceConflict = 5,
-    /*!< 0119H: The SOP Class of an element in the Referenced SOP
-      Instance Sequence did not correspond to the SOP class registered
-      for this SOP Instance at the SCP */
-
+
+    /**
+     * 0131H: The Transaction UID of the Storage Commitment Request is
+     * already in use
+     **/
     OrthancPluginStorageCommitmentFailureReason_DuplicateTransactionUID = 6
-    /*!< 0131H: The Transaction UID of the Storage Commitment Request
-      is already in use */
   } OrthancPluginStorageCommitmentFailureReason;
 
 
@@ -1024,6 +1061,71 @@
 
 
   /**
+   * Mode specifying how to load a DICOM instance.
+   * @see OrthancPluginLoadDicomInstance()
+   **/
+  typedef enum
+  {
+    /**
+     * Load the whole DICOM file, including pixel data
+     **/
+    OrthancPluginLoadDicomInstanceMode_WholeDicom = 1,
+
+    /**
+     * Load the whole DICOM file until pixel data, which speeds up the
+     * loading
+     **/
+    OrthancPluginLoadDicomInstanceMode_UntilPixelData = 2,
+
+    /**
+     * Load the whole DICOM file until pixel data, and replace pixel
+     * data by an empty tag whose VR (value representation) is the
+     * same as those of the original DICOM file
+     **/
+    OrthancPluginLoadDicomInstanceMode_EmptyPixelData = 3,
+
+    _OrthancPluginLoadDicomInstanceMode_INTERNAL = 0x7fffffff
+  } OrthancPluginLoadDicomInstanceMode;
+
+
+  /**
+   * The log levels supported by Orthanc.
+   *
+   * These values must match those of enumeration "LogLevel" in the
+   * Orthanc Core.
+   **/
+  typedef enum
+  {
+    OrthancPluginLogLevel_Error = 0,    /*!< Error log level */
+    OrthancPluginLogLevel_Warning = 1,  /*!< Warning log level */
+    OrthancPluginLogLevel_Info = 2,     /*!< Info log level */
+    OrthancPluginLogLevel_Trace = 3,    /*!< Trace log level */
+
+    _OrthancPluginLogLevel_INTERNAL = 0x7fffffff
+  } OrthancPluginLogLevel;
+
+
+  /**
+   * The log categories supported by Orthanc.
+   *
+   * These values must match those of enumeration "LogCategory" in the
+   * Orthanc Core.
+   **/
+  typedef enum
+  {
+    OrthancPluginLogCategory_Generic = (1 << 0),  /*!< Generic (default) category */
+    OrthancPluginLogCategory_Plugins = (1 << 1),  /*!< Plugin engine related logs (shall not be used by plugins) */
+    OrthancPluginLogCategory_Http    = (1 << 2),  /*!< HTTP related logs */
+    OrthancPluginLogCategory_Sqlite  = (1 << 3),  /*!< SQLite related logs (shall not be used by plugins) */
+    OrthancPluginLogCategory_Dicom   = (1 << 4),  /*!< DICOM related logs */
+    OrthancPluginLogCategory_Jobs    = (1 << 5),  /*!< jobs related logs */
+    OrthancPluginLogCategory_Lua     = (1 << 6),  /*!< Lua related logs (shall not be used by plugins) */
+
+    _OrthancPluginLogCategory_INTERNAL = 0x7fffffff
+  } OrthancPluginLogCategory;
+
+
+  /**
    * @brief A 32-bit memory buffer allocated by the core system of Orthanc.
    *
    * A memory buffer allocated by the core system of Orthanc. When the
@@ -1694,11 +1796,31 @@
    * @param job The job of interest.
    * @return The statistics, as a JSON object encoded as a string.
    * @ingroup Toolbox
+   * @deprecated This signature should not be used anymore since Orthanc SDK 1.11.3.
    **/  
   typedef const char* (*OrthancPluginJobGetContent) (void* job);
 
 
   /**
+   * @brief Callback to retrieve the content of one custom job.
+   *
+   * Signature of a callback function that returns human-readable
+   * statistics about the job. This statistics must be formatted as a
+   * JSON object. This information is notably displayed in the "Jobs"
+   * tab of "Orthanc Explorer".
+   *
+   * @param target The target memory buffer where to store the JSON string.
+   * This buffer must be allocated using OrthancPluginCreateMemoryBuffer()
+   * and will be freed by the Orthanc core.
+   * @param job The job of interest.
+   * @return 0 if success, other value if error.
+   * @ingroup Toolbox
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginJobGetContent2) (OrthancPluginMemoryBuffer* target,
+                                                                 void* job);
+
+
+  /**
    * @brief Callback to serialize one custom job.
    * 
    * Signature of a callback function that returns a serialized
@@ -1712,11 +1834,33 @@
    * @return The serialized job, as a JSON object encoded as a string.
    * @see OrthancPluginRegisterJobsUnserializer()
    * @ingroup Toolbox
+   * @deprecated This signature should not be used anymore since Orthanc SDK 1.11.3.
    **/  
   typedef const char* (*OrthancPluginJobGetSerialized) (void* job);
 
 
   /**
+   * @brief Callback to serialize one custom job.
+   *
+   * Signature of a callback function that returns a serialized
+   * version of the job, formatted as a JSON object. This
+   * serialization is stored in the Orthanc database, and is used to
+   * reload the job on the restart of Orthanc. The "unserialization"
+   * callback (with OrthancPluginJobsUnserializer signature) will
+   * receive this serialized object.
+   *
+   * @param target The target memory buffer where to store the JSON string.
+   * This buffer must be allocated using OrthancPluginCreateMemoryBuffer()
+   * and will be freed by the Orthanc core.
+   * @param job The job of interest.
+   * @return 1 if the serialization has succeeded, 0 if serialization is
+   * not implemented for this type of job, or -1 in case of error.
+   **/
+  typedef int32_t (*OrthancPluginJobGetSerialized2) (OrthancPluginMemoryBuffer* target,
+                                                     void* job);
+
+
+  /**
    * @brief Callback to execute one step of a custom job.
    * 
    * Signature of a callback function that executes one step in the
@@ -1796,7 +1940,8 @@
    * Signature of a callback function that is called by Orthanc
    * whenever a monitoring tool (such as Prometheus) asks the current
    * values of the metrics. This callback gives the plugin a chance to
-   * update its metrics, by calling OrthancPluginSetMetricsValue().
+   * update its metrics, by calling OrthancPluginSetMetricsValue() or
+   * OrthancPluginSetMetricsIntegerValue().
    * This is typically useful for metrics that are expensive to
    * acquire.
    * 
@@ -1947,18 +2092,19 @@
    * @param expectedRevision Expected revision.
    * @return 1 if and only if the versions are compatible. If the
    * result is 0, the initialization of the plugin should fail.
-   * @see OrthancPluginCheckVersion
+   * @see OrthancPluginCheckVersion()
    * @ingroup Callbacks
    **/
-  ORTHANC_PLUGIN_INLINE int  OrthancPluginCheckVersionAdvanced(
+  ORTHANC_PLUGIN_INLINE int32_t  OrthancPluginCheckVersionAdvanced(
     OrthancPluginContext* context,
-    int expectedMajor,
-    int expectedMinor,
-    int expectedRevision)
-  {
-    int major, minor, revision;
-
-    if (sizeof(int32_t) != sizeof(OrthancPluginErrorCode) ||
+    int32_t expectedMajor,
+    int32_t expectedMinor,
+    int32_t expectedRevision)
+  {
+    int32_t major, minor, revision;
+
+    if (sizeof(int) != sizeof(int32_t) || /* Ensure binary compatibility with Orthanc SDK <= 1.12.1 */
+        sizeof(int32_t) != sizeof(OrthancPluginErrorCode) ||
         sizeof(int32_t) != sizeof(OrthancPluginHttpMethod) ||
         sizeof(int32_t) != sizeof(_OrthancPluginService) ||
         sizeof(int32_t) != sizeof(_OrthancPluginProperty) ||
@@ -1979,7 +2125,9 @@
         sizeof(int32_t) != sizeof(OrthancPluginMetricsType) ||
         sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode) ||
         sizeof(int32_t) != sizeof(OrthancPluginStorageCommitmentFailureReason) ||
-        sizeof(int32_t) != sizeof(OrthancPluginReceivedInstanceAction))
+        sizeof(int32_t) != sizeof(OrthancPluginLoadDicomInstanceMode) ||
+        sizeof(int32_t) != sizeof(OrthancPluginLogLevel) ||
+        sizeof(int32_t) != sizeof(OrthancPluginLogCategory))
     {
       /* Mismatch in the size of the enumerations */
       return 0;
@@ -2053,10 +2201,10 @@
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @return 1 if and only if the versions are compatible. If the
    * result is 0, the initialization of the plugin should fail.
-   * @see OrthancPluginCheckVersionAdvanced
+   * @see OrthancPluginCheckVersionAdvanced()
    * @ingroup Callbacks
    **/
-  ORTHANC_PLUGIN_INLINE int  OrthancPluginCheckVersion(
+  ORTHANC_PLUGIN_INLINE int32_t  OrthancPluginCheckVersion(
     OrthancPluginContext* context)
   {
     return OrthancPluginCheckVersionAdvanced(
@@ -2413,7 +2561,7 @@
    * @param uri The URI in the built-in Orthanc API.
    * @return 0 if success, or the error code if failure.
    * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
-   * @see OrthancPluginRestApiGetAfterPlugins
+   * @see OrthancPluginRestApiGetAfterPlugins()
    * @ingroup Orthanc
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiGet(
@@ -2443,7 +2591,7 @@
    * @param uri The URI in the built-in Orthanc API.
    * @return 0 if success, or the error code if failure.
    * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
-   * @see OrthancPluginRestApiGet
+   * @see OrthancPluginRestApiGet()
    * @ingroup Orthanc
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiGetAfterPlugins(
@@ -2480,7 +2628,7 @@
    * @param bodySize The size of the body.
    * @return 0 if success, or the error code if failure.
    * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
-   * @see OrthancPluginRestApiPostAfterPlugins
+   * @see OrthancPluginRestApiPostAfterPlugins()
    * @ingroup Orthanc
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiPost(
@@ -2515,7 +2663,7 @@
    * @param bodySize The size of the body.
    * @return 0 if success, or the error code if failure.
    * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
-   * @see OrthancPluginRestApiPost
+   * @see OrthancPluginRestApiPost()
    * @ingroup Orthanc
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiPostAfterPlugins(
@@ -2544,7 +2692,7 @@
    * @param uri The URI to delete in the built-in Orthanc API.
    * @return 0 if success, or the error code if failure.
    * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
-   * @see OrthancPluginRestApiDeleteAfterPlugins
+   * @see OrthancPluginRestApiDeleteAfterPlugins()
    * @ingroup Orthanc
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiDelete(
@@ -2567,7 +2715,7 @@
    * @param uri The URI to delete in the built-in Orthanc API.
    * @return 0 if success, or the error code if failure.
    * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
-   * @see OrthancPluginRestApiDelete
+   * @see OrthancPluginRestApiDelete()
    * @ingroup Orthanc
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiDeleteAfterPlugins(
@@ -2592,7 +2740,7 @@
    * @param bodySize The size of the body.
    * @return 0 if success, or the error code if failure.
    * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
-   * @see OrthancPluginRestApiPutAfterPlugins
+   * @see OrthancPluginRestApiPutAfterPlugins()
    * @ingroup Orthanc
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiPut(
@@ -2628,7 +2776,7 @@
    * @param bodySize The size of the body.
    * @return 0 if success, or the error code if failure.
    * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
-   * @see OrthancPluginRestApiPut
+   * @see OrthancPluginRestApiPut()
    * @ingroup Orthanc
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiPutAfterPlugins(
@@ -3198,7 +3346,7 @@
    * @return 1 if the metadata is present, 0 if it is absent, -1 in case of error.
    * @ingroup DicomInstance
    **/
-  ORTHANC_PLUGIN_INLINE int  OrthancPluginHasInstanceMetadata(
+  ORTHANC_PLUGIN_INLINE int32_t  OrthancPluginHasInstanceMetadata(
     OrthancPluginContext*              context,
     const OrthancPluginDicomInstance*  instance,
     const char*                        metadata)
@@ -3288,7 +3436,7 @@
    * @ingroup Callbacks
    * @deprecated Please use OrthancPluginRegisterStorageArea2()
    **/
-  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea(
+  ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea(
     OrthancPluginContext*       context,
     OrthancPluginStorageCreate  create,
     OrthancPluginStorageRead    read,
@@ -3385,7 +3533,7 @@
    * OrthancPluginFreeString().
    * @see OrthancPluginGetConfiguration()
    **/
-  ORTHANC_PLUGIN_INLINE char *OrthancPluginGetConfigurationPath(OrthancPluginContext* context)
+  ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE char *OrthancPluginGetConfigurationPath(OrthancPluginContext* context)
   {
     char* result;
 
@@ -3461,8 +3609,12 @@
    * 
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param uri The root URI for this plugin.
+   *
+   * @deprecated This function should not be used anymore because the
+   * result of the call to "OrthancPluginGetName()" depends on the
+   * system. Use "OrthancPluginSetRootUri2()" instead.
    **/ 
-  ORTHANC_PLUGIN_INLINE void OrthancPluginSetRootUri(
+  ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE void OrthancPluginSetRootUri(
     OrthancPluginContext*  context,
     const char*            uri)
   {
@@ -3476,6 +3628,32 @@
 
 
   /**
+   * @brief Set the URI where the plugin provides its Web interface.
+   *
+   * For plugins that come with a Web interface, this function
+   * declares the entry path where to find this interface. This
+   * information is notably used in the "Plugins" page of Orthanc
+   * Explorer.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param plugin Identifier of your plugin (it must match "OrthancPluginGetName()").
+   * @param uri The root URI for this plugin.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSetRootUri2(
+    OrthancPluginContext*  context,
+    const char*            plugin,
+    const char*            uri)
+  {
+    _OrthancPluginSetPluginProperty params;
+    params.plugin = plugin;
+    params.property = _OrthancPluginProperty_RootUri;
+    params.value = uri;
+
+    context->InvokeService(context, _OrthancPluginService_SetPluginProperty, &params);
+  }
+
+
+  /**
    * @brief Set a description for this plugin.
    *
    * Set a description for this plugin. It is displayed in the
@@ -3483,8 +3661,12 @@
    * 
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param description The description.
+   *
+   * @deprecated This function should not be used anymore because the
+   * result of the call to "OrthancPluginGetName()" depends on the
+   * system. Use "OrthancPluginSetDescription2()" instead.
    **/ 
-  ORTHANC_PLUGIN_INLINE void OrthancPluginSetDescription(
+  ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE void OrthancPluginSetDescription(
     OrthancPluginContext*  context,
     const char*            description)
   {
@@ -3498,6 +3680,30 @@
 
 
   /**
+   * @brief Set a description for this plugin.
+   *
+   * Set a description for this plugin. It is displayed in the
+   * "Plugins" page of Orthanc Explorer.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param plugin Identifier of your plugin (it must match "OrthancPluginGetName()").
+   * @param description The description.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSetDescription2(
+    OrthancPluginContext*  context,
+    const char*            plugin,
+    const char*            description)
+  {
+    _OrthancPluginSetPluginProperty params;
+    params.plugin = plugin;
+    params.property = _OrthancPluginProperty_Description;
+    params.value = description;
+
+    context->InvokeService(context, _OrthancPluginService_SetPluginProperty, &params);
+  }
+
+
+  /**
    * @brief Extend the JavaScript code of Orthanc Explorer.
    *
    * Add JavaScript code to customize the default behavior of Orthanc
@@ -3505,8 +3711,12 @@
    * 
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param javascript The custom JavaScript code.
+   *
+   * @deprecated This function should not be used anymore because the
+   * result of the call to "OrthancPluginGetName()" depends on the
+   * system. Use "OrthancPluginExtendOrthancExplorer2()" instead.
    **/ 
-  ORTHANC_PLUGIN_INLINE void OrthancPluginExtendOrthancExplorer(
+  ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE void OrthancPluginExtendOrthancExplorer(
     OrthancPluginContext*  context,
     const char*            javascript)
   {
@@ -3519,6 +3729,30 @@
   }
 
 
+  /**
+   * @brief Extend the JavaScript code of Orthanc Explorer.
+   *
+   * Add JavaScript code to customize the default behavior of Orthanc
+   * Explorer. This can for instance be used to add new buttons.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param plugin Identifier of your plugin (it must match "OrthancPluginGetName()").
+   * @param javascript The custom JavaScript code.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginExtendOrthancExplorer2(
+    OrthancPluginContext*  context,
+    const char*            plugin,
+    const char*            javascript)
+  {
+    _OrthancPluginSetPluginProperty params;
+    params.plugin = plugin;
+    params.property = _OrthancPluginProperty_OrthancExplorer;
+    params.value = javascript;
+
+    context->InvokeService(context, _OrthancPluginService_SetPluginProperty, &params);
+  }
+
+
   typedef struct
   {
     char**       result;
@@ -4789,7 +5023,7 @@
    * @deprecated This function should not be used anymore. Use "OrthancPluginRestApiPut()" on
    * "/{patients|studies|series|instances}/{id}/attachments/{name}" instead.
    **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginStorageAreaCreate(
+  ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginStorageAreaCreate(
     OrthancPluginContext*       context,
     OrthancPluginStorageArea*   storageArea,
     const char*                 uuid,
@@ -4833,7 +5067,7 @@
    * @deprecated This function should not be used anymore. Use "OrthancPluginRestApiGet()" on
    * "/{patients|studies|series|instances}/{id}/attachments/{name}" instead.
    **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginStorageAreaRead(
+  ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginStorageAreaRead(
     OrthancPluginContext*       context,
     OrthancPluginMemoryBuffer*  target,
     OrthancPluginStorageArea*   storageArea,
@@ -4872,7 +5106,7 @@
    * @deprecated This function should not be used anymore. Use "OrthancPluginRestApiDelete()" on
    * "/{patients|studies|series|instances}/{id}/attachments/{name}" instead.
    **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginStorageAreaRemove(
+  ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginStorageAreaRemove(
     OrthancPluginContext*       context,
     OrthancPluginStorageArea*   storageArea,
     const char*                 uuid,
@@ -5106,7 +5340,7 @@
    * @return The NULL value if the case of an error, or the JSON
    * string. This string must be freed by OrthancPluginFreeString().
    * @ingroup Toolbox
-   * @see OrthancPluginDicomInstanceToJson
+   * @see OrthancPluginDicomInstanceToJson()
    **/
   ORTHANC_PLUGIN_INLINE char* OrthancPluginDicomBufferToJson(
     OrthancPluginContext*           context,
@@ -5155,7 +5389,7 @@
    * @return The NULL value if the case of an error, or the JSON
    * string. This string must be freed by OrthancPluginFreeString().
    * @ingroup Toolbox
-   * @see OrthancPluginDicomInstanceToJson
+   * @see OrthancPluginDicomInstanceToJson()
    **/
   ORTHANC_PLUGIN_INLINE char* OrthancPluginDicomInstanceToJson(
     OrthancPluginContext*           context,
@@ -5212,7 +5446,7 @@
    * @param afterPlugins If 0, the built-in API of Orthanc is used.
    * If 1, the API is tainted by the plugins.
    * @return 0 if success, or the error code if failure.
-   * @see OrthancPluginRestApiGet, OrthancPluginRestApiGetAfterPlugins
+   * @see OrthancPluginRestApiGet(), OrthancPluginRestApiGetAfterPlugins()
    * @ingroup Orthanc
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiGet2(
@@ -5475,8 +5709,8 @@
    * @param flags Flags governing the output.
    * @return 0 if success, other value if error.
    * @ingroup Toolbox
-   * @see OrthancPluginCreateDicom2
-   * @see OrthancPluginDicomBufferToJson
+   * @see OrthancPluginCreateDicom2()
+   * @see OrthancPluginDicomBufferToJson()
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCreateDicom(
     OrthancPluginContext*          context,
@@ -5850,7 +6084,7 @@
    * @ingroup Callbacks
    * @deprecated Please instead use OrthancPluginRegisterIncomingHttpRequestFilter2()
    **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterIncomingHttpRequestFilter(
+  ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterIncomingHttpRequestFilter(
     OrthancPluginContext*                   context,
     OrthancPluginIncomingHttpRequestFilter  callback)
   {
@@ -6809,8 +7043,9 @@
    * @return The newly allocated job. It must be freed with OrthancPluginFreeJob(),
    * as long as it is not submitted with OrthancPluginSubmitJob().
    * @ingroup Toolbox
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginJob *OrthancPluginCreateJob(
+   * @deprecated This signature should not be used anymore since Orthanc SDK 1.11.3.
+   **/
+  ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginJob *OrthancPluginCreateJob(
     OrthancPluginContext           *context,
     void                           *job,
     OrthancPluginJobFinalize        finalize,
@@ -6853,6 +7088,92 @@
 
   typedef struct
   {
+    OrthancPluginJob**              target;
+    void                           *job;
+    OrthancPluginJobFinalize        finalize;
+    const char                     *type;
+    OrthancPluginJobGetProgress     getProgress;
+    OrthancPluginJobGetContent2     getContent;
+    OrthancPluginJobGetSerialized2  getSerialized;
+    OrthancPluginJobStep            step;
+    OrthancPluginJobStop            stop;
+    OrthancPluginJobReset           reset;
+  } _OrthancPluginCreateJob2;
+
+  /**
+   * @brief Create a custom job.
+   *
+   * This function creates a custom job to be run by the jobs engine
+   * of Orthanc.
+   * 
+   * Orthanc starts one dedicated thread per custom job that is
+   * running. It is guaranteed that all the callbacks will only be
+   * called from this single dedicated thread, in mutual exclusion: As
+   * a consequence, it is *not* mandatory to protect the various
+   * callbacks by mutexes.
+   * 
+   * The custom job can nonetheless launch its own processing threads
+   * on the first call to the "step()" callback, and stop them once
+   * the "stop()" callback is called.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param job The job to be executed.
+   * @param finalize The finalization callback.
+   * @param type The type of the job, provided to the job unserializer. 
+   * See OrthancPluginRegisterJobsUnserializer().
+   * @param getProgress The progress callback.
+   * @param getContent The content callback.
+   * @param getSerialized The serialization callback.
+   * @param step The callback to execute the individual steps of the job.
+   * @param stop The callback that is invoked once the job leaves the "running" state.
+   * @param reset The callback that is invoked if a stopped job is started again.
+   * @return The newly allocated job. It must be freed with OrthancPluginFreeJob(),
+   * as long as it is not submitted with OrthancPluginSubmitJob().
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginJob *OrthancPluginCreateJob2(
+    OrthancPluginContext           *context,
+    void                           *job,
+    OrthancPluginJobFinalize        finalize,
+    const char                     *type,
+    OrthancPluginJobGetProgress     getProgress,
+    OrthancPluginJobGetContent2     getContent,
+    OrthancPluginJobGetSerialized2  getSerialized,
+    OrthancPluginJobStep            step,
+    OrthancPluginJobStop            stop,
+    OrthancPluginJobReset           reset)
+  {
+    OrthancPluginJob* target = NULL;
+
+    _OrthancPluginCreateJob2 params;
+    memset(&params, 0, sizeof(params));
+
+    params.target = &target;
+    params.job = job;
+    params.finalize = finalize;
+    params.type = type;
+    params.getProgress = getProgress;
+    params.getContent = getContent;
+    params.getSerialized = getSerialized;
+    params.step = step;
+    params.stop = stop;
+    params.reset = reset;
+
+    if (context->InvokeService(context, _OrthancPluginService_CreateJob2, &params) != OrthancPluginErrorCode_Success ||
+        target == NULL)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  typedef struct
+  {
     OrthancPluginJob*   job;
   } _OrthancPluginFreeJob;
 
@@ -6881,7 +7202,7 @@
   {
     char**             resultId;
     OrthancPluginJob  *job;
-    int                priority;
+    int32_t            priority;
   } _OrthancPluginSubmitJob;
 
   /**
@@ -6900,7 +7221,7 @@
   ORTHANC_PLUGIN_INLINE char *OrthancPluginSubmitJob(
     OrthancPluginContext   *context,
     OrthancPluginJob       *job,
-    int                     priority)
+    int32_t                 priority)
   {
     char* resultId = NULL;
 
@@ -7041,11 +7362,12 @@
   } _OrthancPluginSetMetricsValue;
 
   /**
-   * @brief Set the value of a metrics.
-   *
-   * This function sets the value of a metrics to monitor the behavior
-   * of the plugin through tools such as Prometheus. The values of all
-   * the metrics are stored within the Orthanc context.
+   * @brief Set the value of a floating-point metrics.
+   *
+   * This function sets the value of a floating-point metrics to
+   * monitor the behavior of the plugin through tools such as
+   * Prometheus. The values of all the metrics are stored within the
+   * Orthanc context.
    * 
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param name The name of the metrics to be set.
@@ -7053,6 +7375,7 @@
    * @param type The type of the metrics. This parameter is only taken into consideration
    * the first time this metrics is set.
    * @ingroup Toolbox
+   * @see OrthancPluginSetMetricsIntegerValue()
    **/
   ORTHANC_PLUGIN_INLINE void OrthancPluginSetMetricsValue(
     OrthancPluginContext*     context,
@@ -7078,7 +7401,8 @@
    * @brief Register a callback to refresh the metrics.
    *
    * This function registers a callback to refresh the metrics. The
-   * callback must make calls to OrthancPluginSetMetricsValue().
+   * callback must make calls to OrthancPluginSetMetricsValue() or
+   * OrthancPluginSetMetricsIntegerValue().
    *
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param callback The callback function to handle the refresh.
@@ -7120,7 +7444,7 @@
    * @deprecated OrthancPluginEncodeDicomWebJson2()
    * @ingroup Toolbox
    **/
-  ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebJson(
+  ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebJson(
     OrthancPluginContext*                context,
     const void*                          dicom,
     uint32_t                             dicomSize,
@@ -7162,7 +7486,7 @@
    * @deprecated OrthancPluginEncodeDicomWebXml2()
    * @ingroup Toolbox
    **/
-  ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebXml(
+  ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebXml(
     OrthancPluginContext*                context,
     const void*                          dicom,
     uint32_t                             dicomSize,
@@ -8388,7 +8712,7 @@
    * @return The NULL value if the case of an error, or the JSON
    * string. This string must be freed by OrthancPluginFreeString().
    * @ingroup DicomInstance
-   * @see OrthancPluginDicomBufferToJson
+   * @see OrthancPluginDicomBufferToJson()
    **/
   ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceAdvancedJson(
     OrthancPluginContext*              context,
@@ -8745,8 +9069,8 @@
    * Check out the global configuration option "Dictionary" of Orthanc.
    * @return 0 if success, other value if error.
    * @ingroup Toolbox
-   * @see OrthancPluginCreateDicom
-   * @see OrthancPluginDicomBufferToJson
+   * @see OrthancPluginCreateDicom()
+   * @see OrthancPluginDicomBufferToJson()
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCreateDicom2(
     OrthancPluginContext*          context,
@@ -8813,7 +9137,7 @@
    * @param afterPlugins If 0, the built-in API of Orthanc is used.
    * If 1, the API is tainted by the plugins.
    * @return 0 if success, or the error code if failure.
-   * @see OrthancPluginRestApiGet2, OrthancPluginRestApiPost, OrthancPluginRestApiPut, OrthancPluginRestApiDelete
+   * @see OrthancPluginRestApiGet2(), OrthancPluginRestApiPost(), OrthancPluginRestApiPut(), OrthancPluginRestApiDelete()
    * @ingroup Orthanc
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginCallRestApi(
@@ -9195,6 +9519,212 @@
     context->InvokeService(context, _OrthancPluginService_RegisterStorageArea3, &params);
   }
 
+  /**
+   * @brief Signature of a callback function that is triggered when
+   * the Orthanc core requests an operation from the database plugin.
+   * Both request and response are encoded as protobuf buffers.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginCallDatabaseBackendV4) (
+    OrthancPluginMemoryBuffer64* response,
+    void* backend,
+    const void* request,
+    uint64_t requestSize);
+
+  /**
+   * @brief Signature of a callback function that is triggered when
+   * the database plugin must be finalized.
+   * @ingroup Callbacks
+   **/
+  typedef void (*OrthancPluginFinalizeDatabaseBackendV4) (void* backend);
+
+  typedef struct
+  {
+    void*                                   backend;
+    uint32_t                                maxDatabaseRetries;
+    OrthancPluginCallDatabaseBackendV4      operations;
+    OrthancPluginFinalizeDatabaseBackendV4  finalize;
+  } _OrthancPluginRegisterDatabaseBackendV4;
+
+  /**
+   * @brief Register a custom database back-end.
+   *
+   * This function was added in Orthanc SDK 1.12.0. It uses Google
+   * Protocol Buffers for the communications between the Orthanc core
+   * and database plugins. Check out "OrthancDatabasePlugin.proto" for
+   * the definition of the protobuf messages.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param backend Pointer to the custom database backend.
+   * @param maxDatabaseRetries Maximum number of retries if transaction doesn't succeed.
+   * If no retry is successful, OrthancPluginErrorCode_DatabaseCannotSerialize is generated.
+   * @param operations Access to the operations of the custom database backend.
+   * @param finalize Callback to deallocate the custom database backend.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterDatabaseBackendV4(
+    OrthancPluginContext*                   context,
+    void*                                   backend,
+    uint32_t                                maxDatabaseRetries,
+    OrthancPluginCallDatabaseBackendV4      operations,
+    OrthancPluginFinalizeDatabaseBackendV4  finalize)
+  {
+    _OrthancPluginRegisterDatabaseBackendV4 params;
+    params.backend = backend;
+    params.maxDatabaseRetries = maxDatabaseRetries;
+    params.operations = operations;
+    params.finalize = finalize;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackendV4, &params);
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginDicomInstance**        target;
+    const char*                         instanceId;
+    OrthancPluginLoadDicomInstanceMode  mode;
+  } _OrthancPluginLoadDicomInstance;
+
+  /**
+   * @brief Load a DICOM instance from the Orthanc server.
+   *
+   * This function loads a DICOM instance from the content of the
+   * Orthanc database. The function returns a new pointer to a data
+   * structure that is managed by the Orthanc core.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instanceId The Orthanc identifier of the DICOM instance of interest.
+   * @param mode Flag specifying how to deal with pixel data.
+   * @return The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance().
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginDicomInstance* OrthancPluginLoadDicomInstance(
+    OrthancPluginContext*               context,
+    const char*                         instanceId,
+    OrthancPluginLoadDicomInstanceMode  mode)
+  {
+    OrthancPluginDicomInstance* target = NULL;
+
+    _OrthancPluginLoadDicomInstance params;
+    params.target = &target;
+    params.instanceId = instanceId;
+    params.mode = mode;
+
+    if (context->InvokeService(context, _OrthancPluginService_LoadDicomInstance, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  typedef struct
+  {
+    const char*               name;
+    int64_t                   value;
+    OrthancPluginMetricsType  type;
+  } _OrthancPluginSetMetricsIntegerValue;
+
+  /**
+   * @brief Set the value of an integer metrics.
+   *
+   * This function sets the value of an integer metrics to monitor the
+   * behavior of the plugin through tools such as Prometheus. The
+   * values of all the metrics are stored within the Orthanc context.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param name The name of the metrics to be set.
+   * @param value The value of the metrics.
+   * @param type The type of the metrics. This parameter is only taken into consideration
+   * the first time this metrics is set.
+   * @ingroup Toolbox
+   * @see OrthancPluginSetMetricsValue()
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSetMetricsIntegerValue(
+    OrthancPluginContext*     context,
+    const char*               name,
+    int64_t                   value,
+    OrthancPluginMetricsType  type)
+  {
+    _OrthancPluginSetMetricsIntegerValue params;
+    params.name = name;
+    params.value = value;
+    params.type = type;
+    context->InvokeService(context, _OrthancPluginService_SetMetricsIntegerValue, &params);
+  }
+
+
+  /**
+   * @brief Set the name of the current thread.
+   *
+   * This function gives a name to the thread that is calling this
+   * function. This name is used in the Orthanc logs. This function
+   * must only be called from threads that the plugin has created
+   * itself.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param threadName The name of the current thread. A thread name cannot be longer than 16 characters.
+   * @return 0 if success, other value if error.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSetCurrentThreadName(
+    OrthancPluginContext*  context,
+    const char*            threadName)
+  {
+    return context->InvokeService(context, _OrthancPluginService_SetCurrentThreadName, threadName);
+  }
+
+
+  typedef struct
+  {
+    /* Note: This structure is also defined in Logging.h and it must be binary compatible */
+    const char*               message;
+    const char*               plugin;
+    const char*               file;
+    uint32_t                  line;
+    OrthancPluginLogCategory  category;
+    OrthancPluginLogLevel     level;
+  } _OrthancPluginLogMessage;
+
+
+  /**
+   * @brief Log a message.
+   *
+   * Log a message using the Orthanc logging system.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param message The message to be logged.
+   * @param plugin The plugin name.
+   * @param file The filename in the plugin code.
+   * @param line The file line in the plugin code.
+   * @param category The category.
+   * @param level The level of the message.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginLogMessage(
+    OrthancPluginContext* context,
+    const char* message,
+    const char* plugin,
+    const char* file,
+    uint32_t line,
+    OrthancPluginLogCategory category,
+    OrthancPluginLogLevel level)
+  {
+    _OrthancPluginLogMessage m;
+    m.message = message;
+    m.plugin = plugin;
+    m.file = file;
+    m.line = line;
+    m.category = category;
+    m.level = level;
+    context->InvokeService(context, _OrthancPluginService_LogMessage, &m);
+  }
+
 #ifdef  __cplusplus
 }
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,951 @@
+/**
+ * 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/>.
+ **/
+
+
+/**
+ * This Protocol Buffers prototype describes the exchanges between the
+ * Orthanc core and its database plugins. The various calls correspond
+ * to the "IDatabaseWrapper" interface in the source code of Orthanc.
+ *
+ * WARNING: *NEVER* modify or remove existing entries. It is only
+ * allowed to *add* new stuff.
+ **/
+
+syntax = "proto3";
+
+/**
+ * Turn off protobuf reflection to avoid clashes between the Orthanc
+ * core and the database plugin, otherwise both will try to register
+ * the same messages in the process-wide descriptor pool, which would
+ * result in protobuf error "File already exists in database".
+ **/
+option optimize_for = LITE_RUNTIME;
+
+package Orthanc.DatabasePluginMessages;
+
+
+/**
+ * Data structures that are common with the Orthanc core.
+ **/
+
+message FileInfo {
+  string  uuid = 1;
+  int32   content_type = 2;      // opaque "FileContentType" in Orthanc
+  uint64  uncompressed_size = 3;
+  string  uncompressed_hash = 4;
+  int32   compression_type = 5;  // opaque "CompressionType" in Orthanc
+  uint64  compressed_size = 6;
+  string  compressed_hash = 7;
+  string  custom_data = 8;       // added in v 1.12.5
+}
+
+enum ResourceType {
+  RESOURCE_PATIENT = 0;
+  RESOURCE_STUDY = 1;
+  RESOURCE_SERIES = 2;
+  RESOURCE_INSTANCE = 3;
+}
+
+enum ConstraintType {
+  CONSTRAINT_EQUAL = 0;
+  CONSTRAINT_SMALLER_OR_EQUAL = 1;
+  CONSTRAINT_GREATER_OR_EQUAL = 2;
+  CONSTRAINT_WILDCARD = 3;
+  CONSTRAINT_LIST = 4;
+}
+
+enum LabelsConstraintType {
+  LABELS_CONSTRAINT_ALL = 0;
+  LABELS_CONSTRAINT_ANY = 1;
+  LABELS_CONSTRAINT_NONE = 2;
+}
+
+message ServerIndexChange {
+  int64         seq = 1;
+  int32         change_type = 2;   // opaque "ChangeType" in Orthanc
+  ResourceType  resource_type = 3;
+  string        public_id = 4;
+  string        date = 5;
+}
+
+message ExportedResource {
+  int64         seq = 1;
+  ResourceType  resource_type = 2;
+  string        public_id = 3;
+  string        modality = 4;
+  string        date = 5;
+  string        patient_id = 6;
+  string        study_instance_uid = 7;
+  string        series_instance_uid = 8;
+  string        sop_instance_uid = 9;
+}
+
+message DatabaseConstraint {
+  ResourceType     level = 1;
+  uint32           tag_group = 2;
+  uint32           tag_element = 3;
+  bool             is_identifier_tag = 4;
+  bool             is_case_sensitive = 5;
+  bool             is_mandatory = 6;
+  ConstraintType   type = 7;
+  repeated string  values = 8;
+}
+
+
+/**
+ * Database-level operations.
+ **/
+
+enum DatabaseOperation {
+  OPERATION_GET_SYSTEM_INFORMATION = 0;
+  OPERATION_OPEN = 1;
+  OPERATION_CLOSE = 2;
+  OPERATION_FLUSH_TO_DISK = 3;
+  OPERATION_START_TRANSACTION = 4;
+  OPERATION_UPGRADE = 5;
+  OPERATION_FINALIZE_TRANSACTION = 6;
+  OPERATION_MEASURE_LATENCY = 7;         // New in Orthanc 1.12.3
+}
+
+enum TransactionType {
+  TRANSACTION_READ_ONLY = 0;
+  TRANSACTION_READ_WRITE = 1;
+}
+
+message GetSystemInformation {
+  message Request {
+  }
+  message Response {
+    uint32 database_version = 1;
+    bool supports_flush_to_disk = 2;
+    bool supports_revisions = 3;
+    bool supports_labels = 4;
+    bool supports_increment_global_property = 5;
+    bool has_update_and_get_statistics = 6;
+    bool has_measure_latency = 7;
+  }
+}
+
+message Open {
+  message Request {
+    message IdentifierTag {
+      ResourceType level = 1;
+      uint32 group = 2;
+      uint32 element = 3;
+      string name = 4;
+    }
+    repeated IdentifierTag identifier_tags = 1;
+  }
+  message Response {
+  }
+}
+
+message Close {
+  message Request {
+  }
+  message Response {
+  }
+}
+
+message FlushToDisk {
+  message Request {
+  }
+  message Response {
+  }
+}
+
+message StartTransaction {
+  message Request {
+    TransactionType type = 1;
+  }
+  message Response {
+    sfixed64 transaction = 1;
+  }
+}
+
+message Upgrade {
+  /**
+   * It is guaranteed that a read-write transaction is created by the
+   * Orthanc core before executing this operation.
+   **/
+  message Request {
+    uint32 target_version = 1;
+    sfixed64 storage_area = 2;
+    sfixed64 transaction = 3;
+  }
+  message Response {
+  }
+}
+
+message FinalizeTransaction {
+  message Request {
+    sfixed64 transaction = 1;
+  }
+  message Response {
+  }
+}
+
+message MeasureLatency {
+  message Request {
+  }
+  message Response {
+    int64 latency_us = 1;
+  }
+}
+
+
+message DatabaseRequest {
+  sfixed64           database = 1;
+  DatabaseOperation  operation = 2;
+
+  GetSystemInformation.Request  get_system_information = 100;
+  Open.Request                  open = 101;
+  Close.Request                 close = 102;
+  FlushToDisk.Request           flush_to_disk = 103;
+  StartTransaction.Request      start_transaction = 104;
+  Upgrade.Request               upgrade = 105;
+  FinalizeTransaction.Request   finalize_transaction = 106; 
+  MeasureLatency.Request        measure_latency = 107;
+}
+
+message DatabaseResponse {
+  GetSystemInformation.Response  get_system_information = 100;
+  Open.Response                  open = 101;
+  Close.Response                 close = 102;
+  FlushToDisk.Response           flush_to_disk = 103;
+  StartTransaction.Response      start_transaction = 104;
+  Upgrade.Response               upgrade = 105;
+  FinalizeTransaction.Response   finalize_transaction = 106;
+  MeasureLatency.Response        measure_latency = 107;
+}
+
+
+/**
+ * Transaction-level operations.
+ **/
+
+enum TransactionOperation {
+  OPERATION_ROLLBACK = 0;
+  OPERATION_COMMIT = 1;
+  OPERATION_ADD_ATTACHMENT = 2;
+  OPERATION_CLEAR_CHANGES = 3;
+  OPERATION_CLEAR_EXPORTED_RESOURCES = 4;
+  OPERATION_DELETE_ATTACHMENT = 5;
+  OPERATION_DELETE_METADATA = 6;
+  OPERATION_DELETE_RESOURCE = 7;
+  OPERATION_GET_ALL_METADATA = 8;
+  OPERATION_GET_ALL_PUBLIC_IDS = 9;
+  OPERATION_GET_ALL_PUBLIC_IDS_WITH_LIMITS = 10;
+  OPERATION_GET_CHANGES = 11;
+  OPERATION_GET_CHILDREN_INTERNAL_ID = 12;
+  OPERATION_GET_CHILDREN_PUBLIC_ID = 13;
+  OPERATION_GET_EXPORTED_RESOURCES = 14;
+  OPERATION_GET_LAST_CHANGE = 15;
+  OPERATION_GET_LAST_EXPORTED_RESOURCE = 16;
+  OPERATION_GET_MAIN_DICOM_TAGS = 17;
+  OPERATION_GET_PUBLIC_ID = 18;
+  OPERATION_GET_RESOURCES_COUNT = 19;
+  OPERATION_GET_RESOURCE_TYPE = 20;
+  OPERATION_GET_TOTAL_COMPRESSED_SIZE = 21;
+  OPERATION_GET_TOTAL_UNCOMPRESSED_SIZE = 22;
+  OPERATION_IS_PROTECTED_PATIENT = 23;
+  OPERATION_LIST_AVAILABLE_ATTACHMENTS = 24;
+  OPERATION_LOG_CHANGE = 25;
+  OPERATION_LOG_EXPORTED_RESOURCE = 26;
+  OPERATION_LOOKUP_ATTACHMENT = 27;
+  OPERATION_LOOKUP_GLOBAL_PROPERTY = 28;
+  OPERATION_LOOKUP_METADATA = 29;
+  OPERATION_LOOKUP_PARENT = 30;
+  OPERATION_LOOKUP_RESOURCE = 31;
+  OPERATION_SELECT_PATIENT_TO_RECYCLE = 32;
+  OPERATION_SELECT_PATIENT_TO_RECYCLE_WITH_AVOID = 33;
+  OPERATION_SET_GLOBAL_PROPERTY = 34;
+  OPERATION_CLEAR_MAIN_DICOM_TAGS = 35;
+  OPERATION_SET_METADATA = 36;
+  OPERATION_SET_PROTECTED_PATIENT = 37;
+  OPERATION_IS_DISK_SIZE_ABOVE = 38;
+  OPERATION_LOOKUP_RESOURCES = 39;
+  OPERATION_CREATE_INSTANCE = 40;
+  OPERATION_SET_RESOURCES_CONTENT = 41;
+  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
+}
+
+message Rollback {
+  message Request {
+  }
+  message Response {
+  }
+}
+
+message Commit {
+  message Request {
+    int64 file_size_delta = 1;
+  }
+  message Response {
+  }
+}
+
+message AddAttachment {
+  message Request {
+    int64 id = 1;
+    FileInfo attachment = 2;
+    int64 revision = 3;
+  }
+  message Response {
+  }
+}
+
+message ClearChanges {
+  message Request {
+  }
+  message Response {
+  }
+}
+
+message ClearExportedResources {
+  message Request {
+  }
+  message Response {
+  }
+}
+
+message DeleteAttachment {
+  message Request {
+    int64 id = 1;
+    int32 type = 2;
+  }
+  message Response {
+    FileInfo deleted_attachment = 1;
+  }
+}
+
+message DeleteMetadata {
+  message Request {
+    int64 id = 1;
+    int32 type = 2;
+  }
+  message Response {
+  }
+}
+
+message DeleteResource {
+  message Request {
+    int64 id = 1;
+  }
+  message Response {
+    message Resource {
+      ResourceType level = 1;
+      string public_id = 2;
+    }
+    repeated FileInfo deleted_attachments = 1;
+    repeated Resource deleted_resources = 2;
+    bool is_remaining_ancestor = 3;
+    Resource remaining_ancestor = 4;
+  }
+}
+
+message GetAllMetadata {
+  message Request {
+    int64 id = 1;
+  }
+  message Response {
+    message Metadata {
+      int32 type = 1;
+      string value = 2;
+    }
+    repeated Metadata metadata = 1;
+  }
+}
+
+message GetAllPublicIds {
+  message Request {
+    ResourceType resource_type = 1;
+  }
+  message Response {
+    repeated string ids = 1;
+  }
+}
+
+message GetAllPublicIdsWithLimits {
+  message Request {
+    ResourceType resource_type = 1;
+    int64 since = 2;
+    uint32 limit = 3;
+  }
+  message Response {
+    repeated string ids = 1;
+  }
+}
+
+message GetChanges {
+  message Request {
+    int64 since = 1;
+    uint32 limit = 2;
+  }
+  message Response {
+    repeated ServerIndexChange changes = 1;
+    bool done = 2;
+  }
+}
+
+message GetChildrenInternalId {
+  message Request {
+    int64 id = 1;
+  }
+  message Response {
+    repeated int64 ids = 1;
+  }
+}
+
+message GetChildrenPublicId {
+  message Request {
+    int64 id = 1;
+  }
+  message Response {
+    repeated string ids = 1;
+  }
+}
+
+message GetExportedResources {
+  message Request {
+    int64 since = 1;
+    uint32 limit = 2;
+  }
+  message Response {
+    repeated ExportedResource resources = 1;
+    bool done = 2;
+  }
+}
+
+message GetLastChange {
+  message Request {
+  }
+  message Response {
+    bool found = 1;
+    ServerIndexChange change = 2;
+  }
+}
+
+message GetLastExportedResource {
+  message Request {
+  }
+  message Response {
+    bool found = 1;
+    ExportedResource resource = 2;
+  }
+}
+
+message GetMainDicomTags {
+  message Request {
+    int64 id = 1;
+  }
+  message Response {
+    message Tag {
+      uint32 group = 1;
+      uint32 element = 2;
+      string value = 3;
+    }
+    repeated Tag tags = 1;
+  }
+}
+
+message GetPublicId {
+  message Request {
+    int64 id = 1;
+  }
+  message Response {
+    string id = 1;
+  }
+}
+
+message GetResourcesCount {
+  message Request {
+    ResourceType type = 1;
+  }
+  message Response {
+    uint64 count = 1;
+  }
+}
+
+message GetResourceType {
+  message Request {
+    int64 id = 1;
+  }
+  message Response {
+    ResourceType type = 1;
+  }
+}
+
+message GetTotalCompressedSize {
+  message Request {
+  }
+  message Response {
+    uint64 size = 1;
+  }
+}
+
+message GetTotalUncompressedSize {
+  message Request {
+  }
+  message Response {
+    uint64 size = 1;
+  }
+}
+
+message IsProtectedPatient {
+  message Request {
+    int64 patient_id = 1;
+  }
+  message Response {
+    bool protected_patient = 1;
+  }
+}
+
+message ListAvailableAttachments {
+  message Request {
+    int64 id = 1;
+  }
+  message Response {
+    repeated int32 attachments = 1;
+  }
+}
+
+message LogChange {
+  message Request {
+    int32         change_type = 1;
+    ResourceType  resource_type = 2;
+    int64         resource_id = 3;
+    string        date = 4;
+  }
+  message Response {
+  }
+}
+
+message LogExportedResource {
+  message Request {
+    ResourceType  resource_type = 1;
+    string        public_id = 2;
+    string        modality = 3;
+    string        date = 4;
+    string        patient_id = 5;
+    string        study_instance_uid = 6;
+    string        series_instance_uid = 7;
+    string        sop_instance_uid = 8;
+  }
+  message Response {
+  }
+}
+
+message LookupAttachment {
+  message Request {
+    int64 id = 1;
+    int32 content_type = 2;
+  }
+  message Response {
+    bool found = 1;
+    FileInfo attachment = 2;
+    int64 revision = 3;
+  }
+}
+
+message LookupGlobalProperty {
+  message Request {
+    string server_id = 1;
+    int32 property = 2;
+  }
+  message Response {
+    bool found = 1;
+    string value = 2;
+  }
+}
+
+message LookupMetadata {
+  message Request {
+    int64 id = 1;
+    int32 metadata_type = 2;
+  }
+  message Response {
+    bool found = 1;
+    string value = 2;
+    int64 revision = 3;
+  }
+}
+
+message LookupParent {
+  message Request {
+    int64 id = 1;
+  }
+  message Response {
+    bool found = 1;
+    int64 parent = 2;
+  }
+}
+
+message LookupResource {
+  message Request {
+    string public_id = 1;
+  }
+  message Response {
+    bool found = 1;
+    int64 internal_id = 2;
+    ResourceType type = 3;
+  }
+}
+
+message SelectPatientToRecycle {
+  message Request {
+  }
+  message Response {
+    bool found = 1;
+    int64 patient_id = 2;
+  }
+}
+
+message SelectPatientToRecycleWithAvoid {
+  message Request {
+    int64 patient_id_to_avoid = 1;
+  }
+  message Response {
+    bool found = 1;
+    int64 patient_id = 2;
+  }
+}
+
+message SetGlobalProperty {
+  message Request {
+    string server_id = 1;
+    int32 property = 2;
+    string value = 3;
+  }
+  message Response {
+  }
+}
+
+message IncrementGlobalProperty {
+  message Request {
+    string server_id = 1;
+    int32 property = 2;
+    int64 increment = 3;
+  }
+  message Response {
+    int64 new_value = 1;
+  }
+}
+
+message UpdateAndGetStatistics {
+  message Request {
+  }
+  message Response {
+    int64 patients_count = 1;
+    int64 studies_count = 2;
+    int64 series_count = 3;
+    int64 instances_count = 4;
+    int64 total_compressed_size = 5;
+    int64 total_uncompressed_size = 6;
+  }
+}
+
+message ClearMainDicomTags {
+  message Request {
+    int64 id = 1;
+  }
+  message Response {
+  }
+}
+
+message SetMetadata {
+  message Request {
+    int64 id = 1;
+    int32 metadata_type = 2;
+    string value = 3;
+    int64 revision = 4;
+  }
+  message Response {
+  }
+}
+
+message SetProtectedPatient {
+  message Request {
+    int64 patient_id = 1;
+    bool protected_patient = 2;
+  }
+  message Response {
+  }
+}
+
+message IsDiskSizeAbove {
+  message Request {
+    uint64 threshold = 1;
+  }
+  message Response {
+    bool result = 1;
+  }
+}
+
+message LookupResources {
+  message Request {
+    repeated DatabaseConstraint lookup = 1;
+    ResourceType query_level = 2;
+    uint32 limit = 3;
+    bool retrieve_instances_ids = 4;
+    repeated string labels = 5;                  // New in Orthanc 1.12.0
+    LabelsConstraintType labels_constraint = 6;  // New in Orthanc 1.12.0
+  }
+  message Response {
+    repeated string resources_ids = 1;
+    repeated string instances_ids = 2;  // Only filled if "retrieve_instances" is true
+  }
+}
+
+message CreateInstance {
+  message Request {
+    string patient = 1;
+    string study = 2;
+    string series = 3;
+    string instance = 4;
+  }
+  message Response {
+    bool is_new_instance = 1;
+    int64 instance_id = 2;
+
+    // The fields below are only set if "is_new_instance" is true
+    bool is_new_patient = 3;
+    bool is_new_study = 4;
+    bool is_new_series = 5;
+    int64 patient_id = 6;
+    int64 study_id = 7;
+    int64 series_id = 8;
+  }
+}
+
+message SetResourcesContent {
+  message Request {
+    message Tag {
+      int64 resource_id = 1;
+      bool is_identifier = 2;
+      uint32 group = 3;
+      uint32 element = 4;
+      string value = 5;
+    }
+
+    message Metadata {
+      int64 resource_id = 1;
+      int32 metadata = 2;
+      string value = 3;
+    }
+
+    repeated Tag tags = 1;
+    repeated Metadata metadata = 2;
+  }
+  message Response {
+  }
+}
+
+message GetChildrenMetadata {
+  message Request {
+    int64 id = 1;
+    int32 metadata = 2;
+  }
+  message Response {
+    repeated string values = 1;
+  }
+}
+
+message GetLastChangeIndex {
+  message Request {
+  }
+  message Response {
+    int64 result = 1;
+  }
+}
+
+message LookupResourceAndParent {
+  message Request {
+    string public_id = 1;
+  }
+  message Response {
+    bool found = 1;
+    int64 id = 2;
+    ResourceType type = 3;
+    string parent_public_id = 4;  // Only for study, series, or instance
+  }
+}
+
+message AddLabel {
+  message Request {
+    int64 id = 1;
+    string label = 2;
+  }
+  message Response {
+  }
+}
+
+message RemoveLabel {
+  message Request {
+    int64 id = 1;
+    string label = 2;
+  }
+  message Response {
+  }
+}
+
+message ListLabels {
+  message Request {
+    bool single_resource = 1;
+    int64 id = 2;  // Only if "single_resource" is "true"
+  }
+  message Response {
+    repeated string labels = 1;
+  }
+}
+
+message TransactionRequest {
+  sfixed64              transaction = 1;
+  TransactionOperation  operation = 2;
+
+  Rollback.Request                        rollback = 100;
+  Commit.Request                          commit = 101;
+  AddAttachment.Request                   add_attachment = 102;
+  ClearChanges.Request                    clear_changes = 103;
+  ClearExportedResources.Request          clear_exported_resources = 104;
+  DeleteAttachment.Request                delete_attachment = 105;
+  DeleteMetadata.Request                  delete_metadata = 106;
+  DeleteResource.Request                  delete_resource = 107;
+  GetAllMetadata.Request                  get_all_metadata = 108;
+  GetAllPublicIds.Request                 get_all_public_ids = 109;
+  GetAllPublicIdsWithLimits.Request       get_all_public_ids_with_limits = 110;
+  GetChanges.Request                      get_changes = 111;
+  GetChildrenInternalId.Request           get_children_internal_id = 112;
+  GetChildrenPublicId.Request             get_children_public_id = 113;
+  GetExportedResources.Request            get_exported_resources = 114;
+  GetLastChange.Request                   get_last_change = 115;
+  GetLastExportedResource.Request         get_last_exported_resource = 116;
+  GetMainDicomTags.Request                get_main_dicom_tags = 117;
+  GetPublicId.Request                     get_public_id = 118;
+  GetResourcesCount.Request               get_resources_count = 119;
+  GetResourceType.Request                 get_resource_type = 120;
+  GetTotalCompressedSize.Request          get_total_compressed_size = 121;
+  GetTotalUncompressedSize.Request        get_total_uncompressed_size = 122;
+  IsProtectedPatient.Request              is_protected_patient = 123;
+  ListAvailableAttachments.Request        list_available_attachments = 124;
+  LogChange.Request                       log_change = 125;
+  LogExportedResource.Request             log_exported_resource = 126;
+  LookupAttachment.Request                lookup_attachment = 127;
+  LookupGlobalProperty.Request            lookup_global_property = 128;
+  LookupMetadata.Request                  lookup_metadata = 129;
+  LookupParent.Request                    lookup_parent = 130;
+  LookupResource.Request                  lookup_resource = 131;
+  SelectPatientToRecycle.Request          select_patient_to_recycle = 132;
+  SelectPatientToRecycleWithAvoid.Request select_patient_to_recycle_with_avoid = 133;
+  SetGlobalProperty.Request               set_global_property = 134;
+  ClearMainDicomTags.Request              clear_main_dicom_tags = 135;
+  SetMetadata.Request                     set_metadata = 136;
+  SetProtectedPatient.Request             set_protected_patient = 137;
+  IsDiskSizeAbove.Request                 is_disk_size_above = 138;
+  LookupResources.Request                 lookup_resources = 139;
+  CreateInstance.Request                  create_instance = 140;
+  SetResourcesContent.Request             set_resources_content = 141;
+  GetChildrenMetadata.Request             get_children_metadata = 142;
+  GetLastChangeIndex.Request              get_last_change_index = 143;
+  LookupResourceAndParent.Request         lookup_resource_and_parent = 144;
+  AddLabel.Request                        add_label = 145;
+  RemoveLabel.Request                     remove_label = 146;
+  ListLabels.Request                      list_labels = 147;
+  IncrementGlobalProperty.Request         increment_global_property = 148;
+  UpdateAndGetStatistics.Request          update_and_get_statistics = 149;
+}
+
+message TransactionResponse {
+  Rollback.Response                        rollback = 100;
+  Commit.Response                          commit = 101;
+  AddAttachment.Response                   add_attachment = 102;
+  ClearChanges.Response                    clear_changes = 103;
+  ClearExportedResources.Response          clear_exported_resources = 104;
+  DeleteAttachment.Response                delete_attachment = 105;
+  DeleteMetadata.Response                  delete_metadata = 106;
+  DeleteResource.Response                  delete_resource = 107;
+  GetAllMetadata.Response                  get_all_metadata = 108;
+  GetAllPublicIds.Response                 get_all_public_ids = 109;
+  GetAllPublicIdsWithLimits.Response       get_all_public_ids_with_limits = 110;
+  GetChanges.Response                      get_changes = 111;
+  GetChildrenInternalId.Response           get_children_internal_id = 112;
+  GetChildrenPublicId.Response             get_children_public_id = 113;
+  GetExportedResources.Response            get_exported_resources = 114;
+  GetLastChange.Response                   get_last_change = 115;
+  GetLastExportedResource.Response         get_last_exported_resource = 116;
+  GetMainDicomTags.Response                get_main_dicom_tags = 117;
+  GetPublicId.Response                     get_public_id = 118;
+  GetResourcesCount.Response               get_resources_count = 119;
+  GetResourceType.Response                 get_resource_type = 120;
+  GetTotalCompressedSize.Response          get_total_compressed_size = 121;
+  GetTotalUncompressedSize.Response        get_total_uncompressed_size = 122;
+  IsProtectedPatient.Response              is_protected_patient = 123;
+  ListAvailableAttachments.Response        list_available_attachments = 124;
+  LogChange.Response                       log_change = 125;
+  LogExportedResource.Response             log_exported_resource = 126;
+  LookupAttachment.Response                lookup_attachment = 127;
+  LookupGlobalProperty.Response            lookup_global_property = 128;
+  LookupMetadata.Response                  lookup_metadata = 129;
+  LookupParent.Response                    lookup_parent = 130;
+  LookupResource.Response                  lookup_resource = 131;
+  SelectPatientToRecycle.Response          select_patient_to_recycle = 132;
+  SelectPatientToRecycleWithAvoid.Response select_patient_to_recycle_with_avoid = 133;
+  SetGlobalProperty.Response               set_global_property = 134;
+  ClearMainDicomTags.Response              clear_main_dicom_tags = 135;
+  SetMetadata.Response                     set_metadata = 136;
+  SetProtectedPatient.Response             set_protected_patient = 137;
+  IsDiskSizeAbove.Response                 is_disk_size_above = 138;
+  LookupResources.Response                 lookup_resources = 139;
+  CreateInstance.Response                  create_instance = 140;
+  SetResourcesContent.Response             set_resources_content = 141;
+  GetChildrenMetadata.Response             get_children_metadata = 142;
+  GetLastChangeIndex.Response              get_last_change_index = 143;
+  LookupResourceAndParent.Response         lookup_resource_and_parent = 144;
+  AddLabel.Response                        add_label = 145;
+  RemoveLabel.Response                     remove_label = 146;
+  ListLabels.Response                      list_labels = 147;
+  IncrementGlobalProperty.Response         increment_global_property = 148;
+  UpdateAndGetStatistics.Response          update_and_get_statistics = 149;
+}
+
+enum RequestType {
+  REQUEST_DATABASE = 0;
+  REQUEST_TRANSACTION = 1;
+}
+
+message Request {
+  RequestType         type = 1;
+  DatabaseRequest     database_request = 2;
+  TransactionRequest  transaction_request = 3;
+}
+
+message Response {
+  DatabaseResponse     database_response = 2;
+  TransactionResponse  transaction_response = 3;
+}
--- a/OrthancServer/Plugins/Samples/AdvancedStorage/Plugin.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/AdvancedStorage/Plugin.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -19,6 +19,7 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
 
+#define ORTHANC_PLUGIN_NAME "advanced-storage"
 
 #include "../../../../OrthancFramework/Sources/Compatibility.h"
 #include "../../../../OrthancFramework/Sources/OrthancException.h"
@@ -379,7 +380,7 @@
 {
   try
   {
-    OrthancPlugins::LogInfo(std::string("Creating attachment \"") + uuid + "\"");
+    LOG(INFO) << "Creating attachment \"" << uuid << "\"";
 
     //TODO_CUSTOM_DATA: get tags from the Rest API...
     Json::Value tags;
@@ -409,7 +410,7 @@
 
   if (!Orthanc::SystemToolbox::IsRegularFile(path))
   {
-    OrthancPlugins::LogError(std::string("The path does not point to a regular file: ") + path);
+    LOG(ERROR) << "The path does not point to a regular file: " << path;
     return OrthancPluginErrorCode_InexistentFile;
   }
 
@@ -419,7 +420,7 @@
     f.open(path, std::ifstream::in | std::ifstream::binary);
     if (!f.good())
     {
-      OrthancPlugins::LogError(std::string("The path does not point to a regular file: ") + path);
+      LOG(ERROR) << "The path does not point to a regular file: " << path;
       return OrthancPluginErrorCode_InexistentFile;
     }
 
@@ -431,7 +432,7 @@
     // The ReadWhole must allocate the buffer itself
     if (OrthancPluginCreateMemoryBuffer64(OrthancPlugins::GetGlobalContext(), target, fileSize) != OrthancPluginErrorCode_Success)
     {
-      OrthancPlugins::LogError(std::string("Unable to allocate memory to read file: ") + path);
+      LOG(ERROR) << "Unable to allocate memory to read file: " << path;
       return OrthancPluginErrorCode_NotEnoughMemory;
     }
 
@@ -444,7 +445,7 @@
   }
   catch (...)
   {
-    OrthancPlugins::LogError(std::string("Unexpected error while reading: ") + path);
+    LOG(ERROR) << "Unexpected error while reading: " << path;
     return OrthancPluginErrorCode_StorageAreaPlugin;
   }
 
@@ -464,7 +465,7 @@
 
   if (!Orthanc::SystemToolbox::IsRegularFile(path))
   {
-    OrthancPlugins::LogError(std::string("The path does not point to a regular file: ") + path);
+    LOG(ERROR) << "The path does not point to a regular file: " << path;
     return OrthancPluginErrorCode_InexistentFile;
   }
 
@@ -474,7 +475,7 @@
     f.open(path, std::ifstream::in | std::ifstream::binary);
     if (!f.good())
     {
-      OrthancPlugins::LogError(std::string("The path does not point to a regular file: ") + path);
+      LOG(ERROR) << "The path does not point to a regular file: " << path;
       return OrthancPluginErrorCode_InexistentFile;
     }
 
@@ -487,7 +488,7 @@
   }
   catch (...)
   {
-    OrthancPlugins::LogError(std::string("Unexpected error while reading: ") + path);
+    LOG(ERROR) << "Unexpected error while reading: " << path;
     return OrthancPluginErrorCode_StorageAreaPlugin;
   }
 
@@ -549,8 +550,8 @@
       return -1;
     }
 
-    OrthancPlugins::LogWarning("AdvancedStorage plugin is initializing");
-    OrthancPluginSetDescription(context, "Provides alternative layout for your storage.");
+    LOG(WARNING) << "AdvancedStorage plugin is initializing";
+    OrthancPluginSetDescription2(context, ORTHANC_PLUGIN_NAME, "Provides alternative layout for your storage.");
 
     OrthancPlugins::OrthancConfiguration orthancConfiguration;
 
@@ -673,7 +674,7 @@
     }
     else
     {
-      OrthancPlugins::LogWarning("AdvancedStorage plugin is disabled by the configuration file");
+      LOG(WARNING) << "AdvancedStorage plugin is disabled by the configuration file";
     }
 
     return 0;
@@ -682,18 +683,18 @@
 
   ORTHANC_PLUGINS_API void OrthancPluginFinalize()
   {
-    OrthancPlugins::LogWarning("AdvancedStorage plugin is finalizing");
+    LOG(WARNING) << "AdvancedStorage plugin is finalizing";
   }
 
 
   ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
   {
-    return "advanced-storage";
+    return ORTHANC_PLUGIN_NAME;
   }
 
 
   ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
   {
-    return ORTHANC_PLUGIN_VERSION;
+    return ADVANCED_STORAGE_VERSION;
   }
 }
--- a/OrthancServer/Plugins/Samples/AutomatedJpeg2kCompression/CMakeLists.txt	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/AutomatedJpeg2kCompression/CMakeLists.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -25,3 +26,5 @@
 include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake)
 
 add_library(AutomatedJpeg2kCompression SHARED Plugin.cpp)
+
+DefineSourceBasenameForTarget(AutomatedJpeg2kCompression)
--- a/OrthancServer/Plugins/Samples/AutomatedJpeg2kCompression/Plugin.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/AutomatedJpeg2kCompression/Plugin.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Plugins/Samples/Basic/CMakeLists.txt	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/Basic/CMakeLists.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -25,3 +26,5 @@
 include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake)
 
 add_library(PluginTest SHARED Plugin.c)
+
+DefineSourceBasenameForTarget(PluginTest)
--- a/OrthancServer/Plugins/Samples/Basic/Plugin.c	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/Basic/Plugin.c	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -20,6 +21,8 @@
  **/
 
 
+#define PLUGIN_NAME "sample"
+
 #include <orthanc/OrthancCPlugin.h>
 
 #include <string.h>
@@ -282,6 +285,63 @@
 }
 
 
+OrthancPluginErrorCode CallbackDicomWeb(OrthancPluginRestOutput* output,
+                                        const char* url,
+                                        const OrthancPluginHttpRequest* request)
+{
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "GET");
+  }
+  else
+  {
+    OrthancPluginLoadDicomInstanceMode mode = OrthancPluginLoadDicomInstanceMode_WholeDicom;
+    OrthancPluginDicomInstance* instance;
+    char* json;
+
+    if (request->getCount == 1)
+    {
+      if (strcmp(request->getKeys[0], "until-pixel-data") == 0)
+      {
+        mode = OrthancPluginLoadDicomInstanceMode_UntilPixelData;
+      }
+      else if (strcmp(request->getKeys[0], "empty-pixel-data") == 0)
+      {
+        mode = OrthancPluginLoadDicomInstanceMode_EmptyPixelData;
+      }
+      else
+      {
+        return OrthancPluginErrorCode_ParameterOutOfRange;
+      }
+    }
+    
+    instance = OrthancPluginLoadDicomInstance(context, request->groups[0], mode);
+    if (instance == NULL)
+    {
+      return OrthancPluginErrorCode_UnknownResource;
+    }
+
+    json = OrthancPluginEncodeDicomWebXml(context,
+                                          OrthancPluginGetInstanceData(context, instance),
+                                          OrthancPluginGetInstanceSize(context, instance),
+                                          DicomWebBinaryCallback);
+    OrthancPluginFreeDicomInstance(context, instance);
+
+    if (json != NULL)
+    {
+      OrthancPluginAnswerBuffer(context, output, json, strlen(json), "application/json");
+      OrthancPluginFreeString(context, json);
+    }
+    else
+    {
+      return OrthancPluginErrorCode_InternalError;
+    }
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
 OrthancPluginErrorCode OnStoredCallback(const OrthancPluginDicomInstance* instance,
                                         const char* instanceId)
 {
@@ -511,6 +571,7 @@
   OrthancPluginRegisterRestCallback(context, "/forward/(built-in)(/.+)", Callback5);
   OrthancPluginRegisterRestCallback(context, "/forward/(plugins)(/.+)", Callback5);
   OrthancPluginRegisterRestCallback(context, "/plugin/create", CallbackCreateDicom);
+  OrthancPluginRegisterRestCallback(context, "/instances/([^/]+)/dicom-web", CallbackDicomWeb);
 
   OrthancPluginRegisterOnStoredInstanceCallback(context, OnStoredCallback);
   OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
@@ -520,9 +581,9 @@
     
   
   /* Declare several properties of the plugin */
-  OrthancPluginSetRootUri(context, "/plugin/hello");
-  OrthancPluginSetDescription(context, "This is the description of the sample plugin that can be seen in Orthanc Explorer.");
-  OrthancPluginExtendOrthancExplorer(context, "alert('Hello Orthanc! From sample plugin with love.');");
+  OrthancPluginSetRootUri2(context, PLUGIN_NAME, "/plugin/hello");
+  OrthancPluginSetDescription2(context, PLUGIN_NAME, "This is the description of the sample plugin that can be seen in Orthanc Explorer.");
+  OrthancPluginExtendOrthancExplorer2(context, PLUGIN_NAME, "alert('Hello Orthanc! From sample plugin with love.');");
 
   customError = OrthancPluginRegisterErrorCode(context, 4, 402, "Hello world");
   
@@ -544,7 +605,7 @@
 
 ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
 {
-  return "sample";
+  return PLUGIN_NAME;
 }
 
 
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -61,6 +62,7 @@
 namespace OrthancPlugins
 {
   static OrthancPluginContext* globalContext_ = NULL;
+  static std::string pluginName_;
 
 
   void SetGlobalContext(OrthancPluginContext* context)
@@ -80,6 +82,20 @@
   }
 
 
+  void SetGlobalContext(OrthancPluginContext* context,
+                        const char* pluginName)
+  {
+    SetGlobalContext(context);
+    pluginName_ = pluginName;
+  }
+
+
+  void ResetGlobalContext()
+  {
+    globalContext_ = NULL;
+    pluginName_.clear();
+  }
+
   bool HasGlobalContext()
   {
     return globalContext_ != NULL;
@@ -99,6 +115,66 @@
   }
 
 
+#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1
+  void LogMessage(OrthancPluginLogLevel level,
+                  const char* file,
+                  uint32_t line,
+                  const std::string& message)
+  {
+    if (HasGlobalContext())
+    {
+#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1
+      const char* pluginName = (pluginName_.empty() ? NULL : pluginName_.c_str());
+      OrthancPluginLogMessage(GetGlobalContext(), message.c_str(), pluginName, file, line, OrthancPluginLogCategory_Generic, level);
+#else
+      switch (level)
+      {
+        case OrthancPluginLogLevel_Error:
+          OrthancPluginLogError(GetGlobalContext(), message.c_str());
+          break;
+
+        case OrthancPluginLogLevel_Warning:
+          OrthancPluginLogWarning(GetGlobalContext(), message.c_str());
+          break;
+
+        case OrthancPluginLogLevel_Info:
+          OrthancPluginLogInfo(GetGlobalContext(), message.c_str());
+          break;
+
+        default:
+          ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
+      }
+#endif
+    }
+  }
+#endif
+
+
+  void LogError(const std::string& message)
+  {
+    if (HasGlobalContext())
+    {
+      OrthancPluginLogError(GetGlobalContext(), message.c_str());
+    }
+  }
+
+  void LogWarning(const std::string& message)
+  {
+    if (HasGlobalContext())
+    {
+      OrthancPluginLogWarning(GetGlobalContext(), message.c_str());
+    }
+  }
+
+  void LogInfo(const std::string& message)
+  {
+    if (HasGlobalContext())
+    {
+      OrthancPluginLogInfo(GetGlobalContext(), message.c_str());
+    }
+  }
+
+
   void MemoryBuffer::Check(OrthancPluginErrorCode code)
   {
     if (code != OrthancPluginErrorCode_Success)
@@ -229,7 +305,7 @@
 
     if (!ReadJson(target, buffer_.data, buffer_.size))
     {
-      LogError("Cannot convert some memory buffer to JSON");
+      ORTHANC_PLUGINS_LOG_ERROR("Cannot convert some memory buffer to JSON");
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
   }
@@ -253,14 +329,15 @@
   // helper class to convert std::map of headers to the plugin SDK C structure
   class PluginHttpHeaders
   {
+  private:
     std::vector<const char*> headersKeys_;
     std::vector<const char*> headersValues_;
+
   public:
-
-    PluginHttpHeaders(const std::map<std::string, std::string>& httpHeaders)
+    explicit PluginHttpHeaders(const std::map<std::string, std::string>& httpHeaders)
     {
       for (std::map<std::string, std::string>::const_iterator
-           it = httpHeaders.begin(); it != httpHeaders.end(); it++)
+             it = httpHeaders.begin(); it != httpHeaders.end(); ++it)
       {
         headersKeys_.push_back(it->first.c_str());
         headersValues_.push_back(it->second.c_str());
@@ -399,7 +476,7 @@
     }
     else
     {
-      LogError("Cannot parse JSON: " + std::string(err));
+      ORTHANC_PLUGINS_LOG_ERROR("Cannot parse JSON: " + std::string(err));
       return false;
     }
 #endif
@@ -560,13 +637,13 @@
   {
     if (str_ == NULL)
     {
-      LogError("Cannot convert an empty memory buffer to JSON");
+      ORTHANC_PLUGINS_LOG_ERROR("Cannot convert an empty memory buffer to JSON");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
 
     if (!ReadJson(target, str_))
     {
-      LogError("Cannot convert some memory buffer to JSON");
+      ORTHANC_PLUGINS_LOG_ERROR("Cannot convert some memory buffer to JSON");
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
   }
@@ -576,13 +653,13 @@
   {
     if (str_ == NULL)
     {
-      LogError("Cannot convert an empty memory buffer to JSON");
+      ORTHANC_PLUGINS_LOG_ERROR("Cannot convert an empty memory buffer to JSON");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
 
     if (!ReadJsonWithoutComments(target, str_))
     {
-      LogError("Cannot convert some memory buffer to JSON");
+      ORTHANC_PLUGINS_LOG_ERROR("Cannot convert some memory buffer to JSON");
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
   }
@@ -620,7 +697,7 @@
 
     if (body.size() > 0xffffffffu)
     {
-      LogError("Cannot handle body size > 4GB");
+      ORTHANC_PLUGINS_LOG_ERROR("Cannot handle body size > 4GB");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
 
@@ -640,7 +717,7 @@
 
     if (body.size() > 0xffffffffu)
     {
-      LogError("Cannot handle body size > 4GB");
+      ORTHANC_PLUGINS_LOG_ERROR("Cannot handle body size > 4GB");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
 
@@ -683,34 +760,6 @@
     }
   }
 
-
-  void LogError(const std::string& message)
-  {
-    if (HasGlobalContext())
-    {
-      OrthancPluginLogError(GetGlobalContext(), message.c_str());
-    }
-  }
-
-
-  void LogWarning(const std::string& message)
-  {
-    if (HasGlobalContext())
-    {
-      OrthancPluginLogWarning(GetGlobalContext(), message.c_str());
-    }
-  }
-
-
-  void LogInfo(const std::string& message)
-  {
-    if (HasGlobalContext())
-    {
-      OrthancPluginLogInfo(GetGlobalContext(), message.c_str());
-    }
-  }
-
-
   void OrthancConfiguration::LoadConfiguration()
   {
     OrthancString str;
@@ -718,7 +767,7 @@
 
     if (str.GetContent() == NULL)
     {
-      LogError("Cannot access the Orthanc configuration");
+      ORTHANC_PLUGINS_LOG_ERROR("Cannot access the Orthanc configuration");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
 
@@ -726,7 +775,7 @@
 
     if (configuration_.type() != Json::objectValue)
     {
-      LogError("Unable to read the Orthanc configuration");
+      ORTHANC_PLUGINS_LOG_ERROR("Unable to read the Orthanc configuration");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
   }
@@ -750,6 +799,12 @@
     }
   }
 
+  OrthancConfiguration::OrthancConfiguration(const Json::Value& configuration, const std::string& path) :
+    configuration_(configuration),
+    path_(path)
+  {
+  }
+
 
   std::string OrthancConfiguration::GetPath(const std::string& key) const
   {
@@ -788,8 +843,8 @@
     {
       if (configuration_[key].type() != Json::objectValue)
       {
-        LogError("The configuration section \"" + target.path_ +
-                 "\" is not an associative array as expected");
+        ORTHANC_PLUGINS_LOG_ERROR("The configuration section \"" + target.path_ +
+                                  "\" is not an associative array as expected");
 
         ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
       }
@@ -811,8 +866,8 @@
 
     if (configuration_[key].type() != Json::stringValue)
     {
-      LogError("The configuration option \"" + GetPath(key) +
-               "\" is not a string as expected");
+      ORTHANC_PLUGINS_LOG_ERROR("The configuration option \"" + GetPath(key) +
+                                "\" is not a string as expected");
 
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
@@ -843,8 +898,8 @@
         return true;
 
       default:
-        LogError("The configuration option \"" + GetPath(key) +
-                 "\" is not an integer as expected");
+        ORTHANC_PLUGINS_LOG_ERROR("The configuration option \"" + GetPath(key) +
+                                  "\" is not an integer as expected");
 
         ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
@@ -862,8 +917,8 @@
 
     if (tmp < 0)
     {
-      LogError("The configuration option \"" + GetPath(key) +
-               "\" is not a positive integer as expected");
+      ORTHANC_PLUGINS_LOG_ERROR("The configuration option \"" + GetPath(key) +
+                                "\" is not a positive integer as expected");
 
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
@@ -887,8 +942,8 @@
 
     if (configuration_[key].type() != Json::booleanValue)
     {
-      LogError("The configuration option \"" + GetPath(key) +
-               "\" is not a Boolean as expected");
+      ORTHANC_PLUGINS_LOG_ERROR("The configuration option \"" + GetPath(key) +
+                                "\" is not a Boolean as expected");
 
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
@@ -923,8 +978,8 @@
         return true;
 
       default:
-        LogError("The configuration option \"" + GetPath(key) +
-                 "\" is not an integer as expected");
+        ORTHANC_PLUGINS_LOG_ERROR("The configuration option \"" + GetPath(key) +
+                                  "\" is not an integer as expected");
 
         ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
@@ -983,8 +1038,8 @@
         break;
     }
 
-    LogError("The configuration option \"" + GetPath(key) +
-             "\" is not a list of strings as expected");
+    ORTHANC_PLUGINS_LOG_ERROR("The configuration option \"" + GetPath(key) +
+                              "\" is not a list of strings as expected");
 
     ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
   }
@@ -1104,8 +1159,8 @@
 
     if (configuration_[key].type() != Json::objectValue)
     {
-      LogError("The configuration option \"" + GetPath(key) +
-               "\" is not a string as expected");
+      ORTHANC_PLUGINS_LOG_ERROR("The configuration option \"" + GetPath(key) +
+                                "\" is not an object as expected");
 
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
@@ -1122,8 +1177,8 @@
       }
       else
       {
-        LogError("The configuration option \"" + GetPath(key) +
-                 "\" is not a dictionary mapping strings to strings");
+        ORTHANC_PLUGINS_LOG_ERROR("The configuration option \"" + GetPath(key) +
+                                  "\" is not a dictionary mapping strings to strings");
 
         ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
       }
@@ -1145,7 +1200,7 @@
   {
     if (image_ == NULL)
     {
-      LogError("Trying to access a NULL image");
+      ORTHANC_PLUGINS_LOG_ERROR("Trying to access a NULL image");
       ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
     }
   }
@@ -1171,7 +1226,7 @@
 
     if (image_ == NULL)
     {
-      LogError("Cannot create an image");
+      ORTHANC_PLUGINS_LOG_ERROR("Cannot create an image");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
   }
@@ -1188,7 +1243,7 @@
 
     if (image_ == NULL)
     {
-      LogError("Cannot create an image accessor");
+      ORTHANC_PLUGINS_LOG_ERROR("Cannot create an image accessor");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
   }
@@ -1202,7 +1257,7 @@
 
     if (image_ == NULL)
     {
-      LogError("Cannot uncompress a PNG image");
+      ORTHANC_PLUGINS_LOG_ERROR("Cannot uncompress a PNG image");
       ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
     }
   }
@@ -1215,7 +1270,7 @@
     image_ = OrthancPluginUncompressImage(GetGlobalContext(), data, size, OrthancPluginImageFormat_Jpeg);
     if (image_ == NULL)
     {
-      LogError("Cannot uncompress a JPEG image");
+      ORTHANC_PLUGINS_LOG_ERROR("Cannot uncompress a JPEG image");
       ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
     }
   }
@@ -1229,7 +1284,7 @@
     image_ = OrthancPluginDecodeDicomImage(GetGlobalContext(), data, size, frame);
     if (image_ == NULL)
     {
-      LogError("Cannot uncompress a DICOM image");
+      ORTHANC_PLUGINS_LOG_ERROR("Cannot uncompress a DICOM image");
       ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
     }
   }
@@ -1451,6 +1506,27 @@
   }
 
 
+  bool RestApiGet(Json::Value& result,
+                  const std::string& uri,
+                  const std::map<std::string, std::string>& httpHeaders,
+                  bool applyPlugins)
+  {
+    MemoryBuffer answer;
+
+    if (!answer.RestApiGet(uri, httpHeaders, applyPlugins))
+    {
+      return false;
+    }
+    else
+    {
+      if (!answer.IsEmpty())
+      {
+        answer.ToJson(result);
+      }
+      return true;
+    }
+  }
+
 
   bool RestApiGet(Json::Value& result,
                   const std::string& uri,
@@ -1622,13 +1698,13 @@
                                    unsigned int minor,
                                    unsigned int revision)
   {
-    LogError("Your version of the Orthanc core (" +
-             std::string(GetGlobalContext()->orthancVersion) +
-             ") is too old to run this plugin (version " +
-             boost::lexical_cast<std::string>(major) + "." +
-             boost::lexical_cast<std::string>(minor) + "." +
-             boost::lexical_cast<std::string>(revision) +
-             " is required)");
+    ORTHANC_PLUGINS_LOG_ERROR("Your version of the Orthanc core (" +
+                              std::string(GetGlobalContext()->orthancVersion) +
+                              ") is too old to run this plugin (version " +
+                              boost::lexical_cast<std::string>(major) + "." +
+                              boost::lexical_cast<std::string>(minor) + "." +
+                              boost::lexical_cast<std::string>(revision) +
+                              " is required)");
   }
 
   bool CheckMinimalVersion(const char* version,
@@ -1642,18 +1718,19 @@
       return true;
     }
 
-    // Parse the version
-    int aa, bb, cc;
-    if (
 #ifdef _MSC_VER
-      sscanf_s
+#define ORTHANC_SCANF sscanf_s
 #else
-      sscanf
+#define ORTHANC_SCANF sscanf
 #endif
-      (version, "%4d.%4d.%4d", &aa, &bb, &cc) != 3 ||
-      aa < 0 ||
-      bb < 0 ||
-      cc < 0)
+
+    // Parse the version
+    int aa, bb, cc = 0;
+    if ((ORTHANC_SCANF(version, "%4d.%4d.%4d", &aa, &bb, &cc) != 3 &&
+         ORTHANC_SCANF(version, "%4d.%4d", &aa, &bb) != 2) ||
+        aa < 0 ||
+        bb < 0 ||
+        cc < 0)
     {
       return false;
     }
@@ -1707,7 +1784,7 @@
   {
     if (!HasGlobalContext())
     {
-      LogError("Bad Orthanc context in the plugin");
+      ORTHANC_PLUGINS_LOG_ERROR("Bad Orthanc context in the plugin");
       return false;
     }
 
@@ -1744,7 +1821,7 @@
     }
     else
     {
-      LogError("Inexistent peer: " + name);
+      ORTHANC_PLUGINS_LOG_ERROR("Inexistent peer: " + name);
       ORTHANC_PLUGINS_THROW_EXCEPTION(UnknownResource);
     }
   }
@@ -1885,7 +1962,8 @@
 
   bool OrthancPeers::DoGet(MemoryBuffer& target,
                            size_t index,
-                           const std::string& uri) const
+                           const std::string& uri,
+                           const std::map<std::string, std::string>& headers) const
   {
     if (index >= index_.size())
     {
@@ -1894,10 +1972,12 @@
 
     OrthancPlugins::MemoryBuffer answer;
     uint16_t status;
+    PluginHttpHeaders pluginHeaders(headers);
+
     OrthancPluginErrorCode code = OrthancPluginCallPeerApi
       (GetGlobalContext(), *answer, NULL, &status, peers_,
        static_cast<uint32_t>(index), OrthancPluginHttpMethod_Get, uri.c_str(),
-       0, NULL, NULL, NULL, 0, timeout_);
+       pluginHeaders.GetSize(), pluginHeaders.GetKeys(), pluginHeaders.GetValues(), NULL, 0, timeout_);
 
     if (code == OrthancPluginErrorCode_Success)
     {
@@ -1913,21 +1993,23 @@
 
   bool OrthancPeers::DoGet(MemoryBuffer& target,
                            const std::string& name,
-                           const std::string& uri) const
+                           const std::string& uri,
+                           const std::map<std::string, std::string>& headers) const
   {
     size_t index;
     return (LookupName(index, name) &&
-            DoGet(target, index, uri));
+            DoGet(target, index, uri, headers));
   }
 
 
   bool OrthancPeers::DoGet(Json::Value& target,
                            size_t index,
-                           const std::string& uri) const
+                           const std::string& uri,
+                           const std::map<std::string, std::string>& headers) const
   {
     MemoryBuffer buffer;
 
-    if (DoGet(buffer, index, uri))
+    if (DoGet(buffer, index, uri, headers))
     {
       buffer.ToJson(target);
       return true;
@@ -1941,11 +2023,12 @@
 
   bool OrthancPeers::DoGet(Json::Value& target,
                            const std::string& name,
-                           const std::string& uri) const
+                           const std::string& uri,
+                           const std::map<std::string, std::string>& headers) const
   {
     MemoryBuffer buffer;
 
-    if (DoGet(buffer, name, uri))
+    if (DoGet(buffer, name, uri, headers))
     {
       buffer.ToJson(target);
       return true;
@@ -1960,22 +2043,24 @@
   bool OrthancPeers::DoPost(MemoryBuffer& target,
                             const std::string& name,
                             const std::string& uri,
-                            const std::string& body) const
+                            const std::string& body,
+                            const std::map<std::string, std::string>& headers) const
   {
     size_t index;
     return (LookupName(index, name) &&
-            DoPost(target, index, uri, body));
+            DoPost(target, index, uri, body, headers));
   }
 
 
   bool OrthancPeers::DoPost(Json::Value& target,
                             size_t index,
                             const std::string& uri,
-                            const std::string& body) const
+                            const std::string& body,
+                            const std::map<std::string, std::string>& headers) const
   {
     MemoryBuffer buffer;
 
-    if (DoPost(buffer, index, uri, body))
+    if (DoPost(buffer, index, uri, body, headers))
     {
       buffer.ToJson(target);
       return true;
@@ -1990,11 +2075,12 @@
   bool OrthancPeers::DoPost(Json::Value& target,
                             const std::string& name,
                             const std::string& uri,
-                            const std::string& body) const
+                            const std::string& body,
+                            const std::map<std::string, std::string>& headers) const
   {
     MemoryBuffer buffer;
 
-    if (DoPost(buffer, name, uri, body))
+    if (DoPost(buffer, name, uri, body, headers))
     {
       buffer.ToJson(target);
       return true;
@@ -2009,7 +2095,8 @@
   bool OrthancPeers::DoPost(MemoryBuffer& target,
                             size_t index,
                             const std::string& uri,
-                            const std::string& body) const
+                            const std::string& body,
+                            const std::map<std::string, std::string>& headers) const
   {
     if (index >= index_.size())
     {
@@ -2018,16 +2105,18 @@
 
     if (body.size() > 0xffffffffu)
     {
-      LogError("Cannot handle body size > 4GB");
+      ORTHANC_PLUGINS_LOG_ERROR("Cannot handle body size > 4GB");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
 
     OrthancPlugins::MemoryBuffer answer;
     uint16_t status;
+    PluginHttpHeaders pluginHeaders(headers);
+
     OrthancPluginErrorCode code = OrthancPluginCallPeerApi
       (GetGlobalContext(), *answer, NULL, &status, peers_,
        static_cast<uint32_t>(index), OrthancPluginHttpMethod_Post, uri.c_str(),
-       0, NULL, NULL, body.empty() ? NULL : body.c_str(), body.size(), timeout_);
+       pluginHeaders.GetSize(), pluginHeaders.GetKeys(), pluginHeaders.GetValues(), body.empty() ? NULL : body.c_str(), body.size(), timeout_);
 
     if (code == OrthancPluginErrorCode_Success)
     {
@@ -2043,7 +2132,8 @@
 
   bool OrthancPeers::DoPut(size_t index,
                            const std::string& uri,
-                           const std::string& body) const
+                           const std::string& body,
+                           const std::map<std::string, std::string>& headers) const
   {
     if (index >= index_.size())
     {
@@ -2052,16 +2142,18 @@
 
     if (body.size() > 0xffffffffu)
     {
-      LogError("Cannot handle body size > 4GB");
+      ORTHANC_PLUGINS_LOG_ERROR("Cannot handle body size > 4GB");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
 
     OrthancPlugins::MemoryBuffer answer;
     uint16_t status;
+    PluginHttpHeaders pluginHeaders(headers);
+
     OrthancPluginErrorCode code = OrthancPluginCallPeerApi
       (GetGlobalContext(), *answer, NULL, &status, peers_,
        static_cast<uint32_t>(index), OrthancPluginHttpMethod_Put, uri.c_str(),
-       0, NULL, NULL, body.empty() ? NULL : body.c_str(), body.size(), timeout_);
+       pluginHeaders.GetSize(), pluginHeaders.GetKeys(), pluginHeaders.GetValues(), body.empty() ? NULL : body.c_str(), body.size(), timeout_);
 
     if (code == OrthancPluginErrorCode_Success)
     {
@@ -2076,16 +2168,18 @@
 
   bool OrthancPeers::DoPut(const std::string& name,
                            const std::string& uri,
-                           const std::string& body) const
+                           const std::string& body,
+                           const std::map<std::string, std::string>& headers) const
   {
     size_t index;
     return (LookupName(index, name) &&
-            DoPut(index, uri, body));
+            DoPut(index, uri, body, headers));
   }
 
 
   bool OrthancPeers::DoDelete(size_t index,
-                              const std::string& uri) const
+                              const std::string& uri,
+                              const std::map<std::string, std::string>& headers) const
   {
     if (index >= index_.size())
     {
@@ -2094,10 +2188,12 @@
 
     OrthancPlugins::MemoryBuffer answer;
     uint16_t status;
+    PluginHttpHeaders pluginHeaders(headers);
+
     OrthancPluginErrorCode code = OrthancPluginCallPeerApi
       (GetGlobalContext(), *answer, NULL, &status, peers_,
        static_cast<uint32_t>(index), OrthancPluginHttpMethod_Delete, uri.c_str(),
-       0, NULL, NULL, NULL, 0, timeout_);
+       pluginHeaders.GetSize(), pluginHeaders.GetKeys(), pluginHeaders.GetValues(), NULL, 0, timeout_);
 
     if (code == OrthancPluginErrorCode_Success)
     {
@@ -2111,11 +2207,12 @@
 
 
   bool OrthancPeers::DoDelete(const std::string& name,
-                              const std::string& uri) const
+                              const std::string& uri,
+                              const std::map<std::string, std::string>& headers) const
   {
     size_t index;
     return (LookupName(index, name) &&
-            DoDelete(index, uri));
+            DoDelete(index, uri, headers));
   }
 #endif
 
@@ -2152,6 +2249,36 @@
   }
 
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 11, 3)
+  static OrthancPluginErrorCode CopyStringToMemoryBuffer(OrthancPluginMemoryBuffer* target,
+                                                         const std::string& source)
+  {
+    if (OrthancPluginCreateMemoryBuffer(globalContext_, target, source.size()) != OrthancPluginErrorCode_Success)
+    {
+      return OrthancPluginErrorCode_NotEnoughMemory;
+    }
+    else
+    {
+      if (!source.empty())
+      {
+        memcpy(target->data, source.c_str(), source.size());
+      }
+      
+      return OrthancPluginErrorCode_Success;
+    }
+  }
+#endif
+
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 11, 3)
+  OrthancPluginErrorCode OrthancJob::CallbackGetContent(OrthancPluginMemoryBuffer* target,
+                                                        void* job)
+  {
+    assert(job != NULL);
+    OrthancJob& that = *reinterpret_cast<OrthancJob*>(job);
+    return CopyStringToMemoryBuffer(target, that.content_);
+  }
+#else
   const char* OrthancJob::CallbackGetContent(void* job)
   {
     assert(job != NULL);
@@ -2165,8 +2292,33 @@
       return 0;
     }
   }
-
-
+#endif
+
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 11, 3)
+  int32_t OrthancJob::CallbackGetSerialized(OrthancPluginMemoryBuffer* target,
+                                            void* job)
+  {
+    assert(job != NULL);
+    OrthancJob& that = *reinterpret_cast<OrthancJob*>(job);
+    
+    if (that.hasSerialized_)
+    {
+      if (CopyStringToMemoryBuffer(target, that.serialized_) == OrthancPluginErrorCode_Success)
+      {
+        return 1;
+      }
+      else
+      {
+        return -1;
+      }
+    }
+    else
+    {
+      return 0;
+    }
+  }
+#else
   const char* OrthancJob::CallbackGetSerialized(void* job)
   {
     assert(job != NULL);
@@ -2189,6 +2341,7 @@
       return 0;
     }
   }
+#endif
 
 
   OrthancPluginJobStepStatus OrthancJob::CallbackStep(void* job)
@@ -2320,10 +2473,15 @@
       ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_NullPointer);
     }
 
-    OrthancPluginJob* orthanc = OrthancPluginCreateJob(
-      GetGlobalContext(), job, CallbackFinalize, job->jobType_.c_str(),
-      CallbackGetProgress, CallbackGetContent, CallbackGetSerialized,
-      CallbackStep, CallbackStop, CallbackReset);
+    OrthancPluginJob* orthanc =
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 11, 3)
+      OrthancPluginCreateJob2
+#else
+      OrthancPluginCreateJob
+#endif
+      (GetGlobalContext(), job, CallbackFinalize, job->jobType_.c_str(),
+       CallbackGetProgress, CallbackGetContent, CallbackGetSerialized,
+       CallbackStep, CallbackStop, CallbackReset);
 
     if (orthanc == NULL)
     {
@@ -2350,7 +2508,7 @@
 
     if (id == NULL)
     {
-      LogError("Plugin cannot submit job");
+      ORTHANC_PLUGINS_LOG_ERROR("Plugin cannot submit job");
       OrthancPluginFreeJob(GetGlobalContext(), orthanc);
       ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
     }
@@ -2419,7 +2577,7 @@
           throw Orthanc::OrthancException(static_cast<Orthanc::ErrorCode>(status["ErrorCode"].asInt()),
                                           status["ErrorDescription"].asString());
 #else
-          LogError("Exception while executing the job: " + status["ErrorDescription"].asString());
+          ORTHANC_PLUGINS_LOG_ERROR("Exception while executing the job: " + status["ErrorDescription"].asString());
           ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(status["ErrorCode"].asInt());          
 #endif
         }
@@ -2444,7 +2602,7 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
                                       "Expected a JSON object in the body");
 #else
-      LogError("Expected a JSON object in the body");
+      ORTHANC_PLUGINS_LOG_ERROR("Expected a JSON object in the body");
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
 #endif
     }
@@ -2460,7 +2618,7 @@
                                         "Option \"" + std::string(KEY_SYNCHRONOUS) +
                                         "\" must be Boolean");
 #else
-        LogError("Option \"" + std::string(KEY_SYNCHRONOUS) + "\" must be Boolean");
+        ORTHANC_PLUGINS_LOG_ERROR("Option \"" + std::string(KEY_SYNCHRONOUS) + "\" must be Boolean");
         ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
 #endif
       }
@@ -2479,7 +2637,7 @@
                                         "Option \"" + std::string(KEY_ASYNCHRONOUS) +
                                         "\" must be Boolean");
 #else
-        LogError("Option \"" + std::string(KEY_ASYNCHRONOUS) + "\" must be Boolean");
+        ORTHANC_PLUGINS_LOG_ERROR("Option \"" + std::string(KEY_ASYNCHRONOUS) + "\" must be Boolean");
         ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
 #endif
       }
@@ -2493,14 +2651,14 @@
 
     if (body.isMember(KEY_PRIORITY))
     {
-      if (body[KEY_PRIORITY].type() != Json::booleanValue)
+      if (body[KEY_PRIORITY].type() != Json::intValue)
       {
 #if HAS_ORTHANC_EXCEPTION == 1
         throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
                                         "Option \"" + std::string(KEY_PRIORITY) +
                                         "\" must be an integer");
 #else
-        LogError("Option \"" + std::string(KEY_PRIORITY) + "\" must be an integer");
+        ORTHANC_PLUGINS_LOG_ERROR("Option \"" + std::string(KEY_PRIORITY) + "\" must be an integer");
         ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
 #endif
       }
@@ -3021,7 +3179,7 @@
 
     if (body.size() > 0xffffffffu)
     {
-      LogError("Cannot handle body size > 4GB");
+      ORTHANC_PLUGINS_LOG_ERROR("Cannot handle body size > 4GB");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
 
@@ -3166,7 +3324,7 @@
     
     if (!ReadJson(answerBody, body))
     {
-      LogError("Cannot convert HTTP answer body to JSON");
+      ORTHANC_PLUGINS_LOG_ERROR("Cannot convert HTTP answer body to JSON");
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
   }
@@ -3643,6 +3801,27 @@
 #endif
 
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 1)
+  DicomInstance* DicomInstance::Load(const std::string& instanceId,
+                                     OrthancPluginLoadDicomInstanceMode mode)
+  {
+    OrthancPluginDicomInstance* instance = OrthancPluginLoadDicomInstance(
+      GetGlobalContext(), instanceId.c_str(), mode);
+
+    if (instance == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin);
+    }
+    else
+    {
+      boost::movelib::unique_ptr<DicomInstance> result(new DicomInstance(instance));
+      result->toFree_ = true;
+      return result.release();
+    }
+  }
+#endif
+
+
 #if HAS_ORTHANC_PLUGIN_WEBDAV == 1
   static std::vector<std::string> WebDavConvertPath(uint32_t pathSize,
                                                     const char* const*  pathItems)
@@ -3891,4 +4070,48 @@
       result[request->headersKeys[i]] = request->headersValues[i];
     }    
   }
+
+#if !ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 4)
+  static void SetPluginProperty(const std::string& pluginIdentifier,
+                                _OrthancPluginProperty property,
+                                const std::string& value)
+  {
+    _OrthancPluginSetPluginProperty params;
+    params.plugin = pluginIdentifier.c_str();
+    params.property = property;
+    params.value = value.c_str();
+
+    GetGlobalContext()->InvokeService(GetGlobalContext(), _OrthancPluginService_SetPluginProperty, &params);
+  }
+#endif
+
+  void SetRootUri(const std::string& pluginIdentifier,
+                  const std::string& uri)
+  {
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 4)
+    OrthancPluginSetRootUri2(GetGlobalContext(), pluginIdentifier.c_str(), uri.c_str());
+#else
+    SetPluginProperty(pluginIdentifier, _OrthancPluginProperty_RootUri, uri);
+#endif
+  }
+
+  void SetDescription(const std::string& pluginIdentifier,
+                      const std::string& description)
+  {
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 4)
+    OrthancPluginSetDescription2(GetGlobalContext(), pluginIdentifier.c_str(), description.c_str());
+#else
+    SetPluginProperty(pluginIdentifier, _OrthancPluginProperty_Description, description);
+#endif
+  }
+
+  void ExtendOrthancExplorer(const std::string& pluginIdentifier,
+                             const std::string& javascript)
+  {
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 4)
+    OrthancPluginExtendOrthancExplorer2(GetGlobalContext(), pluginIdentifier.c_str(), javascript.c_str());
+#else
+    SetPluginProperty(pluginIdentifier, _OrthancPluginProperty_OrthancExplorer, javascript);
+#endif
+  }
 }
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -127,6 +128,44 @@
 #  define HAS_ORTHANC_PLUGIN_WEBDAV  0
 #endif
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 4)
+#  define HAS_ORTHANC_PLUGIN_LOG_MESSAGE  1
+#else
+#  define HAS_ORTHANC_PLUGIN_LOG_MESSAGE  0
+#endif
+
+
+// Macro to tag a function as having been deprecated
+#if (__cplusplus >= 201402L)  // C++14
+#  define ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED(f) [[deprecated]] f
+#elif defined(__GNUC__) || defined(__clang__)
+#  define ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED(f) f __attribute__((deprecated))
+#elif defined(_MSC_VER)
+#  define ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED(f) __declspec(deprecated) f
+#else
+#  define ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED
+#endif
+
+
+#if !defined(__ORTHANC_FILE__)
+#  if defined(_MSC_VER)
+#    pragma message("Warning: Macro __ORTHANC_FILE__ is not defined, this will leak the full path of the source files in the binaries")
+#  else
+#    warning Warning: Macro __ORTHANC_FILE__ is not defined, this will leak the full path of the source files in the binaries
+#  endif
+#  define __ORTHANC_FILE__ __FILE__
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1
+#  define ORTHANC_PLUGINS_LOG_ERROR(msg)   ::OrthancPlugins::LogMessage(OrthancPluginLogLevel_Error, __ORTHANC_FILE__, __LINE__, msg)
+#  define ORTHANC_PLUGINS_LOG_WARNING(msg) ::OrthancPlugins::LogMessage(OrthancPluginLogLevel_Warning, __ORTHANC_FILE__, __LINE__, msg)
+#  define ORTHANC_PLUGINS_LOG_INFO(msg)    ::OrthancPlugins::LogMessage(OrthancPluginLogLevel_Info, __ORTHANC_FILE__, __LINE__, msg)
+#else
+#  define ORTHANC_PLUGINS_LOG_ERROR(msg)   ::OrthancPlugins::LogError(msg)
+#  define ORTHANC_PLUGINS_LOG_WARNING(msg) ::OrthancPlugins::LogWarning(msg)
+#  define ORTHANC_PLUGINS_LOG_INFO(msg)    ::OrthancPlugins::LogInfo(msg)
+#endif
 
 
 namespace OrthancPlugins
@@ -137,6 +176,11 @@
 
   void SetGlobalContext(OrthancPluginContext* context);
 
+  void SetGlobalContext(OrthancPluginContext* context,
+                        const char* pluginName);
+
+  void ResetGlobalContext();
+
   bool HasGlobalContext();
 
   OrthancPluginContext* GetGlobalContext();
@@ -346,10 +390,12 @@
     void LoadConfiguration();
     
   public:
-    OrthancConfiguration();
+    OrthancConfiguration(); // loads the full Orthanc configuration
 
     explicit OrthancConfiguration(bool load);
 
+    explicit OrthancConfiguration(const Json::Value& configuration, const std::string& path);  // e.g. to load a section from a default json content
+
     const Json::Value& GetJson() const
     {
       return configuration_;
@@ -533,6 +579,11 @@
                   const std::string& uri,
                   bool applyPlugins);
 
+  bool RestApiGet(Json::Value& result,
+                  const std::string& uri,
+                  const std::map<std::string, std::string>& httpHeaders,
+                  bool applyPlugins);
+
   bool RestApiGetString(std::string& result,
                         const std::string& uri,
                         bool applyPlugins);
@@ -628,11 +679,33 @@
   const char* AutodetectMimeType(const std::string& path);
 #endif
 
+#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1
+  void LogMessage(OrthancPluginLogLevel level,
+                  const char* file,
+                  uint32_t line,
+                  const std::string& message);
+#endif
+
+#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1
+  // Use macro ORTHANC_PLUGINS_LOG_ERROR() instead
+  ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED(void LogError(const std::string& message));
+#else
   void LogError(const std::string& message);
+#endif
 
+#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1
+  // Use macro ORTHANC_PLUGINS_LOG_WARNING() instead
+  ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED(void LogWarning(const std::string& message));
+#else
   void LogWarning(const std::string& message);
+#endif
 
+#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1
+  // Use macro ORTHANC_PLUGINS_LOG_INFO() instead
+  ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED(void LogInfo(const std::string& message));
+#else
   void LogInfo(const std::string& message);
+#endif
 
   void ReportMinimalOrthancVersion(unsigned int major,
                                    unsigned int minor,
@@ -755,53 +828,65 @@
 
     bool DoGet(MemoryBuffer& target,
                size_t index,
-               const std::string& uri) const;
+               const std::string& uri,
+               const std::map<std::string, std::string>& headers) const;
 
     bool DoGet(MemoryBuffer& target,
                const std::string& name,
-               const std::string& uri) const;
+               const std::string& uri,
+               const std::map<std::string, std::string>& headers) const;
 
     bool DoGet(Json::Value& target,
                size_t index,
-               const std::string& uri) const;
+               const std::string& uri,
+               const std::map<std::string, std::string>& headers) const;
 
     bool DoGet(Json::Value& target,
                const std::string& name,
-               const std::string& uri) const;
+               const std::string& uri,
+               const std::map<std::string, std::string>& headers) const;
 
     bool DoPost(MemoryBuffer& target,
                 size_t index,
                 const std::string& uri,
-                const std::string& body) const;
+                const std::string& body,
+                const std::map<std::string, std::string>& headers) const;
 
     bool DoPost(MemoryBuffer& target,
                 const std::string& name,
                 const std::string& uri,
-                const std::string& body) const;
+                const std::string& body,
+                const std::map<std::string, std::string>& headers) const;
 
     bool DoPost(Json::Value& target,
                 size_t index,
                 const std::string& uri,
-                const std::string& body) const;
+                const std::string& body,
+                const std::map<std::string, std::string>& headers) const;
 
     bool DoPost(Json::Value& target,
                 const std::string& name,
                 const std::string& uri,
-                const std::string& body) const;
+                const std::string& body,
+                const std::map<std::string, std::string>& headers) const;
 
     bool DoPut(size_t index,
                const std::string& uri,
-               const std::string& body) const;
+               const std::string& body,
+               const std::map<std::string, std::string>& headers) const;
 
     bool DoPut(const std::string& name,
                const std::string& uri,
-               const std::string& body) const;
+               const std::string& body,
+               const std::map<std::string, std::string>& headers) const;
 
     bool DoDelete(size_t index,
-                  const std::string& uri) const;
+                  const std::string& uri,
+                  const std::map<std::string, std::string>& headers) const;
 
     bool DoDelete(const std::string& name,
-                  const std::string& uri) const;
+                  const std::string& uri,
+                  const std::map<std::string, std::string>& headers) const;
   };
 #endif
 
@@ -821,9 +906,19 @@
 
     static float CallbackGetProgress(void* job);
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 11, 3)
+    static OrthancPluginErrorCode CallbackGetContent(OrthancPluginMemoryBuffer* target,
+                                                     void* job);
+#else
     static const char* CallbackGetContent(void* job);
+#endif
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 11, 3)
+    static int32_t CallbackGetSerialized(OrthancPluginMemoryBuffer* target,
+                                         void* job);
+#else
     static const char* CallbackGetSerialized(void* job);
+#endif
 
     static OrthancPluginJobStepStatus CallbackStep(void* job);
 
@@ -1235,6 +1330,11 @@
 
     ~DicomInstance();
 
+    const OrthancPluginDicomInstance* GetObject() const
+    {
+      return instance_;
+    }
+
     std::string GetRemoteAet() const;
 
     const void* GetBuffer() const
@@ -1289,6 +1389,11 @@
                                     size_t size,
                                     const std::string& transferSyntax);
 #endif
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 1)
+    static DicomInstance* Load(const std::string& instanceId,
+                               OrthancPluginLoadDicomInstanceMode mode);
+#endif
   };
 
 // helper method to convert Http headers from the plugin SDK to a std::map
@@ -1394,4 +1499,13 @@
                          IWebDavCollection& collection);
   };
 #endif
+
+  void SetRootUri(const std::string& pluginIdentifier,
+                  const std::string& uri);
+
+  void SetDescription(const std::string& pluginIdentifier,
+                      const std::string& description);
+
+  void ExtendOrthancExplorer(const std::string& pluginIdentifier,
+                             const std::string& javascript);
 }
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginException.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginException.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Plugins/Samples/Common/OrthancPlugins.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPlugins.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginsExports.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginsExports.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancServer/Plugins/Samples/ConnectivityChecks/CMakeLists.txt	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/ConnectivityChecks/CMakeLists.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -50,8 +51,9 @@
 
 EmbedResources(
   --framework-path=${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources
+  --target=ConnectivityChecksResources
   WEB_RESOURCES  ${CMAKE_CURRENT_SOURCE_DIR}/WebResources
-  LIBRARIES      ${JAVASCRIPT_LIBS_DIR}
+  LIBRARIES      ${CONNECTIVITY_CHECKS_JAVASCRIPT_DIR}
   )
 
 add_definitions(
@@ -76,6 +78,8 @@
   Plugin.cpp
   )
 
+DefineSourceBasenameForTarget(ConnectivityChecks)
+
 set_target_properties(
   ConnectivityChecks PROPERTIES 
   VERSION ${PLUGIN_VERSION} 
--- a/OrthancServer/Plugins/Samples/ConnectivityChecks/JavaScriptLibraries.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/ConnectivityChecks/JavaScriptLibraries.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -18,7 +19,7 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-set(BASE_URL "http://orthanc.osimis.io/ThirdPartyDownloads")
+set(BASE_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads")
 
 DownloadPackage(
   "da0189f7c33bf9f652ea65401e0a3dc9"
@@ -40,8 +41,8 @@
   "${BASE_URL}/jquery-3.4.1.min.js")
 
 
-set(JAVASCRIPT_LIBS_DIR  ${CMAKE_CURRENT_BINARY_DIR}/javascript-libs)
-file(MAKE_DIRECTORY ${JAVASCRIPT_LIBS_DIR})
+set(CONNECTIVITY_CHECKS_JAVASCRIPT_DIR  ${CMAKE_CURRENT_BINARY_DIR}/connectivity-checks-javascript)
+file(MAKE_DIRECTORY ${CONNECTIVITY_CHECKS_JAVASCRIPT_DIR})
 
 file(COPY
   ${CMAKE_CURRENT_BINARY_DIR}/axios-0.19.0/dist/axios.min.js
@@ -51,12 +52,12 @@
   ${CMAKE_CURRENT_BINARY_DIR}/vue-2.6.10/dist/vue.min.js
   ${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/jquery-3.4.1.min.js
   DESTINATION
-  ${JAVASCRIPT_LIBS_DIR}/js
+  ${CONNECTIVITY_CHECKS_JAVASCRIPT_DIR}/js
   )
 
 file(COPY
   ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-4.3.1/dist/css/bootstrap.min.css
   ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-4.3.1/dist/css/bootstrap.min.css.map
   DESTINATION
-  ${JAVASCRIPT_LIBS_DIR}/css
+  ${CONNECTIVITY_CHECKS_JAVASCRIPT_DIR}/css
   )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/ConnectivityChecks/OrthancFrameworkDependencies.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,37 @@
+/**
+ * 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/>.
+ **/
+
+
+/**
+ * Remove the dependency upon ICU in plugins, as this greatly increase
+ * the size of the resulting binaries, since they must embed the ICU
+ * dictionary.
+ **/
+
+#define ORTHANC_ENABLE_ICU 0
+
+#include "../../../../OrthancFramework/Sources/ChunkedBuffer.cpp"
+#include "../../../../OrthancFramework/Sources/Enumerations.cpp"
+#include "../../../../OrthancFramework/Sources/Logging.cpp"
+#include "../../../../OrthancFramework/Sources/OrthancException.cpp"
+#include "../../../../OrthancFramework/Sources/SystemToolbox.cpp"
+#include "../../../../OrthancFramework/Sources/Toolbox.cpp"
--- a/OrthancServer/Plugins/Samples/ConnectivityChecks/Plugin.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/ConnectivityChecks/Plugin.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -20,7 +21,7 @@
  **/
 
 
-#include <EmbeddedResources.h>
+#include <ConnectivityChecksResources.h>
 #include <orthanc/OrthancCPlugin.h>
 
 #include "../../../../OrthancFramework/Sources/OrthancException.h"
@@ -86,8 +87,8 @@
     }
 
     /* Register the callbacks */
-    OrthancPluginSetDescription(context_, "Utilities to check connectivity to DICOM modalities, DICOMweb servers and Orthanc peers.");
-    OrthancPluginSetRootUri(context_, ROOT_URI "/app/index.html");
+    OrthancPluginSetDescription2(context_, ORTHANC_PLUGIN_NAME, "Utilities to check connectivity to DICOM modalities, DICOMweb servers and Orthanc peers.");
+    OrthancPluginSetRootUri2(context_, ORTHANC_PLUGIN_NAME, ROOT_URI "/app/index.html");
     OrthancPluginRegisterRestCallback(context_, ROOT_URI "/libs/(.*)", ServeStaticResource<Orthanc::EmbeddedResources::LIBRARIES>);
     OrthancPluginRegisterRestCallback(context_, ROOT_URI "/app/(.*)", ServeStaticResource<Orthanc::EmbeddedResources::WEB_RESOURCES>);
  
--- a/OrthancServer/Plugins/Samples/ConnectivityChecks/WebResources/app.js	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/ConnectivityChecks/WebResources/app.js	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Plugins/Samples/CustomImageDecoder/CMakeLists.txt	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/CustomImageDecoder/CMakeLists.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -25,3 +26,5 @@
 include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake)
 
 add_library(PluginTest SHARED Plugin.cpp)
+
+DefineSourceBasenameForTarget(PluginTest)
--- a/OrthancServer/Plugins/Samples/CustomImageDecoder/Plugin.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/CustomImageDecoder/Plugin.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/CMakeLists.txt	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/DelayedDeletion/CMakeLists.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -78,6 +79,8 @@
   Plugin.cpp
   )
 
+DefineSourceBasenameForTarget(DelayedDeletion)
+
 set_target_properties(
   DelayedDeletion PROPERTIES 
   VERSION ${PLUGIN_VERSION} 
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/LargeDeleteJob.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/DelayedDeletion/LargeDeleteJob.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -1,3 +1,26 @@
+/**
+ * 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 "LargeDeleteJob.h"
 
 #include "../../../../OrthancFramework/Sources/Logging.h"
@@ -146,6 +169,7 @@
   levels_(levels),
   posResources_(0),
   posInstances_(0),
+  posSeries_(0),
   posDelete_(0)
 {
   if (resources.size() != levels.size())
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/LargeDeleteJob.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/DelayedDeletion/LargeDeleteJob.h	Tue Sep 24 11:39:52 2024 +0200
@@ -1,3 +1,26 @@
+/**
+ * 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 "../Common/OrthancPluginCppWrapper.h"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/DelayedDeletion/OrthancFrameworkDependencies.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,49 @@
+/**
+ * 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/>.
+ **/
+
+
+/**
+ * Remove the dependency upon ICU in plugins, as this greatly increase
+ * the size of the resulting binaries, since they must embed the ICU
+ * dictionary.
+ **/
+
+#define ORTHANC_ENABLE_ICU 0
+
+#include "../../../../OrthancFramework/Sources/ChunkedBuffer.cpp"
+#include "../../../../OrthancFramework/Sources/Compression/DeflateBaseCompressor.cpp"
+#include "../../../../OrthancFramework/Sources/Compression/GzipCompressor.cpp"
+#include "../../../../OrthancFramework/Sources/DicomFormat/DicomTag.cpp"
+#include "../../../../OrthancFramework/Sources/Enumerations.cpp"
+#include "../../../../OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp"
+#include "../../../../OrthancFramework/Sources/Logging.cpp"
+#include "../../../../OrthancFramework/Sources/MultiThreading/SharedMessageQueue.cpp"
+#include "../../../../OrthancFramework/Sources/OrthancException.cpp"
+#include "../../../../OrthancFramework/Sources/SQLite/Connection.cpp"
+#include "../../../../OrthancFramework/Sources/SQLite/FunctionContext.cpp"
+#include "../../../../OrthancFramework/Sources/SQLite/Statement.cpp"
+#include "../../../../OrthancFramework/Sources/SQLite/StatementId.cpp"
+#include "../../../../OrthancFramework/Sources/SQLite/StatementReference.cpp"
+#include "../../../../OrthancFramework/Sources/SQLite/Transaction.cpp"
+#include "../../../../OrthancFramework/Sources/StringMemoryBuffer.cpp"
+#include "../../../../OrthancFramework/Sources/SystemToolbox.cpp"
+#include "../../../../OrthancFramework/Sources/Toolbox.cpp"
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/PendingDeletionsDatabase.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/DelayedDeletion/PendingDeletionsDatabase.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -1,3 +1,26 @@
+/**
+ * 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 "PendingDeletionsDatabase.h"
 
 #include "../../../../OrthancFramework/Sources/SQLite/Statement.h"
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/PendingDeletionsDatabase.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/DelayedDeletion/PendingDeletionsDatabase.h	Tue Sep 24 11:39:52 2024 +0200
@@ -1,3 +1,26 @@
+/**
+ * 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
 
 
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/Plugin.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/DelayedDeletion/Plugin.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -1,10 +1,32 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-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 "PendingDeletionsDatabase.h"
 
 #include "../../../../OrthancFramework/Sources/FileStorage/FilesystemStorage.h"
 #include "../../../../OrthancFramework/Sources/Logging.h"
 #include "../../../../OrthancFramework/Sources/MultiThreading/SharedMessageQueue.h"
-#include "../../../../OrthancServer/Plugins/Engine/PluginsEnumerations.h"
-#include "../../../../OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h"
+#include "../Common/OrthancPluginCppWrapper.h"
 
 #include <boost/thread.hpp>
 
@@ -45,6 +67,25 @@
 static unsigned int                                 throttleDelayMs_ = 0;
 
 
+static Orthanc::FileContentType Convert(OrthancPluginContentType type)
+{
+  switch (type)
+  {
+    case OrthancPluginContentType_Dicom:
+      return Orthanc::FileContentType_Dicom;
+
+    case OrthancPluginContentType_DicomAsJson:
+      return Orthanc::FileContentType_DicomAsJson;
+
+    case OrthancPluginContentType_DicomUntilPixelData:
+      return Orthanc::FileContentType_DicomUntilPixelData;
+
+    default:
+      return Orthanc::FileContentType_Unknown;
+  }
+}
+
+
 static OrthancPluginErrorCode StorageCreate(const char* uuid,
                                             const void* content,
                                             int64_t size,
@@ -52,7 +93,7 @@
 {
   try
   {
-    storage_->Create(uuid, content, size, Orthanc::Plugins::Convert(type));
+    storage_->Create(uuid, content, size, Convert(type));
     return OrthancPluginErrorCode_Success;
   }
   catch (Orthanc::OrthancException& e)
@@ -72,12 +113,12 @@
 {
   try
   {
-    std::unique_ptr<Orthanc::IMemoryBuffer> buffer(storage_->Read(uuid, Orthanc::Plugins::Convert(type)));
+    std::unique_ptr<Orthanc::IMemoryBuffer> buffer(storage_->Read(uuid, Convert(type)));
 
     // copy from a buffer allocated on plugin's heap into a buffer allocated on core's heap
     if (OrthancPluginCreateMemoryBuffer64(OrthancPlugins::GetGlobalContext(), target, buffer->GetSize()) != OrthancPluginErrorCode_Success)
     {
-      OrthancPlugins::LogError("Delayed deletion plugin: error while reading object " + std::string(uuid) + ", cannot allocate memory of size " + boost::lexical_cast<std::string>(buffer->GetSize()) + " bytes");
+      LOG(ERROR) << "Delayed deletion plugin: error while reading object " << uuid << ", cannot allocate memory of size " << buffer->GetSize() << " bytes";
       return OrthancPluginErrorCode_StorageAreaPlugin;
     }
 
@@ -103,7 +144,7 @@
 {
   try
   {
-    std::unique_ptr<Orthanc::IMemoryBuffer> buffer(storage_->ReadRange(uuid, Orthanc::Plugins::Convert(type), rangeStart, rangeStart + target->size));
+    std::unique_ptr<Orthanc::IMemoryBuffer> buffer(storage_->ReadRange(uuid, Convert(type), rangeStart, rangeStart + target->size));
 
     assert(buffer->GetSize() == target->size);
 
@@ -130,7 +171,7 @@
   try
   {
     LOG(INFO) << "DelayedDeletion - Scheduling delayed deletion of " << uuid;
-    db_->Enqueue(uuid, Orthanc::Plugins::Convert(type));
+    db_->Enqueue(uuid, Convert(type));
     
     return OrthancPluginErrorCode_Success;
   }
@@ -146,6 +187,8 @@
 
 static void DeletionWorker()
 {
+  OrthancPluginSetCurrentThreadName(OrthancPlugins::GetGlobalContext(), "DELETION");
+
   static const unsigned int GRANULARITY = 100;  // In milliseconds
 
   while (continue_)
@@ -247,8 +290,8 @@
 {
   ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
   {
-    OrthancPlugins::SetGlobalContext(context);
-    Orthanc::Logging::InitializePluginContext(context);
+    OrthancPlugins::SetGlobalContext(context, ORTHANC_PLUGIN_NAME);
+    Orthanc::Logging::InitializePluginContext(context, ORTHANC_PLUGIN_NAME);
     
 
     /* Check the version of the Orthanc core */
@@ -264,7 +307,7 @@
       return -1;
     }
 
-    OrthancPluginSetDescription(context, "Plugin removing files from storage asynchronously.");
+    OrthancPluginSetDescription2(context, ORTHANC_PLUGIN_NAME, "Plugin removing files from storage asynchronously.");
 
     OrthancPlugins::OrthancConfiguration orthancConfig;
 
--- a/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -20,8 +21,10 @@
  **/
 
 
+#define HOUSEKEEPER_NAME "housekeeper"
+
+#include "../Common/OrthancPluginCppWrapper.h"
 #include "../../../../OrthancFramework/Sources/Compatibility.h"
-#include "../Common/OrthancPluginCppWrapper.h"
 
 #include <boost/thread.hpp>
 #include <boost/algorithm/string.hpp>
@@ -45,6 +48,10 @@
 static bool triggerOnMainDicomTagsChange_ = true;
 static bool triggerOnUnnecessaryDicomAsJsonFiles_ = true;
 static bool triggerOnIngestTranscodingChange_ = true;
+static bool triggerOnDicomWebCacheChange_ = true;
+static std::string limitMainDicomTagsReconstructLevel_ = "";
+static std::string limitToChange_ = "";
+static std::string limitToUrl_ = "";
 
 
 struct RunningPeriod
@@ -85,7 +92,7 @@
     }
     else
     {
-      OrthancPlugins::LogWarning("Housekeeper: invalid schedule: unknown 'day': " + weekday);      
+      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: invalid schedule: unknown 'day': " + weekday);
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
 
@@ -127,7 +134,7 @@
     Json::Value::Members names = scheduleConfiguration.getMemberNames();
 
     for (Json::Value::Members::const_iterator it = names.begin();
-      it != names.end(); it++)
+      it != names.end(); ++it)
     {
       for (Json::Value::ArrayIndex i = 0; i < scheduleConfiguration[*it].size(); i++)
       {
@@ -144,7 +151,7 @@
     }
 
     for (std::list<RunningPeriod>::const_iterator it = runningPeriods_.begin();
-      it != runningPeriods_.end(); it++)
+      it != runningPeriods_.end(); ++it)
     {
       if (it->isInPeriod())
       {
@@ -166,6 +173,7 @@
   std::string seriesMainDicomTagsSignature;
   std::string instancesMainDicomTagsSignature;
   std::string ingestTranscoding;
+  std::string dicomWebVersion;
   bool storageCompressionEnabled;
 
   DbConfiguration()
@@ -186,6 +194,7 @@
     seriesMainDicomTagsSignature.clear();
     instancesMainDicomTagsSignature.clear();
     ingestTranscoding.clear();
+    dicomWebVersion.clear();
   }
 
   void ToJson(Json::Value& target)
@@ -210,6 +219,7 @@
       target["OrthancVersion"] = orthancVersion;
       target["StorageCompressionEnabled"] = storageCompressionEnabled;
       target["IngestTranscoding"] = ingestTranscoding;
+      target["DicomWebVersion"] = dicomWebVersion;
     }
   }
 
@@ -218,6 +228,14 @@
     if (!source.isNull())
     {
       orthancVersion = source["OrthancVersion"].asString();
+      if (source.isMember("DicomWebVersion"))
+      {
+        dicomWebVersion = source["DicomWebVersion"].asString();
+      }
+      else
+      {
+        dicomWebVersion = "1.14"; // the first change that requires processing has been introduced between 1.14 & 1.15
+      }
 
       const Json::Value& signatures = source["MainDicomTagsSignature"];
       patientsMainDicomTagsSignature = signatures["Patient"].asString();
@@ -321,6 +339,7 @@
     pluginStatus_.lastTimeStarted = boost::date_time::not_a_date_time;
     
     pluginStatus_.lastProcessedConfiguration.orthancVersion = "1.9.0"; // when we don't know, we assume some files were stored with Orthanc 1.9.0 (last version saving the dicom-as-json files)
+    pluginStatus_.lastProcessedConfiguration.dicomWebVersion = "1.14"; // the first change that requires processing has been introduced between 1.14 & 1.15
 
     // default main dicom tags signature are the one from Orthanc 1.4.2 (last time the list was changed):
     pluginStatus_.lastProcessedConfiguration.patientsMainDicomTagsSignature = "0010,0010;0010,0020;0010,0030;0010,0040;0010,1000";
@@ -360,12 +379,19 @@
   configuration.ingestTranscoding = systemInfo["IngestTranscoding"].asString();
 
   configuration.orthancVersion = OrthancPlugins::GetGlobalContext()->orthancVersion;
+
+  Json::Value pluginInfo;
+  if (OrthancPlugins::RestApiGet(pluginInfo, "/plugins/dicom-web", false))
+  {
+    configuration.dicomWebVersion = pluginInfo["Version"].asString();
+  }
 }
 
-static void CheckNeedsProcessing(bool& needsReconstruct, bool& needsReingest, const DbConfiguration& current, const DbConfiguration& last)
+static void CheckNeedsProcessing(bool& needsReconstruct, bool& needsReingest, bool& needsDicomWebCaching, const DbConfiguration& current, const DbConfiguration& last)
 {
   needsReconstruct = false;
   needsReingest = false;
+  needsDicomWebCaching = false;
 
   if (!last.IsDefined())
   {
@@ -378,12 +404,12 @@
   {
     if (triggerOnUnnecessaryDicomAsJsonFiles_)
     {
-      OrthancPlugins::LogWarning("Housekeeper: your storage might still contain some dicom-as-json files -> will perform housekeeping");
+      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: your storage might still contain some dicom-as-json files -> will perform housekeeping");
       needsReconstruct = true;  // the default reconstruct removes the dicom-as-json
     }
     else
     {
-      OrthancPlugins::LogWarning("Housekeeper: your storage might still contain some dicom-as-json files but the trigger has been disabled");
+      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: your storage might still contain some dicom-as-json files but the trigger has been disabled");
     }
   }
 
@@ -391,12 +417,12 @@
   {
     if (triggerOnMainDicomTagsChange_)
     {
-      OrthancPlugins::LogWarning("Housekeeper: Patient main dicom tags have changed, -> will perform housekeeping");
+      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: Patient main dicom tags have changed, -> will perform housekeeping");
       needsReconstruct = true;
     }
     else
     {
-      OrthancPlugins::LogWarning("Housekeeper: Patient main dicom tags have changed but the trigger is disabled");
+      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: Patient main dicom tags have changed but the trigger is disabled");
     }
   }
 
@@ -404,12 +430,12 @@
   {
     if (triggerOnMainDicomTagsChange_)
     {
-      OrthancPlugins::LogWarning("Housekeeper: Study main dicom tags have changed, -> will perform housekeeping");
+      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: Study main dicom tags have changed, -> will perform housekeeping");
       needsReconstruct = true;
     }
     else
     {
-      OrthancPlugins::LogWarning("Housekeeper: Study main dicom tags have changed but the trigger is disabled");
+      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: Study main dicom tags have changed but the trigger is disabled");
     }
   }
 
@@ -417,12 +443,12 @@
   {
     if (triggerOnMainDicomTagsChange_)
     {
-      OrthancPlugins::LogWarning("Housekeeper: Series main dicom tags have changed, -> will perform housekeeping");
+      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: Series main dicom tags have changed, -> will perform housekeeping");
       needsReconstruct = true;
     }
     else
     {
-      OrthancPlugins::LogWarning("Housekeeper: Series main dicom tags have changed but the trigger is disabled");
+      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: Series main dicom tags have changed but the trigger is disabled");
     }
   }
 
@@ -430,12 +456,12 @@
   {
     if (triggerOnMainDicomTagsChange_)
     {
-      OrthancPlugins::LogWarning("Housekeeper: Instance main dicom tags have changed, -> will perform housekeeping");
+      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: Instance main dicom tags have changed, -> will perform housekeeping");
       needsReconstruct = true;
     }
     else
     {
-      OrthancPlugins::LogWarning("Housekeeper: Instance main dicom tags have changed but the trigger is disabled");
+      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: Instance main dicom tags have changed but the trigger is disabled");
     }
   }
 
@@ -445,18 +471,18 @@
     {
       if (current.storageCompressionEnabled)
       {
-        OrthancPlugins::LogWarning("Housekeeper: storage compression is now enabled -> will perform housekeeping");
+        ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: storage compression is now enabled -> will perform housekeeping");
       }
       else
       {
-        OrthancPlugins::LogWarning("Housekeeper: storage compression is now disabled -> will perform housekeeping");
+        ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: storage compression is now disabled -> will perform housekeeping");
       }
       
       needsReingest = true;
     }
     else
     {
-      OrthancPlugins::LogWarning("Housekeeper: storage compression has changed but the trigger is disabled");
+      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: storage compression has changed but the trigger is disabled");
     }
   }
 
@@ -464,19 +490,47 @@
   {
     if (triggerOnIngestTranscodingChange_)
     {
-      OrthancPlugins::LogWarning("Housekeeper: ingest transcoding has changed -> will perform housekeeping");
+      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: ingest transcoding has changed -> will perform housekeeping");
       
       needsReingest = true;
     }
     else
     {
-      OrthancPlugins::LogWarning("Housekeeper: ingest transcoding has changed but the trigger is disabled");
+      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: ingest transcoding has changed but the trigger is disabled");
     }
   }
 
+  if (!current.dicomWebVersion.empty())
+  {
+    if (last.dicomWebVersion.empty())
+    {
+      if (triggerOnDicomWebCacheChange_)
+      {
+        ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: DicomWEB plugin is enabled and the housekeeper has never run, you might miss series metadata cache -> will perform housekeeping");
+      }
+      needsDicomWebCaching = triggerOnDicomWebCacheChange_;
+    }
+    else
+    {
+      const char* lastDicomWebVersion = last.dicomWebVersion.c_str();
+
+      if (!OrthancPlugins::CheckMinimalVersion(lastDicomWebVersion, 1, 15, 0))
+      {
+        if (triggerOnDicomWebCacheChange_)
+        {
+          ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: DicomWEB plugin might miss series metadata cache -> will perform housekeeping");
+          needsDicomWebCaching = true;
+        }
+        else
+        {
+          ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: DicomWEB plugin might miss series metadata cache but the trigger has been disabled");
+        }
+      }
+    }
+  }
 }
 
-static bool ProcessChanges(bool needsReconstruct, bool needsReingest, const DbConfiguration& currentDbConfiguration)
+static bool ProcessChanges(bool needsReconstruct, bool needsReingest, bool needsDicomWebCaching, const DbConfiguration& currentDbConfiguration)
 {
   Json::Value changes;
 
@@ -488,36 +542,75 @@
     OrthancPlugins::RestApiGet(changes, "/changes?since=" + boost::lexical_cast<std::string>(pluginStatus_.lastProcessedChange) + "&limit=100", false);
   }
 
-  for (Json::ArrayIndex i = 0; i < changes["Changes"].size(); i++)
+  if (changes["Changes"].size() > 0)
   {
-    const Json::Value& change = changes["Changes"][i];
-    int64_t seq = change["Seq"].asInt64();
-
-    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
+    for (Json::ArrayIndex i = 0; i < changes["Changes"].size(); i++)
     {
-      Json::Value result;
-      Json::Value request;
-      if (needsReingest)
+      const Json::Value& change = changes["Changes"][i];
+      int64_t seq = change["Seq"].asInt64();
+
+      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
       {
-        request["ReconstructFiles"] = true;
+        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);
+          }
+        }
       }
-      OrthancPlugins::RestApiPost(result, "/studies/" + change["ID"].asString() + "/reconstruct", request, false);
+
+      {
+        boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);
+
+        pluginStatus_.lastProcessedChange = seq;
+
+        if (seq >= pluginStatus_.lastChangeToProcess)  // we are done !
+        {
+          return true;
+        }
+      }
+
+      if (change["ChangeType"] == "NewStudy")
+      {
+        boost::this_thread::sleep(boost::posix_time::milliseconds(throttleDelay_ * 1000));
+      }
     }
-
+  }
+  else
+  {
+    // if the change list is empty and Done is true, it means that there is nothing to process anymore
+    if (changes["Done"].asBool())
     {
       boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);
 
-      pluginStatus_.lastProcessedChange = seq;
+      pluginStatus_.lastProcessedChange = changes["Last"].asInt64();
 
-      if (seq >= pluginStatus_.lastChangeToProcess)  // we are done !
-      {
-        return true;
-      }
-    }
-
-    if (change["ChangeType"] == "NewStudy")
-    {
-      boost::this_thread::sleep(boost::posix_time::milliseconds(throttleDelay_ * 1000));
+      return true;
     }
   }
 
@@ -527,6 +620,8 @@
 
 static void WorkerThread()
 {
+  OrthancPluginSetCurrentThreadName(OrthancPlugins::GetGlobalContext(), "HOUSEKEEPER");
+
   DbConfiguration currentDbConfiguration;
 
   OrthancPluginLogWarning(OrthancPlugins::GetGlobalContext(), "Starting Housekeeper worker thread");
@@ -539,13 +634,14 @@
   bool needsReingest = false;
   bool needsFullProcessing = false;
   bool needsProcessing = false;
+  bool needsDicomWebCaching = false;
 
   {
     boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);
 
     // compare with last full processed configuration
-    CheckNeedsProcessing(needsReconstruct, needsReingest, currentDbConfiguration, pluginStatus_.lastProcessedConfiguration);
-    needsFullProcessing = needsReconstruct || needsReingest;
+    CheckNeedsProcessing(needsReconstruct, needsReingest, needsDicomWebCaching, currentDbConfiguration, pluginStatus_.lastProcessedConfiguration);
+    needsFullProcessing = needsReconstruct || needsReingest || needsDicomWebCaching;
     needsProcessing = needsFullProcessing;
 
       // if a processing was in progress, check if the config has changed since
@@ -555,15 +651,16 @@
 
       bool needsReconstruct2 = false;
       bool needsReingest2 = false;
+      bool needsDicomWebCaching2 = false;
 
-      CheckNeedsProcessing(needsReconstruct2, needsReingest2, currentDbConfiguration, pluginStatus_.currentlyProcessingConfiguration);
-      needsFullProcessing = needsReconstruct2 || needsReingest2;  // if the configuration has changed compared to the config being processed, we need a full processing again
+      CheckNeedsProcessing(needsReconstruct2, needsReingest2, needsDicomWebCaching2, currentDbConfiguration, pluginStatus_.currentlyProcessingConfiguration);
+      needsFullProcessing = needsReconstruct2 || needsReingest2 || needsDicomWebCaching2;  // if the configuration has changed compared to the config being processed, we need a full processing again
     }
   }
 
-  if (!needsProcessing)
+  if (!needsProcessing && !force_)
   {
-    OrthancPlugins::LogWarning("Housekeeper: everything has been processed already !");
+    ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: everything has been processed already !");
     return;
   }
 
@@ -571,11 +668,11 @@
   {
     if (force_)
     {
-      OrthancPlugins::LogWarning("Housekeeper: forcing execution -> will perform housekeeping");
+      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: forcing execution -> will perform housekeeping");
     }
     else
     {
-      OrthancPlugins::LogWarning("Housekeeper: the DB configuration has changed since last run, will reprocess the whole DB !");
+      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: the DB configuration has changed since last run, will reprocess the whole DB !");
     }
     
     Json::Value changes;
@@ -591,7 +688,7 @@
   }
   else
   {
-    OrthancPlugins::LogWarning("Housekeeper: the DB configuration has not changed since last run, will continue processing changes");
+    ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: the DB configuration has not changed since last run, will continue processing changes");
   }
 
   bool completed = false;
@@ -606,16 +703,15 @@
   {
     if (runningPeriods_.isInPeriod())
     {
-      completed = ProcessChanges(needsReconstruct, needsReingest, currentDbConfiguration);
+      completed = ProcessChanges(needsReconstruct, needsReingest, needsDicomWebCaching, currentDbConfiguration);
       SaveStatusInDb();
       
       if (!completed)
       {
         boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);
     
-        OrthancPlugins::LogInfo("Housekeeper: processed changes " + 
-                                boost::lexical_cast<std::string>(pluginStatus_.lastProcessedChange) + 
-                                " / " + boost::lexical_cast<std::string>(pluginStatus_.lastChangeToProcess));
+        ORTHANC_PLUGINS_LOG_INFO("Housekeeper: processed changes " + boost::lexical_cast<std::string>(pluginStatus_.lastProcessedChange) +
+                                 " / " + boost::lexical_cast<std::string>(pluginStatus_.lastChangeToProcess));
         
         boost::this_thread::sleep(boost::posix_time::milliseconds(throttleDelay_ * 100));  // wait 1/10 of the delay between changes
       }
@@ -626,9 +722,11 @@
     {
       if (!loggedNotRightPeriodChangeMessage)
       {
-        OrthancPlugins::LogInfo("Housekeeper: entering quiet period");
+        ORTHANC_PLUGINS_LOG_INFO("Housekeeper: entering quiet period");
         loggedNotRightPeriodChangeMessage = true;
       }
+
+      boost::this_thread::sleep(boost::posix_time::milliseconds(1000));
     }
   }  
 
@@ -698,7 +796,7 @@
 
   ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
   {
-    OrthancPlugins::SetGlobalContext(c);
+    OrthancPlugins::SetGlobalContext(c, HOUSEKEEPER_NAME);
 
     /* Check the version of the Orthanc core */
     if (OrthancPluginCheckVersion(c) == 0)
@@ -709,8 +807,8 @@
       return -1;
     }
 
-    OrthancPlugins::LogWarning("Housekeeper plugin is initializing");
-    OrthancPluginSetDescription(c, "Optimizes your DB and storage.");
+    ORTHANC_PLUGINS_LOG_WARNING("Housekeeper plugin is initializing");
+    OrthancPluginSetDescription2(c, HOUSEKEEPER_NAME, "Optimizes your DB and storage.");
 
     OrthancPlugins::OrthancConfiguration orthancConfiguration;
 
@@ -760,8 +858,13 @@
             "Triggers" : {
               "StorageCompressionChange": true,
               "MainDicomTagsChange": true,
-              "UnnecessaryDicomAsJsonFiles": true
-            }
+              "UnnecessaryDicomAsJsonFiles": 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"
 
           }
         }
@@ -774,11 +877,41 @@
 
       if (housekeeper.GetJson().isMember("Triggers"))
       {
-        triggerOnStorageCompressionChange_ = housekeeper.GetBooleanValue("StorageCompressionChange", true);
+        OrthancPlugins::OrthancConfiguration triggers;
+        housekeeper.GetSection(triggers, "Triggers");
+        triggerOnStorageCompressionChange_ = triggers.GetBooleanValue("StorageCompressionChange", true);
+
+        triggerOnMainDicomTagsChange_ = triggers.GetBooleanValue("MainDicomTagsChange", true);
+        triggerOnUnnecessaryDicomAsJsonFiles_ = triggers.GetBooleanValue("UnnecessaryDicomAsJsonFiles", true);
+        triggerOnIngestTranscodingChange_ = triggers.GetBooleanValue("IngestTranscodingChange", true);
+        triggerOnDicomWebCacheChange_ = triggers.GetBooleanValue("DicomWebCacheChange", true);
+      }
 
-        triggerOnMainDicomTagsChange_ = housekeeper.GetBooleanValue("MainDicomTagsChange", true);
-        triggerOnUnnecessaryDicomAsJsonFiles_ = housekeeper.GetBooleanValue("UnnecessaryDicomAsJsonFiles", true);
-        triggerOnIngestTranscodingChange_ = housekeeper.GetBooleanValue("IngestTranscodingChange", true);
+      limitMainDicomTagsReconstructLevel_ = housekeeper.GetStringValue("LimitMainDicomTagsReconstructLevel", "");
+      if (limitMainDicomTagsReconstructLevel_ != "Patient" && limitMainDicomTagsReconstructLevel_ != "Study"
+        && limitMainDicomTagsReconstructLevel_ != "Series" && limitMainDicomTagsReconstructLevel_ != "Instance")
+      {
+        ORTHANC_PLUGINS_LOG_ERROR("Housekeeper invalid value for 'LimitMainDicomTagsReconstructLevel': '" + limitMainDicomTagsReconstructLevel_ + "'");
+      }
+      else if (limitMainDicomTagsReconstructLevel_ == "Patient")
+      {
+        limitToChange_ = "NewPatient";
+        limitToUrl_ = "patients";
+      }
+      else if (limitMainDicomTagsReconstructLevel_ == "Study")
+      {
+        limitToChange_ = "NewStudy";
+        limitToUrl_ = "studies";
+      }
+      else if (limitMainDicomTagsReconstructLevel_ == "Series")
+      {
+        limitToChange_ = "NewSeries";
+        limitToUrl_ = "series";
+      }
+      else if (limitMainDicomTagsReconstructLevel_ == "Instance")
+      {
+        limitToChange_ = "NewInstance";
+        limitToUrl_ = "instances";
       }
 
       if (housekeeper.GetJson().isMember("Schedule"))
@@ -787,12 +920,12 @@
       }
 
       OrthancPluginRegisterOnChangeCallback(c, OnChangeCallback);
-      OrthancPluginRegisterRestCallback(c, "/housekeeper/status", GetPluginStatus);   // for bacward compatiblity with version 1.11.0
+      OrthancPluginRegisterRestCallback(c, "/housekeeper/status", GetPluginStatus);   // for backward compatiblity with version 1.11.0
       OrthancPluginRegisterRestCallback(c, "/plugins/housekeeper/status", GetPluginStatus);
     }
     else
     {
-      OrthancPlugins::LogWarning("Housekeeper plugin is disabled by the configuration file");
+      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper plugin is disabled by the configuration file");
     }
 
     return 0;
@@ -801,13 +934,13 @@
 
   ORTHANC_PLUGINS_API void OrthancPluginFinalize()
   {
-    OrthancPlugins::LogWarning("Housekeeper plugin is finalizing");
+    ORTHANC_PLUGINS_LOG_WARNING("Housekeeper plugin is finalizing");
   }
 
 
   ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
   {
-    return "housekeeper";
+    return HOUSEKEEPER_NAME;
   }
 
 
--- a/OrthancServer/Plugins/Samples/ModalityWorklists/CMakeLists.txt	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/ModalityWorklists/CMakeLists.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -40,6 +41,8 @@
   ${BOOST_SOURCES}
   )
 
+DefineSourceBasenameForTarget(ModalityWorklists)
+
 message("Setting the version of the plugin to ${MODALITY_WORKLISTS_VERSION}")
 add_definitions(
   -DMODALITY_WORKLISTS_VERSION="${MODALITY_WORKLISTS_VERSION}"
--- a/OrthancServer/Plugins/Samples/ModalityWorklists/Plugin.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/ModalityWorklists/Plugin.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -20,6 +21,8 @@
  **/
 
 
+#define MODALITY_WORKLISTS_NAME "worklists"
+
 #include "../../../../OrthancFramework/Sources/Compatibility.h"
 #include "../Common/OrthancPluginCppWrapper.h"
 
@@ -52,7 +55,7 @@
 
     if (code != OrthancPluginErrorCode_Success)
     {
-      OrthancPlugins::LogError("Error while adding an answer to a worklist request");
+      ORTHANC_PLUGINS_LOG_ERROR("Error while adding an answer to a worklist request");
       ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
     }
 
@@ -75,8 +78,8 @@
   dicom.DicomToJson(json, OrthancPluginDicomToJsonFormat_Short,
                     static_cast<OrthancPluginDicomToJsonFlags>(0), 0);
 
-  OrthancPlugins::LogInfo("Received worklist query from remote modality " +
-                          std::string(issuerAet) + ":\n" + json.toStyledString());
+  ORTHANC_PLUGINS_LOG_INFO("Received worklist query from remote modality " +
+                           std::string(issuerAet) + ":\n" + json.toStyledString());
 
   if (!filterIssuerAet_)
   {
@@ -151,11 +154,12 @@
 
     fs::path source(folder_);
     fs::directory_iterator end;
-    unsigned int parsedFilesCount = 0;
-    unsigned int matchedWorklistCount = 0;
 
     try
     {
+      unsigned int parsedFilesCount = 0;
+      unsigned int matchedWorklistCount = 0;
+      
       for (fs::directory_iterator it(source); it != end; ++it)
       {
         fs::file_type type(it->status().type());
@@ -163,7 +167,7 @@
         if (type == fs::regular_file ||
             type == fs::reparse_file)   // cf. BitBucket issue #11
         {
-          std::string extension = fs::extension(it->path());
+          std::string extension = it->path().extension().string();
           std::transform(extension.begin(), extension.end(), extension.begin(), tolower);  // Convert to lowercase
 
           if (extension == ".wl")
@@ -182,21 +186,19 @@
                 return OrthancPluginErrorCode_Success;
               }
               
-              OrthancPlugins::LogInfo("Worklist matched: " + it->path().string());
+              ORTHANC_PLUGINS_LOG_INFO("Worklist matched: " + it->path().string());
               matchedWorklistCount++;
             }
           }
         }
       }
 
-      std::ostringstream message;
-      message << "Worklist C-Find: parsed " << parsedFilesCount
-              << " files, found " << matchedWorklistCount << " match(es)";
-      OrthancPlugins::LogInfo(message.str());
+      ORTHANC_PLUGINS_LOG_INFO("Worklist C-Find: parsed " + boost::lexical_cast<std::string>(parsedFilesCount) +
+                               " files, found " + boost::lexical_cast<std::string>(matchedWorklistCount) + " match(es)");
     }
     catch (fs::filesystem_error&)
     {
-      OrthancPlugins::LogError("Inexistent folder while scanning for worklists: " + source.string());
+      ORTHANC_PLUGINS_LOG_ERROR("Inexistent folder while scanning for worklists: " + source.string());
       return OrthancPluginErrorCode_DirectoryExpected;
     }
 
@@ -213,7 +215,7 @@
 {
   ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
   {
-    OrthancPlugins::SetGlobalContext(c);
+    OrthancPlugins::SetGlobalContext(c, MODALITY_WORKLISTS_NAME);
 
     /* Check the version of the Orthanc core */
     if (OrthancPluginCheckVersion(c) == 0)
@@ -224,8 +226,8 @@
       return -1;
     }
 
-    OrthancPlugins::LogWarning("Sample worklist plugin is initializing");
-    OrthancPluginSetDescription(c, "Serve DICOM modality worklists from a folder with Orthanc.");
+    ORTHANC_PLUGINS_LOG_WARNING("Sample worklist plugin is initializing");
+    OrthancPluginSetDescription2(c, MODALITY_WORKLISTS_NAME, "Serve DICOM modality worklists from a folder with Orthanc.");
 
     OrthancPlugins::OrthancConfiguration configuration;
 
@@ -237,12 +239,12 @@
     {
       if (worklists.LookupStringValue(folder_, "Database"))
       {
-        OrthancPlugins::LogWarning("The database of worklists will be read from folder: " + folder_);
+        ORTHANC_PLUGINS_LOG_WARNING("The database of worklists will be read from folder: " + folder_);
         OrthancPluginRegisterWorklistCallback(OrthancPlugins::GetGlobalContext(), Callback);
       }
       else
       {
-        OrthancPlugins::LogError("The configuration option \"Worklists.Database\" must contain a path");
+        ORTHANC_PLUGINS_LOG_ERROR("The configuration option \"Worklists.Database\" must contain a path");
         return -1;
       }
 
@@ -251,7 +253,7 @@
     }
     else
     {
-      OrthancPlugins::LogWarning("Worklist server is disabled by the configuration file");
+      ORTHANC_PLUGINS_LOG_WARNING("Worklist server is disabled by the configuration file");
     }
 
     return 0;
@@ -260,13 +262,13 @@
 
   ORTHANC_PLUGINS_API void OrthancPluginFinalize()
   {
-    OrthancPlugins::LogWarning("Sample worklist plugin is finalizing");
+    ORTHANC_PLUGINS_LOG_WARNING("Sample worklist plugin is finalizing");
   }
 
 
   ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
   {
-    return "worklists";
+    return MODALITY_WORKLISTS_NAME;
   }
 
 
--- a/OrthancServer/Plugins/Samples/ModalityWorklists/README	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/ModalityWorklists/README	Tue Sep 24 11:39:52 2024 +0200
@@ -8,4 +8,4 @@
 build process of Orthanc.
 
 Documentation is available in the Orthanc Book:
-http://book.orthanc-server.com/plugins/worklists-plugin.html
+https://orthanc.uclouvain.be/book/plugins/worklists-plugin.html
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/CMakeLists.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,75 @@
+# 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/>.
+
+
+cmake_minimum_required(VERSION 2.8)
+
+project(MultitenantDicom)
+
+SET(ORTHANC_PLUGIN_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")
+
+include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake)
+include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake)
+
+set(ENABLE_LOCALE ON CACHE INTERNAL "")
+set(ENABLE_ZLIB ON CACHE INTERNAL "")  # This is necessary for static builds with ICU
+set(ENABLE_DCMTK ON CACHE INTERNAL "")
+set(ENABLE_DCMTK_NETWORKING ON CACHE INTERNAL "")
+set(ENABLE_DCMTK_TRANSCODING OFF CACHE INTERNAL "")
+
+set(ENABLE_MODULE_DICOM ON CACHE INTERNAL "")
+set(ENABLE_MODULE_IMAGES ON CACHE INTERNAL "")
+set(ENABLE_MODULE_JOBS OFF CACHE INTERNAL "")
+
+include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake)
+
+add_library(MultitenantDicom SHARED 
+  DicomFilter.cpp
+  FindRequestHandler.cpp
+  MoveRequestHandler.cpp
+  MultitenantDicomServer.cpp
+  Plugin.cpp
+  PluginToolbox.cpp
+  StoreRequestHandler.cpp
+
+  ${CMAKE_SOURCE_DIR}/../Common/OrthancPluginCppWrapper.cpp
+  ${ORTHANC_CORE_SOURCES}
+  ${ORTHANC_DICOM_SOURCES}
+  ${AUTOGENERATED_SOURCES}
+  )
+
+DefineSourceBasenameForTarget(MultitenantDicom)
+
+target_link_libraries(MultitenantDicom ${DCMTK_LIBRARIES})
+
+message("Setting the version of the plugin to ${ORTHANC_PLUGIN_VERSION}")
+add_definitions(-DORTHANC_PLUGIN_VERSION="${ORTHANC_PLUGIN_VERSION}")
+
+set_target_properties(MultitenantDicom PROPERTIES 
+  VERSION ${ORTHANC_PLUGIN_VERSION} 
+  SOVERSION ${ORTHANC_PLUGIN_VERSION})
+
+install(
+  TARGETS MultitenantDicom
+  RUNTIME DESTINATION lib    # Destination for Windows
+  LIBRARY DESTINATION share/orthanc/plugins    # Destination for Linux
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,209 @@
+/**
+ * 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 "DicomFilter.h"
+
+#include "PluginToolbox.h"
+
+#include "../../../../OrthancFramework/Sources/Logging.h"
+#include "../../../../OrthancFramework/Sources/OrthancException.h"
+
+#include "../Common/OrthancPluginCppWrapper.h"
+
+
+DicomFilter::DicomFilter() :
+  hasAcceptedTransferSyntaxes_(false)
+{
+  {
+    OrthancPlugins::OrthancConfiguration config;
+    alwaysAllowEcho_ = config.GetBooleanValue("DicomAlwaysAllowEcho", true);
+    alwaysAllowFind_ = config.GetBooleanValue("DicomAlwaysAllowFind", false);
+    alwaysAllowMove_ = config.GetBooleanValue("DicomAlwaysAllowMove", false);
+    alwaysAllowStore_ = config.GetBooleanValue("DicomAlwaysAllowStore", true);
+    unknownSopClassAccepted_ = config.GetBooleanValue("UnknownSopClassAccepted", false);
+    isStrict_ = config.GetBooleanValue("StrictAetComparison", false);
+    checkModalityHost_ = config.GetBooleanValue("DicomCheckModalityHost", false);
+  }
+}
+
+
+bool DicomFilter::IsAllowedConnection(const std::string& remoteIp,
+                                      const std::string& remoteAet,
+                                      const std::string& calledAet)
+{
+  boost::shared_lock<boost::shared_mutex>  lock(mutex_);
+
+  LOG(INFO) << "Incoming connection from AET " << remoteAet
+            << " on IP " << remoteIp << ", calling AET " << calledAet;
+
+  if (alwaysAllowEcho_ ||
+      alwaysAllowFind_ ||
+      alwaysAllowMove_ ||
+      alwaysAllowStore_)
+  {
+    return true;
+  }
+  else
+  {
+    std::string name;
+    Orthanc::RemoteModalityParameters parameters;
+
+    if (!PluginToolbox::LookupAETitle(name, parameters, isStrict_, remoteAet))
+    {
+      LOG(WARNING) << "Modality \"" << remoteAet
+                   << "\" is not listed in the \"DicomModalities\" configuration option";
+      return false;
+    }
+    else if (!checkModalityHost_ ||
+             remoteIp == parameters.GetHost())
+    {
+      return true;
+    }
+    else
+    {
+      LOG(WARNING) << "Forbidding access from AET \"" << remoteAet
+                   << "\" given its hostname (" << remoteIp << ") does not match "
+                   << "the \"DicomModalities\" configuration option ("
+                   << parameters.GetHost() << " was expected)";
+      return false;
+    }
+  }
+}
+
+
+bool DicomFilter::IsAllowedRequest(const std::string& remoteIp,
+                                   const std::string& remoteAet,
+                                   const std::string& calledAet,
+                                   Orthanc::DicomRequestType type)
+{
+  boost::shared_lock<boost::shared_mutex>  lock(mutex_);
+
+  LOG(INFO) << "Incoming " << EnumerationToString(type) << " request from AET "
+            << remoteAet << " on IP " << remoteIp << ", calling AET " << calledAet;
+
+  if (type == Orthanc::DicomRequestType_Echo &&
+      alwaysAllowEcho_)
+  {
+    // Incoming C-Echo requests are always accepted, even from unknown AET
+    return true;
+  }
+  else if (type == Orthanc::DicomRequestType_Find &&
+           alwaysAllowFind_)
+  {
+    // Incoming C-Find requests are always accepted, even from unknown AET
+    return true;
+  }
+  else if (type == Orthanc::DicomRequestType_Store &&
+           alwaysAllowStore_)
+  {
+    // Incoming C-Store requests are always accepted, even from unknown AET
+    return true;
+  }
+  else if (type == Orthanc::DicomRequestType_Move &&
+           alwaysAllowMove_)
+  {
+    // Incoming C-Move requests are always accepted, even from unknown AET
+    return true;
+  }
+  else
+  {
+    std::string name;
+    Orthanc::RemoteModalityParameters parameters;
+
+    if (!PluginToolbox::LookupAETitle(name, parameters, isStrict_, remoteAet))
+    {
+      LOG(WARNING) << "DICOM authorization rejected for AET " << remoteAet
+                   << " on IP " << remoteIp << ": This AET is not listed in "
+                   << "configuration option \"DicomModalities\"";
+      return false;
+    }
+    else
+    {
+      if (parameters.IsRequestAllowed(type))
+      {
+        return true;
+      }
+      else
+      {
+        LOG(WARNING) << "DICOM authorization rejected for AET " << remoteAet
+                     << " on IP " << remoteIp << ": The DICOM command "
+                     << EnumerationToString(type) << " is not allowed for this modality "
+                     << "according to configuration option \"DicomModalities\"";
+        return false;
+      }
+    }
+  }
+}
+
+
+void DicomFilter::GetAcceptedTransferSyntaxes(std::set<Orthanc::DicomTransferSyntax>& target,
+                                              const std::string& remoteIp,
+                                              const std::string& remoteAet,
+                                              const std::string& calledAet)
+{
+  boost::unique_lock<boost::shared_mutex>  lock(mutex_);
+
+  if (!hasAcceptedTransferSyntaxes_)
+  {
+    Json::Value syntaxes;
+
+    if (!OrthancPlugins::RestApiGet(syntaxes, "/tools/accepted-transfer-syntaxes", false) ||
+        syntaxes.type() != Json::arrayValue)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+    else
+    {
+      for (Json::Value::ArrayIndex i = 0; i < syntaxes.size(); i++)
+      {
+        Orthanc::DicomTransferSyntax syntax;
+
+        if (syntaxes[i].type() != Json::stringValue)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+        else if (Orthanc::LookupTransferSyntax(syntax, syntaxes[i].asString()))
+        {
+          acceptedTransferSyntaxes_.insert(syntax);
+        }
+        else
+        {
+          LOG(WARNING) << "Unknown transfer syntax: " << syntaxes[i].asString();
+        }
+      }
+    }
+
+    hasAcceptedTransferSyntaxes_ = true;
+  }
+
+  target = acceptedTransferSyntaxes_;
+}
+
+
+bool DicomFilter::IsUnknownSopClassAccepted(const std::string& remoteIp,
+                                            const std::string& remoteAet,
+                                            const std::string& calledAet)
+{
+  boost::shared_lock<boost::shared_mutex>  lock(mutex_);
+  return unknownSopClassAccepted_;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.h	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,68 @@
+/**
+ * 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/Compatibility.h"
+#include "../../../../OrthancFramework/Sources/DicomNetworking/IApplicationEntityFilter.h"
+
+#include <boost/thread/shared_mutex.hpp>
+
+
+class DicomFilter : public Orthanc::IApplicationEntityFilter
+{
+private:
+  boost::shared_mutex mutex_;
+
+  bool alwaysAllowEcho_;
+  bool alwaysAllowFind_;
+  bool alwaysAllowMove_;
+  bool alwaysAllowStore_;
+  bool unknownSopClassAccepted_;
+  bool isStrict_;
+  bool checkModalityHost_;
+
+  bool hasAcceptedTransferSyntaxes_;
+  std::set<Orthanc::DicomTransferSyntax>  acceptedTransferSyntaxes_;
+
+public:
+  DicomFilter();
+
+  virtual bool IsAllowedConnection(const std::string& remoteIp,
+                                   const std::string& remoteAet,
+                                   const std::string& calledAet) ORTHANC_OVERRIDE;
+
+  virtual bool IsAllowedRequest(const std::string& remoteIp,
+                                const std::string& remoteAet,
+                                const std::string& calledAet,
+                                Orthanc::DicomRequestType type) ORTHANC_OVERRIDE;
+
+  virtual void GetAcceptedTransferSyntaxes(std::set<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;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/FindRequestHandler.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,124 @@
+/**
+ * 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 "FindRequestHandler.h"
+
+#include "PluginToolbox.h"
+
+#include "../../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
+#include "../../../../OrthancFramework/Sources/OrthancException.h"
+
+#include "../Common/OrthancPluginCppWrapper.h"
+
+
+void FindRequestHandler::Handle(Orthanc::DicomFindAnswers& answers,
+                                const Orthanc::DicomMap& input,
+                                const std::list<Orthanc::DicomTag>& sequencesToReturn,
+                                const std::string& remoteIp,
+                                const std::string& remoteAet,
+                                const std::string& calledAet,
+                                Orthanc::ModalityManufacturer manufacturer)
+{
+  std::set<Orthanc::DicomTag> tags;
+  input.GetTags(tags);
+
+  Json::Value request = Json::objectValue;
+  request["Expand"] = true;
+  PluginToolbox::AddLabelsToFindRequest(request, labels_, constraint_);
+
+  Json::Value query = Json::objectValue;
+  std::string level;
+
+  for (std::set<Orthanc::DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
+  {
+    std::string s;
+
+    if (input.LookupStringValue(s, *it, false) &&
+        !s.empty())
+    {
+      if (*it == Orthanc::DICOM_TAG_QUERY_RETRIEVE_LEVEL)
+      {
+        level = s;
+      }
+      else 
+      {
+        query[it->Format()] = s;
+      }
+    }
+  }
+
+  if (level.empty())
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, "Missing QueryRetrieveLevel in DICOM C-FIND request");
+  }
+
+  request["Level"] = EnumerationToString(PluginToolbox::ParseQueryRetrieveLevel(level));
+  request["Query"] = query;
+
+  Json::Value response;
+  if (!OrthancPlugins::RestApiPost(response, "/tools/find", request, false) ||
+      response.type() != Json::arrayValue)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, "Invalid DICOM C-FIND request");
+  }
+
+  for (Json::Value::ArrayIndex i = 0; i < response.size(); i++)
+  {
+    if (response[i].type() != Json::objectValue ||
+        !response[i].isMember(KEY_MAIN_DICOM_TAGS))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    if (response[i].isMember(KEY_PATIENT_MAIN_DICOM_TAGS) &&
+        response[i][KEY_PATIENT_MAIN_DICOM_TAGS].type() != Json::objectValue)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+            
+    Orthanc::DicomMap m;
+
+    for (std::set<Orthanc::DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
+    {
+      const std::string tag = Orthanc::FromDcmtkBridge::GetTagName(*it, "");
+
+      if (response[i][KEY_MAIN_DICOM_TAGS].isMember(tag) &&
+          response[i][KEY_MAIN_DICOM_TAGS][tag].type() == Json::stringValue)
+      {
+        m.SetValue(*it, response[i][KEY_MAIN_DICOM_TAGS][tag].asString(), false);
+      }
+      else if (response[i].isMember(KEY_PATIENT_MAIN_DICOM_TAGS) &&
+               response[i][KEY_PATIENT_MAIN_DICOM_TAGS].isMember(tag) &&
+               response[i][KEY_PATIENT_MAIN_DICOM_TAGS][tag].type() == Json::stringValue)
+      {
+        m.SetValue(*it, response[i][KEY_PATIENT_MAIN_DICOM_TAGS][tag].asString(), false);
+      }        
+    }
+            
+    m.SetValue(Orthanc::DICOM_TAG_QUERY_RETRIEVE_LEVEL, level, false);
+    m.SetValue(Orthanc::DICOM_TAG_RETRIEVE_AE_TITLE, retrieveAet_, false);
+    answers.Add(m);
+  }
+
+  answers.SetComplete(true);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/FindRequestHandler.h	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,56 @@
+/**
+ * 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 "PluginEnumerations.h"
+
+#include "../../../../OrthancFramework/Sources/DicomNetworking/IFindRequestHandler.h"
+
+
+class FindRequestHandler : public Orthanc::IFindRequestHandler
+{
+private:
+  // Everything is constant, so no need for a mutex
+  const std::string           retrieveAet_;
+  const std::set<std::string> labels_;
+  const LabelsConstraint      constraint_;
+        
+public:
+  FindRequestHandler(const std::string& retrieveAet,
+                     const std::set<std::string>& labels,
+                     LabelsConstraint constraint) :
+    retrieveAet_(retrieveAet),
+    labels_(labels),
+    constraint_(constraint)
+  {
+  }
+
+  virtual void Handle(Orthanc::DicomFindAnswers& answers,
+                      const Orthanc::DicomMap& input,
+                      const std::list<Orthanc::DicomTag>& sequencesToReturn,
+                      const std::string& remoteIp,
+                      const std::string& remoteAet,
+                      const std::string& calledAet,
+                      Orthanc::ModalityManufacturer manufacturer) ORTHANC_OVERRIDE;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/MoveRequestHandler.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,231 @@
+/**
+ * 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 "MoveRequestHandler.h"
+
+#include "PluginToolbox.h"
+
+#include "../../../../OrthancFramework/Sources/OrthancException.h"
+#include "../../../../OrthancFramework/Sources/Toolbox.h"
+
+#include "../Common/OrthancPluginCppWrapper.h"
+
+
+class MoveRequestHandler::Iterator : public Orthanc::IMoveRequestIterator
+{
+private:
+  std::string  targetModality_;
+  Json::Value  body_;
+  bool         done_;
+
+public:
+  Iterator(const std::string& targetModality,
+           const Json::Value& body) :
+    targetModality_(targetModality),
+    body_(body),
+    done_(false)
+  {
+  }
+
+  virtual unsigned int GetSubOperationCount() const ORTHANC_OVERRIDE
+  {
+    return 1;
+  }
+
+  virtual Status DoNext() ORTHANC_OVERRIDE
+  {
+    Json::Value answer;
+
+    if (done_)
+    {
+      return Status_Failure;
+    }
+    else if (OrthancPlugins::RestApiPost(answer, "/modalities/" + targetModality_ + "/store", body_, false))
+    {
+      done_ = true;
+      return Status_Success;
+    }
+    else
+    {
+      done_ = true;
+      return Status_Failure;
+    }
+  }
+};
+
+
+void MoveRequestHandler::ExecuteLookup(std::set<std::string>& publicIds,
+                                       Orthanc::ResourceType level,
+                                       const Orthanc::DicomTag& tag,
+                                       const std::string& value) const
+{
+  std::vector<std::string> tokens;
+  Orthanc::Toolbox::TokenizeString(tokens, value, '\\');
+
+  for (size_t i = 0; i < tokens.size(); i++)
+  {
+    if (!tokens[i].empty())
+    {
+      Json::Value request = Json::objectValue;
+      request["Level"] = Orthanc::EnumerationToString(level);
+      request["Query"][tag.Format()] = tokens[i];
+      PluginToolbox::AddLabelsToFindRequest(request, labels_, constraint_);
+
+      Json::Value response;
+      if (OrthancPlugins::RestApiPost(response, "/tools/find", request, false) &&
+          response.type() == Json::arrayValue)
+      {
+        for (Json::Value::ArrayIndex j = 0; j < response.size(); j++)
+        {
+          if (response[j].type() != Json::stringValue)
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+          }
+          else
+          {
+            publicIds.insert(response[j].asString());
+          }
+        }
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+    }
+  }
+}
+
+
+void MoveRequestHandler::LookupIdentifiers(std::set<std::string>& publicIds,
+                                           Orthanc::ResourceType level,
+                                           const Orthanc::DicomMap& input) const
+{
+  std::string value;
+
+  switch (level)
+  {
+    case Orthanc::ResourceType_Patient:
+      if (input.LookupStringValue(value, Orthanc::DICOM_TAG_PATIENT_ID, false) &&
+          !value.empty())
+      {
+        ExecuteLookup(publicIds, level, Orthanc::DICOM_TAG_PATIENT_ID, value);
+      }
+      break;
+
+    case Orthanc::ResourceType_Study:
+      if (input.LookupStringValue(value, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) &&
+          !value.empty())
+      {
+        ExecuteLookup(publicIds, level, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, value);
+      }
+      else if (input.LookupStringValue(value, Orthanc::DICOM_TAG_ACCESSION_NUMBER, false) &&
+               !value.empty())
+      {
+        ExecuteLookup(publicIds, level, Orthanc::DICOM_TAG_ACCESSION_NUMBER, value);
+      }
+      break;
+
+    case Orthanc::ResourceType_Series:
+      if (input.LookupStringValue(value, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false) &&
+          !value.empty())
+      {
+        ExecuteLookup(publicIds, level, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, value);
+      }
+      break;
+
+    case Orthanc::ResourceType_Instance:
+      if (input.LookupStringValue(value, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false) &&
+          !value.empty())
+      {
+        ExecuteLookup(publicIds, level, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, value);
+      }
+      break;
+
+    default:
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+  }
+}
+
+
+Orthanc::IMoveRequestIterator* MoveRequestHandler::Handle(const std::string& targetAet,
+                                                          const Orthanc::DicomMap& input,
+                                                          const std::string& originatorIp,
+                                                          const std::string& originatorAet,
+                                                          const std::string& calledAet,
+                                                          uint16_t originatorId)
+{
+  std::set<std::string> publicIds;
+
+  std::string s;
+  if (input.LookupStringValue(s, Orthanc::DICOM_TAG_QUERY_RETRIEVE_LEVEL, false) &&
+      !s.empty())
+  {
+    LookupIdentifiers(publicIds, PluginToolbox::ParseQueryRetrieveLevel(s), input);
+  }
+  else
+  {
+    // The query level is not present in the C-Move request, which
+    // does not follow the DICOM standard. This is for instance the
+    // behavior of Tudor DICOM. Try and automatically deduce the
+    // query level: Start from the instance level, going up to the
+    // patient level until a valid DICOM identifier is found.
+    LookupIdentifiers(publicIds, Orthanc::ResourceType_Instance, input);
+
+    if (publicIds.empty())
+    {
+      LookupIdentifiers(publicIds, Orthanc::ResourceType_Series, input);
+    }
+
+    if (publicIds.empty())
+    {
+      LookupIdentifiers(publicIds, Orthanc::ResourceType_Study, input);
+    }
+
+    if (publicIds.empty())
+    {
+      LookupIdentifiers(publicIds, Orthanc::ResourceType_Patient, input);
+    }
+  }
+
+  Json::Value resources = Json::arrayValue;
+  for (std::set<std::string>::const_iterator it = publicIds.begin(); it != publicIds.end(); ++it)
+  {
+    resources.append(*it);
+  }
+
+  std::string targetName;
+  Orthanc::RemoteModalityParameters targetParameters;
+  if (!PluginToolbox::LookupAETitle(targetName, targetParameters, isStrictAet_, targetAet))
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, "Unknown target AET: " + targetAet);
+  }
+
+  Json::Value body;
+  body["CalledAet"] = calledAet;
+  body["MoveOriginatorAet"] = originatorAet;
+  body["MoveOriginatorID"] = originatorId;
+  body["Resources"] = resources;
+  body["Synchronous"] = isSynchronous_;
+
+  return new Iterator(targetName, body);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/MoveRequestHandler.h	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,69 @@
+/**
+ * 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 "PluginEnumerations.h"
+
+#include "../../../../OrthancFramework/Sources/DicomNetworking/IMoveRequestHandler.h"
+
+
+class MoveRequestHandler : public Orthanc::IMoveRequestHandler
+{
+private:
+  class Iterator;
+
+  // Everything is constant, so no need for a mutex
+  const std::set<std::string>  labels_;
+  const LabelsConstraint       constraint_;
+  const bool                   isStrictAet_;
+  const bool                   isSynchronous_;
+
+  void ExecuteLookup(std::set<std::string>& publicIds,
+                     Orthanc::ResourceType level,
+                     const Orthanc::DicomTag& tag,
+                     const std::string& value) const;
+
+  void LookupIdentifiers(std::set<std::string>& publicIds,
+                         Orthanc::ResourceType level,
+                         const Orthanc::DicomMap& input) const;
+  
+public:
+  MoveRequestHandler(const std::set<std::string>& labels,
+                     LabelsConstraint constraint,
+                     bool isStrictAet,
+                     bool isSynchronous) :
+    labels_(labels),
+    constraint_(constraint),
+    isStrictAet_(isStrictAet),
+    isSynchronous_(isSynchronous)
+  {
+  }
+
+  virtual Orthanc::IMoveRequestIterator* Handle(const std::string& targetAet,
+                                                const Orthanc::DicomMap& input,
+                                                const std::string& originatorIp,
+                                                const std::string& originatorAet,
+                                                const std::string& calledAet,
+                                                uint16_t originatorId) ORTHANC_OVERRIDE;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,146 @@
+/**
+ * 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 "MultitenantDicomServer.h"
+
+#include "FindRequestHandler.h"
+#include "MoveRequestHandler.h"
+#include "PluginToolbox.h"
+#include "StoreRequestHandler.h"
+
+#include "../../../../OrthancFramework/Sources/Logging.h"
+#include "../../../../OrthancFramework/Sources/SerializationToolbox.h"
+
+#include "../Common/OrthancPluginCppWrapper.h"
+
+
+bool MultitenantDicomServer::IsSameAETitle(const std::string& aet1,
+                                           const std::string& aet2)
+{
+  boost::mutex::scoped_lock lock(mutex_);
+  return PluginToolbox::IsSameAETitle(isStrictAet_, aet1, aet2);
+}
+
+
+bool MultitenantDicomServer::LookupAETitle(Orthanc::RemoteModalityParameters& parameters,
+                                           const std::string& aet)
+{
+  boost::mutex::scoped_lock lock(mutex_);
+
+  std::string name;
+  return PluginToolbox::LookupAETitle(name, parameters, isStrictAet_, server_->GetApplicationEntityTitle());
+}
+
+
+Orthanc::IFindRequestHandler* MultitenantDicomServer::ConstructFindRequestHandler()
+{
+  boost::mutex::scoped_lock lock(mutex_);
+  return new FindRequestHandler(server_->GetApplicationEntityTitle(), labels_, labelsConstraint_);
+}
+
+
+Orthanc::IMoveRequestHandler* MultitenantDicomServer::ConstructMoveRequestHandler()
+{
+  boost::mutex::scoped_lock lock(mutex_);
+  return new MoveRequestHandler(labels_, labelsConstraint_, isStrictAet_, isSynchronousCMove_);
+}
+
+
+Orthanc::IStoreRequestHandler* MultitenantDicomServer::ConstructStoreRequestHandler()
+{
+  boost::mutex::scoped_lock lock(mutex_);
+  return new StoreRequestHandler(labels_, labelsStoreLevels_);
+}
+
+
+MultitenantDicomServer::MultitenantDicomServer(const Json::Value& serverConfig)
+{
+  PluginToolbox::ParseLabels(labels_, labelsConstraint_, serverConfig);
+
+  if (serverConfig.isMember(KEY_LABELS_STORE_LEVELS))
+  {
+    std::set<std::string> levels;
+    Orthanc::SerializationToolbox::ReadSetOfStrings(levels, serverConfig, KEY_LABELS_STORE_LEVELS);
+    for (std::set<std::string>::const_iterator it = levels.begin(); it != levels.end(); ++it)
+    {
+      labelsStoreLevels_.insert(Orthanc::StringToResourceType(it->c_str()));
+    }
+  }
+  else
+  {
+    labelsStoreLevels_.insert(Orthanc::ResourceType_Study);
+    labelsStoreLevels_.insert(Orthanc::ResourceType_Series);
+    labelsStoreLevels_.insert(Orthanc::ResourceType_Instance);
+  }
+  
+  server_.reset(new Orthanc::DicomServer);
+
+  {
+    OrthancPlugins::OrthancConfiguration globalConfig;
+    isSynchronousCMove_ = globalConfig.GetBooleanValue(KEY_SYNCHRONOUS_C_MOVE, true);
+    isStrictAet_ = globalConfig.GetBooleanValue(KEY_STRICT_AET_COMPARISON, false);
+
+    server_->SetCalledApplicationEntityTitleCheck(globalConfig.GetBooleanValue("DicomCheckCalledAet", false));
+    server_->SetAssociationTimeout(globalConfig.GetUnsignedIntegerValue("DicomScpTimeout", 30));
+    server_->SetThreadsCount(globalConfig.GetUnsignedIntegerValue("DicomThreadsCount", 1));
+    server_->SetMaximumPduLength(globalConfig.GetUnsignedIntegerValue("MaximumPduLength", 16384));
+  }
+
+  server_->SetRemoteModalities(*this);
+  server_->SetApplicationEntityFilter(filter_);
+  server_->SetPortNumber(Orthanc::SerializationToolbox::ReadUnsignedInteger(serverConfig, "Port"));
+  server_->SetApplicationEntityTitle(Orthanc::SerializationToolbox::ReadString(serverConfig, KEY_AET));
+  server_->SetFindRequestHandlerFactory(*this);
+  server_->SetMoveRequestHandlerFactory(*this);
+  server_->SetStoreRequestHandlerFactory(*this);
+}
+
+
+void MultitenantDicomServer::Start()
+{
+  boost::mutex::scoped_lock lock(mutex_);
+
+  if (server_->GetPortNumber() < 1024)
+  {
+    LOG(WARNING) << "The DICOM port is privileged ("
+                 << server_->GetPortNumber() << " is below 1024), "
+                 << "make sure you run Orthanc as root/administrator";
+  }
+
+  server_->Start();
+  LOG(WARNING) << "Started multitenant DICOM server listening with AET " << server_->GetApplicationEntityTitle()
+               << " on port: " << server_->GetPortNumber();
+}
+
+
+void MultitenantDicomServer::Stop()
+{
+  boost::mutex::scoped_lock lock(mutex_);
+
+  if (server_.get() != NULL)
+  {
+    LOG(WARNING) << "Stopping multitenant DICOM server listening with AET " << server_->GetApplicationEntityTitle()
+                 << " on port: " << server_->GetPortNumber();
+    server_->Stop();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.h	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,69 @@
+/**
+ * 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 "DicomFilter.h"
+#include "PluginEnumerations.h"
+
+#include "../../../../OrthancFramework/Sources/DicomNetworking/DicomServer.h"
+
+#include <boost/thread/mutex.hpp>
+
+
+class MultitenantDicomServer :
+  private Orthanc::DicomServer::IRemoteModalities,
+  private Orthanc::IFindRequestHandlerFactory,
+  private Orthanc::IMoveRequestHandlerFactory,
+  private Orthanc::IStoreRequestHandlerFactory
+{
+private:
+  virtual bool IsSameAETitle(const std::string& aet1,
+                             const std::string& aet2) ORTHANC_OVERRIDE;
+
+  virtual bool LookupAETitle(Orthanc::RemoteModalityParameters& parameters,
+                             const std::string& aet) ORTHANC_OVERRIDE;
+
+  virtual Orthanc::IFindRequestHandler* ConstructFindRequestHandler() ORTHANC_OVERRIDE;
+
+  virtual Orthanc::IMoveRequestHandler* ConstructMoveRequestHandler() ORTHANC_OVERRIDE;
+
+  virtual Orthanc::IStoreRequestHandler* ConstructStoreRequestHandler() ORTHANC_OVERRIDE;
+
+  boost::mutex  mutex_;
+
+  std::set<std::string>                  labels_;
+  LabelsConstraint                       labelsConstraint_;
+  std::set<Orthanc::ResourceType>        labelsStoreLevels_;
+  bool                                   isSynchronousCMove_;
+  bool                                   isStrictAet_;
+  DicomFilter                            filter_;
+  std::unique_ptr<Orthanc::DicomServer>  server_;
+
+public:
+  explicit MultitenantDicomServer(const Json::Value& serverConfig);
+
+  void Start();
+
+  void Stop();
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/NOTES.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,8 @@
+
+Linux Standard Base (LSB)
+=========================
+
+$ mkdir lsb
+$ cd lsb
+$ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../../../../OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake -DUSE_LEGACY_JSONCPP=ON -DUSE_LEGACY_BOOST=ON -G Ninja
+$ ninja
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/OrthancFrameworkDependencies.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -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-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(_WIN32)
+#  if !defined(NOMINMAX)
+#    define NOMINMAX
+#  endif
+// Make sure that "winsock2.h" is included before "winsock.h"
+#  include <winsock2.h>
+#endif
+
+#include "../../../../OrthancFramework/Sources/ChunkedBuffer.cpp"
+#include "../../../../OrthancFramework/Sources/Compression/DeflateBaseCompressor.cpp"
+#include "../../../../OrthancFramework/Sources/Compression/GzipCompressor.cpp"
+#include "../../../../OrthancFramework/Sources/Compression/ZlibCompressor.cpp"
+#include "../../../../OrthancFramework/Sources/DicomFormat/DicomArray.cpp"
+#include "../../../../OrthancFramework/Sources/DicomFormat/DicomElement.cpp"
+#include "../../../../OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp"
+#include "../../../../OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.cpp"
+#include "../../../../OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.cpp"
+#include "../../../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp"
+#include "../../../../OrthancFramework/Sources/DicomFormat/DicomPath.cpp"
+#include "../../../../OrthancFramework/Sources/DicomFormat/DicomTag.cpp"
+#include "../../../../OrthancFramework/Sources/DicomFormat/DicomValue.cpp"
+#include "../../../../OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp"
+#include "../../../../OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.cpp"
+#include "../../../../OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.cpp"
+#include "../../../../OrthancFramework/Sources/DicomNetworking/DicomServer.cpp"
+#include "../../../../OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp"
+#include "../../../../OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.cpp"
+#include "../../../../OrthancFramework/Sources/DicomNetworking/Internals/FindScp.cpp"
+#include "../../../../OrthancFramework/Sources/DicomNetworking/Internals/GetScp.cpp"
+#include "../../../../OrthancFramework/Sources/DicomNetworking/Internals/MoveScp.cpp"
+#include "../../../../OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp"
+#include "../../../../OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.cpp"
+#include "../../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp"
+#include "../../../../OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.cpp"
+#include "../../../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp"
+#include "../../../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp"
+#include "../../../../OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.cpp"
+#include "../../../../OrthancFramework/Sources/Enumerations.cpp"
+#include "../../../../OrthancFramework/Sources/HttpServer/HttpOutput.cpp"
+#include "../../../../OrthancFramework/Sources/Images/IImageWriter.cpp"
+#include "../../../../OrthancFramework/Sources/Images/Image.cpp"
+#include "../../../../OrthancFramework/Sources/Images/ImageAccessor.cpp"
+#include "../../../../OrthancFramework/Sources/Images/ImageBuffer.cpp"
+#include "../../../../OrthancFramework/Sources/Images/ImageProcessing.cpp"
+#include "../../../../OrthancFramework/Sources/Images/JpegErrorManager.cpp"
+#include "../../../../OrthancFramework/Sources/Images/JpegReader.cpp"
+#include "../../../../OrthancFramework/Sources/Images/JpegWriter.cpp"
+#include "../../../../OrthancFramework/Sources/Images/PamReader.cpp"
+#include "../../../../OrthancFramework/Sources/Images/PamWriter.cpp"
+#include "../../../../OrthancFramework/Sources/Images/PngReader.cpp"
+#include "../../../../OrthancFramework/Sources/Images/PngWriter.cpp"
+#include "../../../../OrthancFramework/Sources/Logging.cpp"
+#include "../../../../OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.cpp"
+#include "../../../../OrthancFramework/Sources/MultiThreading/SharedMessageQueue.cpp"
+#include "../../../../OrthancFramework/Sources/OrthancException.cpp"
+#include "../../../../OrthancFramework/Sources/OrthancFramework.cpp"
+#include "../../../../OrthancFramework/Sources/RestApi/RestApiOutput.cpp"
+#include "../../../../OrthancFramework/Sources/SerializationToolbox.cpp"
+#include "../../../../OrthancFramework/Sources/SystemToolbox.cpp"
+#include "../../../../OrthancFramework/Sources/TemporaryFile.cpp"
+#include "../../../../OrthancFramework/Sources/Toolbox.cpp"
+
+namespace Orthanc
+{
+  void HttpClient::GlobalInitialize()
+  {
+  }
+
+  void HttpClient::GlobalFinalize()
+  {
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/Plugin.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,211 @@
+/**
+ * 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/>.
+ **/
+
+
+#define ORTHANC_PLUGIN_NAME "multitenant-dicom"
+
+#include "MultitenantDicomServer.h"
+
+#include "../../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
+#include "../../../../OrthancFramework/Sources/Logging.h"
+#include "../../../../OrthancFramework/Sources/OrthancException.h"
+#include "../../../../OrthancFramework/Sources/OrthancFramework.h"
+
+#include "../Common/OrthancPluginCppWrapper.h"
+
+
+typedef std::list<MultitenantDicomServer*> DicomServers;
+
+static DicomServers dicomServers_;
+
+
+static OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType,
+                                               OrthancPluginResourceType resourceType,
+                                               const char* resourceId)
+{
+  switch (changeType)
+  {
+    case OrthancPluginChangeType_OrthancStarted:
+    {
+      for (DicomServers::iterator it = dicomServers_.begin(); it != dicomServers_.end(); ++it)
+      {    
+        if (*it != NULL)
+        {
+          try
+          {
+            (*it)->Start();
+          }
+          catch (Orthanc::OrthancException& e)
+          {
+            LOG(ERROR) << "Exception while stopping the multitenant DICOM server: " << e.What();
+          }
+        }
+      }
+
+      break;
+    }
+    
+    case OrthancPluginChangeType_OrthancStopped:
+    {
+      for (DicomServers::iterator it = dicomServers_.begin(); it != dicomServers_.end(); ++it)
+      {    
+        if (*it != NULL)
+        {
+          try
+          {
+            (*it)->Stop();
+          }
+          catch (Orthanc::OrthancException& e)
+          {
+            LOG(ERROR) << "Exception while stopping the multitenant DICOM server: " << e.What();
+          }
+        }
+      }
+      
+      break;
+    }
+    
+    default:
+      break;
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+static void MyInitialization(const OrthancPlugins::OrthancConfiguration& config)
+{
+  static const char* const LOCALE = "Locale";
+  static const char* const DEFAULT_ENCODING = "DefaultEncoding";
+
+  /**
+   * This function is a simplified version of function
+   * "Orthanc::OrthancInitialize()" that is executed when starting the
+   * Orthanc server.
+   **/
+
+  Orthanc::InitializeFramework(config.GetStringValue(LOCALE, ""), false /* loadPrivateDictionary */);
+
+  std::string encoding;
+  if (config.LookupStringValue(encoding, DEFAULT_ENCODING))
+  {
+    Orthanc::SetDefaultDicomEncoding(Orthanc::StringToEncoding(encoding.c_str()));
+  }
+  else
+  {
+    Orthanc::SetDefaultDicomEncoding(Orthanc::ORTHANC_DEFAULT_DICOM_ENCODING);
+  }
+}
+
+
+extern "C"
+{
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
+  {
+    OrthancPlugins::SetGlobalContext(context, ORTHANC_PLUGIN_NAME);
+    Orthanc::Logging::InitializePluginContext(context, ORTHANC_PLUGIN_NAME);
+
+    /* Check the version of the Orthanc core */
+    if (OrthancPluginCheckVersion(OrthancPlugins::GetGlobalContext()) == 0)
+    {
+      char info[1024];
+      sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin",
+              OrthancPlugins::GetGlobalContext()->orthancVersion,
+              ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+      OrthancPluginLogError(OrthancPlugins::GetGlobalContext(), info);
+      return -1;
+    }
+
+    OrthancPluginSetDescription2(context, ORTHANC_PLUGIN_NAME, "Multitenant plugin for Orthanc.");
+
+    OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
+    
+    try
+    {
+      OrthancPlugins::OrthancConfiguration globalConfig;
+      MyInitialization(globalConfig);
+
+      OrthancPlugins::OrthancConfiguration pluginConfig;
+      globalConfig.GetSection(pluginConfig, KEY_MULTITENANT_DICOM);
+
+      if (pluginConfig.GetJson().isMember(KEY_SERVERS))
+      {
+        const Json::Value& servers = pluginConfig.GetJson() [KEY_SERVERS];
+
+        if (servers.type() != Json::arrayValue)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType, "Configuration option \"" +
+                                          std::string(KEY_MULTITENANT_DICOM) + "." + std::string(KEY_SERVERS) + "\" must be an array");
+        }
+        else
+        {
+          for (Json::Value::ArrayIndex i = 0; i < servers.size(); i++)
+          {
+            dicomServers_.push_back(new MultitenantDicomServer(servers[i]));
+          }
+        }
+      }
+      
+      return 0;
+    }
+    catch (Orthanc::OrthancException& e)
+    {
+      LOG(ERROR) << "Exception while starting the multitenant DICOM server: " << e.What();
+      return -1;
+    }
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+    for (DicomServers::iterator it = dicomServers_.begin(); it != dicomServers_.end(); ++it)
+    {    
+      if (*it != NULL)
+      {
+        try
+        {
+          delete *it;
+        }
+        catch (Orthanc::OrthancException& e)
+        {
+          LOG(ERROR) << "Exception while destroying the multitenant DICOM server: " << e.What();
+        }
+      }
+    }
+
+    Orthanc::FinalizeFramework();
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return ORTHANC_PLUGIN_NAME;
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return ORTHANC_PLUGIN_VERSION;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/PluginEnumerations.h	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,48 @@
+/**
+ * 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
+
+
+enum LabelsConstraint
+{
+  LabelsConstraint_All,
+  LabelsConstraint_Any,
+  LabelsConstraint_None
+};
+
+
+#define KEY_AET                      "AET"
+#define KEY_ALL                      "All"
+#define KEY_ANY                      "Any"
+#define KEY_LABELS                   "Labels"
+#define KEY_LABELS_CONSTRAINT        "LabelsConstraint"
+#define KEY_LABELS_STORE_LEVELS      "LabelsStoreLevels"
+#define KEY_MAIN_DICOM_TAGS          "MainDicomTags"
+#define KEY_MULTITENANT_DICOM        "MultitenantDicom"
+#define KEY_NONE                     "None"
+#define KEY_PATIENT_MAIN_DICOM_TAGS  "PatientMainDicomTags"
+#define KEY_QUERY                    "Query"
+#define KEY_SERVERS                  "Servers"
+#define KEY_STRICT_AET_COMPARISON    "StrictAetComparison"
+#define KEY_SYNCHRONOUS_C_MOVE       "SynchronousCMove"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/PluginToolbox.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -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-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 "PluginToolbox.h"
+
+#include "../../../../OrthancFramework/Sources/OrthancException.h"
+#include "../../../../OrthancFramework/Sources/SerializationToolbox.h"
+#include "../../../../OrthancFramework/Sources/Toolbox.h"
+
+#include "../Common/OrthancPluginCppWrapper.h"
+
+
+namespace PluginToolbox
+{
+  bool IsValidLabel(const std::string& label)
+  {
+    if (label.empty())
+    {
+      return false;
+    }
+
+    if (label.size() > 64)
+    {
+      // This limitation is for MySQL, which cannot use a TEXT
+      // column of undefined length as a primary key
+      return false;
+    }
+      
+    for (size_t i = 0; i < label.size(); i++)
+    {
+      if (!(label[i] == '_' ||
+            label[i] == '-' ||
+            (label[i] >= 'a' && label[i] <= 'z') ||
+            (label[i] >= 'A' && label[i] <= 'Z') ||
+            (label[i] >= '0' && label[i] <= '9')))
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+
+  Orthanc::ResourceType ParseQueryRetrieveLevel(const std::string& level)
+  {
+    if (level == "PATIENT")
+    {
+      return Orthanc::ResourceType_Patient;
+    }
+    else if (level == "STUDY")
+    {
+      return Orthanc::ResourceType_Study;
+    }
+    else if (level == "SERIES")
+    {
+      return Orthanc::ResourceType_Series;
+    }
+    else if (level == "INSTANCE")
+    {
+      return Orthanc::ResourceType_Instance;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, "Bad value for QueryRetrieveLevel in DICOM C-FIND: " + level);
+    }
+  }
+
+
+  bool IsSameAETitle(bool isStrict,
+                     const std::string& aet1,
+                     const std::string& aet2)
+  {
+    if (isStrict)
+    {
+      // Case-sensitive matching
+      return aet1 == aet2;
+    }
+    else
+    {
+      // Case-insensitive matching (default)
+      std::string tmp1, tmp2;
+      Orthanc::Toolbox::ToLowerCase(tmp1, aet1);
+      Orthanc::Toolbox::ToLowerCase(tmp2, aet2);
+      return tmp1 == tmp2;
+    }
+  }
+
+
+  bool LookupAETitle(std::string& name,
+                     Orthanc::RemoteModalityParameters& parameters,
+                     bool isStrict,
+                     const std::string& aet)
+  {
+    Json::Value modalities;
+    if (!OrthancPlugins::RestApiGet(modalities, "/modalities?expand", false))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Unable to obtain the list of the remote modalities");
+    }
+
+    std::vector<std::string> names = modalities.getMemberNames();
+    for (size_t i = 0; i < names.size(); i++)
+    {
+      parameters = Orthanc::RemoteModalityParameters(modalities[names[i]]);
+      
+      if (IsSameAETitle(isStrict, parameters.GetApplicationEntityTitle(), aet))
+      {
+        name = names[i];
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+
+  void ParseLabels(std::set<std::string>& targetLabels,
+                   LabelsConstraint& targetConstraint,
+                   const Json::Value& serverConfig)
+  {
+    Orthanc::SerializationToolbox::ReadSetOfStrings(targetLabels, serverConfig, KEY_LABELS);
+
+    for (std::set<std::string>::const_iterator it = targetLabels.begin(); it != targetLabels.end(); ++it)
+    {
+      if (!PluginToolbox::IsValidLabel(*it))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, "Invalid label: " + *it);
+      }
+    }
+
+    std::string s = Orthanc::SerializationToolbox::ReadString(serverConfig, KEY_LABELS_CONSTRAINT, KEY_ALL);
+    targetConstraint = PluginToolbox::StringToLabelsConstraint(s);
+  }
+  
+
+  void AddLabelsToFindRequest(Json::Value& request,
+                              const std::set<std::string>& labels,
+                              LabelsConstraint constraint)
+  {
+    Json::Value items = Json::arrayValue;
+    for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
+    {
+      items.append(*it);
+    }
+
+    request[KEY_LABELS] = items;
+
+    switch (constraint)
+    {
+      case LabelsConstraint_All:
+        request[KEY_LABELS_CONSTRAINT] = KEY_ALL;
+        break;
+
+      case LabelsConstraint_Any:
+        request[KEY_LABELS_CONSTRAINT] = KEY_ANY;
+        break;
+
+      case LabelsConstraint_None:
+        request[KEY_LABELS_CONSTRAINT] = KEY_NONE;
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  LabelsConstraint StringToLabelsConstraint(const std::string& s)
+  {
+    if (s == KEY_ALL)
+    {
+      return LabelsConstraint_All;
+    }
+    else if (s == KEY_ANY)
+    {
+      return LabelsConstraint_Any;
+    }
+    else if (s == KEY_NONE)
+    {
+      return LabelsConstraint_None;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, "Bad value for constraint of labels: " + s);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/PluginToolbox.h	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,56 @@
+/**
+ * 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 "PluginEnumerations.h"
+
+#include "../../../../OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h"
+
+#include <json/value.h>
+
+namespace PluginToolbox
+{
+  bool IsValidLabel(const std::string& label);
+  
+  Orthanc::ResourceType ParseQueryRetrieveLevel(const std::string& level);
+  
+  bool IsSameAETitle(bool isStrict,
+                     const std::string& aet1,
+                     const std::string& aet2);
+
+  bool LookupAETitle(std::string& name,
+                     Orthanc::RemoteModalityParameters& parameters,
+                     bool isStrict,
+                     const std::string& aet);
+
+  void ParseLabels(std::set<std::string>& targetLabels,
+                   LabelsConstraint& targetConstraint,
+                   const Json::Value& serverConfig);
+  
+  void AddLabelsToFindRequest(Json::Value& request,
+                              const std::set<std::string>& labels,
+                              LabelsConstraint constraint);
+  
+  LabelsConstraint StringToLabelsConstraint(const std::string& s);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/StoreRequestHandler.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -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-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 "StoreRequestHandler.h"
+
+#include "../../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
+#include "../../../../OrthancFramework/Sources/Logging.h"
+#include "../../../../OrthancFramework/Sources/OrthancException.h"
+#include "../../../../OrthancFramework/Sources/SerializationToolbox.h"
+
+#include "../Common/OrthancPluginCppWrapper.h"
+
+#include <dcmtk/dcmnet/diutil.h>
+
+
+uint16_t StoreRequestHandler::Handle(DcmDataset& dicom,
+                                     const std::string& remoteIp,
+                                     const std::string& remoteAet,
+                                     const std::string& calledAet)
+{
+  std::string buffer;
+  std::string errorMessage;
+
+  if (!Orthanc::FromDcmtkBridge::SaveToMemoryBuffer(buffer, dicom, errorMessage))
+  {
+    LOG(ERROR) << "Cannot write DICOM file to memory: " << errorMessage;
+    return STATUS_STORE_Error_CannotUnderstand;
+  }
+
+  Json::Value info;
+  if (!OrthancPlugins::RestApiPost(info, "/instances", buffer, false))
+  {
+    LOG(ERROR) << "Cannot store the DICOM file";
+    return STATUS_STORE_Refused_OutOfResources;
+  }
+
+  for (std::set<Orthanc::ResourceType>::const_iterator level = levels_.begin(); level != levels_.end(); ++level)
+  {
+    for (std::set<std::string>::const_iterator label = labels_.begin(); label != labels_.end(); ++label)
+    {
+      std::string uri;
+      switch (*level)
+      {
+        case Orthanc::ResourceType_Patient:
+          uri = "/patients/" + Orthanc::SerializationToolbox::ReadString(info, "ParentPatient") + "/labels/" + *label;
+          break;
+
+        case Orthanc::ResourceType_Study:
+          uri = "/studies/" + Orthanc::SerializationToolbox::ReadString(info, "ParentStudy") + "/labels/" + *label;
+          break;
+
+        case Orthanc::ResourceType_Series:
+          uri = "/series/" + Orthanc::SerializationToolbox::ReadString(info, "ParentSeries") + "/labels/" + *label;
+          break;
+
+        case Orthanc::ResourceType_Instance:
+          uri = "/instances/" + Orthanc::SerializationToolbox::ReadString(info, "ID") + "/labels/" + *label;
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      Json::Value tmp;
+      if (!OrthancPlugins::RestApiPut(tmp, uri, std::string(""), false))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Cannot set label");
+      }
+    }
+  }
+
+  return STATUS_Success;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/StoreRequestHandler.h	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,48 @@
+/**
+ * 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/DicomNetworking/IStoreRequestHandler.h"
+
+
+class StoreRequestHandler : public Orthanc::IStoreRequestHandler
+{
+private:
+  // Everything is constant, so no need for a mutex
+  const std::set<std::string> labels_;
+  const std::set<Orthanc::ResourceType> levels_;
+
+public:
+  StoreRequestHandler(const std::set<std::string>& labels,
+                      const std::set<Orthanc::ResourceType>& levels) :
+    labels_(labels),
+    levels_(levels)
+  {
+  }
+
+  virtual uint16_t Handle(DcmDataset& dicom,
+                          const std::string& remoteIp,
+                          const std::string& remoteAet,
+                          const std::string& calledAet) ORTHANC_OVERRIDE;
+};
--- a/OrthancServer/Plugins/Samples/Sanitizer/CMakeLists.txt	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/Sanitizer/CMakeLists.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -51,6 +52,8 @@
     ${ORTHANC_DICOM_SOURCES}
     )
 
+DefineSourceBasenameForTarget(Sanitizer)
+
 target_link_libraries(Sanitizer ${DCMTK_LIBRARIES})
 
 
--- a/OrthancServer/Plugins/Samples/Sanitizer/Plugin.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/Sanitizer/Plugin.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -30,7 +31,7 @@
 #include <string.h>
 #include <iostream>
 
-
+#define ORTHANC_PLUGIN_NAME  "sanitizer"
 
 
 OrthancPluginReceivedInstanceAction ReceivedInstanceCallback(OrthancPluginMemoryBuffer64* modifiedDicomBuffer,
@@ -70,8 +71,8 @@
       return -1;
     }
     
-    OrthancPlugins::LogWarning("Sanitizer plugin is initializing");
-    OrthancPluginSetDescription(c, "Sample plugin to sanitize incoming DICOM instances.");
+    ORTHANC_PLUGINS_LOG_WARNING("Sanitizer plugin is initializing");
+    OrthancPlugins::SetDescription(ORTHANC_PLUGIN_NAME, "Sample plugin to sanitize incoming DICOM instances.");
 
     OrthancPluginRegisterReceivedInstanceCallback(c, ReceivedInstanceCallback);
 
@@ -81,14 +82,14 @@
 
   ORTHANC_PLUGINS_API void OrthancPluginFinalize()
   {
-    OrthancPlugins::LogWarning("Sanitizer plugin is finalizing");
+    ORTHANC_PLUGINS_LOG_WARNING("Sanitizer plugin is finalizing");
     Orthanc::FinalizeFramework();
   }
 
 
   ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
   {
-    return "sanitizer";
+    return ORTHANC_PLUGIN_NAME;
   }
 
 
--- a/OrthancServer/Plugins/Samples/ServeFolders/CMakeLists.txt	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/ServeFolders/CMakeLists.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -40,6 +41,8 @@
   ${BOOST_SOURCES}
   )
 
+DefineSourceBasenameForTarget(ServeFolders)
+
 add_definitions(-DHAS_ORTHANC_EXCEPTION=0)
 
 message("Setting the version of the plugin to ${SERVE_FOLDERS_VERSION}")
--- a/OrthancServer/Plugins/Samples/ServeFolders/Plugin.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/ServeFolders/Plugin.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -20,6 +21,8 @@
  **/
 
 
+#define SERVE_FOLDERS_NAME "serve-folders"
+
 #include "../Common/OrthancPluginCppWrapper.h"
 
 #include <json/value.h>
@@ -92,7 +95,7 @@
   }
   else
   {
-    OrthancPlugins::LogWarning("ServeFolders: Unknown MIME type for extension \"" + extension + "\"");
+    ORTHANC_PLUGINS_LOG_WARNING("ServeFolders: Unknown MIME type for extension \"" + extension + "\"");
     return "application/octet-stream";
   }
 }
@@ -107,7 +110,7 @@
   std::map<std::string, std::string>::const_iterator found = folders_.find(uri);
   if (found == folders_.end())
   {
-    OrthancPlugins::LogError("Unknown URI in plugin server-folders: " + uri);
+    ORTHANC_PLUGINS_LOG_ERROR("Unknown URI in plugin server-folders: " + uri);
     OrthancPluginSendHttpStatusCode(OrthancPlugins::GetGlobalContext(), output, 404);
     return false;
   }
@@ -263,7 +266,7 @@
 {
   if (folders.type() != Json::objectValue)
   {
-    OrthancPlugins::LogError("The list of folders to be served is badly formatted (must be a JSON object)");
+    ORTHANC_PLUGINS_LOG_ERROR("The list of folders to be served is badly formatted (must be a JSON object)");
     ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
   }
 
@@ -275,8 +278,8 @@
   {
     if (folders[*it].type() != Json::stringValue)
     {
-      OrthancPlugins::LogError("The folder to be served \"" + *it + 
-                               "\" must be associated with a string value (its mapped URI)");
+      ORTHANC_PLUGINS_LOG_ERROR("The folder to be served \"" + *it + 
+                                "\" must be associated with a string value (its mapped URI)");
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
 
@@ -297,7 +300,7 @@
 
     if (baseUri.empty())
     {
-      OrthancPlugins::LogError("The URI of a folder to be served cannot be empty");
+      ORTHANC_PLUGINS_LOG_ERROR("The URI of a folder to be served cannot be empty");
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
 
@@ -305,7 +308,7 @@
     const std::string folder = folders[*it].asString();
     if (!boost::filesystem::is_directory(folder))
     {
-      OrthancPlugins::LogError("Trying to serve an inexistent folder: " + folder);
+      ORTHANC_PLUGINS_LOG_ERROR("Trying to serve an inexistent folder: " + folder);
       ORTHANC_PLUGINS_THROW_EXCEPTION(InexistentFile);
     }
 
@@ -324,7 +327,7 @@
 {
   if (extensions.type() != Json::objectValue)
   {
-    OrthancPlugins::LogError("The list of extensions is badly formatted (must be a JSON object)");
+    ORTHANC_PLUGINS_LOG_ERROR("The list of extensions is badly formatted (must be a JSON object)");
     ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
   }
 
@@ -335,8 +338,8 @@
   {
     if (extensions[*it].type() != Json::stringValue)
     {
-      OrthancPlugins::LogError("The file extension \"" + *it + 
-                               "\" must be associated with a string value (its MIME type)");
+      ORTHANC_PLUGINS_LOG_ERROR("The file extension \"" + *it + 
+                                "\" must be associated with a string value (its MIME type)");
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
 
@@ -354,13 +357,13 @@
 
     if (mime.empty())
     {
-      OrthancPlugins::LogWarning("ServeFolders: Removing MIME type for file extension \"." +
-                                 name + "\"");
+      ORTHANC_PLUGINS_LOG_WARNING("ServeFolders: Removing MIME type for file extension \"." +
+                                  name + "\"");
     }
     else
     {
-      OrthancPlugins::LogWarning("ServeFolders: Associating file extension \"." + name + 
-                                 "\" with MIME type \"" + mime + "\"");
+      ORTHANC_PLUGINS_LOG_WARNING("ServeFolders: Associating file extension \"." + name +
+                                  "\" with MIME type \"" + mime + "\"");
     }
   }  
 }
@@ -390,17 +393,16 @@
     if (configuration.LookupBooleanValue(tmp, "AllowCache"))
     {
       allowCache_ = tmp;
-      OrthancPlugins::LogWarning("ServeFolders: Requesting the HTTP client to " +
-                                 std::string(tmp ? "enable" : "disable") + 
-                                 " its caching mechanism");
+      ORTHANC_PLUGINS_LOG_WARNING("ServeFolders: Requesting the HTTP client to " +
+                                  std::string(tmp ? "enable" : "disable") +
+                                  " its caching mechanism");
     }
 
     if (configuration.LookupBooleanValue(tmp, "GenerateETag"))
     {
       generateETag_ = tmp;
-      OrthancPlugins::LogWarning("ServeFolders: The computation of an ETag for the "
-                                 "served resources is " +
-                                 std::string(tmp ? "enabled" : "disabled"));
+      ORTHANC_PLUGINS_LOG_WARNING("ServeFolders: The computation of an ETag for the served resources is " +
+                                  std::string(tmp ? "enabled" : "disabled"));
     }
 
     OrthancPlugins::OrthancConfiguration extensions;
@@ -410,8 +412,7 @@
 
   if (folders_.empty())
   {
-    OrthancPlugins::LogWarning("ServeFolders: Empty configuration file: "
-                               "No additional folder will be served!");
+    ORTHANC_PLUGINS_LOG_WARNING("ServeFolders: Empty configuration file: No additional folder will be served!");
   }
 }
 
@@ -420,7 +421,7 @@
 {
   ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
   {
-    OrthancPlugins::SetGlobalContext(context);
+    OrthancPlugins::SetGlobalContext(context, SERVE_FOLDERS_NAME);
 
     /* Check the version of the Orthanc core */
     if (OrthancPluginCheckVersion(context) == 0)
@@ -432,8 +433,8 @@
     }
 
     RegisterDefaultExtensions();
-    OrthancPluginSetDescription(context, "Serve additional folders with the HTTP server of Orthanc.");
-    OrthancPluginSetRootUri(context, INDEX_URI);
+    OrthancPluginSetDescription2(context, SERVE_FOLDERS_NAME, "Serve additional folders with the HTTP server of Orthanc.");
+    OrthancPluginSetRootUri2(context, SERVE_FOLDERS_NAME, INDEX_URI);
     OrthancPlugins::RegisterRestCallback<ListServedFolders>(INDEX_URI, true);
 
     try
@@ -442,8 +443,8 @@
     }
     catch (OrthancPlugins::PluginException& e)
     {
-      OrthancPlugins::LogError("Error while initializing the ServeFolders plugin: " + 
-                               std::string(e.What(context)));
+      ORTHANC_PLUGINS_LOG_ERROR("Error while initializing the ServeFolders plugin: " +
+                                std::string(e.What(context)));
     }
 
     return 0;
@@ -457,7 +458,7 @@
 
   ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
   {
-    return "serve-folders";
+    return SERVE_FOLDERS_NAME;
   }
 
 
--- a/OrthancServer/Plugins/Samples/ServeFolders/README	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/ServeFolders/README	Tue Sep 24 11:39:52 2024 +0200
@@ -8,4 +8,4 @@
 build process of Orthanc.
 
 Documentation is available in the Orthanc Book:
-http://book.orthanc-server.com/plugins/serve-folders.html
+https://orthanc.uclouvain.be/book/plugins/serve-folders.html
--- a/OrthancServer/Plugins/Samples/StorageArea/CMakeLists.txt	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/StorageArea/CMakeLists.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancServer/Plugins/Samples/StorageArea/Plugin.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/StorageArea/Plugin.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Plugins/Samples/StorageCommitmentScp/CMakeLists.txt	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/StorageCommitmentScp/CMakeLists.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -40,6 +41,8 @@
   ${BOOST_SOURCES}
   )
 
+DefineSourceBasenameForTarget(StorageCommitmentScp)
+
 message("Setting the version of the plugin to ${PLUGIN_VERSION}")
 add_definitions(
   -DPLUGIN_VERSION="${PLUGIN_VERSION}"
--- a/OrthancServer/Plugins/Samples/StorageCommitmentScp/Plugin.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/StorageCommitmentScp/Plugin.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -24,6 +25,8 @@
 
 #include <json/value.h>
 
+#define ORTHANC_PLUGIN_NAME  "storage-commitment-scp"
+
 
 class StorageCommitmentSample : public OrthancPlugins::IStorageCommitmentScpHandler
 {
@@ -86,7 +89,7 @@
       return -1;
     }
 
-    OrthancPluginSetDescription(c, "Sample storage commitment SCP plugin.");
+    OrthancPlugins::SetDescription(ORTHANC_PLUGIN_NAME, "Sample storage commitment SCP plugin.");
 
     OrthancPluginRegisterStorageCommitmentScpCallback(
       c, StorageCommitmentScp,
@@ -104,7 +107,7 @@
 
   ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
   {
-    return "storage-commitment-scp";
+    return ORTHANC_PLUGIN_NAME;
   }
 
 
--- a/OrthancServer/Plugins/Samples/WebDavFilesystem/CMakeLists.txt	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/WebDavFilesystem/CMakeLists.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -40,3 +41,5 @@
   ${JSONCPP_SOURCES}
   ${BOOST_SOURCES}
   )
+
+DefineSourceBasenameForTarget(WebDavFilesystem)
--- a/OrthancServer/Plugins/Samples/WebDavFilesystem/Plugin.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/WebDavFilesystem/Plugin.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -164,7 +165,7 @@
 
     if (content_.find(name) != content_.end())
     {
-      OrthancPlugins::LogError("Already existing: " + name);
+      ORTHANC_PLUGINS_LOG_ERROR("Already existing: " + name);
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadRequest);
     }
     else
@@ -177,7 +178,7 @@
   {
     if (content_.find(name) != content_.end())
     {
-      OrthancPlugins::LogError("Already existing: " + name);
+      ORTHANC_PLUGINS_LOG_ERROR("Already existing: " + name);
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadRequest);
     }
     else
@@ -192,7 +193,7 @@
 
     if (found == content_.end())
     {
-      OrthancPlugins::LogError("Cannot delete inexistent path: " + name);
+      ORTHANC_PLUGINS_LOG_ERROR("Cannot delete inexistent path: " + name);
       ORTHANC_PLUGINS_THROW_EXCEPTION(InexistentItem);
     }
     else
@@ -215,7 +216,7 @@
   {
     if (path.empty())
     {
-      OrthancPlugins::LogError("Empty path");
+      ORTHANC_PLUGINS_LOG_ERROR("Empty path");
       ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
     }
     else
--- a/OrthancServer/Plugins/Samples/WebSkeleton/CMakeLists.txt	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/WebSkeleton/CMakeLists.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -32,3 +33,5 @@
 add_library(WebSkeleton SHARED 
   ${AUTOGENERATED_SOURCES}
   )
+
+DefineSourceBasenameForTarget(WebSkeleton)
--- a/OrthancServer/Plugins/Samples/WebSkeleton/Configuration.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/WebSkeleton/Configuration.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Plugins/Samples/WebSkeleton/Framework/EmbedResources.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/WebSkeleton/Framework/EmbedResources.py	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -224,7 +225,7 @@
 
 cpp = open(TARGET_BASE_FILENAME + '.cpp', 'w')
 
-print os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
+print(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
 
 cpp.write("""
 #include "%s.h"
--- a/OrthancServer/Plugins/Samples/WebSkeleton/Framework/Framework.cmake	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/WebSkeleton/Framework/Framework.cmake	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -33,7 +34,7 @@
     "${AUTOGENERATED_DIR}/EmbeddedResources.h"
     "${AUTOGENERATED_DIR}/EmbeddedResources.cpp"
     COMMAND 
-    python
+    ${PYTHON_EXECUTABLE}
     "${CMAKE_CURRENT_SOURCE_DIR}/Framework/EmbedResources.py"
     "${AUTOGENERATED_DIR}/EmbeddedResources"
     STATIC_RESOURCES
--- a/OrthancServer/Plugins/Samples/WebSkeleton/Framework/Plugin.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Plugins/Samples/WebSkeleton/Framework/Plugin.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Resources/Configuration.json	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Configuration.json	Tue Sep 24 11:39:52 2024 +0200
@@ -1,6 +1,11 @@
 {
   /**
    * General configuration of Orthanc
+   *
+   * As a general recommendation, you should always favour absolute 
+   * paths when you need to provide a path to a file or a directory.  
+   * This is not done in this configuration file since we don't know 
+   * where Orthanc is going to be installed.
    **/
 
   // The logical name of this instance of Orthanc. This one is
@@ -49,8 +54,10 @@
   // By default, the patients are recycled ("Recycle" mode).
   // In "Reject" mode, the sender will receive a 0xA700 DIMSE status code
   // if the instance was sent through C-Store, a 507 HTTP status code
-  // if using the Rest API and a 0xA700 Failure reason when using
-  // DicomWeb Stow-RS 
+  // if using the REST API and a 0xA700 Failure reason when using
+  // DicomWeb Stow-RS.
+  // Note: this value is taken into account only if you have set 
+  // a MaximumStorageSize != 0 or a MaximumPatientCount != 0
   // Allowed values: "Recycle", "Reject"
   // (new in Orthanc 1.11.2)
   "MaximumStorageMode" : "Recycle",
@@ -69,7 +76,7 @@
   // The period (in seconds) between 2 calls of the "OnHeartBeat"
   // lua callback.  O means the heart beat is disabled.
   // TODO: text below for Orthanc book:
-  // Note: that the period is not actually the delay between
+  // Note: that the period is actually not the delay between
   // the end of an execution and the triggering of the next one.
   // Since there is only one lua context, if other lua code is being
   // executed, the heart beat might be delayed even more.
@@ -90,6 +97,13 @@
   // this value to "1".
   "ConcurrentJobs" : 2,
 
+  // Defines the number of threads that are used to execute each type of
+  // jobs (for the jobs that can be parallelized).
+  // A value of "0" indicates to use all the available CPU logical cores.
+  // (new in Orthanc 1.11.3)
+  "JobsEngineThreadsCount" : {
+    "ResourceModification": 1     // for /anonymize, /modify
+  },
 
   /**
    * Configuration of the HTTP server
@@ -117,7 +131,12 @@
   // Enable HTTP compression to improve network bandwidth utilization,
   // at the expense of more computations on the server. Orthanc
   // supports the "gzip" and "deflate" HTTP encodings.
-  "HttpCompressionEnabled" : true,
+  // When working on a LAN or on localhost, you should typically set
+  // this configuration to false while when working on low-bandwidth,
+  // you should set it to true.
+  // Note in versions up to 1.12.1, the default value was "true" and is
+  // "false" since 1.12.2.
+  "HttpCompressionEnabled" : false,
 
   // Enable the publication of the content of the Orthanc server as a
   // WebDAV share (new in Orthanc 1.8.0). On the localhost, the WebDAV
@@ -190,6 +209,7 @@
   // 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,
 
 
@@ -210,23 +230,24 @@
   // if "SslEnabled" is true.
   "SslCertificate" : "certificate.pem",
 
-  // Sets the minimum accepted SSL protocol version
+  // Sets the minimum accepted SSL protocol version for the HTTP server
   // (cf. "ssl_protocol_version" option of civetweb). By default,
-  // require SSL 1.2. This option is only meaningful if "SslEnabled"
+  // require TLS 1.2 or 1.3. This option is only meaningful if "SslEnabled"
   // is true. (new in Orthanc 1.8.2)
   //
   // Value => Protocols
-  //   0      SSL2+SSL3+TLS1.0+TLS1.1+TLS1.2
-  //   1      SSL3+TLS1.0+TLS1.1+TLS1.2
-  //   2      TLS1.0+TLS1.1+TLS1.2
-  //   3      TLS1.1+TLS1.2
-  //   4      TLS1.2
+  //   0      SSL2+SSL3+TLS1.0+TLS1.1+TLS1.2+TLS1.3
+  //   1      SSL3+TLS1.0+TLS1.1+TLS1.2+TLS1.3
+  //   2      TLS1.0+TLS1.1+TLS1.2+TLS1.3
+  //   3      TLS1.1+TLS1.2+TLS1.3
+  //   4      TLS1.2+TLS1.3
+  //   5      TLS1.3
   "SslMinimumProtocolVersion" : 4,
 
-  // Set the accepted ciphers for SSL connections. The ciphers must be
-  // provided as a list of strings. If not set, this will default to
-  // FIPS 140-2 ciphers. This option is only meaningful if
-  // "SslEnabled" is true. (new in Orthanc 1.8.2)
+  // Set the accepted ciphers for SSL connections for the HTTP server. 
+  // The ciphers must be provided as a list of strings. If not set, 
+  // this will default to FIPS 140-2 ciphers. This option is only 
+  // meaningful if "SslEnabled" is true. (new in Orthanc 1.8.2)
   /**
     "SslCiphersAccepted" : [ "AES128-GCM-SHA256" ],
   **/
@@ -236,10 +257,10 @@
   "SslVerifyPeers" : false,
 
   // Path to a file containing the concatenation of the client SSL
-  // certificate(s) that are trusted to verify the identify of remote
-  // HTTP clients. The individual certificate(s) must be stored in the
-  // PEM format. This option is only meaningful if "SslVerifyPeers"
-  // is true.
+  // certificate(s) that are trusted to verify the identity of remote
+  // HTTP clients. The individual certificate(s) or root CAs must be 
+  // stored in the PEM format. This option is only meaningful 
+  // if "SslVerifyPeers" is true.  
   "SslTrustedClientCertificates" : "trustedClientCertificates.pem",
   
   // Whether or not the password protection is enabled (using HTTP
@@ -293,10 +314,41 @@
   // Whether Orthanc rejects DICOM TLS connections to/from remote
   // modalities that do not provide a certificate. Setting this option
   // to "true" (resp. "false") corresponds to "--require-peer-cert"
-  // (resp. "--verify-peer-cert") in the DCMTK command-line
+  // (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
+  // By default, require TLS 1.2 or 1.3. This option is only meaningful 
+  // if "DicomTlsEnabled" is true (new in Orthanc 1.12.4).
+  // Note that, internally, Orthanc is configured to use the BCP195 profile
+  // by default.  As soon as you switch to another protocol version, you
+  // must also provide the list of supported cipher suites.
+  // This configuration applies to Orthanc acting both as SCU and SCP.
+  // Value => Protocols
+  //   0      use default BCP 195 profile and default cipher suites
+  //   1      SSL3+TLS1.0+TLS1.1+TLS1.2+TLS1.3
+  //   2      TLS1.0+TLS1.1+TLS1.2+TLS1.3
+  //   3      TLS1.1+TLS1.2+TLS1.3
+  //   4      TLS1.2+TLS1.3
+  //   5      TLS1.3
+  "DicomTlsMinimumProtocolVersion" : 0,
+
+  // Set the accepted ciphers for TLS connections for the DICOM server. 
+  // The ciphers must be provided as a list of strings. If not set, 
+  // this will default to BCP195 ciphers if DicomTlsMinimumProtocolVersion is 0
+  // or to an empty list for other values. This option is only 
+  // meaningful if "DicomTlsEnabled" is true. (new in Orthanc 1.12.4).
+  // This configuration must be provided if DicomTlsMinimumProtocolVersion != 0.
+  // The list of valid cipher names are available in 
+  // https://www.openssl.org/docs/man3.3/man1/openssl-ciphers.html
+  // The OpenSSL names are used.
+  /**
+     "DicomTlsCiphersAccepted" : []
+  **/
+  
   // Whether the Orthanc SCP allows incoming C-ECHO requests, even
   // from SCU modalities it does not know about (i.e. that are not
   // listed in the "DicomModalities" option above). Orthanc 1.3.0
@@ -425,8 +477,8 @@
     //  "AllowStore" : true,
     //  "AllowStorageCommitment" : false,  // new in 1.6.0
     //  "AllowTranscoding" : true,         // new in 1.7.0
-    //  "UseDicomTls" : false              // new in 1.9.0
-    //  "LocalAet" : "HELLO"               // new in 1.9.0
+    //  "UseDicomTls" : false,             // new in 1.9.0
+    //  "LocalAet" : "HELLO",              // new in 1.9.0
     //  "Timeout" : 60                     // new in 1.9.1
     //}
   },
@@ -444,6 +496,7 @@
   // 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
@@ -460,11 +513,15 @@
 
   // Number of threads that are used by the embedded DICOM server.
   // This defines the number of concurrent DICOM operations that can
-  // be run. Note: This is not limiting the number of concurrent
-  // connections. With a single thread, if a C-Find is received during
-  // e.g the transcoding of an incoming C-Store, it will have to wait
-  // until the end of the C-Store before being processed. (new in
-  // Orthanc 1.10.0, before this version, the value was fixed to 4)
+  // be run when Orthanc is acting as SCP. 
+  // Note: This is not limiting the number of concurrent connections
+  // but the number of concurrent DICOM operations.
+  // E.g, with a single thread, if a C-Find is received during
+  // e.g the transcoding of an incoming C-Store, the C-Find will 
+  // be processed only at the end of the C-Store operation but both
+  // DICOM assocations will remain active. 
+  // (new in Orthanc 1.10.0, before this version, the value was 
+  // fixed to 4)
   "DicomThreadsCount" : 4,
 
   // The list of the known Orthanc peers. This option is ignored if
@@ -600,6 +657,10 @@
   // Mongoose.
   "KeepAlive" : true,
 
+  // Defines the Keep-Alive timeout in seconds.
+  // (new in Orthanc 1.11.3)
+  "KeepAliveTimeout" : 1,
+
   // Enable or disable Nagle's algorithm. Only taken into
   // consideration if Orthanc is compiled to use CivetWeb. Experiments
   // show that best performance can be obtained by setting both
@@ -754,6 +815,11 @@
   // with Orthanc 1.5.8, this URI is disabled by default for security.
   "ExecuteLuaEnabled" : false,
 
+  // Whether the REST API can write to the filesystem (e.g. in 
+  // /instances/../export route). Starting with Orthanc 1.12.0, 
+  // this URI is disabled by default for security.
+  "RestApiWriteToFileSystemEnabled": false,
+
   // Set the timeout while serving HTTP requests by the embedded Web
   // server, in seconds. This corresponds to option
   // "request_timeout_ms" of Mongoose/Civetweb. It will set the socket
@@ -835,9 +901,9 @@
 
   // If "DeidentifyLogs" is true, this sets the DICOM standard to
   // follow for the deidentification/anonymization of the query
-  // contents. Possible values are "2008", "2017c" and "2021b" (new
-  // in Orthanc 1.8.2)
-  "DeidentifyLogsDicomVersion" : "2021b",
+  // contents. Possible values are "2008", "2017c", "2021b" (new
+  // in Orthanc 1.8.2), and "2023b" (new in Orthanc 1.12.1)
+  "DeidentifyLogsDicomVersion" : "2023b",
 
   // Maximum length of the PDU (Protocol Data Unit) in the DICOM
   // network protocol, expressed in bytes. This value affects both
@@ -884,7 +950,8 @@
   "ZipLoaderThreads": 0,
 
   // Extra Main Dicom tags that are stored in DB together with all default
-  // Main Dicom tags that are already stored (TODO: see book new page). 
+  // Main Dicom tags that are already stored.
+  // see https://orthanc.uclouvain.be/book/faq/main-dicom-tags.html 
   // (new in Orthanc 1.11.0)
   // Sequences tags are not supported.
   /**
@@ -909,7 +976,7 @@
 
   // Enables/disables warnings in the logs.
   // "true" enables a warning.  All warnings are enabled by default
-  // TODO: see book new page
+  // see https://orthanc.uclouvain.be/book/faq/main-dicom-tags.html#warnings
   // (new in Orthanc 1.11.0)
   "Warnings" : {
     // A "RequestedTags" has been read from storage which is slower than
@@ -922,8 +989,12 @@
     // 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.  TODO: also check for "rebuild DB" 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
   }
 
 }
--- a/OrthancServer/Resources/DicomConformanceStatement.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/DicomConformanceStatement.py	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancServer/Resources/Fonts/GenerateFont.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Fonts/GenerateFont.py	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancServer/Resources/GenerateAnonymizationProfile.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/GenerateAnonymizationProfile.py	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -26,7 +27,7 @@
 import xml.etree.ElementTree as ET
 
 # Usage:
-# ./GenerateAnonymizationProfile.py https://raw.githubusercontent.com/jodogne/dicom-specification/master/2021b/part15.xml
+# ./GenerateAnonymizationProfile.py https://raw.githubusercontent.com/jodogne/dicom-specification/master/2023b/part15.xml > ../../OrthancFramework/Sources/DicomParsing/DicomModification_Anonymization2023b.impl.h
 
 if len(sys.argv) != 2:
     raise Exception('Please provide the path or the URL to the part15.xml file from the DICOM standard')
--- a/OrthancServer/Resources/Graveyard/DatabaseOptimizations/LookupIdentifierQuery.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Graveyard/DatabaseOptimizations/LookupIdentifierQuery.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Resources/Graveyard/DatabaseOptimizations/LookupIdentifierQuery.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Graveyard/DatabaseOptimizations/LookupIdentifierQuery.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Resources/Graveyard/DatabaseOptimizations/LookupResource.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Graveyard/DatabaseOptimizations/LookupResource.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Resources/Graveyard/DatabaseOptimizations/LookupResource.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Graveyard/DatabaseOptimizations/LookupResource.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Resources/Graveyard/DatabasePluginSample/Database.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Graveyard/DatabasePluginSample/Database.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Resources/Graveyard/DatabasePluginSample/Database.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Graveyard/DatabasePluginSample/Database.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Resources/Graveyard/DatabasePluginSample/DatabaseWrapperBase.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Graveyard/DatabasePluginSample/DatabaseWrapperBase.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Resources/Graveyard/DatabasePluginSample/DatabaseWrapperBase.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Graveyard/DatabasePluginSample/DatabaseWrapperBase.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Resources/Graveyard/DatabasePluginSample/Plugin.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Graveyard/DatabasePluginSample/Plugin.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/ImplementationNotes/memory_consumption.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,149 @@
+In Orthanc 1.11.3, we have introduced a Housekeeper thread that 
+tries to give back unused memory back to the system.  This is implemented 
+by calling malloc_trim every 30s (note: on 1.11.3 and 1.12.0, the interval
+was 100ms which caused high idle CPU load).
+
+
+Here is how we validated the effect of this new feature:
+-------------------------------------------------------
+
+We compared the behaviour of 2 osimis/orthanc Docker images from the mainline
+on Feb 1st 2023.  One image without the call to malloc_trim and the other with
+this call.
+
+
+1st test: unconstrained Docker containers
+.........................................
+
+5 large studies are uploaded to each instance of Orthanc (around 1GB in total).
+A script triggers anonymization of these studies as quick as possible.
+We compare the memory used by the containers after 2 minutes of execution 
+(using `docker stats`):
+- without malloc_trim:                   1500 MB
+- with malloc_trim:                       410 MB
+
+
+2nd test: memory constrained Docker containers
+..............................................
+
+Each Orthanc container is limited to 400MB (through the docker-compose configuration
+`mem_limit: 400m`)
+5 large studies are uploaded to each instance of Orthanc (around 1GB in total).
+Each study is anonymized manually, one by one and then, we repeat the operation.
+We compare the memory used by the containers after each anonymization 
+(using `docker stats`):
+            
+# study            without malloc_trim         with_malloc_trim
+0                       ~   50 MB                     ~   50 MB
+1                       ~  140 MB                     ~  140 MB
+2                       ~  390 MB                     ~  340 MB
+3                       ~  398 MB                     ~  345 MB
+4                  out-of-memory crash                ~  345 MB
+5..20                                                 ~  380 MB (stable)
+
+
+3rd test: memory constrained Docker containers
+..............................................
+
+In this last test, we lowered the memory allocation to 300MB and have been able to
+run the first test script for at least 7 minutes (we did not try longer !).  The 
+consumed memory is most of the time around 99% but it seems that the memory constrain
+is handled correctly.  Note that, in this configuration, 128 MB are used by the Dicom
+Cache.
+
+The same test without malloc_trim could never run for more than 35 seconds.
+
+
+4th test: performance impact of malloc_trim and available memory
+................................................................
+
+In this test, we have measured the time required to anonymize a 2000 instances study
+with various configurations.  It appears that malloc_trim or the total amount
+of memory available in the system has no significant impact on performance.
+
+- No malloc trim, 300 MB in the system:       ~ 38s
+- No malloc trim, 1500 MB in the system:      ~ 38s
+- With malloc trim, 300 MB in the system:     ~ 38s
+- With malloc trim, 1500 MB in the system:    ~ 38s
+
+
+Conclusion: 
+----------
+
+The use of malloc_trim reduces the overall memory consumption of Orthanc
+and avoids some of the out-of-memory situations.
+
+However, it does not guarantee that Orthanc will never reach a
+out-of-memory error, especially on very constrained systems.  
+
+Depending on the allocation pattern, the Orthanc memory can get
+very fragmented and increase regularly since malloc_trim only releases memory
+at the end of each of malloc arena.  However, note that, even long before the 
+introduction of malloc_trim, we have observed Orthanc instances running for years
+without ever reaching out-of-memory errors and Orthanc is usually considered as
+very stable.
+
+Moreover, before each release, Orthanc integration tests are run against Valgrind
+and no memory leaks have been identified.
+
+
+malloc_trim documentation
+-------------------------
+
+from (https://stackoverflow.com/questions/40513716/malloc-trim0-releases-fastbins-of-thread-arenas)
+
+    If possible, gives memory back to the system (via negative
+    arguments to sbrk) if there is unused memory at the `high' end of
+    the malloc pool. You can call this after freeing large blocks of
+    memory to potentially reduce the system-level memory requirements
+    of a program. However, it cannot guarantee to reduce memory. Under
+    some allocation patterns, some large free blocks of memory will be
+    locked between two used chunks, so they cannot be given back to
+    the system.
+
+    The `pad' argument to malloc_trim represents the amount of free
+    trailing space to leave untrimmed. If this argument is zero,
+    only the minimum amount of memory to maintain internal data
+    structures will be left (one page or less). Non-zero arguments
+    can be supplied to maintain enough trailing space to service
+    future expected allocations without having to re-obtain memory
+    from the system.
+
+    Malloc_trim returns 1 if it actually released any memory, else 0.
+    On systems that do not support "negative sbrks", it will always
+    return 0.
+
+
+glibc internals
+---------------
+
+Lots of useful info here: https://man7.org/linux/man-pages/man3/mallopt.3.html
+
+summary:
+- malloc uses sbrk() or mmap() to allocate memory.  mmap() is used to allocate 
+  large memory chunks, larger than M_MMAP_THRESHOLD.
+- about mmap(): On the other hand, there are some disadvantages to
+              the use of mmap(2): deallocated space is not placed on the
+              free list for reuse by later allocations; memory may be
+              wasted because mmap(2) allocations must be page-aligned;
+              and the kernel must perform the expensive task of zeroing
+              out memory allocated via mmap(2).  Balancing these factors
+              leads to a default setting of 128*1024 for the
+              M_MMAP_THRESHOLD parameter.
+- free() employs sbrk() to release memory back to the system and M_TRIM_THRESHOLD
+  specifies the minimum size that is released.  So, even without
+  malloc_trim, Orthanc is able to give back memory to the system.
+- free() never gives back block allocated by mmap() to the system, only malloc_trim() does !
+
+UPDATE on June 2023:
+-------------------
+
+Given this discussion: https://discourse.orthanc-server.org/t/onchange-callbacks-and-cpu-loads/3534,
+changed the interval from 100ms to 30s.
+We also added a metrics to monitor the duration: orthanc_memory_trimming_duration_ms
+
+Good reference article:
+https://www.algolia.com/blog/engineering/when-allocators-are-hoarding-your-precious-memory/
+
+
+
--- a/OrthancServer/Resources/OrthancPlugin.doxygen	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/OrthancPlugin.doxygen	Tue Sep 24 11:39:52 2024 +0200
@@ -1944,7 +1944,8 @@
 # recursively expanded use the := operator instead of the = operator.
 # This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
 
-PREDEFINED             = ORTHANC_PLUGIN_INLINE=
+PREDEFINED             = ORTHANC_PLUGIN_INLINE= \
+                         ORTHANC_PLUGIN_DEPRECATED=
 
 # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
 # tag can be used to specify a list of macro names that should be expanded. The
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/PreventProtobufDirectoryLeaks.py	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,41 @@
+#!/usr/bin/python
+
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2023 Osimis S.A., Belgium
+# Copyright (C) 2024-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/>.
+
+
+import sys
+
+if len(sys.argv) != 2:
+    raise Exception('Bad number of arguments in %s' % sys.argv[0])
+
+with open(sys.argv[1], 'r') as f:
+    s = f.read()
+
+s = s.replace('__FILE__', '__ORTHANC_FILE__')
+
+s = """
+#if !defined(__ORTHANC_FILE__)
+#  define __ORTHANC_FILE__ __FILE__
+#endif
+""" + s
+
+with open(sys.argv[1], 'w') as f:
+    f.write(s)
--- a/OrthancServer/Resources/RunCppCheck.sh	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/RunCppCheck.sh	Tue Sep 24 11:39:52 2024 +0200
@@ -12,31 +12,33 @@
 constParameter:../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp
 knownArgument:../../OrthancFramework/UnitTestsSources/ImageTests.cpp
 knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp
-nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:322
-stlFindInsert:../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1403
-stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:165
-stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:73
-stlFindInsert:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:372
-stlFindInsert:../../OrthancServer/Sources/OrthancWebDav.cpp:377
-stlFindInsert:../../OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp:40
-stlFindInsert:../../OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp:190
-syntaxError:../../OrthancFramework/Sources/SQLite/FunctionContext.h:52
-syntaxError:../../OrthancFramework/UnitTestsSources/DicomMapTests.cpp:72
-syntaxError:../../OrthancFramework/UnitTestsSources/ZipTests.cpp:132
-syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:310
-uninitMemberVar:../../OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp:416
+nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:316
+stlFindInsert:../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1477
+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/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:../../OrthancFramework/Sources/FileStorage/StorageAccessor.cpp
-unreadVariable:../../OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp:1115
+unreadVariable:../../OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp:1123
 unusedFunction
-useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:90
-useInitializationList:../../OrthancFramework/Sources/Images/PngWriter.cpp:98
-useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:274
-assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:276
-assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:1018
-assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:289
-assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:388
-assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3460
-assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:241
+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/ServerJobs/ResourceModificationJob.cpp:286
+assertWithSideEffect:../../OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp:454
 EOF
 
 ${CPPCHECK} --enable=all --quiet --std=c++11 \
@@ -48,6 +50,7 @@
             -DCIVETWEB_HAS_WEBDAV_WRITING=1 \
             -DDCMTK_VERSION_NUMBER=365 \
             -DHAVE_MALLOPT=1 \
+            -DHAVE_MALLOC_TRIM=1 \
             -DMONGOOSE_USE_CALLBACKS=1 \
             -DJSONCPP_VERSION_MAJOR=1 \
             -DJSONCPP_VERSION_MINOR=0 \
@@ -86,6 +89,7 @@
             -D__cplusplus=201103 \
             -D__linux__ \
             -UNDEBUG \
+            -DHAS_ORTHANC_EXCEPTION=1 \
             \
             ../../OrthancFramework/Sources \
             ../../OrthancFramework/UnitTestsSources \
@@ -93,5 +97,11 @@
             ../../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/Samples/CppHelpers/Logging/ILogger.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/CppHelpers/Logging/ILogger.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Resources/Samples/CppHelpers/Logging/NullLogger.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/CppHelpers/Logging/NullLogger.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Resources/Samples/CppHelpers/Logging/OrthancLogger.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/CppHelpers/Logging/OrthancLogger.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Resources/Samples/CppHelpers/Logging/OrthancLogger.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/CppHelpers/Logging/OrthancLogger.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Resources/Samples/CppHelpers/Logging/OrthancPluginLogger.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/CppHelpers/Logging/OrthancPluginLogger.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Resources/Samples/CppHelpers/Logging/OrthancPluginLogger.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/CppHelpers/Logging/OrthancPluginLogger.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Resources/Samples/ImportDicomFiles/ImportDicomFiles.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/ImportDicomFiles/ImportDicomFiles.py	Tue Sep 24 11:39:52 2024 +0200
@@ -1,10 +1,11 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancServer/Resources/Samples/ImportDicomFiles/OrthancImport.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/ImportDicomFiles/OrthancImport.py	Tue Sep 24 11:39:52 2024 +0200
@@ -1,10 +1,11 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancServer/Resources/Samples/Lua/CallWebService.js	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/Lua/CallWebService.js	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Resources/Samples/Python/AnonymizeAllPatients.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/Python/AnonymizeAllPatients.py	Tue Sep 24 11:39:52 2024 +0200
@@ -4,8 +4,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancServer/Resources/Samples/Python/ArchiveAllPatients.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/Python/ArchiveAllPatients.py	Tue Sep 24 11:39:52 2024 +0200
@@ -4,8 +4,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancServer/Resources/Samples/Python/ArchiveStudiesInTimeRange.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/Python/ArchiveStudiesInTimeRange.py	Tue Sep 24 11:39:52 2024 +0200
@@ -4,8 +4,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
@@ -30,8 +31,8 @@
 def PrintHelp():
     print('Download ZIP archives for all the studies generated '
           'during a given time range (according to the StudyDate tag)\n')
-    print('Usage: %s <URL> <StartDate> <EndDate> <TargetFolder>\n' % sys.argv[0])
-    print('Example: %s http://127.0.0.1:8042/ 20150101 20151231 /tmp/\n' % sys.argv[0])
+    print('Usage: %s <URL> <StartDate> <EndDate> <TargetFolder> [login] [password]\n' % sys.argv[0])
+    print('Example: %s http://127.0.0.1:8042/ 20150101 20151231 /tmp/ login password\n' % sys.argv[0])
     exit(-1)
 
 def CheckIsDate(date):
@@ -40,13 +41,15 @@
         exit(-1)
 
 
-if len(sys.argv) != 5:
+if len(sys.argv) < 5:
     PrintHelp()
 
 URL = sys.argv[1]
 START = sys.argv[2]
 END = sys.argv[3]
 TARGET = sys.argv[4]
+if len(sys.argv) >= 7:
+    RestToolbox.SetCredentials(sys.argv[5], sys.argv[6])
 
 CheckIsDate(START)
 CheckIsDate(END)
--- a/OrthancServer/Resources/Samples/Python/AutoClassify.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/Python/AutoClassify.py	Tue Sep 24 11:39:52 2024 +0200
@@ -4,8 +4,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancServer/Resources/Samples/Python/ChangesLoop.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/Python/ChangesLoop.py	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancServer/Resources/Samples/Python/ContinuousPatientAnonymization.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/Python/ContinuousPatientAnonymization.py	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancServer/Resources/Samples/Python/DeleteAllStudies.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/Python/DeleteAllStudies.py	Tue Sep 24 11:39:52 2024 +0200
@@ -4,8 +4,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Python/DicomizeImage.py	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,117 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+
+# 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/>.
+
+
+#
+# This sample Python script illustrates how to DICOM-ize a JPEG image,
+# a PNG image, or a PDF file using the route "/tools/create-dicom" of
+# the REST API of Orthanc. Make sure to adapt the parameters of the
+# DICOM-ization below.
+#
+# The following command-line will install the required library:
+#
+#  $ sudo pip3 install requests
+#
+
+import base64
+import imghdr
+import json
+import os
+import requests
+
+
+########################################
+##  Parameters for the DICOM-ization  ##
+########################################
+
+PATH = os.path.join(os.getenv('HOME'), 'Downloads', 'Spy 11B.jpg')
+
+URL = 'http://localhost:8042/'
+USERNAME = 'orthanc'
+PASSWORD = 'orthanc'
+
+TAGS = {
+    'ConversionType' : 'SI',  # Scanned Image
+    'InstanceNumber' : '1',
+    'Laterality' : '',
+    'Modality' : 'OT',
+    'PatientOrientation' : '',
+    'SOPClassUID' : '1.2.840.10008.5.1.4.1.1.7',  # Secondary Capture Image Storage
+    'SeriesNumber' : '1',
+    }
+
+if True:
+    # Case 1: Attach the new DICOM image as a new series in an
+    # existing study. In this case, "PARENT_STUDY" indicates the
+    # Orthanc identifier of the parent study:
+    # https://orthanc.uclouvain.be/book/faq/orthanc-ids.html
+    PARENT_STUDY = '66c8e41e-ac3a9029-0b85e42a-8195ee0a-92c2e62e'
+
+else:
+    # Case 2: Create a new study
+    PARENT_STUDY = None
+    STUDY_TAGS = {
+        'PatientID' : 'Test',
+        'PatientName' : 'Hello^World',
+        'PatientSex' : 'O',
+        
+        'PatientBirthDate' : None,
+        'StudyID' : 'Test',
+        'ReferringPhysicianName' : None,
+        'AccessionNumber' : None,
+    }
+
+    TAGS.update(STUDY_TAGS)
+
+
+
+########################################
+##  Application of the DICOM-ization  ##
+########################################
+
+if imghdr.what(PATH) == 'jpeg':
+    mime = 'image/jpeg'
+elif imghdr.what(PATH) == 'png':
+    mime = 'image/png'
+elif os.path.splitext(PATH) [1] == '.pdf':
+    mime = 'application/pdf'
+else:
+    raise Exception('The input image is neither JPEG, nor PNG, nor PDF')
+
+with open(PATH, 'rb') as f:
+    content = f.read()
+
+data = 'data:%s;base64,%s' % (mime, base64.b64encode(content).decode('ascii'))
+
+arguments = {
+    'Content': data,
+    'Tags': TAGS,
+}
+
+if PARENT_STUDY != None:
+    arguments['Parent'] = PARENT_STUDY
+
+r = requests.post('%s/tools/create-dicom' % URL,
+                  json.dumps(arguments),
+                  auth = requests.auth.HTTPBasicAuth(USERNAME, PASSWORD))
+r.raise_for_status()
--- a/OrthancServer/Resources/Samples/Python/DownloadAnonymized.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/Python/DownloadAnonymized.py	Tue Sep 24 11:39:52 2024 +0200
@@ -4,8 +4,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancServer/Resources/Samples/Python/HighPerformanceAutoRouting.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/Python/HighPerformanceAutoRouting.py	Tue Sep 24 11:39:52 2024 +0200
@@ -4,8 +4,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancServer/Resources/Samples/Python/ManualModification.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/Python/ManualModification.py	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Python/MicroCTDicomization.py	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,191 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+
+# 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/>.
+
+
+#
+# This sample Python script illustrates how to DICOM-ize a micro-CT
+# acquisition, then to upload it to Orthanc.
+#
+# This sample assumes that the slices of the micro-CT are encoded as
+# TIFF files, that are all stored inside the same ZIP archive. Make
+# sure to adapt the parameters of the DICOM-ization below.
+#
+# The following command-line will install the required libraries:
+#
+#  $ sudo pip3 install libtiff numpy pydicom requests
+#
+
+import datetime
+import io
+import os
+import tempfile
+import zipfile
+
+import libtiff
+import numpy
+import pydicom
+import pydicom._storage_sopclass_uids
+import pydicom.datadict
+import pydicom.tag
+import requests
+import requests.auth
+
+
+########################################
+##  Parameters for the DICOM-ization  ##
+########################################
+
+ZIP = os.path.join(os.getenv('HOME'), 'Downloads', 'SpyII_mb.zip')
+
+URL = 'http://localhost:8042/'
+USERNAME = 'orthanc'
+PASSWORD = 'orthanc'
+
+VOXEL_WIDTH = 1
+VOXEL_HEIGHT = 1
+VOXEL_DEPTH = 1
+
+TAGS = {
+    'PatientID' : 'Test',
+    'PatientName' : 'Hello^World',
+    'StudyDate' : datetime.datetime.now().strftime('%Y%m%d'),
+    'StudyTime' : datetime.datetime.now().strftime('%H%M%S'),
+
+    'AccessionNumber' : None,
+    'AcquisitionNumber' : None,
+    'KVP' : None,
+    'Laterality' : None,
+    'Manufacturer' : None,
+    'PatientBirthDate' : '',
+    'PatientPosition' : None,
+    'PatientSex' : 'O',
+    'PositionReferenceIndicator' : None,
+    'ReferringPhysicianName' : None,
+    'SeriesNumber' : 1,
+    'StudyID' : 'Test',
+    }
+
+
+
+########################################
+##  Application of the DICOM-ization  ##
+########################################
+
+# Add the DICOM unique identifiers
+for tag in [ 'StudyInstanceUID',
+             'SeriesInstanceUID',
+             'FrameOfReferenceUID' ]:
+    if not tag in TAGS:
+        TAGS[tag] = pydicom.uid.generate_uid()
+
+
+def CreateDicomDataset(tif, sliceIndex):
+    image = tif.read_image().astype(numpy.uint16)
+
+    meta = pydicom.Dataset()
+    meta.MediaStorageSOPClassUID = pydicom._storage_sopclass_uids.CTImageStorage
+    meta.MediaStorageSOPInstanceUID = pydicom.uid.generate_uid()
+    meta.TransferSyntaxUID = pydicom.uid.ImplicitVRLittleEndian
+
+    dataset = pydicom.Dataset()
+    dataset.file_meta = meta
+
+    dataset.is_little_endian = True
+    dataset.is_implicit_VR = True
+    dataset.SOPClassUID = meta.MediaStorageSOPClassUID
+    dataset.SOPInstanceUID = meta.MediaStorageSOPInstanceUID
+    dataset.Modality = 'CT'
+
+    for (key, value) in TAGS.items():
+        tag = pydicom.tag.Tag(key)
+        vr = pydicom.datadict.dictionary_VR(tag)
+        dataset.add_new(tag, vr, value)
+
+    assert(image.dtype == numpy.uint16)
+    dataset.BitsStored = 16
+    dataset.BitsAllocated = 16
+    dataset.SamplesPerPixel = 1
+    dataset.HighBit = 15
+
+    dataset.Rows = image.shape[0]
+    dataset.Columns = image.shape[1]
+    dataset.InstanceNumber = (sliceIndex + 1)
+    dataset.ImagePositionPatient = r'0\0\%f' % (-float(sliceIndex) * VOXEL_DEPTH)
+    dataset.ImageOrientationPatient = r'1\0\0\0\-1\0'
+    dataset.SliceThickness = VOXEL_DEPTH
+    dataset.ImageType = r'ORIGINAL\PRIMARY\AXIAL'
+    dataset.RescaleIntercept = '0'
+    dataset.RescaleSlope = '1'
+    dataset.PixelSpacing = r'%f\%f' % (VOXEL_HEIGHT, VOXEL_WIDTH)
+    dataset.PhotometricInterpretation = 'MONOCHROME2'
+    dataset.PixelRepresentation = 1
+
+    minValue = numpy.min(image)
+    maxValue = numpy.max(image)
+    dataset.WindowWidth = maxValue - minValue
+    dataset.WindowCenter = (minValue + maxValue) / 2.0
+
+    pydicom.dataset.validate_file_meta(dataset.file_meta, enforce_standard=True)
+    dataset.PixelData = image.tobytes()
+
+    return dataset
+
+
+# Create a temporary file, as libtiff is not able to read from BytesIO()
+with tempfile.NamedTemporaryFile() as tmp:
+    sliceIndex = 0
+
+    # Loop over the files in the ZIP archive, after having sorted them
+    with zipfile.ZipFile(ZIP, 'r') as z:
+        for path in sorted(z.namelist()):
+
+            # Ignore folders in the ZIP archive
+            info = z.getinfo(path)
+            if info.is_dir():
+                continue
+
+            # Extract the current file from the ZIP archive, into the temporary file
+            print('DICOM-izing: %s' % path)
+            data = z.read(path)
+
+            with open(tmp.name, 'wb') as f:
+                f.write(data)
+
+            # Try and decode the TIFF file
+            try:
+                tif = libtiff.TIFF.open(tmp.name)
+            except:
+                # Not a TIFF file, ignore
+                continue
+
+            # Create a DICOM dataset from the TIFF
+            dataset = CreateDicomDataset(tif, sliceIndex)
+            b = io.BytesIO()
+            dataset.save_as(b, False)
+
+            # Upload the DICOM dataset to Orthanc
+            r = requests.post('%s/instances' % URL, b.getvalue(),
+                              auth = requests.auth.HTTPBasicAuth(USERNAME, PASSWORD))
+            r.raise_for_status()
+
+            sliceIndex += 1
--- a/OrthancServer/Resources/Samples/Python/Replicate.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/Python/Replicate.py	Tue Sep 24 11:39:52 2024 +0200
@@ -3,8 +3,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancServer/Resources/Samples/Python/RestToolbox.py	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/Python/RestToolbox.py	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancServer/Resources/Samples/README.txt	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/README.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -4,4 +4,4 @@
 
 The integration tests of Orthanc provide many samples about the
 features of the REST API of Orthanc:
-https://hg.orthanc-server.com/orthanc-tests/file/tip/Tests/Tests.py
+https://orthanc.uclouvain.be/hg/orthanc-tests/file/default/Tests/Tests.py
--- a/OrthancServer/Resources/Samples/Tools/CMakeLists.txt	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/Tools/CMakeLists.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- a/OrthancServer/Resources/Samples/Tools/RecoverCompressedFile.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/Tools/RecoverCompressedFile.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Resources/Samples/WebApplications/DrawingDicomizer.js	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/WebApplications/DrawingDicomizer.js	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Resources/Samples/WebApplications/DrawingDicomizer/orthanc.js	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/WebApplications/DrawingDicomizer/orthanc.js	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Resources/Samples/WebApplications/NodeToolbox.js	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Samples/WebApplications/NodeToolbox.js	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Resources/Testing/Issue32/Cpp/CMakeLists.txt	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Resources/Testing/Issue32/Cpp/CMakeLists.txt	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,53 @@
+/**
+ * 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
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.h	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,54 @@
+/**
+ * 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	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -152,7 +153,7 @@
     static void ApplyLevel(SetOfResources& candidates,
                            IDatabaseWrapper::ITransaction& transaction,
                            ILookupResources& compatibility,
-                           const std::vector<DatabaseConstraint>& lookup,
+                           const DatabaseConstraints& lookup,
                            ResourceType level)
     {
       typedef std::set<const DatabaseConstraint*>  SetOfConstraints;
@@ -165,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 DatabaseConstraint& 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);
           }
         }
       }
@@ -305,7 +308,7 @@
 
     void DatabaseLookup::ApplyLookupResources(std::list<std::string>& resourcesId,
                                               std::list<std::string>* instancesId,
-                                              const std::vector<DatabaseConstraint>& lookup,
+                                              const DatabaseConstraints& lookup,
                                               ResourceType queryLevel,
                                               size_t limit)
     {
@@ -319,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	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -45,7 +46,7 @@
 
       void ApplyLookupResources(std::list<std::string>& resourcesId,
                                 std::list<std::string>* instancesId,
-                                const std::vector<DatabaseConstraint>& lookup,
+                                const DatabaseConstraints& lookup,
                                 ResourceType queryLevel,
                                 size_t limit);
     };
--- a/OrthancServer/Sources/Database/Compatibility/ICreateInstance.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/ICreateInstance.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/Database/Compatibility/ICreateInstance.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/ICreateInstance.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/Database/Compatibility/IGetChildrenMetadata.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/IGetChildrenMetadata.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/Database/Compatibility/IGetChildrenMetadata.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/IGetChildrenMetadata.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/Database/Compatibility/ILookupResourceAndParent.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/ILookupResourceAndParent.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/Database/Compatibility/ILookupResourceAndParent.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/ILookupResourceAndParent.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/Database/Compatibility/ILookupResources.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/ILookupResources.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -34,7 +35,7 @@
       ILookupResources& compatibility,
       std::list<std::string>& resourcesId,
       std::list<std::string>* instancesId,
-      const std::vector<DatabaseConstraint>& lookup,
+      const DatabaseConstraints& lookup,
       ResourceType queryLevel,
       size_t limit)
     {
--- a/OrthancServer/Sources/Database/Compatibility/ILookupResources.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/ILookupResources.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -59,7 +60,7 @@
                         ILookupResources& compatibility,
                         std::list<std::string>& resourcesId,
                         std::list<std::string>* instancesId,
-                        const std::vector<DatabaseConstraint>& lookup,
+                        const DatabaseConstraints& lookup,
                         ResourceType queryLevel,
                         size_t limit);
     };
--- a/OrthancServer/Sources/Database/Compatibility/ISetResourcesContent.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/ISetResourcesContent.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/Database/Compatibility/SetOfResources.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/SetOfResources.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/Database/Compatibility/SetOfResources.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/SetOfResources.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/Database/IDatabaseListener.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/IDatabaseListener.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -26,6 +27,7 @@
 #include "../../../OrthancFramework/Sources/FileStorage/FileInfo.h"
 #include "../../../OrthancFramework/Sources/FileStorage/IStorageArea.h"
 #include "../ExportedResource.h"
+#include "../Search/ISqlLookupFormatter.h"
 #include "../ServerIndexChange.h"
 #include "IDatabaseListener.h"
 
@@ -35,14 +37,108 @@
 
 namespace Orthanc
 {
-  class DatabaseConstraint;
+  class DatabaseConstraints;
   class ResourcesContent;
 
-  
   class IDatabaseWrapper : public boost::noncopyable
   {
   public:
-    struct CreateInstanceResult
+    class Capabilities
+    {
+    private:
+      bool hasFlushToDisk_;
+      bool hasRevisionsSupport_;
+      bool hasLabelsSupport_;
+      bool hasAtomicIncrementGlobalProperty_;
+      bool hasUpdateAndGetStatistics_;
+      bool hasMeasureLatency_;
+      bool hasAttachmentCustomDataSupport_;
+
+    public:
+      Capabilities() :
+        hasFlushToDisk_(false),
+        hasRevisionsSupport_(false),
+        hasLabelsSupport_(false),
+        hasAtomicIncrementGlobalProperty_(false),
+        hasUpdateAndGetStatistics_(false),
+        hasMeasureLatency_(false),
+        hasAttachmentCustomDataSupport_(false)
+      {
+      }
+
+      void SetFlushToDisk(bool value)
+      {
+        hasFlushToDisk_ = value;
+      }
+
+      bool HasFlushToDisk() const
+      {
+        return hasFlushToDisk_;
+      }
+
+      void SetRevisionsSupport(bool value)
+      {
+        hasRevisionsSupport_ = value;
+      }
+
+      bool HasRevisionsSupport() const
+      {
+        return hasRevisionsSupport_;
+      }
+
+      void SetLabelsSupport(bool value)
+      {
+        hasLabelsSupport_ = value;
+      }
+
+      bool HasLabelsSupport() const
+      {
+        return hasLabelsSupport_;
+      }
+
+      void SetAttachmentCustomDataSupport(bool value)
+      {
+        hasAttachmentCustomDataSupport_ = value;
+      }
+
+      bool HasAttachmentCustomDataSupport() const
+      {
+        return hasAttachmentCustomDataSupport_;
+      }
+
+      void SetAtomicIncrementGlobalProperty(bool value)
+      {
+        hasAtomicIncrementGlobalProperty_ = value;
+      }
+
+      bool HasAtomicIncrementGlobalProperty() const
+      {
+        return hasAtomicIncrementGlobalProperty_;
+      }
+
+      void SetUpdateAndGetStatistics(bool value)
+      {
+        hasUpdateAndGetStatistics_ = value;
+      }
+
+      bool HasUpdateAndGetStatistics() const
+      {
+        return hasUpdateAndGetStatistics_;
+      }
+
+      void SetMeasureLatency(bool value)
+      {
+        hasMeasureLatency_ = value;
+      }
+
+      bool HasMeasureLatency() const
+      {
+        return hasMeasureLatency_;
+      }
+    };
+
+
+    struct CreateInstanceResult : public boost::noncopyable
     {
       bool     isNewPatient_;
       bool     isNewStudy_;
@@ -94,13 +190,13 @@
 
       virtual void GetAllPublicIds(std::list<std::string>& target,
                                    ResourceType resourceType,
-                                   size_t since,
-                                   size_t limit) = 0;
+                                   int64_t since,
+                                   uint32_t limit) = 0;
 
       virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
                               bool& done /*out*/,
                               int64_t since,
-                              uint32_t maxResults) = 0;
+                              uint32_t limit) = 0;
 
       virtual void GetChildrenInternalId(std::list<int64_t>& target,
                                          int64_t id) = 0;
@@ -111,7 +207,7 @@
       virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
                                         bool& done /*out*/,
                                         int64_t since,
-                                        uint32_t maxResults) = 0;
+                                        uint32_t limit) = 0;
 
       virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/) = 0;
 
@@ -130,15 +226,16 @@
     
       virtual uint64_t GetTotalUncompressedSize() = 0;
 
-      virtual bool IsExistingResource(int64_t internalId) = 0;
-
       virtual bool IsProtectedPatient(int64_t internalId) = 0;
 
       virtual void ListAvailableAttachments(std::set<FileContentType>& target,
                                             int64_t id) = 0;
 
-      virtual void LogChange(int64_t internalId,
-                             const ServerIndexChange& change) = 0;
+      virtual void LogChange(ChangeType changeType,
+                             ResourceType resourceType,
+                             int64_t internalId,
+                             const std::string& publicId,  /* only for compatibility with V1 and V2 plugins */
+                             const std::string& date) = 0;
 
       virtual void LogExportedResource(const ExportedResource& resource) = 0;
     
@@ -197,9 +294,11 @@
     
       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 DatabaseConstraints& lookup,
                                         ResourceType queryLevel,
-                                        size_t limit) = 0;
+                                        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
@@ -235,6 +334,35 @@
                                            ResourceType& type,
                                            std::string& parentPublicId,
                                            const std::string& publicId) = 0;
+
+
+      /**
+       * Primitives introduced in Orthanc 1.12.0
+       **/
+
+      virtual void AddLabel(int64_t resource,
+                            const std::string& label) = 0;
+
+      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;
+
+      virtual int64_t IncrementGlobalProperty(GlobalProperty property,
+                                              int64_t increment,
+                                              bool shared) = 0;
+
+      virtual void UpdateAndGetStatistics(int64_t& patientsCount,
+                                          int64_t& studiesCount,
+                                          int64_t& seriesCount,
+                                          int64_t& instancesCount,
+                                          int64_t& compressedSize,
+                                          int64_t& uncompressedSize) = 0;
     };
 
 
@@ -248,8 +376,6 @@
 
     virtual void FlushToDisk() = 0;
 
-    virtual bool HasFlushToDisk() const = 0;
-
     virtual ITransaction* StartTransaction(TransactionType type,
                                            IDatabaseListener& listener) = 0;
 
@@ -258,8 +384,8 @@
     virtual void Upgrade(unsigned int targetVersion,
                          IStorageArea& storageArea) = 0;
 
-    virtual bool HasRevisionsSupport() const = 0;
+    virtual const Capabilities GetDatabaseCapabilities() const = 0;
 
-    virtual bool HasAttachmentCustomDataSupport() const = 0;
+    virtual uint64_t MeasureLatency() = 0;
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/InstallLabelsTable.sql	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,29 @@
+-- Orthanc - A Lightweight, RESTful DICOM Store
+-- Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+-- Department, University Hospital of Liege, Belgium
+-- Copyright (C) 2017-2023 Osimis S.A., Belgium
+-- Copyright (C) 2024-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/>.
+
+
+CREATE TABLE Labels(
+       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
+       label TEXT NOT NULL,
+       PRIMARY KEY(id, label)  -- Prevents duplicates
+       );
+
+CREATE INDEX LabelsIndex1 ON Labels(id);
+CREATE INDEX LabelsIndex2 ON Labels(label);  -- This index allows efficient lookups
--- a/OrthancServer/Sources/Database/InstallTrackAttachmentsSize.sql	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/InstallTrackAttachmentsSize.sql	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 -- Orthanc - A Lightweight, RESTful DICOM Store
 -- Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 -- Department, University Hospital of Liege, Belgium
--- Copyright (C) 2017-2022 Osimis S.A., Belgium
--- Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+-- Copyright (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
--- a/OrthancServer/Sources/Database/PrepareDatabase.sql	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/PrepareDatabase.sql	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 -- Orthanc - A Lightweight, RESTful DICOM Store
 -- Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 -- Department, University Hospital of Liege, Belgium
--- Copyright (C) 2017-2022 Osimis S.A., Belgium
--- Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+-- Copyright (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
@@ -94,6 +95,13 @@
        patientId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE
        );
 
+-- New in Orthanc 1.12.0
+CREATE TABLE Labels(
+       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
+       label TEXT NOT NULL,
+       PRIMARY KEY(id, label)  -- Prevents duplicates
+       );
+
 CREATE INDEX ChildrenIndex ON Resources(parentId);
 CREATE INDEX PublicIndex ON Resources(publicId);
 CREATE INDEX ResourceTypeIndex ON Resources(resourceType);
@@ -111,6 +119,10 @@
 
 CREATE INDEX ChangesIndex ON Changes(internalId);
 
+-- New in Orthanc 1.12.0
+CREATE INDEX LabelsIndex1 ON Labels(id);
+CREATE INDEX LabelsIndex2 ON Labels(label);  -- This index allows efficient lookups
+
 CREATE TRIGGER AttachedFileDeleted
 AFTER DELETE ON AttachedFiles
 BEGIN
--- a/OrthancServer/Sources/Database/ResourcesContent.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/ResourcesContent.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -140,13 +141,13 @@
     for (std::list<TagValue>::const_iterator
            it = tags_.begin(); it != tags_.end(); ++it)
     {
-      if (it->isIdentifier_)
+      if (it->IsIdentifier())
       {
-        compatibility.SetIdentifierTag(it->resourceId_, it->tag_,  it->value_);
+        compatibility.SetIdentifierTag(it->GetResourceId(), it->GetTag(),  it->GetValue());
       }
       else
       {
-        compatibility.SetMainDicomTag(it->resourceId_, it->tag_,  it->value_);
+        compatibility.SetMainDicomTag(it->GetResourceId(), it->GetTag(),  it->GetValue());
       }
     }
 
@@ -154,7 +155,7 @@
            it = metadata_.begin(); it != metadata_.end(); ++it)
     {
       assert(isNewResource_);
-      compatibility.SetMetadata(it->resourceId_, it->metadata_,  it->value_, 0 /* initial revision number */);
+      compatibility.SetMetadata(it->GetResourceId(), it->GetType(),  it->GetValue(), 0 /* initial revision number */);
     }
   }
 }
--- a/OrthancServer/Sources/Database/ResourcesContent.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/ResourcesContent.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -39,13 +40,15 @@
   class ResourcesContent : public boost::noncopyable
   {
   public:
-    struct TagValue
+    class TagValue
     {
+    private:
       int64_t      resourceId_;
       bool         isIdentifier_;
       DicomTag     tag_;
       std::string  value_;
 
+    public:
       TagValue(int64_t resourceId,
                bool isIdentifier,
                const DicomTag& tag,
@@ -56,22 +59,59 @@
         value_(value)
       {
       }
+
+      int64_t GetResourceId() const
+      {
+        return resourceId_;
+      }
+
+      bool IsIdentifier() const
+      {
+        return isIdentifier_;
+      }
+
+      const DicomTag& GetTag() const
+      {
+        return tag_;
+      }
+
+      const std::string& GetValue() const
+      {
+        return value_;
+      }
     };
 
-    struct Metadata
+    class Metadata
     {
+    private:
       int64_t       resourceId_;
-      MetadataType  metadata_;
+      MetadataType  type_;
       std::string   value_;
 
+    public:
       Metadata(int64_t  resourceId,
-               MetadataType metadata,
+               MetadataType type,
                const std::string& value) :
         resourceId_(resourceId),
-        metadata_(metadata),
+        type_(type),
         value_(value)
       {
       }
+
+      int64_t GetResourceId() const
+      {
+        return resourceId_;
+      }
+
+      MetadataType GetType() const
+      {
+        return type_;
+      }
+
+      const std::string& GetValue() const
+      {
+        return value_;
+      }
     };
 
     typedef std::list<TagValue>  ListTags;
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -235,11 +236,11 @@
     void GetChangesInternal(std::list<ServerIndexChange>& target,
                             bool& done,
                             SQLite::Statement& s,
-                            uint32_t maxResults)
+                            uint32_t limit)
     {
       target.clear();
 
-      while (target.size() < maxResults && s.Step())
+      while (target.size() < limit && s.Step())
       {
         int64_t seq = s.ColumnInt64(0);
         ChangeType changeType = static_cast<ChangeType>(s.ColumnInt(1));
@@ -252,18 +253,18 @@
         target.push_back(ServerIndexChange(seq, changeType, resourceType, publicId, date));
       }
 
-      done = !(target.size() == maxResults && s.Step());
+      done = !(target.size() == limit && s.Step());
     }
 
 
     void GetExportedResourcesInternal(std::list<ExportedResource>& target,
                                       bool& done,
                                       SQLite::Statement& s,
-                                      uint32_t maxResults)
+                                      uint32_t limit)
     {
       target.clear();
 
-      while (target.size() < maxResults && s.Step())
+      while (target.size() < limit && s.Step())
       {
         int64_t seq = s.ColumnInt64(0);
         ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(1));
@@ -282,7 +283,7 @@
         target.push_back(resource);
       }
 
-      done = !(target.size() == maxResults && s.Step());
+      done = !(target.size() == limit && s.Step());
     }
 
 
@@ -343,14 +344,16 @@
 
     virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
                                       std::list<std::string>* instancesId,
-                                      const std::vector<DatabaseConstraint>& lookup,
+                                      const DatabaseConstraints& lookup,
                                       ResourceType queryLevel,
-                                      size_t limit) ORTHANC_OVERRIDE
+                                      const std::set<std::string>& labels,
+                                      LabelsConstraint labelsConstraint,
+                                      uint32_t limit) ORTHANC_OVERRIDE
     {
       LookupFormatter formatter;
 
       std::string sql;
-      LookupFormatter::Apply(sql, formatter, lookup, queryLevel, limit);
+      LookupFormatter::Apply(sql, formatter, lookup, queryLevel, labels, labelsConstraint, limit);
 
       sql = "CREATE TEMPORARY TABLE Lookup AS " + sql;
     
@@ -437,7 +440,7 @@
     virtual int64_t CreateResource(const std::string& publicId,
                                    ResourceType type) ORTHANC_OVERRIDE
     {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(NULL, ?, ?, NULL)");
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Resources (internalId, resourceType, publicId, parentId) VALUES(NULL, ?, ?, NULL)");
       s.BindInt(0, type);
       s.BindString(1, publicId);
       s.Run();
@@ -535,8 +538,8 @@
 
     virtual void GetAllPublicIds(std::list<std::string>& target,
                                  ResourceType resourceType,
-                                 size_t since,
-                                 size_t limit) ORTHANC_OVERRIDE
+                                 int64_t since,
+                                 uint32_t limit) ORTHANC_OVERRIDE
     {
       if (limit == 0)
       {
@@ -562,12 +565,12 @@
     virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
                             bool& done /*out*/,
                             int64_t since,
-                            uint32_t maxResults) ORTHANC_OVERRIDE
+                            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, maxResults + 1);
-      GetChangesInternal(target, done, s, maxResults);
+      s.BindInt(1, limit + 1);
+      GetChangesInternal(target, done, s, limit);
     }
 
 
@@ -614,13 +617,13 @@
     virtual void GetExportedResources(std::list<ExportedResource>& target,
                                       bool& done,
                                       int64_t since,
-                                      uint32_t maxResults) ORTHANC_OVERRIDE
+                                      uint32_t limit) ORTHANC_OVERRIDE
     {
       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);
+      s.BindInt(1, limit + 1);
+      GetExportedResourcesInternal(target, done, s, limit);
     }
 
 
@@ -757,15 +760,6 @@
     }
 
 
-    virtual bool IsExistingResource(int64_t internalId) ORTHANC_OVERRIDE
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                          "SELECT * FROM Resources WHERE internalId=?");
-      s.BindInt64(0, internalId);
-      return s.Step();
-    }
-
-
     virtual bool IsProtectedPatient(int64_t internalId) ORTHANC_OVERRIDE
     {
       SQLite::Statement s(db_, SQLITE_FROM_HERE,
@@ -791,14 +785,17 @@
     }
 
 
-    virtual void LogChange(int64_t internalId,
-                           const ServerIndexChange& change) ORTHANC_OVERRIDE
+    virtual void LogChange(ChangeType changeType,
+                           ResourceType resourceType,
+                           int64_t internalId,
+                           const std::string& /* publicId - unused */,
+                           const std::string& date) ORTHANC_OVERRIDE
     {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes VALUES(NULL, ?, ?, ?, ?)");
-      s.BindInt(0, change.GetChangeType());
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes (seq, changeType, internalId, resourceType, date) VALUES(NULL, ?, ?, ?, ?)");
+      s.BindInt(0, changeType);
       s.BindInt64(1, internalId);
-      s.BindInt(2, change.GetResourceType());
-      s.BindString(3, change.GetDate());
+      s.BindInt(2, resourceType);
+      s.BindString(3, date);
       s.Run();
     }
 
@@ -806,7 +803,7 @@
     virtual void LogExportedResource(const ExportedResource& resource) ORTHANC_OVERRIDE
     {
       SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                          "INSERT INTO ExportedResources VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?)");
+                          "INSERT INTO ExportedResources (seq, resourceType, publicId, remoteModality, patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid, date) VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?)");
 
       s.BindInt(0, resource.GetResourceType());
       s.BindString(1, resource.GetPublicId());
@@ -1001,7 +998,7 @@
       // The "shared" info is not used by the SQLite database, as it
       // can only be used by one Orthanc server.
       
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO GlobalProperties VALUES(?, ?)");
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO GlobalProperties (property, value) VALUES(?, ?)");
       s.BindInt(0, property);
       s.BindString(1, value);
       s.Run();
@@ -1013,7 +1010,7 @@
                                   const DicomTag& tag,
                                   const std::string& value) ORTHANC_OVERRIDE
     {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO DicomIdentifiers VALUES(?, ?, ?, ?)");
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO DicomIdentifiers (id, tagGroup, tagElement, value) VALUES(?, ?, ?, ?)");
       s.BindInt64(0, id);
       s.BindInt(1, tag.GetGroup());
       s.BindInt(2, tag.GetElement());
@@ -1033,7 +1030,7 @@
       }
       else if (IsProtectedPatient(internalId))
       {
-        SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)");
+        SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO PatientRecyclingOrder (seq, patientId) VALUES(NULL, ?)");
         s.BindInt64(0, internalId);
         s.Run();
       }
@@ -1049,7 +1046,7 @@
                                  const DicomTag& tag,
                                  const std::string& value) ORTHANC_OVERRIDE
     {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)");
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags (id, tagGroup, tagElement, value) VALUES(?, ?, ?, ?)");
       s.BindInt64(0, id);
       s.BindInt(1, tag.GetGroup());
       s.BindInt(2, tag.GetElement());
@@ -1099,11 +1096,75 @@
 
       {
         SQLite::Statement s(db_, SQLITE_FROM_HERE,
-                            "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)");
+                            "INSERT INTO PatientRecyclingOrder (seq, patientId) VALUES(NULL, ?)");
         s.BindInt64(0, patient);
         s.Run();
       }
     }
+
+
+    virtual void AddLabel(int64_t resource,
+                          const std::string& label) ORTHANC_OVERRIDE
+    {
+      if (label.empty())
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+      else
+      {
+        SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR IGNORE INTO Labels (id, label) VALUES(?, ?)");
+        s.BindInt64(0, resource);
+        s.BindString(1, label);
+        s.Run();
+      }
+    }
+
+
+    virtual void RemoveLabel(int64_t resource,
+                             const std::string& label) ORTHANC_OVERRIDE
+    {
+      if (label.empty())
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+      else
+      {
+        SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Labels WHERE id=? AND label=?");
+        s.BindInt64(0, resource);
+        s.BindString(1, label);
+        s.Run();
+      }
+    }
+
+
+    virtual void ListLabels(std::set<std::string>& target,
+                            int64_t resource) ORTHANC_OVERRIDE
+    {
+      target.clear();
+
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "SELECT label FROM Labels WHERE id=?");
+      s.BindInt64(0, resource);
+
+      while (s.Step())
+      {
+        target.insert(s.ColumnString(0));
+      }
+    }
+
+
+    virtual void ListAllLabels(std::set<std::string>& target) ORTHANC_OVERRIDE
+    {
+      target.clear();
+
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "SELECT DISTINCT label FROM Labels");
+
+      while (s.Step())
+      {
+        target.insert(s.ColumnString(0));
+      }
+    }
   };
 
 
@@ -1298,6 +1359,9 @@
     signalRemainingAncestor_(NULL),
     version_(0)
   {
+    // TODO: implement revisions in SQLite
+    dbCapabilities_.SetFlushToDisk(true);
+    dbCapabilities_.SetLabelsSupport(true);
     db_.Open(path);
   }
 
@@ -1307,6 +1371,9 @@
     signalRemainingAncestor_(NULL),
     version_(0)
   {
+    // TODO: implement revisions in SQLite
+    dbCapabilities_.SetFlushToDisk(true);
+    dbCapabilities_.SetLabelsSupport(true);
     db_.OpenInMemory();
   }
 
@@ -1384,20 +1451,29 @@
                                "Incompatible version of the Orthanc database: " + tmp);
       }
 
-      // New in Orthanc 1.5.1
-      if (version_ >= 6)
+      if (version_ == 6)
       {
-        if (!transaction->LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast, true /* unused in SQLite */) 
-            || tmp != "1")
+        // New in Orthanc 1.5.1
+        if (!transaction->LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast, true /* unused in SQLite */) ||
+            tmp != "1")
         {
           LOG(INFO) << "Installing the SQLite triggers to track the size of the attachments";
           std::string query;
           ServerResources::GetFileResource(query, ServerResources::INSTALL_TRACK_ATTACHMENTS_SIZE);
           db_.Execute(query);
         }
+
+        // New in Orthanc 1.12.0
+        if (!db_.DoesTableExist("Labels"))
+        {
+          LOG(INFO) << "Installing the \"Labels\" table";
+          std::string query;
+          ServerResources::GetFileResource(query, ServerResources::INSTALL_LABELS_TABLE);
+          db_.Execute(query);
+        }
       }
 
-      // New in Orthanc 1.12.0
+      // New in Orthanc 1.12.5
       if (version_ >= 6)
       {
         if (!transaction->LookupGlobalProperty(tmp, GlobalProperty_SQLiteHasCustomDataAndRevision, true /* unused in SQLite */) 
@@ -1528,7 +1604,7 @@
   int64_t SQLiteDatabaseWrapper::UnitTestsTransaction::CreateResource(const std::string& publicId,
                                                                       ResourceType type)
   {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(NULL, ?, ?, NULL)");
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Resources (internalId, resourceType, publicId, parentId) VALUES(NULL, ?, ?, NULL)");
     s.BindInt(0, type);
     s.BindString(1, publicId);
     s.Run();
@@ -1550,7 +1626,7 @@
                                                                      const DicomTag& tag,
                                                                      const std::string& value)
   {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO DicomIdentifiers VALUES(?, ?, ?, ?)");
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO DicomIdentifiers (id, tagGroup, tagElement, value) VALUES(?, ?, ?, ?)");
     s.BindInt64(0, id);
     s.BindInt(1, tag.GetGroup());
     s.BindInt(2, tag.GetElement());
@@ -1563,7 +1639,7 @@
                                                                     const DicomTag& tag,
                                                                     const std::string& value)
   {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)");
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags (id, tagGroup, tagElement, value) VALUES(?, ?, ?, ?)");
     s.BindInt64(0, id);
     s.BindInt(1, tag.GetGroup());
     s.BindInt(2, tag.GetElement());
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -22,7 +23,7 @@
 
 #pragma once
 
-#include "IDatabaseWrapper.h"
+#include "BaseDatabaseWrapper.h"
 
 #include "../../../OrthancFramework/Sources/SQLite/Connection.h"
 
@@ -35,7 +36,7 @@
    * translates low-level requests into SQL statements. Mutual
    * exclusion MUST be implemented at a higher level.
    **/
-  class SQLiteDatabaseWrapper : public IDatabaseWrapper
+  class SQLiteDatabaseWrapper : public BaseDatabaseWrapper
   {
   private:
     class TransactionBase;
@@ -51,6 +52,7 @@
     TransactionBase*          activeTransaction_;
     SignalRemainingAncestor*  signalRemainingAncestor_;
     unsigned int              version_;
+    IDatabaseWrapper::Capabilities  dbCapabilities_;
 
     void GetChangesInternal(std::list<ServerIndexChange>& target,
                             bool& done,
@@ -79,11 +81,6 @@
 
     virtual void FlushToDisk() ORTHANC_OVERRIDE;
 
-    virtual bool HasFlushToDisk() const ORTHANC_OVERRIDE
-    {
-      return true;
-    }
-
     virtual unsigned int GetDatabaseVersion() ORTHANC_OVERRIDE
     {
       return version_;
@@ -92,14 +89,14 @@
     virtual void Upgrade(unsigned int targetVersion,
                          IStorageArea& storageArea) ORTHANC_OVERRIDE;
 
-    virtual bool HasRevisionsSupport() const ORTHANC_OVERRIDE
+    virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE
     {
-      return true;
+      return dbCapabilities_;
     }
 
-    virtual bool HasAttachmentCustomDataSupport() const ORTHANC_OVERRIDE
+    virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE
     {
-      return true;
+      throw OrthancException(ErrorCode_NotImplemented);
     }
 
     /**
@@ -108,7 +105,7 @@
      * "UnitTestsTransaction" give access to additional information
      * about the underlying SQLite database to be used in unit tests.
      **/
-    class UnitTestsTransaction : public ITransaction
+    class UnitTestsTransaction : public BaseDatabaseWrapper::BaseTransaction
     {
     protected:
       SQLite::Connection& db_;
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -362,7 +363,8 @@
       }
 
       {
-        const std::set<DicomTag>& tags = DicomMap::GetMainDicomTags(level);
+        std::set<DicomTag> tags;
+        DicomMap::GetMainDicomTags(tags, level);
 
         for (std::set<DicomTag>::const_iterator
                tag = tags.begin(); tag != tags.end(); ++tag)
@@ -375,15 +377,6 @@
       }
     }
 
-  public:
-    MainDicomTagsRegistry()
-    {
-      LoadTags(ResourceType_Patient);
-      LoadTags(ResourceType_Study);
-      LoadTags(ResourceType_Series);
-      LoadTags(ResourceType_Instance); 
-    }
-
     void LookupTag(ResourceType& level,
                    DicomTagType& type,
                    const DicomTag& tag) const
@@ -402,6 +395,43 @@
         type = it->second.GetType();
       }
     }
+
+  public:
+    MainDicomTagsRegistry()
+    {
+      LoadTags(ResourceType_Patient);
+      LoadTags(ResourceType_Study);
+      LoadTags(ResourceType_Series);
+      LoadTags(ResourceType_Instance);
+    }
+
+    void NormalizeLookup(DatabaseConstraints& target,
+                         const DatabaseLookup& source,
+                         ResourceType queryLevel) const
+    {
+      target.Clear();
+
+      for (size_t i = 0; i < source.GetConstraintsCount(); i++)
+      {
+        ResourceType level;
+        DicomTagType type;
+
+        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.AddConstraint(source.GetConstraint(i).ConvertToDatabaseConstraint(level, type));
+        }
+      }
+    }
   };
 
 
@@ -414,7 +444,7 @@
 
     if (changeType <= ChangeType_INTERNAL_LastLogged)
     {
-      transaction_.LogChange(internalId, change);
+      transaction_.LogChange(changeType, resourceType, internalId, publicId, change.GetDate());
     }
 
     GetTransactionContext().SignalChange(change);
@@ -469,38 +499,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:
@@ -631,6 +629,7 @@
         {
           if (attempt >= maxRetries_)
           {
+            LOG(ERROR) << "Maximum transactions retries reached " << e.GetDetails();
             throw;
           }
           else
@@ -653,7 +652,6 @@
   StatelessDatabaseOperations::StatelessDatabaseOperations(IDatabaseWrapper& db) : 
     db_(db),
     mainDicomTagsRegistry_(new MainDicomTagsRegistry),
-    hasFlushToDisk_(db.HasFlushToDisk()),
     maxRetries_(0)
   {
   }
@@ -714,13 +712,14 @@
                                                    const std::string& publicId,
                                                    ResourceType level,
                                                    const std::set<DicomTag>& requestedTags,
-                                                   ExpandResourceDbFlags expandFlags)
+                                                   ExpandResourceFlags expandFlags)
   {    
     class Operations : public ReadOnlyOperationsT6<
-      bool&, ExpandedResource&, const std::string&, ResourceType, const std::set<DicomTag>&, ExpandResourceDbFlags>
+      bool&, ExpandedResource&, const std::string&, ResourceType, const std::set<DicomTag>&, ExpandResourceFlags>
     {
     private:
-  
+      bool hasLabelsSupport_;
+
       static bool LookupStringMetadata(std::string& result,
                                        const std::map<MetadataType, std::string>& metadata,
                                        MetadataType type)
@@ -762,6 +761,11 @@
 
 
     public:
+      explicit Operations(bool hasLabelsSupport) :
+        hasLabelsSupport_(hasLabelsSupport)
+      {
+      }
+
       virtual void ApplyTuple(ReadOnlyTransaction& transaction,
                               const Tuple& tuple) ORTHANC_OVERRIDE
       {
@@ -777,7 +781,7 @@
         else
         {
           ExpandedResource& target = tuple.get<1>();
-          ExpandResourceDbFlags expandFlags = tuple.get<5>();
+          ExpandResourceFlags expandFlags = tuple.get<5>();
 
           // Set information about the parent resource (if it exists)
           if (type == ResourceType_Patient)
@@ -797,16 +801,15 @@
             target.parentId_ = parent;
           }
 
-          target.type_ = type;
-          target.id_ = tuple.get<2>();
-
-          if (expandFlags & ExpandResourceDbFlags_IncludeChildren)
+          target.SetResource(type, tuple.get<2>());
+
+          if (expandFlags & ExpandResourceFlags_IncludeChildren)
           {
             // List the children resources
             transaction.GetChildrenPublicId(target.childrenIds_, internalId);
           }
 
-          if (expandFlags & ExpandResourceDbFlags_IncludeMetadata)
+          if (expandFlags & ExpandResourceFlags_IncludeMetadata)
           {
             // Extract the metadata
             transaction.GetAllMetadata(target.metadata_, internalId);
@@ -868,10 +871,10 @@
             LookupStringMetadata(target.mainDicomTagsSignature_, target.metadata_, MetadataType_MainDicomTagsSignature);
           }
 
-          if (expandFlags & ExpandResourceDbFlags_IncludeMainDicomTags)
+          if (expandFlags & ExpandResourceFlags_IncludeMainDicomTags)
           {
             // read all tags from DB
-            transaction.GetMainDicomTags(target.tags_, internalId);
+            transaction.GetMainDicomTags(target.GetMainDicomTags(), internalId);
 
             // read all main sequences from DB
             std::string serializedSequences;
@@ -881,7 +884,7 @@
               Toolbox::ReadJson(jsonMetadata, serializedSequences);
 
               assert(jsonMetadata["Version"].asInt() == 1);
-              target.tags_.FromDicomAsJson(jsonMetadata["Sequences"], true /* append */, true /* parseSequences */);
+              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
@@ -894,7 +897,7 @@
               FromDcmtkBridge::ParseListOfTags(savedMainDicomTags, target.mainDicomTagsSignature_);
 
               // read parent main dicom tags as long as we have not gathered all requested tags
-              ResourceType currentLevel = target.type_;
+              ResourceType currentLevel = target.GetLevel();
               int64_t currentInternalId = internalId;
               Toolbox::GetMissingsFromSet(target.missingRequestedTags_, requestedTags, savedMainDicomTags);
 
@@ -930,7 +933,7 @@
                   DicomMap parentTags;
                   transaction.GetMainDicomTags(parentTags, currentParentId);
 
-                  target.tags_.Merge(parentTags);
+                  target.GetMainDicomTags().Merge(parentTags);
                 }
 
                 currentInternalId = currentParentId;
@@ -938,6 +941,12 @@
             }
           }
 
+          if ((expandFlags & ExpandResourceFlags_IncludeLabels) &&
+              hasLabelsSupport_)
+          {
+            transaction.ListLabels(target.labels_, internalId);
+          }
+
           std::string tmp;
 
           if (LookupStringMetadata(tmp, target.metadata_, MetadataType_AnonymizedFrom))
@@ -954,7 +963,7 @@
               type == ResourceType_Study ||
               type == ResourceType_Series)
           {
-            target.isStable_ = !transaction.GetTransactionContext().IsUnstableResource(internalId);
+            target.isStable_ = !transaction.GetTransactionContext().IsUnstableResource(type, internalId);
 
             if (LookupStringMetadata(tmp, target.metadata_, MetadataType_LastUpdate))
             {
@@ -972,7 +981,7 @@
     };
 
     bool found;
-    Operations operations;
+    Operations operations(db_.GetDatabaseCapabilities().HasLabelsSupport());
     operations.Apply(*this, found, target, publicId, level, requestedTags, expandFlags);
     return found;
   }
@@ -1065,7 +1074,7 @@
   void StatelessDatabaseOperations::GetAllUuids(std::list<std::string>& target,
                                                 ResourceType resourceType,
                                                 size_t since,
-                                                size_t limit)
+                                                uint32_t limit)
   {
     if (limit == 0)
     {
@@ -1097,7 +1106,54 @@
                                                         /* out */ uint64_t& countSeries, 
                                                         /* out */ uint64_t& countInstances)
   {
-    class Operations : public ReadOnlyOperationsT6<uint64_t&, uint64_t&, uint64_t&, uint64_t&, uint64_t&, uint64_t&>
+    // Code introduced in Orthanc 1.12.3 that updates and gets all statistics.
+    // I.e, PostgreSQL now store "changes" to apply to the statistics to prevent row locking
+    // of the GlobalIntegers table while multiple clients are inserting/deleting new resources.
+    // Then, the statistics are updated when requested to make sure they are correct.
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      int64_t diskSize_;
+      int64_t uncompressedSize_;
+      int64_t countPatients_;
+      int64_t countStudies_;
+      int64_t countSeries_;
+      int64_t countInstances_;
+
+    public:
+      Operations() :
+        diskSize_(0),
+        uncompressedSize_(0),
+        countPatients_(0),
+        countStudies_(0),
+        countSeries_(0),
+        countInstances_(0)
+      {
+      }
+
+      void GetValues(uint64_t& diskSize,
+                     uint64_t& uncompressedSize,
+                     uint64_t& countPatients, 
+                     uint64_t& countStudies, 
+                     uint64_t& countSeries, 
+                     uint64_t& countInstances) const
+      {
+        diskSize = static_cast<uint64_t>(diskSize_);
+        uncompressedSize = static_cast<uint64_t>(uncompressedSize_);
+        countPatients = static_cast<uint64_t>(countPatients_);
+        countStudies = static_cast<uint64_t>(countStudies_);
+        countSeries = static_cast<uint64_t>(countSeries_);
+        countInstances = static_cast<uint64_t>(countInstances_);
+      }
+
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        transaction.UpdateAndGetStatistics(countPatients_, countStudies_, countSeries_, countInstances_, diskSize_, uncompressedSize_);
+      }
+    };
+
+    // Compatibility with Orthanc SDK <= 1.12.2 that reads each entry individualy
+    class LegacyOperations : public ReadOnlyOperationsT6<uint64_t&, uint64_t&, uint64_t&, uint64_t&, uint64_t&, uint64_t&>
     {
     public:
       virtual void ApplyTuple(ReadOnlyTransaction& transaction,
@@ -1111,10 +1167,20 @@
         tuple.get<5>() = transaction.GetResourcesCount(ResourceType_Instance);
       }
     };
-    
-    Operations operations;
-    operations.Apply(*this, diskSize, uncompressedSize, countPatients,
-                     countStudies, countSeries, countInstances);
+
+    if (GetDatabaseCapabilities().HasUpdateAndGetStatistics())
+    {
+      Operations operations;
+      Apply(operations);
+
+      operations.GetValues(diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances);
+    } 
+    else
+    {   
+      LegacyOperations operations;
+      operations.Apply(*this, diskSize, uncompressedSize, countPatients,
+                       countStudies, countSeries, countInstances);
+    }
   }
 
 
@@ -1620,20 +1686,20 @@
 
     DicomTagConstraint c(tag, ConstraintType_Equal, value, true, true);
 
-    std::vector<DatabaseConstraint> query;
-    query.push_back(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
+    DatabaseConstraints query;
+    query.AddConstraint(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
 
 
     class Operations : public IReadOnlyOperations
     {
     private:
-      std::vector<std::string>&               result_;
-      const std::vector<DatabaseConstraint>&  query_;
-      ResourceType                            level_;
+      std::vector<std::string>&   result_;
+      const DatabaseConstraints&  query_;
+      ResourceType                level_;
       
     public:
       Operations(std::vector<std::string>& result,
-                 const std::vector<DatabaseConstraint>& query,
+                 const DatabaseConstraints& query,
                  ResourceType level) :
         result_(result),
         query_(query),
@@ -1645,7 +1711,8 @@
       {
         // TODO - CANDIDATE FOR "TransactionType_Implicit"
         std::list<std::string> tmp;
-        transaction.ApplyLookupResources(tmp, NULL, query_, level_, 0);
+        std::set<std::string> labels;
+        transaction.ApplyLookupResources(tmp, NULL, query_, level_, labels, LabelsConstraint_Any, 0);
         CopyListToVector(result_, tmp);
       }
     };
@@ -1914,9 +1981,12 @@
                                                          std::vector<std::string>* instancesId,
                                                          const DatabaseLookup& lookup,
                                                          ResourceType queryLevel,
-                                                         size_t limit)
+                                                         const std::set<std::string>& labels,
+                                                         LabelsConstraint labelsConstraint,
+                                                         uint32_t limit)
   {
-    class Operations : public ReadOnlyOperationsT4<bool, const std::vector<DatabaseConstraint>&, ResourceType, size_t>
+    class Operations : public ReadOnlyOperationsT6<bool, const DatabaseConstraints&, ResourceType,
+                                                   const std::set<std::string>&, LabelsConstraint, size_t>
     {
     private:
       std::list<std::string>  resourcesList_;
@@ -1939,21 +2009,35 @@
         // TODO - CANDIDATE FOR "TransactionType_Implicit"
         if (tuple.get<0>())
         {
-          transaction.ApplyLookupResources(resourcesList_, &instancesList_, tuple.get<1>(), tuple.get<2>(), tuple.get<3>());
+          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>());
+          transaction.ApplyLookupResources(
+            resourcesList_, NULL, tuple.get<1>(), tuple.get<2>(), tuple.get<3>(), tuple.get<4>(), tuple.get<5>());
         }
       }
     };
 
-
-    std::vector<DatabaseConstraint> normalized;
-    NormalizeLookup(normalized, lookup, queryLevel);
+    if (!labels.empty() &&
+        !db_.GetDatabaseCapabilities().HasLabelsSupport())
+    {
+      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);
+    }
+
+    DatabaseConstraints normalized;
+
+    assert(mainDicomTagsRegistry_.get() != NULL);
+    mainDicomTagsRegistry_->NormalizeLookup(normalized, lookup, queryLevel);
 
     Operations operations;
-    operations.Apply(*this, (instancesId != NULL), normalized, queryLevel, limit);
+    operations.Apply(*this, (instancesId != NULL), normalized, queryLevel, labels, labelsConstraint, limit);
     
     CopyListToVector(resourcesId, operations.GetResourcesList());
 
@@ -2014,6 +2098,24 @@
             remainingAncestor_["RemainingAncestor"]["Path"] = GetBasePath(remainingLevel, remainingPublicId);
             remainingAncestor_["RemainingAncestor"]["Type"] = EnumerationToString(remainingLevel);
             remainingAncestor_["RemainingAncestor"]["ID"] = remainingPublicId;
+
+            { // update the LastUpdate metadata of all parents
+              std::string now = SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */);
+              ResourcesContent content(true);
+
+              int64_t parentId = 0;
+              if (transaction.LookupResource(parentId, remainingLevel, remainingPublicId))
+              {
+
+                do
+                {
+                  content.AddMetadata(parentId, MetadataType_LastUpdate, now);
+                }
+                while (transaction.LookupParent(parentId, parentId));
+    
+                transaction.SetResourcesContent(content);
+              }
+            }
           }
           else
           {
@@ -2374,13 +2476,16 @@
       uint64_t       newValue_;
       GlobalProperty sequence_;
       bool           shared_;
+      bool           hasAtomicIncrementGlobalProperty_;
 
     public:
       Operations(GlobalProperty sequence,
-                 bool shared) :
+                 bool shared,
+                 bool hasAtomicIncrementGlobalProperty) :
         newValue_(0),  // Dummy initialization
         sequence_(sequence),
-        shared_(shared)
+        shared_(shared),
+        hasAtomicIncrementGlobalProperty_(hasAtomicIncrementGlobalProperty)
       {
       }
 
@@ -2391,36 +2496,43 @@
 
       virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
       {
-        std::string oldString;
-
-        if (transaction.LookupGlobalProperty(oldString, sequence_, shared_))
+        if (hasAtomicIncrementGlobalProperty_)
         {
-          uint64_t oldValue;
-      
-          try
-          {
-            oldValue = boost::lexical_cast<uint64_t>(oldString);
-          }
-          catch (boost::bad_lexical_cast&)
-          {
-            LOG(ERROR) << "Cannot read the global sequence "
-                       << boost::lexical_cast<std::string>(sequence_) << ", resetting it";
-            oldValue = 0;
-          }
-
-          newValue_ = oldValue + 1;
+          newValue_ = static_cast<uint64_t>(transaction.IncrementGlobalProperty(sequence_, shared_, 1));
         }
         else
         {
-          // Initialize the sequence at "1"
-          newValue_ = 1;
+          std::string oldString;
+
+          if (transaction.LookupGlobalProperty(oldString, sequence_, shared_))
+          {
+            uint64_t oldValue;
+        
+            try
+            {
+              oldValue = boost::lexical_cast<uint64_t>(oldString);
+            }
+            catch (boost::bad_lexical_cast&)
+            {
+              LOG(ERROR) << "Cannot read the global sequence "
+                        << boost::lexical_cast<std::string>(sequence_) << ", resetting it";
+              oldValue = 0;
+            }
+
+            newValue_ = oldValue + 1;
+          }
+          else
+          {
+            // Initialize the sequence at "1"
+            newValue_ = 1;
+          }
+
+          transaction.SetGlobalProperty(sequence_, shared_, boost::lexical_cast<std::string>(newValue_));
         }
-
-        transaction.SetGlobalProperty(sequence_, shared_, boost::lexical_cast<std::string>(newValue_));
       }
     };
 
-    Operations operations(sequence, shared);
+    Operations operations(sequence, shared, GetDatabaseCapabilities().HasAtomicIncrementGlobalProperty());
     Apply(operations);
     assert(operations.GetNewValue() != 0);
     return operations.GetNewValue();
@@ -2651,13 +2763,15 @@
   }
 
 
-  void StatelessDatabaseOperations::ReconstructInstance(const ParsedDicomFile& dicom)
+  void StatelessDatabaseOperations::ReconstructInstance(const ParsedDicomFile& dicom, bool limitToThisLevelDicomTags, ResourceType limitToLevel)
   {
     class Operations : public IReadWriteOperations
     {
     private:
       DicomMap                              summary_;
       std::unique_ptr<DicomInstanceHasher>  hasher_;
+      bool                                  limitToThisLevelDicomTags_;
+      ResourceType                          limitToLevel_;
       bool                                  hasTransferSyntax_;
       DicomTransferSyntax                   transferSyntax_;
 
@@ -2699,7 +2813,9 @@
       }
 
     public:
-      explicit Operations(const ParsedDicomFile& dicom)
+      explicit Operations(const ParsedDicomFile& dicom, bool limitToThisLevelDicomTags, ResourceType limitToLevel)
+      : limitToThisLevelDicomTags_(limitToThisLevelDicomTags),
+        limitToLevel_(limitToLevel)
       {
         OrthancConfiguration::DefaultExtractDicomSummary(summary_, dicom);
         hasher_.reset(new DicomInstanceHasher(summary_));
@@ -2727,48 +2843,76 @@
           throw OrthancException(ErrorCode_InternalError);
         }
 
-        transaction.ClearMainDicomTags(patient);
-        transaction.ClearMainDicomTags(study);
-        transaction.ClearMainDicomTags(series);
-        transaction.ClearMainDicomTags(instance);
-
+        if (limitToThisLevelDicomTags_)
         {
           ResourcesContent content(false /* prevent the setting of metadata */);
-          content.AddResource(patient, ResourceType_Patient, summary_);
-          content.AddResource(study, ResourceType_Study, summary_);
-          content.AddResource(series, ResourceType_Series, summary_);
-          content.AddResource(instance, ResourceType_Instance, summary_);
-
+          int64_t resource = -1;
+          if (limitToLevel_ == ResourceType_Patient)
+          {
+            resource = patient;
+          }
+          else if (limitToLevel_ == ResourceType_Study)
+          {
+            resource = study;
+          }
+          else if (limitToLevel_ == ResourceType_Series)
+          {
+            resource = series;
+          }
+          else if (limitToLevel_ == ResourceType_Instance)
+          {
+            resource = instance;
+          }
+
+          transaction.ClearMainDicomTags(resource);
+          content.AddResource(resource, limitToLevel_, summary_);
           transaction.SetResourcesContent(content);
-
-          ReplaceMetadata(transaction, patient, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Patient));    // New in Orthanc 1.11.0
-          ReplaceMetadata(transaction, study, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Study));        // New in Orthanc 1.11.0
-          ReplaceMetadata(transaction, series, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Series));      // New in Orthanc 1.11.0
-          ReplaceMetadata(transaction, instance, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Instance));  // New in Orthanc 1.11.0
-        
-          SetMainDicomSequenceMetadata(transaction, patient, summary_, ResourceType_Patient);
-          SetMainDicomSequenceMetadata(transaction, study, summary_, ResourceType_Study);
-          SetMainDicomSequenceMetadata(transaction, series, summary_, ResourceType_Series);
-          SetMainDicomSequenceMetadata(transaction, instance, summary_, ResourceType_Instance);
+          ReplaceMetadata(transaction, resource, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(limitToLevel_));
         }
-
-        if (hasTransferSyntax_)
+        else
         {
-          ReplaceMetadata(transaction, instance, MetadataType_Instance_TransferSyntax, GetTransferSyntaxUid(transferSyntax_));
+          transaction.ClearMainDicomTags(patient);
+          transaction.ClearMainDicomTags(study);
+          transaction.ClearMainDicomTags(series);
+          transaction.ClearMainDicomTags(instance);
+
+          {
+            ResourcesContent content(false /* prevent the setting of metadata */);
+            content.AddResource(patient, ResourceType_Patient, summary_);
+            content.AddResource(study, ResourceType_Study, summary_);
+            content.AddResource(series, ResourceType_Series, summary_);
+            content.AddResource(instance, ResourceType_Instance, summary_);
+
+            transaction.SetResourcesContent(content);
+
+            ReplaceMetadata(transaction, patient, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Patient));    // New in Orthanc 1.11.0
+            ReplaceMetadata(transaction, study, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Study));        // New in Orthanc 1.11.0
+            ReplaceMetadata(transaction, series, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Series));      // New in Orthanc 1.11.0
+            ReplaceMetadata(transaction, instance, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Instance));  // New in Orthanc 1.11.0
+          
+            SetMainDicomSequenceMetadata(transaction, patient, summary_, ResourceType_Patient);
+            SetMainDicomSequenceMetadata(transaction, study, summary_, ResourceType_Study);
+            SetMainDicomSequenceMetadata(transaction, series, summary_, ResourceType_Series);
+            SetMainDicomSequenceMetadata(transaction, instance, summary_, ResourceType_Instance);
+          }
+
+          if (hasTransferSyntax_)
+          {
+            ReplaceMetadata(transaction, instance, MetadataType_Instance_TransferSyntax, GetTransferSyntaxUid(transferSyntax_));
+          }
+
+          const DicomValue* value;
+          if ((value = summary_.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL &&
+              !value->IsNull() &&
+              !value->IsBinary())
+          {
+            ReplaceMetadata(transaction, instance, MetadataType_Instance_SopClassUid, value->GetContent());
+          }
         }
-
-        const DicomValue* value;
-        if ((value = summary_.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL &&
-            !value->IsNull() &&
-            !value->IsBinary())
-        {
-          ReplaceMetadata(transaction, instance, MetadataType_Instance_SopClassUid, value->GetContent());
-        }
-
       }
     };
 
-    Operations operations(dicom);
+    Operations operations(dicom, limitToThisLevelDicomTags, limitToLevel);
     Apply(operations);
   }
 
@@ -2916,6 +3060,7 @@
                                                  DicomTransferSyntax transferSyntax,
                                                  bool hasPixelDataOffset,
                                                  uint64_t pixelDataOffset,
+                                                 ValueRepresentation pixelDataVR,
                                                  MaxStorageMode maximumStorageMode,
                                                  uint64_t maximumStorageSize,
                                                  unsigned int maximumPatients,
@@ -2935,6 +3080,7 @@
       DicomTransferSyntax                  transferSyntax_;
       bool                                 hasPixelDataOffset_;
       uint64_t                             pixelDataOffset_;
+      ValueRepresentation                  pixelDataVR_;
       MaxStorageMode                       maximumStorageMode_;
       uint64_t                             maximumStorageSize_;
       unsigned int                         maximumPatientCount_;
@@ -3038,6 +3184,7 @@
                  DicomTransferSyntax transferSyntax,
                  bool hasPixelDataOffset,
                  uint64_t pixelDataOffset,
+                 ValueRepresentation pixelDataVR,
                  MaxStorageMode maximumStorageMode,
                  uint64_t maximumStorageSize,
                  unsigned int maximumPatientCount,
@@ -3053,6 +3200,7 @@
         transferSyntax_(transferSyntax),
         hasPixelDataOffset_(hasPixelDataOffset),
         pixelDataOffset_(pixelDataOffset),
+        pixelDataVR_(pixelDataVR),
         maximumStorageMode_(maximumStorageMode),
         maximumStorageSize_(maximumStorageSize),
         maximumPatientCount_(maximumPatientCount),
@@ -3076,261 +3224,287 @@
         
       virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
       {
-        try
+        IDatabaseWrapper::CreateInstanceResult status;
+        int64_t instanceId;
+        
+        bool isNewInstance = transaction.CreateInstance(status, instanceId, hashPatient_,
+                                                        hashStudy_, hashSeries_, hashInstance_);
+
+        if (isReconstruct_ && isNewInstance)
         {
-          IDatabaseWrapper::CreateInstanceResult status;
-          int64_t instanceId;
-
-          // Check whether this instance is already stored
-          if (!transaction.CreateInstance(status, instanceId, hashPatient_,
-                                          hashStudy_, hashSeries_, hashInstance_))
+          // In case of reconstruct, we just want to modify the attachments and some metadata like the TransferSyntex
+          // The DicomTags and many metadata have already been updated before we get here in ReconstructInstance
+          throw OrthancException(ErrorCode_InternalError, "New instance while reconstructing; this should not happen.");
+        }
+
+        // Check whether this instance is already stored
+        if (!isNewInstance && !isReconstruct_)
+        {
+          // The instance already exists
+          if (overwrite_)
           {
-            // The instance already exists
-        
-            if (overwrite_)
+            // Overwrite the old instance
+            LOG(INFO) << "Overwriting instance: " << hashInstance_;
+            transaction.DeleteResource(instanceId);
+
+            // Re-create the instance, now that the old one is removed
+            if (!transaction.CreateInstance(status, instanceId, hashPatient_,
+                                            hashStudy_, hashSeries_, hashInstance_))
             {
-              // Overwrite the old instance
-              LOG(INFO) << "Overwriting instance: " << hashInstance_;
-              transaction.DeleteResource(instanceId);
-
-              // Re-create the instance, now that the old one is removed
-              if (!transaction.CreateInstance(status, instanceId, hashPatient_,
-                                              hashStudy_, hashSeries_, hashInstance_))
-              {
-                throw OrthancException(ErrorCode_InternalError);
-              }
+              // Note that, sometime, it does not create a new instance, 
+              // in very rare occasions in READ COMMITTED mode when multiple clients are pushing the same instance at the same time,
+              // this thread will not create the instance because another thread has created it in the meantime.
+              // At the end, there is always a thread that creates the instance and this is what we expect.
+
+              // Note, we must delete the attachments that have already been stored from this failed insertion (they have not yet been added into the DB)
+              throw OrthancException(ErrorCode_DuplicateResource, "No new instance while overwriting; this might happen if another client has pushed the same instance at the same time.");
             }
-            else
+          }
+          else
+          {
+            // Do nothing if the instance already exists and overwriting is disabled
+            transaction.GetAllMetadata(instanceMetadata_, instanceId);
+            storeStatus_ = StoreStatus_AlreadyStored;
+            return;
+          }
+        }
+
+
+        if (!isReconstruct_)  // don't signal new resources if this is a reconstruction
+        {
+          // Warn about the creation of new resources. The order must be
+          // from instance to patient.
+
+          // NB: In theory, could be sped up by grouping the underlying
+          // calls to "transaction.LogChange()". However, this would only have an
+          // impact when new patient/study/series get created, which
+          // occurs far less often that creating new instances. The
+          // positive impact looks marginal in practice.
+          transaction.LogChange(instanceId, ChangeType_NewInstance, ResourceType_Instance, hashInstance_);
+
+          if (status.isNewSeries_)
+          {
+            transaction.LogChange(status.seriesId_, ChangeType_NewSeries, ResourceType_Series, hashSeries_);
+          }
+      
+          if (status.isNewStudy_)
+          {
+            transaction.LogChange(status.studyId_, ChangeType_NewStudy, ResourceType_Study, hashStudy_);
+          }
+      
+          if (status.isNewPatient_)
+          {
+            transaction.LogChange(status.patientId_, ChangeType_NewPatient, ResourceType_Patient, hashPatient_);
+          }
+        }      
+    
+        // 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)
+        {
+          instanceSize += it->GetCompressedSize();
+        }
+
+        if (!isReconstruct_)  // reconstruction should not affect recycling
+        {
+          if (maximumStorageMode_ == MaxStorageMode_Reject)
+          {
+            if (transaction.HasReachedMaxStorageSize(maximumStorageSize_, instanceSize))
             {
-              // Do nothing if the instance already exists and overwriting is disabled
-              transaction.GetAllMetadata(instanceMetadata_, instanceId);
-              storeStatus_ = StoreStatus_AlreadyStored;
-              return;
+              storeStatus_ = StoreStatus_StorageFull;
+              throw OrthancException(ErrorCode_FullStorage, HttpStatus_507_InsufficientStorage, "Maximum storage size reached"); // throw to cancel the transaction
+            }
+            if (transaction.HasReachedMaxPatientCount(maximumPatientCount_, hashPatient_))
+            {
+              storeStatus_ = StoreStatus_StorageFull;
+              throw OrthancException(ErrorCode_FullStorage, HttpStatus_507_InsufficientStorage, "Maximum patient count reached");  // throw to cancel the transaction
             }
           }
-
-
-          if (!isReconstruct_)  // don't signal new resources if this is a reconstruction
+          else
+          {
+            transaction.Recycle(maximumStorageSize_, maximumPatientCount_,
+                                instanceSize, hashPatient_ /* don't consider the current patient for recycling */);
+          }
+        }  
+    
+        // Attach the files to the newly created instance
+        for (Attachments::const_iterator it = attachments_.begin();
+              it != attachments_.end(); ++it)
+        {
+          if (isReconstruct_)
+          {
+            // we are replacing attachments during a reconstruction
+            transaction.DeleteAttachment(instanceId, it->GetContentType());
+          }
+
+          transaction.AddAttachment(instanceId, *it, 0 /* this is the first revision */);
+        }
+
+        ResourcesContent content(true /* new resource, metadata can be set */);
+
+        // Attach the user-specified metadata (in case of reconstruction, metadata_ contains all past metadata, including the system ones we want to keep)
+        for (MetadataMap::const_iterator 
+                it = metadata_.begin(); it != metadata_.end(); ++it)
+        {
+          switch (it->first.first)
           {
-            // Warn about the creation of new resources. The order must be
-            // from instance to patient.
-
-            // NB: In theory, could be sped up by grouping the underlying
-            // calls to "transaction.LogChange()". However, this would only have an
-            // impact when new patient/study/series get created, which
-            // occurs far less often that creating new instances. The
-            // positive impact looks marginal in practice.
-            transaction.LogChange(instanceId, ChangeType_NewInstance, ResourceType_Instance, hashInstance_);
-
-            if (status.isNewSeries_)
-            {
-              transaction.LogChange(status.seriesId_, ChangeType_NewSeries, ResourceType_Series, hashSeries_);
-            }
-        
-            if (status.isNewStudy_)
-            {
-              transaction.LogChange(status.studyId_, ChangeType_NewStudy, ResourceType_Study, hashStudy_);
-            }
-        
-            if (status.isNewPatient_)
-            {
-              transaction.LogChange(status.patientId_, ChangeType_NewPatient, ResourceType_Patient, hashPatient_);
-            }
-          }      
-      
-          // 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)
+            case ResourceType_Patient:
+              content.AddMetadata(status.patientId_, it->first.second, it->second);
+              break;
+
+            case ResourceType_Study:
+              content.AddMetadata(status.studyId_, it->first.second, it->second);
+              break;
+
+            case ResourceType_Series:
+              content.AddMetadata(status.seriesId_, it->first.second, it->second);
+              break;
+
+            case ResourceType_Instance:
+              SetInstanceMetadata(content, instanceMetadata_, instanceId,
+                                  it->first.second, it->second);
+              break;
+
+            default:
+              throw OrthancException(ErrorCode_ParameterOutOfRange);
+          }
+        }
+
+        if (!isReconstruct_)
+        {
+          // Populate the tags of the newly-created resources
+          content.AddResource(instanceId, ResourceType_Instance, dicomSummary_);
+          SetInstanceMetadata(content, instanceMetadata_, instanceId, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Instance));  // New in Orthanc 1.11.0
+          SetMainDicomSequenceMetadata(content, instanceId, dicomSummary_, ResourceType_Instance);   // new in Orthanc 1.11.1
+
+          if (status.isNewSeries_)
           {
-            instanceSize += it->GetCompressedSize();
+            content.AddResource(status.seriesId_, ResourceType_Series, dicomSummary_);
+            content.AddMetadata(status.seriesId_, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Series));  // New in Orthanc 1.11.0
+            SetMainDicomSequenceMetadata(content, status.seriesId_, dicomSummary_, ResourceType_Series);   // new in Orthanc 1.11.1
           }
 
-          if (!isReconstruct_)  // reconstruction should not affect recycling
+          if (status.isNewStudy_)
+          {
+            content.AddResource(status.studyId_, ResourceType_Study, dicomSummary_);
+            content.AddMetadata(status.studyId_, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Study));  // New in Orthanc 1.11.0
+            SetMainDicomSequenceMetadata(content, status.studyId_, dicomSummary_, ResourceType_Study);   // new in Orthanc 1.11.1
+          }
+
+          if (status.isNewPatient_)
           {
-            if (maximumStorageMode_ == MaxStorageMode_Reject)
-            {
-              if (transaction.HasReachedMaxStorageSize(maximumStorageSize_, instanceSize))
-              {
-                storeStatus_ = StoreStatus_StorageFull;
-                throw OrthancException(ErrorCode_FullStorage, HttpStatus_507_InsufficientStorage, "Maximum storage size reached"); // throw to cancel the transaction
-              }
-              if (transaction.HasReachedMaxPatientCount(maximumPatientCount_, hashPatient_))
-              {
-                storeStatus_ = StoreStatus_StorageFull;
-                throw OrthancException(ErrorCode_FullStorage, HttpStatus_507_InsufficientStorage, "Maximum patient count reached");  // throw to cancel the transaction
-              }
-            }
-            else
+            content.AddResource(status.patientId_, ResourceType_Patient, dicomSummary_);
+            content.AddMetadata(status.patientId_, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Patient));  // New in Orthanc 1.11.0
+            SetMainDicomSequenceMetadata(content, status.patientId_, dicomSummary_, ResourceType_Patient);   // new in Orthanc 1.11.1
+          }
+
+          // Attach the auto-computed metadata for the patient/study/series levels
+          std::string now = SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */);
+          content.AddMetadata(status.seriesId_, MetadataType_LastUpdate, now);
+          content.AddMetadata(status.studyId_, MetadataType_LastUpdate, now);
+          content.AddMetadata(status.patientId_, MetadataType_LastUpdate, now);
+
+          if (status.isNewSeries_)
+          {
+            if (hasExpectedInstances_)
             {
-              transaction.Recycle(maximumStorageSize_, maximumPatientCount_,
-                                  instanceSize, hashPatient_ /* don't consider the current patient for recycling */);
-            }
-          }  
-     
-          // Attach the files to the newly created instance
-          for (Attachments::const_iterator it = attachments_.begin();
-               it != attachments_.end(); ++it)
-          {
-            transaction.AddAttachment(instanceId, *it, 0 /* this is the first revision */);
-          }
-
-      
-          {
-            ResourcesContent content(true /* new resource, metadata can be set */);
-
-
-            // Attach the user-specified metadata (in case of reconstruction, metadata_ contains all past metadata, including the system ones we want to keep)
-            for (MetadataMap::const_iterator 
-                   it = metadata_.begin(); it != metadata_.end(); ++it)
-            {
-              switch (it->first.first)
-              {
-                case ResourceType_Patient:
-                  content.AddMetadata(status.patientId_, it->first.second, it->second);
-                  break;
-
-                case ResourceType_Study:
-                  content.AddMetadata(status.studyId_, it->first.second, it->second);
-                  break;
-
-                case ResourceType_Series:
-                  content.AddMetadata(status.seriesId_, it->first.second, it->second);
-                  break;
-
-                case ResourceType_Instance:
-                  SetInstanceMetadata(content, instanceMetadata_, instanceId,
-                                      it->first.second, it->second);
-                  break;
-
-                default:
-                  throw OrthancException(ErrorCode_ParameterOutOfRange);
-              }
-            }
-
-            // Populate the tags of the newly-created resources
-
-            content.AddResource(instanceId, ResourceType_Instance, dicomSummary_);
-            SetInstanceMetadata(content, instanceMetadata_, instanceId, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Instance));  // New in Orthanc 1.11.0
-            SetMainDicomSequenceMetadata(content, instanceId, dicomSummary_, ResourceType_Instance);   // new in Orthanc 1.11.1
-
-            if (status.isNewSeries_)
-            {
-              content.AddResource(status.seriesId_, ResourceType_Series, dicomSummary_);
-              content.AddMetadata(status.seriesId_, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Series));  // New in Orthanc 1.11.0
-              SetMainDicomSequenceMetadata(content, status.seriesId_, dicomSummary_, ResourceType_Series);   // new in Orthanc 1.11.1
+              content.AddMetadata(status.seriesId_, MetadataType_Series_ExpectedNumberOfInstances,
+                                  boost::lexical_cast<std::string>(expectedInstances_));
             }
 
-            if (status.isNewStudy_)
-            {
-              content.AddResource(status.studyId_, ResourceType_Study, dicomSummary_);
-              content.AddMetadata(status.studyId_, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Study));  // New in Orthanc 1.11.0
-              SetMainDicomSequenceMetadata(content, status.studyId_, dicomSummary_, ResourceType_Study);   // new in Orthanc 1.11.1
-            }
-
-            if (status.isNewPatient_)
-            {
-              content.AddResource(status.patientId_, ResourceType_Patient, dicomSummary_);
-              content.AddMetadata(status.patientId_, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Patient));  // New in Orthanc 1.11.0
-              SetMainDicomSequenceMetadata(content, status.patientId_, dicomSummary_, ResourceType_Patient);   // new in Orthanc 1.11.1
-            }
-
-            // Attach the auto-computed metadata for the patient/study/series levels
-            std::string now = SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */);
-            content.AddMetadata(status.seriesId_, MetadataType_LastUpdate, now);
-            content.AddMetadata(status.studyId_, MetadataType_LastUpdate, now);
-            content.AddMetadata(status.patientId_, MetadataType_LastUpdate, now);
-
-            if (status.isNewSeries_)
-            {
-              if (hasExpectedInstances_)
-              {
-                content.AddMetadata(status.seriesId_, MetadataType_Series_ExpectedNumberOfInstances,
-                                    boost::lexical_cast<std::string>(expectedInstances_));
-              }
-
-              // New in Orthanc 1.9.0
-              content.AddMetadata(status.seriesId_, MetadataType_RemoteAet,
-                                  origin_.GetRemoteAetC());
-            }
-
-            if (hasTransferSyntax_)
-            {
-              // New in Orthanc 1.2.0
-              SetInstanceMetadata(content, instanceMetadata_, instanceId,
-                                  MetadataType_Instance_TransferSyntax,
-                                  GetTransferSyntaxUid(transferSyntax_));
-            }
-
-            if (!isReconstruct_) // don't change origin metadata
-            {        
-              // Attach the auto-computed metadata for the instance level,
-              // reflecting these additions into the input metadata map
-              SetInstanceMetadata(content, instanceMetadata_, instanceId,
-                                  MetadataType_Instance_ReceptionDate, now);
-              SetInstanceMetadata(content, instanceMetadata_, instanceId, MetadataType_RemoteAet,
-                                  origin_.GetRemoteAetC());
-              SetInstanceMetadata(content, instanceMetadata_, instanceId, MetadataType_Instance_Origin, 
-                                  EnumerationToString(origin_.GetRequestOrigin()));
-
-              std::string s;
-
-              if (origin_.LookupRemoteIp(s))
-              {
-                // New in Orthanc 1.4.0
-                SetInstanceMetadata(content, instanceMetadata_, instanceId,
-                                    MetadataType_Instance_RemoteIp, s);
-              }
-
-              if (origin_.LookupCalledAet(s))
-              {
-                // New in Orthanc 1.4.0
-                SetInstanceMetadata(content, instanceMetadata_, instanceId,
-                                    MetadataType_Instance_CalledAet, s);
-              }
-
-              if (origin_.LookupHttpUsername(s))
-              {
-                // New in Orthanc 1.4.0
-                SetInstanceMetadata(content, instanceMetadata_, instanceId,
-                                    MetadataType_Instance_HttpUsername, s);
-              }
-            }
-
-            if (hasPixelDataOffset_)
-            {
-              // New in Orthanc 1.9.1
-              SetInstanceMetadata(content, instanceMetadata_, instanceId,
-                                  MetadataType_Instance_PixelDataOffset,
-                                  boost::lexical_cast<std::string>(pixelDataOffset_));
-            }
-        
-            const DicomValue* value;
-            if ((value = dicomSummary_.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL &&
-                !value->IsNull() &&
-                !value->IsBinary())
-            {
-              SetInstanceMetadata(content, instanceMetadata_, instanceId,
-                                  MetadataType_Instance_SopClassUid, value->GetContent());
-            }
-
-
-            if ((value = dicomSummary_.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL ||
-                (value = dicomSummary_.TestAndGetValue(DICOM_TAG_IMAGE_INDEX)) != NULL)
-            {
-              if (!value->IsNull() && 
-                  !value->IsBinary())
-              {
-                SetInstanceMetadata(content, instanceMetadata_, instanceId,
-                                    MetadataType_Instance_IndexInSeries, Toolbox::StripSpaces(value->GetContent()));
-              }
-            }
-
-        
-            transaction.SetResourcesContent(content);
+            // New in Orthanc 1.9.0
+            content.AddMetadata(status.seriesId_, MetadataType_RemoteAet,
+                                origin_.GetRemoteAetC());
+          }
+          // Attach the auto-computed metadata for the instance level,
+          // reflecting these additions into the input metadata map
+          SetInstanceMetadata(content, instanceMetadata_, instanceId,
+                              MetadataType_Instance_ReceptionDate, now);
+          SetInstanceMetadata(content, instanceMetadata_, instanceId, MetadataType_RemoteAet,
+                              origin_.GetRemoteAetC());
+          SetInstanceMetadata(content, instanceMetadata_, instanceId, MetadataType_Instance_Origin, 
+                              EnumerationToString(origin_.GetRequestOrigin()));
+
+          std::string s;
+
+          if (origin_.LookupRemoteIp(s))
+          {
+            // New in Orthanc 1.4.0
+            SetInstanceMetadata(content, instanceMetadata_, instanceId,
+                                MetadataType_Instance_RemoteIp, s);
+          }
+
+          if (origin_.LookupCalledAet(s))
+          {
+            // New in Orthanc 1.4.0
+            SetInstanceMetadata(content, instanceMetadata_, instanceId,
+                                MetadataType_Instance_CalledAet, s);
+          }
+
+          if (origin_.LookupHttpUsername(s))
+          {
+            // New in Orthanc 1.4.0
+            SetInstanceMetadata(content, instanceMetadata_, instanceId,
+                                MetadataType_Instance_HttpUsername, s);
           }
-
-  
+        }
+
+        // Following metadatas are also updated if reconstructing the instance.
+        // They might be missing since they have been introduced along Orthanc versions.
+
+        if (hasTransferSyntax_)
+        {
+          // New in Orthanc 1.2.0
+          SetInstanceMetadata(content, instanceMetadata_, instanceId,
+                              MetadataType_Instance_TransferSyntax,
+                              GetTransferSyntaxUid(transferSyntax_));
+        }
+
+        if (hasPixelDataOffset_)
+        {
+          // New in Orthanc 1.9.1
+          SetInstanceMetadata(content, instanceMetadata_, instanceId,
+                              MetadataType_Instance_PixelDataOffset,
+                              boost::lexical_cast<std::string>(pixelDataOffset_));
+
+          // New in Orthanc 1.12.1
+          if (dicomSummary_.GuessPixelDataValueRepresentation(transferSyntax_) != pixelDataVR_)
+          {
+            // Store the VR of pixel data if it doesn't comply with the standard
+            SetInstanceMetadata(content, instanceMetadata_, instanceId,
+                                MetadataType_Instance_PixelDataVR,
+                                EnumerationToString(pixelDataVR_));
+          }
+        }
+    
+        const DicomValue* value;
+        if ((value = dicomSummary_.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL &&
+            !value->IsNull() &&
+            !value->IsBinary())
+        {
+          SetInstanceMetadata(content, instanceMetadata_, instanceId,
+                              MetadataType_Instance_SopClassUid, value->GetContent());
+        }
+
+
+        if ((value = dicomSummary_.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL ||
+            (value = dicomSummary_.TestAndGetValue(DICOM_TAG_IMAGE_INDEX)) != NULL)
+        {
+          if (!value->IsNull() && 
+              !value->IsBinary())
+          {
+            SetInstanceMetadata(content, instanceMetadata_, instanceId,
+                                MetadataType_Instance_IndexInSeries, Toolbox::StripSpaces(value->GetContent()));
+          }
+        }
+
+    
+        transaction.SetResourcesContent(content);
+
+
+        if (!isReconstruct_)  // a reconstruct shall not trigger any events
+        {
           // Check whether the series of this new instance is now completed
           int64_t expectedNumberOfInstances;
           if (ComputeExpectedNumberOfInstances(expectedNumberOfInstances, dicomSummary_))
@@ -3347,41 +3521,38 @@
           transaction.LogChange(status.patientId_, ChangeType_NewChildInstance, ResourceType_Patient, hashPatient_);
           
           // Mark the parent resources of this instance as unstable
-          transaction.GetTransactionContext().MarkAsUnstable(status.seriesId_, ResourceType_Series, hashSeries_);
-          transaction.GetTransactionContext().MarkAsUnstable(status.studyId_, ResourceType_Study, hashStudy_);
-          transaction.GetTransactionContext().MarkAsUnstable(status.patientId_, ResourceType_Patient, hashPatient_);
-          transaction.GetTransactionContext().SignalAttachmentsAdded(instanceSize);
-
-          storeStatus_ = StoreStatus_Success;          
+          transaction.GetTransactionContext().MarkAsUnstable(ResourceType_Series, status.seriesId_, hashSeries_);
+          transaction.GetTransactionContext().MarkAsUnstable(ResourceType_Study, status.studyId_, hashStudy_);
+          transaction.GetTransactionContext().MarkAsUnstable(ResourceType_Patient, status.patientId_, hashPatient_);
         }
-        catch (OrthancException& e)
-        {
-          if (e.GetErrorCode() == ErrorCode_DatabaseCannotSerialize)
-          {
-            throw;  // the transaction has failed -> do not commit the current transaction (and retry)
-          }
-          else
-          {
-            LOG(ERROR) << "EXCEPTION [" << e.What() << " - " << e.GetDetails() << "]";
-
-            if (e.GetErrorCode() == ErrorCode_FullStorage)
-            {
-              throw; // do not commit the current transaction
-            }
-
-            // this is an expected failure, exit normaly and commit the current transaction
-            storeStatus_ = StoreStatus_Failure;
-          }
-        }
+
+        transaction.GetTransactionContext().SignalAttachmentsAdded(instanceSize);
+        storeStatus_ = StoreStatus_Success;
       }
     };
 
 
-    Operations operations(instanceMetadata, dicomSummary, attachments, metadata, origin,
-                          overwrite, hasTransferSyntax, transferSyntax, hasPixelDataOffset,
-                          pixelDataOffset, maximumStorageMode, maximumStorageSize, maximumPatients, isReconstruct);
-    Apply(operations);
-    return operations.GetStoreStatus();
+    Operations operations(instanceMetadata, dicomSummary, attachments, metadata, origin, overwrite,
+                          hasTransferSyntax, transferSyntax, hasPixelDataOffset, pixelDataOffset,
+                          pixelDataVR, maximumStorageMode, maximumStorageSize, maximumPatients, isReconstruct);
+
+    try
+    {
+      Apply(operations);
+      return operations.GetStoreStatus();
+    }
+    catch (OrthancException& e)
+    {
+      if (e.GetErrorCode() == ErrorCode_FullStorage)
+      {
+        return StoreStatus_StorageFull;
+      }
+      else
+      {
+        // the transaction has failed -> do not commit the current transaction (and retry)
+        throw;
+      }
+    }
   }
 
 
@@ -3439,7 +3610,7 @@
         int64_t resourceId;
         if (!transaction.LookupResource(resourceId, resourceType, publicId_))
         {
-          status_ = StoreStatus_Failure;  // Inexistent resource
+          throw OrthancException(ErrorCode_InexistentItem, HttpStatus_404_NotFound);
         }
         else
         {
@@ -3511,4 +3682,128 @@
     Apply(operations);
     return operations.GetStatus();
   }
+
+
+  void StatelessDatabaseOperations::ListLabels(std::set<std::string>& target,
+                                               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);
+  }
+
+
+  void StatelessDatabaseOperations::ListAllLabels(std::set<std::string>& target)
+  {
+    class Operations : public ReadOnlyOperationsT1<std::set<std::string>& >
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        transaction.ListAllLabels(tuple.get<0>());
+      }
+    };
+
+    Operations operations;
+    operations.Apply(*this, target);
+  }
+  
+
+  void StatelessDatabaseOperations::AddLabels(const std::string& publicId,
+                                              ResourceType level,
+                                              const std::set<std::string>& labels)
+  {
+    for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
+    {
+      ModifyLabel(publicId, level, *it, LabelOperation_Add);
+    }
+  }
+
+
+  void StatelessDatabaseOperations::ModifyLabel(const std::string& publicId,
+                                                ResourceType level,
+                                                const std::string& label,
+                                                LabelOperation operation)
+  {
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      const std::string& publicId_;
+      ResourceType       level_;
+      const std::string& label_;
+      LabelOperation     operation_;
+
+    public:
+      Operations(const std::string& publicId,
+                 ResourceType level,
+                 const std::string& label,
+                 LabelOperation operation) :
+        publicId_(publicId),
+        level_(level),
+        label_(label),
+        operation_(operation)
+      {
+      }
+
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        ResourceType type;
+        int64_t id;
+        if (!transaction.LookupResource(id, type, publicId_) ||
+            level_ != type)
+        {
+          throw OrthancException(ErrorCode_UnknownResource);
+        }
+        else
+        {
+          switch (operation_)
+          {
+            case LabelOperation_Add:
+              transaction.AddLabel(id, label_);
+              break;
+
+            case LabelOperation_Remove:
+              transaction.RemoveLabel(id, label_);
+              break;
+
+            default:
+              throw OrthancException(ErrorCode_ParameterOutOfRange);
+          }
+        }
+      }
+    };
+
+    ServerToolbox::CheckValidLabel(label);
+    
+    Operations operations(publicId, level, label, operation);
+    Apply(operations);
+  }
+
+
+  bool StatelessDatabaseOperations::HasLabelsSupport()
+  {
+    boost::shared_lock<boost::shared_mutex> lock(mutex_);
+    return db_.GetDatabaseCapabilities().HasLabelsSupport();
+  }
 }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -37,15 +38,18 @@
   class ParsedDicomFile;
   struct ServerIndexChange;
 
-  struct ExpandedResource : public boost::noncopyable
+  class ExpandedResource : public boost::noncopyable
   {
+  private:
     std::string                         id_;
-    DicomMap                            tags_;          // all main tags and main sequences from DB
+    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_;
-    ResourceType                        type_;
     std::string                         anonymizedFrom_;
     std::string                         modifiedFrom_;
     std::string                         lastUpdate_;
@@ -62,18 +66,61 @@
     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 ExpandResourceDbFlags
+  enum ExpandResourceFlags
   {
-    ExpandResourceDbFlags_None                    = 0,
-    ExpandResourceDbFlags_IncludeMetadata         = (1 << 0),
-    ExpandResourceDbFlags_IncludeChildren         = (1 << 1),
-    ExpandResourceDbFlags_IncludeMainDicomTags    = (1 << 2),
+    ExpandResourceFlags_None                    = 0,
+    ExpandResourceFlags_IncludeMetadata         = (1 << 0),
+    ExpandResourceFlags_IncludeChildren         = (1 << 1),
+    ExpandResourceFlags_IncludeMainDicomTags    = (1 << 2),
+    ExpandResourceFlags_IncludeLabels           = (1 << 3),
 
-    ExpandResourceDbFlags_Default = (ExpandResourceDbFlags_IncludeMetadata |
-                                     ExpandResourceDbFlags_IncludeChildren |
-                                     ExpandResourceDbFlags_IncludeMainDicomTags)
+    ExpandResourceFlags_Default = (ExpandResourceFlags_IncludeMetadata |
+                                     ExpandResourceFlags_IncludeChildren |
+                                     ExpandResourceFlags_IncludeMainDicomTags |
+                                     ExpandResourceFlags_IncludeLabels)
   };
 
   class StatelessDatabaseOperations : public boost::noncopyable
@@ -82,6 +129,12 @@
     typedef std::list<FileInfo> Attachments;
     typedef std::map<std::pair<ResourceType, MetadataType>, std::string>  MetadataMap;
 
+    enum LabelOperation
+    {
+      LabelOperation_Add,
+      LabelOperation_Remove
+    };
+
     class ITransactionContext : public IDatabaseListener
     {
     public:
@@ -93,13 +146,14 @@
 
       virtual int64_t GetCompressedSizeDelta() = 0;
 
-      virtual bool IsUnstableResource(int64_t id) = 0;
+      virtual bool IsUnstableResource(Orthanc::ResourceType type,
+                                      int64_t id) = 0;
 
       virtual bool LookupRemainingLevel(std::string& remainingPublicId /* out */,
                                         ResourceType& remainingLevel   /* out */) = 0;
 
-      virtual void MarkAsUnstable(int64_t id,
-                                  Orthanc::ResourceType type,
+      virtual void MarkAsUnstable(Orthanc::ResourceType type,
+                                  int64_t id,
                                   const std::string& publicId) = 0;
 
       virtual void SignalAttachmentsAdded(uint64_t compressedSize) = 0;
@@ -124,7 +178,7 @@
     {
     private:
       ITransactionContext&  context_;
-      
+
     protected:
       IDatabaseWrapper::ITransaction&  transaction_;
       
@@ -155,11 +209,14 @@
 
       void ApplyLookupResources(std::list<std::string>& resourcesId,
                                 std::list<std::string>* instancesId, // Can be NULL if not needed
-                                const std::vector<DatabaseConstraint>& lookup,
+                                const DatabaseConstraints& lookup,
                                 ResourceType queryLevel,
-                                size_t limit)
+                                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, limit);
+        return transaction_.ApplyLookupResources(resourcesId, instancesId, lookup, queryLevel,
+                                                 labels, labelsConstraint, limit);
       }
 
       void GetAllMetadata(std::map<MetadataType, std::string>& target,
@@ -177,7 +234,7 @@
       void GetAllPublicIds(std::list<std::string>& target,
                            ResourceType resourceType,
                            size_t since,
-                           size_t limit)
+                           uint32_t limit)
       {
         return transaction_.GetAllPublicIds(target, resourceType, since, limit);
       }  
@@ -185,9 +242,9 @@
       void GetChanges(std::list<ServerIndexChange>& target /*out*/,
                       bool& done /*out*/,
                       int64_t since,
-                      uint32_t maxResults)
+                      uint32_t limit)
       {
-        transaction_.GetChanges(target, done, since, maxResults);
+        transaction_.GetChanges(target, done, since, limit);
       }
 
       void GetChildrenInternalId(std::list<int64_t>& target,
@@ -205,9 +262,9 @@
       void GetExportedResources(std::list<ExportedResource>& target /*out*/,
                                 bool& done /*out*/,
                                 int64_t since,
-                                uint32_t maxResults)
+                                uint32_t limit)
       {
-        return transaction_.GetExportedResources(target, done, since, maxResults);
+        return transaction_.GetExportedResources(target, done, since, limit);
       }
 
       void GetLastChange(std::list<ServerIndexChange>& target /*out*/)
@@ -310,6 +367,17 @@
       {
         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);
+      }
     };
 
 
@@ -388,6 +456,23 @@
         transaction_.SetGlobalProperty(property, shared, value);
       }
 
+      int64_t IncrementGlobalProperty(GlobalProperty sequence,
+                                      bool shared,
+                                      int64_t increment)
+      {
+        return transaction_.IncrementGlobalProperty(sequence, shared, increment);
+      }
+
+      void UpdateAndGetStatistics(int64_t& patientsCount,
+                                  int64_t& studiesCount,
+                                  int64_t& seriesCount,
+                                  int64_t& instancesCount,
+                                  int64_t& compressedSize,
+                                  int64_t& uncompressedSize)
+      {
+        return transaction_.UpdateAndGetStatistics(patientsCount, studiesCount, seriesCount, instancesCount, compressedSize, uncompressedSize);
+      }
+
       void SetMetadata(int64_t id,
                        MetadataType type,
                        const std::string& value,
@@ -422,6 +507,18 @@
                              unsigned int maximumPatients,
                              uint64_t addedInstanceSize,
                              const std::string& newPatientId);
+
+      void AddLabel(int64_t id,
+                    const std::string& label)
+      {
+        transaction_.AddLabel(id, label);
+      }
+
+      void RemoveLabel(int64_t id,
+                    const std::string& label)
+      {
+        transaction_.RemoveLabel(id, label);
+      }
     };
 
 
@@ -453,17 +550,12 @@
 
     IDatabaseWrapper&                            db_;
     boost::shared_ptr<MainDicomTagsRegistry>     mainDicomTagsRegistry_;  // "shared_ptr" because of PImpl
-    bool                                         hasFlushToDisk_;
 
     // Mutex to protect the configuration options
     boost::shared_mutex                          mutex_;
     std::unique_ptr<ITransactionContextFactory>  factory_;
     unsigned int                                 maxRetries_;
 
-    void NormalizeLookup(std::vector<DatabaseConstraint>& target,
-                         const DatabaseLookup& source,
-                         ResourceType level) const;
-
     void ApplyInternal(IReadOnlyOperations* readOperations,
                        IReadWriteOperations* writeOperations);
 
@@ -488,12 +580,13 @@
       return db_.GetDatabaseVersion();
     }
 
+    const IDatabaseWrapper::Capabilities GetDatabaseCapabilities() const
+    {
+      return db_.GetDatabaseCapabilities();
+    }
+
     void FlushToDisk();
 
-    bool HasFlushToDisk() const
-    {
-      return hasFlushToDisk_;
-    }
 
     void Apply(IReadOnlyOperations& operations);
   
@@ -503,7 +596,7 @@
                         const std::string& publicId,
                         ResourceType level,
                         const std::set<DicomTag>& requestedTags,
-                        ExpandResourceDbFlags expandFlags);
+                        ExpandResourceFlags expandFlags);
 
     void GetAllMetadata(std::map<MetadataType, std::string>& target,
                         const std::string& publicId,
@@ -515,7 +608,7 @@
     void GetAllUuids(std::list<std::string>& target,
                      ResourceType resourceType,
                      size_t since,
-                     size_t limit);
+                     uint32_t limit);
 
     void GetGlobalStatistics(/* out */ uint64_t& diskSize,
                              /* out */ uint64_t& uncompressedSize,
@@ -531,13 +624,13 @@
 
     void GetChanges(Json::Value& target,
                     int64_t since,
-                    unsigned int maxResults);
+                    uint32_t limit);
 
     void GetLastChange(Json::Value& target);
 
     void GetExportedResources(Json::Value& target,
                               int64_t since,
-                              unsigned int maxResults);
+                              uint32_t limit);
 
     void GetLastExportedResource(Json::Value& target);
 
@@ -605,7 +698,9 @@
                               std::vector<std::string>* instancesId,  // Can be NULL if not needed
                               const DatabaseLookup& lookup,
                               ResourceType queryLevel,
-                              size_t limit);
+                              const std::set<std::string>& labels,
+                              LabelsConstraint labelsConstraint,
+                              uint32_t limit);
 
     bool DeleteResource(Json::Value& remainingAncestor /* out */,
                         const std::string& uuid,
@@ -658,7 +753,9 @@
                    const std::string& publicId,
                    ResourceType level);
 
-    void ReconstructInstance(const ParsedDicomFile& dicom);
+    void ReconstructInstance(const ParsedDicomFile& dicom, 
+                             bool limitToThisLevelDicomTags, 
+                             ResourceType limitToLevel_);
 
     StoreStatus Store(std::map<MetadataType, std::string>& instanceMetadata,
                       const DicomMap& dicomSummary,
@@ -670,6 +767,7 @@
                       DicomTransferSyntax transferSyntax,
                       bool hasPixelDataOffset,
                       uint64_t pixelDataOffset,
+                      ValueRepresentation pixelDataVR,
                       MaxStorageMode maximumStorageMode,
                       uint64_t maximumStorageSize,
                       unsigned int maximumPatients,
@@ -683,5 +781,22 @@
                               bool hasOldRevision,
                               int64_t oldRevision,
                               const std::string& oldMd5);
+
+    void ListLabels(std::set<std::string>& target,
+                    const std::string& publicId,
+                    ResourceType level);
+
+    void ListAllLabels(std::set<std::string>& target);
+
+    void ModifyLabel(const std::string& publicId,
+                     ResourceType level,
+                     const std::string& label,
+                     LabelOperation operation);
+
+    void AddLabels(const std::string& publicId,
+                   ResourceType level,
+                   const std::set<std::string>& labels);
+
+    bool HasLabelsSupport();
   };
 }
--- a/OrthancServer/Sources/Database/Upgrade3To4.sql	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/Upgrade3To4.sql	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 -- Orthanc - A Lightweight, RESTful DICOM Store
 -- Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 -- Department, University Hospital of Liege, Belgium
--- Copyright (C) 2017-2022 Osimis S.A., Belgium
--- Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+-- Copyright (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
--- a/OrthancServer/Sources/Database/Upgrade4To5.sql	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/Upgrade4To5.sql	Tue Sep 24 11:39:52 2024 +0200
@@ -1,8 +1,9 @@
 -- Orthanc - A Lightweight, RESTful DICOM Store
 -- Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 -- Department, University Hospital of Liege, Belgium
--- Copyright (C) 2017-2022 Osimis S.A., Belgium
--- Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+-- Copyright (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
--- a/OrthancServer/Sources/Database/VoidDatabaseListener.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/VoidDatabaseListener.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/Database/VoidDatabaseListener.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Database/VoidDatabaseListener.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/DicomInstanceOrigin.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/DicomInstanceOrigin.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/DicomInstanceOrigin.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/DicomInstanceOrigin.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/DicomInstanceToStore.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/DicomInstanceToStore.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -132,9 +133,10 @@
       {
         buffer_.reset(new std::string);
         
-        if (!FromDcmtkBridge::SaveToMemoryBuffer(*buffer_, dataset_))
+        std::string errorMessage;
+        if (!FromDcmtkBridge::SaveToMemoryBuffer(*buffer_, dataset_, errorMessage))
         {
-          throw OrthancException(ErrorCode_InternalError, "Cannot write DICOM file to memory");
+          throw OrthancException(ErrorCode_InternalError, "Cannot write DICOM file to memory: " + errorMessage);
         }
       }
     }
--- a/OrthancServer/Sources/DicomInstanceToStore.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/DicomInstanceToStore.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -47,6 +48,11 @@
 
     MetadataMap          metadata_;
     DicomInstanceOrigin  origin_;
+    bool                 skipIngestTranscoding_;
+
+    DicomInstanceToStore()
+    : skipIngestTranscoding_(false)
+    {}
 
   public:
     virtual ~DicomInstanceToStore()
@@ -64,8 +70,16 @@
 
     static DicomInstanceToStore* CreateFromDcmDataset(DcmDataset& dataset);
 
+    void SetSkipIngestTranscoding(bool value)
+    {
+      skipIngestTranscoding_ = value;
+    }
 
- 
+    bool SkipIngestTranscoding() const
+    {
+      return skipIngestTranscoding_;
+    }
+
     void SetOrigin(const DicomInstanceOrigin& origin)
     {
       origin_ = origin;
--- a/OrthancServer/Sources/EmbeddedResourceHttpHandler.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/EmbeddedResourceHttpHandler.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/EmbeddedResourceHttpHandler.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/EmbeddedResourceHttpHandler.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ExportedResource.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ExportedResource.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ExportedResource.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ExportedResource.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/IDicomImageDecoder.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/IDicomImageDecoder.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/IServerListener.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/IServerListener.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -24,6 +25,7 @@
 
 #include "DicomInstanceToStore.h"
 #include "ServerIndexChange.h"
+#include "JobEvent.h"
 
 #include <json/value.h>
 
@@ -42,6 +44,8 @@
     
     virtual void SignalChange(const ServerIndexChange& change) = 0;
 
+    virtual void SignalJobEvent(const JobEvent& event) = 0;
+
     virtual bool FilterIncomingInstance(const DicomInstanceToStore& instance,
                                         const Json::Value& simplified) = 0;
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/JobEvent.h	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,78 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-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 "ServerEnumerations.h"
+#include "../../OrthancFramework/Sources/IDynamicObject.h"
+#include "../../OrthancFramework/Sources/SystemToolbox.h"
+
+#include <string>
+#include <json/value.h>
+
+namespace Orthanc
+{
+  enum JobEventType
+  {
+    JobEventType_Failure,
+    JobEventType_Submitted,
+    JobEventType_Success
+  };
+
+
+  struct JobEvent : public IDynamicObject
+  {
+  private:
+    JobEventType eventType_;
+    std::string  jobId_;
+
+  public:
+    JobEvent(JobEventType eventType,
+             const std::string& jobId) :
+      eventType_(eventType),
+      jobId_(jobId)
+    {
+    }
+
+    JobEvent(const JobEvent& other) 
+    : eventType_(other.eventType_),
+      jobId_(other.jobId_)
+    {
+    }
+
+    // JobEvent* Clone() const
+    // {
+    //   return new JobEvent(*this);
+    // }
+
+    JobEventType  GetEventType() const
+    {
+      return eventType_;
+    }
+
+    const std::string&  GetJobId() const
+    {
+      return jobId_;
+    }
+  };
+}
--- a/OrthancServer/Sources/LuaScripting.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/LuaScripting.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -239,25 +240,14 @@
   };
 
 
-  class LuaScripting::JobEvent : public LuaScripting::IEvent
+  class LuaScripting::LuaJobEvent : public LuaScripting::IEvent
   {
-  public:
-    enum Type
-    {
-      Type_Failure,
-      Type_Submitted,
-      Type_Success
-    };
-    
   private:
-    Type         type_;
-    std::string  jobId_;
+    JobEvent event_;
 
   public:
-    JobEvent(Type type,
-             const std::string& jobId) :
-      type_(type),
-      jobId_(jobId)
+    explicit LuaJobEvent(const JobEvent& event) :
+      event_(event)
     {
     }
 
@@ -265,17 +255,17 @@
     {
       std::string functionName;
       
-      switch (type_)
+      switch (event_.GetEventType())
       {
-        case Type_Failure:
+        case JobEventType_Failure:
           functionName = "OnJobFailure";
           break;
 
-        case Type_Submitted:
+        case JobEventType_Submitted:
           functionName = "OnJobSubmitted";
           break;
 
-        case Type_Success:
+        case JobEventType_Success:
           functionName = "OnJobSuccess";
           break;
 
@@ -289,7 +279,7 @@
         if (lock.GetLua().IsExistingFunction(functionName.c_str()))
         {
           LuaFunctionCall call(lock.GetLua(), functionName.c_str());
-          call.PushString(jobId_);
+          call.PushString(event_.GetJobId());
           call.Execute();
         }
       }
@@ -787,6 +777,8 @@
 
   void LuaScripting::HeartBeatThread(LuaScripting* that)
   {
+    Logging::SetCurrentThreadName("LUA-HEARTBEAT");
+
     static const unsigned int GRANULARITY = 100;  // In milliseconds
     
     const boost::posix_time::time_duration PERIODICITY =
@@ -823,6 +815,8 @@
 
   void LuaScripting::EventThread(LuaScripting* that)
   {
+    Logging::SetCurrentThreadName("LUA-EVENTS");
+
     for (;;)
     {
       std::unique_ptr<IDynamicObject> event(that->pendingEvents_.Dequeue(100));
@@ -1056,20 +1050,9 @@
   }
 
   
-  void LuaScripting::SignalJobSubmitted(const std::string& jobId)
-  {
-    pendingEvents_.Enqueue(new JobEvent(JobEvent::Type_Submitted, jobId));
-  }
-  
-
-  void LuaScripting::SignalJobSuccess(const std::string& jobId)
+  void LuaScripting::SignalJobEvent(const JobEvent& event)
   {
-    pendingEvents_.Enqueue(new JobEvent(JobEvent::Type_Success, jobId));
-  }
-  
-
-  void LuaScripting::SignalJobFailure(const std::string& jobId)
-  {
-    pendingEvents_.Enqueue(new JobEvent(JobEvent::Type_Failure, jobId));
+    // Lua has its own event thread and queue to dissociate it completely from the main JobEventsThread
+    pendingEvents_.Enqueue(new LuaJobEvent(event));
   }
 }
--- a/OrthancServer/Sources/LuaScripting.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/LuaScripting.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -24,6 +25,7 @@
 
 #include "DicomInstanceToStore.h"
 #include "ServerIndexChange.h"
+#include "JobEvent.h"
 #include "ServerJobs/LuaJobManager.h"
 
 #include "../../OrthancFramework/Sources/MultiThreading/SharedMessageQueue.h"
@@ -47,7 +49,7 @@
     class IEvent;
     class OnStoredInstanceEvent;
     class StableResourceEvent;
-    class JobEvent;
+    class LuaJobEvent;
     class DeleteEvent;
     class UpdateEvent;
 
@@ -128,11 +130,7 @@
 
     void Execute(const std::string& command);
 
-    void SignalJobSubmitted(const std::string& jobId);
-
-    void SignalJobSuccess(const std::string& jobId);
-
-    void SignalJobFailure(const std::string& jobId);
+    void SignalJobEvent(const JobEvent& event);
 
     TimeoutDicomConnectionManager& GetDicomConnectionManager()
     {
--- a/OrthancServer/Sources/OrthancConfiguration.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/OrthancConfiguration.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -44,6 +45,7 @@
 static const char* const TEMPORARY_DIRECTORY = "TemporaryDirectory";
 static const char* const DATABASE_SERVER_IDENTIFIER = "DatabaseServerIdentifier";
 static const char* const WARNINGS = "Warnings";
+static const char* const JOBS_ENGINE_THREADS_COUNT = "JobsEngineThreadsCount";
 
 namespace Orthanc
 {
@@ -113,7 +115,7 @@
     {
       if (!is_directory(it->status()))
       {
-        std::string extension = boost::filesystem::extension(it->path());
+        std::string extension = it->path().extension().string();
         Toolbox::ToLowerCase(extension);
 
         if (extension == ".json")
@@ -273,6 +275,55 @@
     }
   }
 
+  void OrthancConfiguration::LoadJobsEngineThreadsCount()
+  {
+    // default values
+    jobsEngineThreadsCount_["ResourceModification"] = 1;
+
+    if (json_.isMember(JOBS_ENGINE_THREADS_COUNT))
+    {
+      const Json::Value& source = json_[JOBS_ENGINE_THREADS_COUNT];
+      if (source.type() != Json::objectValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Bad format of the \"" + std::string(JOBS_ENGINE_THREADS_COUNT) +
+                               "\" configuration section");
+      }
+
+      Json::Value::Members members = source.getMemberNames();
+
+      for (size_t i = 0; i < members.size(); i++)
+      {
+        const std::string& name = members[i];
+        if (!source[name].isUInt())
+        {
+          throw OrthancException(ErrorCode_BadFileFormat,
+                                 "Bad format for \"" + std::string(JOBS_ENGINE_THREADS_COUNT) + "." + name + 
+                                 "\".  It should be an unsigned integer");
+        }
+        jobsEngineThreadsCount_[name] = source[name].asUInt();
+      }      
+    }
+  }
+
+  unsigned int OrthancConfiguration::GetJobsEngineWorkersThread(const std::string& jobType) const
+  {
+    unsigned int workersThread = 1;
+    
+    const JobsEngineThreadsCount::const_iterator it = jobsEngineThreadsCount_.find(jobType);
+    if (it != jobsEngineThreadsCount_.end())
+    {
+      workersThread = it->second;
+    }
+
+    if (workersThread == 0)
+    {
+      workersThread = SystemToolbox::GetHardwareConcurrency();
+    }
+
+    return workersThread;
+  }
+
   void OrthancConfiguration::LoadPeers()
   {
     if (GetBooleanParameter(ORTHANC_PEERS_IN_DB, false))
@@ -700,7 +751,7 @@
 
     if (lst.type() != Json::arrayValue)
     {
-      throw OrthancException(ErrorCode_BadFileFormat, "Badly formatted list of strings");
+      throw OrthancException(ErrorCode_BadFileFormat, "Badly formatted list of strings: " + key);
     }
 
     for (Json::Value::ArrayIndex i = 0; i < lst.size(); i++)
@@ -709,7 +760,31 @@
     }    
   }
 
-    
+
+  void OrthancConfiguration::GetSetOfStringsParameter(std::set<std::string>& target,
+                                                      const std::string& key) const
+  {
+    target.clear();
+  
+    if (!json_.isMember(key))
+    {
+      return;
+    }
+
+    const Json::Value& lst = json_[key];
+
+    if (lst.type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "Badly formatted set of strings: " + key);
+    }
+
+    for (Json::Value::ArrayIndex i = 0; i < lst.size(); i++)
+    {
+      target.insert(lst[i].asString());
+    }    
+  }
+
+
   bool OrthancConfiguration::IsSameAETitle(const std::string& aet1,
                                            const std::string& aet2) const
   {
@@ -1082,6 +1157,10 @@
         {
           warning = Warnings_002_InconsistentDicomTagsInDb;
         }
+        else if (name == "W003_DecoderFailure")
+        {
+          warning = Warnings_003_DecoderFailure;
+        }
         else
         {
           throw OrthancException(ErrorCode_BadFileFormat, name + " is not recognized as a valid warning name");
--- a/OrthancServer/Sources/OrthancConfiguration.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/OrthancConfiguration.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -51,6 +52,7 @@
   private:
     typedef std::map<std::string, RemoteModalityParameters>   Modalities;
     typedef std::map<std::string, WebServiceParameters>       Peers;
+    typedef std::map<std::string, unsigned int>               JobsEngineThreadsCount;
 
     boost::shared_mutex      mutex_;
     Json::Value              json_;
@@ -60,6 +62,7 @@
     const char*              configurationFileArg_;
     Modalities               modalities_;
     Peers                    peers_;
+    JobsEngineThreadsCount   jobsEngineThreadsCount_;
     ServerIndex*             serverIndex_;
     std::set<Warnings>       disabledWarnings_;
 
@@ -160,6 +163,10 @@
 
     void LoadWarnings();
 
+    void LoadJobsEngineThreadsCount();
+
+    unsigned int GetJobsEngineWorkersThread(const std::string& jobType) const;
+
     void RegisterFont(ServerResources::FileResourceId resource);
 
     bool LookupStringParameter(std::string& target,
@@ -197,7 +204,10 @@
     
     void GetListOfStringsParameter(std::list<std::string>& target,
                                    const std::string& key) const;
-    
+
+    void GetSetOfStringsParameter(std::set<std::string>& target,
+                                  const std::string& key) const;
+
     bool IsSameAETitle(const std::string& aet1,
                        const std::string& aet2) const;
 
--- a/OrthancServer/Sources/OrthancFindRequestHandler.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/OrthancFindRequestHandler.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -59,7 +60,8 @@
     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, ExpandResourceDbFlags_IncludeMainDicomTags, allowStorageAccess);
+    context.ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson,
+                           level, requestedTags, ExpandResourceFlags_IncludeMainDicomTags, allowStorageAccess);
 
     DicomMap result;
 
@@ -84,7 +86,7 @@
       else
       {
         const DicomTag& tag = query.GetElement(i).GetTag();
-        const DicomValue* value = resource.tags_.TestAndGetValue(tag);
+        const DicomValue* value = resource.GetMainDicomTags().TestAndGetValue(tag);
 
         if (value != NULL &&
             !value->IsNull() &&
@@ -172,7 +174,7 @@
   {
     // Whatever the manufacturer, remove the GenericGroupLength tags
     // http://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_7.2.html
-    // https://bugs.orthanc-server.com/show_bug.cgi?id=31
+    // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=31
     if (tag.GetElement() == 0x0000)
     {
       return false;
@@ -438,9 +440,11 @@
       const DicomElement& element = query.GetElement(i);
       const DicomTag tag = element.GetTag();
 
+      // remove tags that are not used for matching
       if (element.GetValue().IsNull() ||
           tag == DICOM_TAG_QUERY_RETRIEVE_LEVEL ||
-          tag == DICOM_TAG_SPECIFIC_CHARACTER_SET)
+          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;
       }
--- a/OrthancServer/Sources/OrthancFindRequestHandler.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/OrthancFindRequestHandler.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/OrthancGetRequestHandler.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/OrthancGetRequestHandler.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/OrthancGetRequestHandler.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/OrthancGetRequestHandler.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/OrthancHttpHandler.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/OrthancHttpHandler.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/OrthancHttpHandler.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/OrthancHttpHandler.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/OrthancInitialization.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/OrthancInitialization.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -51,6 +52,16 @@
 
 #include <dcmtk/dcmnet/diutil.h>  // For DCM_dcmnetLogger
 
+#if ORTHANC_ENABLE_PLUGINS == 1
+#  if defined(__ORTHANC_FILE__)
+//   Prevents the system-wide Google Protobuf library from leaking the
+//   full path of this source file
+#    undef __FILE__
+#    define __FILE__ __ORTHANC_FILE__
+#  endif
+#  include <google/protobuf/any.h>
+#endif
+
 
 static const char* const STORAGE_DIRECTORY = "StorageDirectory";
 static const char* const ORTHANC_STORAGE = "OrthancStorage";
@@ -204,6 +215,8 @@
   {
     static const char* const EXTRA_MAIN_DICOM_TAGS = "ExtraMainDicomTags";
     
+    DicomMap::ResetDefaultMainDicomTags();
+
     if (configuration.type() != Json::objectValue ||
         !configuration.isMember(EXTRA_MAIN_DICOM_TAGS) ||
         configuration[EXTRA_MAIN_DICOM_TAGS].type() != Json::objectValue)
@@ -235,7 +248,7 @@
 
           if (DicomMap::IsComputedTag(tag))
           {
-            LOG(WARNING) << "  - " << tagName << " can not be added in the Extra Main Dicom Tags since the value of this tag is computed when requested";
+            LOG(WARNING) << "  - " << tagName << " cannot be added in the Extra Main Dicom Tags since the value of this tag is computed when requested";
           }
           else
           {
@@ -313,6 +326,10 @@
     
     OrthancConfiguration::WriterLock lock;
 
+#if ORTHANC_ENABLE_PLUGINS == 1
+    GOOGLE_PROTOBUF_VERIFY_VERSION;
+#endif
+    
     InitializeServerEnumerations();
 
     // Read the user-provided configuration
@@ -354,6 +371,7 @@
     LoadCustomDictionary(lock.GetJson());
 
     lock.GetConfiguration().LoadWarnings();
+    lock.GetConfiguration().LoadJobsEngineThreadsCount();
 
     LoadMainDicomTags(lock.GetJson());  // New in Orthanc 1.11.0
 
@@ -361,7 +379,7 @@
 
 #if HAVE_MALLOPT == 1
     // New in Orthanc 1.8.2
-    // https://book.orthanc-server.com/faq/scalability.html#controlling-memory-usage
+    // https://orthanc.uclouvain.be/book/faq/scalability.html#controlling-memory-usage
     unsigned int maxArena = lock.GetConfiguration().GetUnsignedIntegerParameter(MALLOC_ARENA_MAX, 5);
     if (maxArena != 0)
     {
@@ -388,6 +406,10 @@
   {
     OrthancConfiguration::WriterLock lock;
     Orthanc::FinalizeFramework();
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+    google::protobuf::ShutdownProtobufLibrary();
+#endif
   }
 
 
--- a/OrthancServer/Sources/OrthancInitialization.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/OrthancInitialization.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/OrthancMoveRequestHandler.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/OrthancMoveRequestHandler.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -265,7 +266,7 @@
       /**
        * This tokenization fixes issue 154 ("Matching against list of
        * UID-s by C-MOVE").
-       * https://bugs.orthanc-server.com/show_bug.cgi?id=154
+       * https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=154
        **/
 
       std::vector<std::string> tokens;
--- a/OrthancServer/Sources/OrthancMoveRequestHandler.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/OrthancMoveRequestHandler.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -46,6 +47,7 @@
 static const char* const INTERPRET_BINARY_TAGS = "InterpretBinaryTags";
 static const char* const KEEP = "Keep";
 static const char* const KEEP_PRIVATE_TAGS = "KeepPrivateTags";
+static const char* const KEEP_LABELS = "KeepLabels";
 static const char* const KEEP_SOURCE = "KeepSource";
 static const char* const LEVEL = "Level";
 static const char* const PARENT = "Parent";
@@ -85,7 +87,7 @@
     call.GetDocumentation()
       .SetRequestField(TRANSCODE, RestApiCallDocumentation::Type_String,
                        "Transcode the DICOM instances to the provided DICOM transfer syntax: "
-                       "https://book.orthanc-server.com/faq/transcoding.html", false)
+                       "https://orthanc.uclouvain.be/book/faq/transcoding.html", 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)
@@ -111,6 +113,9 @@
   {
     // Check out "DicomModification::ParseAnonymizationRequest()"
     call.GetDocumentation()
+      .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(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)
@@ -119,6 +124,8 @@
                        "configuration option `DeidentifyLogsDicomVersion` for possible values.", false)
       .SetRequestField(KEEP_PRIVATE_TAGS, RestApiCallDocumentation::Type_Boolean,
                        "Keep the private tags from the DICOM instances (defaults to `false`)", false)
+      .SetRequestField(KEEP_LABELS, RestApiCallDocumentation::Type_Boolean,
+                       "Keep the labels of all resources level (defaults to `false`)", false)
       .SetRequestField(REPLACE, RestApiCallDocumentation::Type_JsonObject,
                        "Associative array to change the value of some DICOM tags in the DICOM instances. " INFO_SUBSEQUENCES, false)
       .SetRequestField(REMOVE, RestApiCallDocumentation::Type_JsonListOfStrings,
@@ -262,7 +269,7 @@
         .SetTag("Instances")
         .SetSummary("Modify instance")
         .SetDescription("Download a modified version of the DICOM instance whose Orthanc identifier is provided in the URL: "
-                        "https://book.orthanc-server.com/users/anonymization.html#modification-of-a-single-instance")
+                        "https://orthanc.uclouvain.be/book/users/anonymization.html#modification-of-a-single-instance")
         .SetUriArgument("id", "Orthanc identifier of the instance of interest")
         .AddAnswerType(MimeType_Dicom, "The modified DICOM instance");
       return;
@@ -307,7 +314,7 @@
         .SetTag("Instances")
         .SetSummary("Anonymize instance")
         .SetDescription("Download an anonymized version of the DICOM instance whose Orthanc identifier is provided in the URL: "
-                        "https://book.orthanc-server.com/users/anonymization.html#anonymization-of-a-single-instance")
+                        "https://orthanc.uclouvain.be/book/users/anonymization.html#anonymization-of-a-single-instance")
         .SetUriArgument("id", "Orthanc identifier of the instance of interest")
         .AddAnswerType(MimeType_Dicom, "The anonymized DICOM instance");
       return;
@@ -333,6 +340,15 @@
     }
   }
 
+  static void SetKeepSource(ThreadedSetOfInstancesJob& job,
+                            const Json::Value& body)
+  {
+    if (body.isMember(KEEP_SOURCE))
+    {
+      job.SetKeepSource(SerializationToolbox::ReadBoolean(body, KEEP_SOURCE));
+    }
+  }
+
 
   static void SubmitModificationJob(std::unique_ptr<DicomModification>& modification,
                                     bool isAnonymization,
@@ -343,8 +359,14 @@
                                     const std::set<std::string>& resources)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
+    unsigned int workersCount = 0;
 
-    std::unique_ptr<ResourceModificationJob> job(new ResourceModificationJob(context));
+    {
+      OrthancConfiguration::ReaderLock lock;
+      workersCount = lock.GetConfiguration().GetJobsEngineWorkersThread("ResourceModification");
+    }
+
+    std::unique_ptr<ResourceModificationJob> job(new ResourceModificationJob(context, workersCount));
 
     if (isSingleResource)  // This notably configures the output format
     {
@@ -366,12 +388,16 @@
     for (std::set<std::string>::const_iterator
            it = resources.begin(); it != resources.end(); ++it)
     {
-      context.AddChildInstances(*job, *it);
+      std::list<std::string> instances;
+      context.GetIndex().GetChildInstances(instances, *it);
+      job->AddInstances(instances);
+
+      job->AddParentResource(*it);
     }
-    
-    job->AddTrailingStep();
 
-    OrthancRestApi::GetApi(call).SubmitCommandsJob
+    job->PerformSanityChecks();
+
+    OrthancRestApi::GetApi(call).SubmitThreadedInstancesJob
       (call, job.release(), true /* synchronous by default */, body);
   }
 
@@ -419,7 +445,7 @@
         .SetDescription("Start a job that will modify all the DICOM instances within the " + r +
                         " whose identifier is provided in the URL. The modified DICOM instances will be "
                         "stored into a brand new " + r + ", whose Orthanc identifiers will be returned by the job. "
-                        "https://book.orthanc-server.com/users/anonymization.html#modification-of-studies-or-series")
+                        "https://orthanc.uclouvain.be/book/users/anonymization.html#modification-of-studies-or-series")
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest");
       return;
     }
@@ -492,7 +518,7 @@
         .SetDescription("Start a job that will anonymize all the DICOM instances within the " + r +
                         " whose identifier is provided in the URL. The modified DICOM instances will be "
                         "stored into a brand new " + r + ", whose Orthanc identifiers will be returned by the job. "
-                        "https://book.orthanc-server.com/users/anonymization.html#anonymization-of-patients-studies-or-series")
+                        "https://orthanc.uclouvain.be/book/users/anonymization.html#anonymization-of-patients-studies-or-series")
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest");
       return;
     }
@@ -937,7 +963,7 @@
     InjectTags(dicom, request[TAGS], decodeBinaryTags, privateCreator, force);
 
 
-    // Inject the content (either an image, or a PDF file)
+    // Inject the content (either an image, a PDF file, or a STL/OBJ/MTL file)
     if (request.isMember(CONTENT))
     {
       const Json::Value& content = request[CONTENT];
@@ -978,8 +1004,9 @@
         .SetRequestField(TAGS, RestApiCallDocumentation::Type_JsonObject,
                          "Associative array containing the tags of the new instance to be created", true)
         .SetRequestField(CONTENT, RestApiCallDocumentation::Type_String,
-                         "This field can be used to embed an image (pixel data) or a PDF inside the created DICOM instance. "
-                         "The PNG image, the JPEG image or the PDF file must be provided using their "
+                         "This field can be used to embed an image (pixel data encoded as PNG or JPEG), a PDF, or a "
+                         "3D manufactoring model (MTL/OBJ/STL) inside the created DICOM instance. "
+                         "The file to be encapsulated must be provided using its "
                          "[data URI scheme encoding](https://en.wikipedia.org/wiki/Data_URI_scheme). "
                          "This field can possibly contain a JSON array, in which case a DICOM series is created "
                          "containing one DICOM instance for each item in the `Content` field.", false)
@@ -1036,7 +1063,7 @@
         .SetDescription("Start a new job so as to split the DICOM study whose Orthanc identifier is provided in the URL, "
                         "by taking some of its children series or instances out of it and putting them into a brand new study "
                         "(this new study is created by setting the `StudyInstanceUID` tag to a random identifier): "
-                        "https://book.orthanc-server.com/users/anonymization.html#splitting")
+                        "https://orthanc.uclouvain.be/book/users/anonymization.html#splitting")
         .SetUriArgument("id", "Orthanc identifier of the study of interest")
         .SetRequestField(SERIES, RestApiCallDocumentation::Type_JsonListOfStrings,
                          "The list of series to be separated from the parent study. "
@@ -1045,6 +1072,8 @@
                          "Associative array to change the value of some DICOM tags in the new study. "
                          "These tags must be part of the \"Patient Module Attributes\" or the \"General Study "
                          "Module Attributes\", as specified by the DICOM 2011 standard in Tables C.7-1 and C.7-3.", false)
+        .SetRequestField(KEEP_LABELS, RestApiCallDocumentation::Type_Boolean,
+                         "Keep the labels of all resources level (defaults to `false`)", false)
         .SetRequestField(REMOVE, RestApiCallDocumentation::Type_JsonListOfStrings,
                          "List of tags that must be removed in the new study (from the same modules as in the `Replace` option)", false)
         .SetRequestField(KEEP_SOURCE, RestApiCallDocumentation::Type_Boolean,
@@ -1162,7 +1191,7 @@
         .SetTag("Studies")
         .SetSummary("Merge study")
         .SetDescription("Start a new job so as to move some DICOM resources into the DICOM study whose Orthanc identifier "
-                        "is provided in the URL: https://book.orthanc-server.com/users/anonymization.html#merging")
+                        "is provided in the URL: https://orthanc.uclouvain.be/book/users/anonymization.html#merging")
         .SetUriArgument("id", "Orthanc identifier of the study of interest")
         .SetRequestField(RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings,
                          "The list of DICOM resources (studies, series, and/or instances) to be merged "
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -160,7 +161,7 @@
 
     ServerContext& context = OrthancRestApi::GetContext(call);
 
-    CLOG(INFO, HTTP) << "Receiving a DICOM file of " << call.GetBodySize() << " bytes through HTTP";
+    CLOG(INFO, HTTP) << "Receiving a DICOM file of " << Toolbox::GetHumanFileSize(static_cast<uint64_t>(call.GetBodySize())) << " through HTTP";
 
     if (call.GetBodySize() == 0)
     {
@@ -259,7 +260,7 @@
     resetRequestReceived_(false),
     activeRequests_(context.GetMetricsRegistry(), 
                     "orthanc_rest_api_active_requests", 
-                    MetricsType_MaxOver10Seconds)
+                    MetricsUpdatePolicy_MaxOver10Seconds)
   {
     RegisterSystem(orthancExplorerEnabled);
 
@@ -434,7 +435,32 @@
     SubmitGenericJob(call, raii.release(), isDefaultSynchronous, body);
   }
 
-  
+  void OrthancRestApi::SubmitThreadedInstancesJob(RestApiPostCall& call,
+                                                  ThreadedSetOfInstancesJob* job,
+                                                  bool isDefaultSynchronous,
+                                                  const Json::Value& body) const
+  {
+    std::unique_ptr<ThreadedSetOfInstancesJob> raii(job);
+    
+    if (body.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    job->SetDescription("REST API");
+    
+    if (body.isMember(KEY_PERMISSIVE))
+    {
+      job->SetPermissive(SerializationToolbox::ReadBoolean(body, KEY_PERMISSIVE));
+    }
+    else
+    {
+      job->SetPermissive(false);
+    }
+
+    SubmitGenericJob(call, raii.release(), isDefaultSynchronous, body);
+  }  
+
   void OrthancRestApi::DocumentSubmitGenericJob(RestApiPostCall& call)
   {
     call.GetDocumentation()
@@ -446,7 +472,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");
   }
@@ -464,7 +490,8 @@
   static const std::string GET_SIMPLIFY = "simplify";
   static const std::string GET_FULL = "full";
   static const std::string GET_SHORT = "short";
-  static const std::string GET_REQUESTED_TAGS = "requestedTags";
+  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 POST_SIMPLIFY = "Simplify";
   static const std::string POST_FULL = "Full";
@@ -578,25 +605,36 @@
   {
     requestedTags.clear();
 
+    std::string s;
+
     if (call.HasArgument(GET_REQUESTED_TAGS))
     {
+      s = call.GetArgument(GET_REQUESTED_TAGS, "");
+    }
+    else if (call.HasArgument(GET_REQUESTED_TAGS_OLD))
+    {
+      // This is for backward compatibility with Orthanc <= 1.12.3
+      s = call.GetArgument(GET_REQUESTED_TAGS_OLD, "");
+    }
+
+    if (!s.empty())
+    {
       try
       {
-        FromDcmtkBridge::ParseListOfTags(requestedTags, call.GetArgument("requestedTags", ""));
+        FromDcmtkBridge::ParseListOfTags(requestedTags, s);
       }
       catch (OrthancException& ex)
       {
         throw OrthancException(ErrorCode_BadRequest, std::string("Invalid requestedTags argument: ") + ex.What() + " " + ex.GetDetails());
       }
     }
-
   }
 
   void OrthancRestApi::DocumentRequestedTags(RestApiGetCall& call)
   {
       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: 'requestedTags=0010,0010;PatientBirthDate'.  "
+                          "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);
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -26,6 +27,7 @@
 #include "../../../OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h"
 #include "../../../OrthancFramework/Sources/MetricsRegistry.h"
 #include "../../../OrthancFramework/Sources/RestApi/RestApi.h"
+#include "../ServerJobs/ThreadedSetOfInstancesJob.h"
 #include "../ServerEnumerations.h"
 
 #include <set>
@@ -130,6 +132,12 @@
                            bool isDefaultSynchronous,
                            const Json::Value& body) const;
 
+    void SubmitThreadedInstancesJob(RestApiPostCall& call,
+                                    ThreadedSetOfInstancesJob* job,
+                                    bool isDefaultSynchronous,
+                                    const Json::Value& body) const;
+
+
     static void DocumentSubmitGenericJob(RestApiPostCall& call);
 
     static void DocumentSubmitCommandsJob(RestApiPostCall& call);
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -28,6 +29,7 @@
 #include "../../../OrthancFramework/Sources/Logging.h"
 #include "../../../OrthancFramework/Sources/OrthancException.h"
 #include "../../../OrthancFramework/Sources/SerializationToolbox.h"
+#include "../../../OrthancFramework/Sources/Toolbox.h"
 #include "../OrthancConfiguration.h"
 #include "../ServerContext.h"
 #include "../ServerJobs/ArchiveJob.h"
@@ -43,6 +45,19 @@
 
   static const char* const CONFIG_LOADER_THREADS = "ZipLoaderThreads";
 
+
+  static void AddResourcesOfInterestFromString(ArchiveJob& job,
+                                              const std::string& resourcesList)
+  {
+    std::set<std::string> resources;
+    Toolbox::SplitString(resources, resourcesList, ',');
+
+    for (std::set<std::string>::const_iterator it = resources.begin(); it != resources.end(); ++it)
+    {
+      job.AddResource(*it, false, ResourceType_Patient /* dummy value */);
+    }
+  }
+
   static void AddResourcesOfInterestFromArray(ArchiveJob& job,
                                               const Json::Value& resources)
   {
@@ -61,7 +76,7 @@
       }
       else
       {
-        job.AddResource(resources[i].asString());
+        job.AddResource(resources[i].asString(), false, ResourceType_Patient /* dummy value */);
       }
     }
   }
@@ -93,21 +108,6 @@
     }
   }
 
-
-  static DicomTransferSyntax GetTransferSyntax(const std::string& value)
-  {
-    DicomTransferSyntax syntax;
-    if (LookupTransferSyntax(syntax, value))
-    {
-      return syntax;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange,
-                             "Unknown transfer syntax: " + value);
-    }
-  }
-  
   
   static void GetJobParameters(bool& synchronous,            /* out */
                                bool& extended,               /* out */
@@ -137,7 +137,7 @@
         body.isMember(KEY_TRANSCODE))
     {
       transcode = true;
-      syntax = GetTransferSyntax(SerializationToolbox::ReadString(body, KEY_TRANSCODE));
+      syntax = Orthanc::GetTransferSyntax(SerializationToolbox::ReadString(body, KEY_TRANSCODE));
     }
     else
     {
@@ -507,12 +507,12 @@
                        "the archive in background.", false)
       .SetRequestField(KEY_TRANSCODE, RestApiCallDocumentation::Type_String,
                        "If present, the DICOM files in the archive will be transcoded to the provided "
-                       "transfer syntax: https://book.orthanc-server.com/faq/transcoding.html", false)
+                       "transfer syntax: https://orthanc.uclouvain.be/book/faq/transcoding.html", 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://book.orthanc-server.com/users/advanced-rest.html#jobs")
+                     "generate the archive: https://orthanc.uclouvain.be/book/users/advanced-rest.html#jobs")
       .SetAnswerField("ID", RestApiCallDocumentation::Type_String, "Identifier of the job")
       .SetAnswerField("Path", RestApiCallDocumentation::Type_String, "Path to access the job in the REST API");
 
@@ -528,7 +528,7 @@
   
   template <bool IS_MEDIA,
             bool DEFAULT_IS_EXTENDED  /* only makes sense for media (i.e. not ZIP archives) */ >
-  static void CreateBatch(RestApiPostCall& call)
+  static void CreateBatchPost(RestApiPostCall& call)
   {
     if (call.IsDocumentation())
     {
@@ -556,7 +556,7 @@
       GetJobParameters(synchronous, extended, transcode, transferSyntax,
                        priority, loaderThreads, body, DEFAULT_IS_EXTENDED);
       
-      std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended));
+      std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended, ResourceType_Patient));
       AddResourcesOfInterest(*job, body);
 
       if (transcode)
@@ -576,7 +576,58 @@
   }
   
 
-  template <bool IS_MEDIA>
+  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");
+      call.GetDocumentation()
+        .SetTag("System")
+        .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,
+                            "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,
+                            "A comma separated list of Orthanc resource identifiers to include in the " + m + ".", true);
+      return;
+    }
+
+    ServerContext& context = OrthancRestApi::GetContext(call);
+    bool transcode = false;
+    DicomTransferSyntax transferSyntax = DicomTransferSyntax_LittleEndianImplicit;  // Initialize variable to avoid warnings
+
+    if (call.HasArgument(TRANSCODE))
+    {
+      transcode = true;
+      transferSyntax = GetTransferSyntax(call.GetArgument(TRANSCODE, ""));
+    }
+    
+    if (!call.HasArgument(RESOURCES))
+    {
+      throw OrthancException(Orthanc::ErrorCode_BadRequest, std::string("Missing ") + RESOURCES + " argument");
+    }
+
+    std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, DEFAULT_IS_EXTENDED, ResourceType_Patient));
+    AddResourcesOfInterestFromString(*job, call.GetArgument(RESOURCES, ""));
+
+    if (transcode)
+    {
+      job->SetTranscode(transferSyntax);
+    }
+
+    SubmitJob(call.GetOutput(), context, job, 0, true, "Archive.zip");
+  }
+
+
+  template <ResourceType LEVEL,
+            bool IS_MEDIA>
   static void CreateSingleGet(RestApiGetCall& call)
   {
     static const char* const TRANSCODE = "transcode";
@@ -600,7 +651,7 @@
                             "(including file extension)", false)
         .SetHttpGetArgument(TRANSCODE, RestApiCallDocumentation::Type_String,
                             "If present, the DICOM files in the archive will be transcoded to the provided "
-                            "transfer syntax: https://book.orthanc-server.com/faq/transcoding.html", false)
+                            "transfer syntax: https://orthanc.uclouvain.be/book/faq/transcoding.html", false)
         .AddAnswerType(MimeType_Zip, "ZIP file containing the archive");
       if (IS_MEDIA)
       {
@@ -626,8 +677,8 @@
       extended = false;
     }
 
-    std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended));
-    job->AddResource(id);
+    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))
     {
@@ -645,7 +696,8 @@
   }
 
 
-  template <bool IS_MEDIA>
+  template <ResourceType LEVEL,
+            bool IS_MEDIA>
   static void CreateSinglePost(RestApiPostCall& call)
   {
     if (call.IsDocumentation())
@@ -677,8 +729,8 @@
       GetJobParameters(synchronous, extended, transcode, transferSyntax,
                        priority, loaderThreads, body, false /* by default, not extented */);
       
-      std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended));
-      job->AddResource(id);
+      std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended, LEVEL));
+      job->AddResource(id, true, LEVEL);
 
       if (transcode)
       {
@@ -698,24 +750,32 @@
     
   void OrthancRestApi::RegisterArchive()
   {
-    Register("/patients/{id}/archive", CreateSingleGet<false /* ZIP */>);
-    Register("/patients/{id}/archive", CreateSinglePost<false /* ZIP */>);
-    Register("/patients/{id}/media",   CreateSingleGet<true /* media */>);
-    Register("/patients/{id}/media",   CreateSinglePost<true /* media */>);
-    Register("/series/{id}/archive",   CreateSingleGet<false /* ZIP */>);
-    Register("/series/{id}/archive",   CreateSinglePost<false /* ZIP */>);
-    Register("/series/{id}/media",     CreateSingleGet<true /* media */>);
-    Register("/series/{id}/media",     CreateSinglePost<true /* media */>);
-    Register("/studies/{id}/archive",  CreateSingleGet<false /* ZIP */>);
-    Register("/studies/{id}/archive",  CreateSinglePost<false /* ZIP */>);
-    Register("/studies/{id}/media",    CreateSingleGet<true /* media */>);
-    Register("/studies/{id}/media",    CreateSinglePost<true /* media */>);
+    Register("/patients/{id}/archive", CreateSingleGet<ResourceType_Patient, false /* ZIP */>);
+    Register("/patients/{id}/archive", CreateSinglePost<ResourceType_Patient, false /* ZIP */>);
+    Register("/patients/{id}/media",   CreateSingleGet<ResourceType_Patient, true /* media */>);
+    Register("/patients/{id}/media",   CreateSinglePost<ResourceType_Patient, true /* media */>);
+    Register("/series/{id}/archive",   CreateSingleGet<ResourceType_Series, false /* ZIP */>);
+    Register("/series/{id}/archive",   CreateSinglePost<ResourceType_Series, false /* ZIP */>);
+    Register("/series/{id}/media",     CreateSingleGet<ResourceType_Series, true /* media */>);
+    Register("/series/{id}/media",     CreateSinglePost<ResourceType_Series, true /* media */>);
+    Register("/studies/{id}/archive",  CreateSingleGet<ResourceType_Study, false /* ZIP */>);
+    Register("/studies/{id}/archive",  CreateSinglePost<ResourceType_Study, false /* ZIP */>);
+    Register("/studies/{id}/media",    CreateSingleGet<ResourceType_Study, true /* media */>);
+    Register("/studies/{id}/media",    CreateSinglePost<ResourceType_Study, true /* media */>);
 
     Register("/tools/create-archive",
-             CreateBatch<false /* ZIP */,  false /* extended makes no sense in ZIP */>);
+             CreateBatchPost<false /* ZIP */,  false /* extended makes no sense in ZIP */>);
     Register("/tools/create-media",
-             CreateBatch<true /* media */, false /* not extended by default */>);
+             CreateBatchPost<true /* media */, false /* not extended by default */>);
     Register("/tools/create-media-extended",
-             CreateBatch<true /* media */, true /* extended by default */>);
+             CreateBatchPost<true /* media */, true /* extended by default */>);
+
+    Register("/tools/create-archive",
+             CreateBatchGet<false /* ZIP */,  false /* extended makes no sense in ZIP */>);
+    Register("/tools/create-media",
+             CreateBatchGet<true /* media */, false /* not extended by default */>);
+    Register("/tools/create-media-extended",
+             CreateBatchGet<true /* media */, true /* extended by default */>);
+
   }
 }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestChanges.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestChanges.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -64,7 +65,7 @@
       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("limit", RestApiCallDocumentation::Type_Number, "Limit the number of results", false)
         .SetHttpGetArgument("since", RestApiCallDocumentation::Type_Number, "Show only the resources since the provided index", false)
         .AddAnswerType(MimeType_Json, "The list of changes")
@@ -73,7 +74,7 @@
                         "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")
-        .SetHttpGetSample("https://demo.orthanc-server.com/changes?since=0&limit=2", true);
+        .SetHttpGetSample("https://orthanc.uclouvain.be/demo/changes?since=0&limit=2", true);
       return;
     }
     
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -44,6 +45,9 @@
 {
   static const char* const KEY_LEVEL = "Level";
   static const char* const KEY_LOCAL_AET = "LocalAet";
+  static const char* const KEY_CALLED_AET = "CalledAet";
+  static const char* const KEY_HOST = "Host";
+  static const char* const KEY_PORT = "Port";
   static const char* const KEY_NORMALIZE = "Normalize";
   static const char* const KEY_QUERY = "Query";
   static const char* const KEY_RESOURCES = "Resources";
@@ -112,7 +116,11 @@
       .SetRequestField("Manufacturer", RestApiCallDocumentation::Type_String, "Manufacturer of the remote DICOM "
                        "modality (check configuration option `DicomModalities` for possible values", false)
       .SetRequestField("UseDicomTls", RestApiCallDocumentation::Type_Boolean, "Whether to use DICOM TLS "
-                       "in the SCU connection initiated by Orthanc (new in Orthanc 1.9.0)", false);
+                       "in the SCU connection initiated by Orthanc (new in Orthanc 1.9.0)", false)
+      .SetRequestField(KEY_LOCAL_AET, RestApiCallDocumentation::Type_String, "Whether to override the default DicomAet "
+                       "in the SCU connection initiated by Orthanc to this modality", false)
+      .SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number, "Whether to override the default DicomScuTimeout "
+                       "in the SCU connection initiated by Orthanc to this modality", false);
 
     if (includePermissions)
     {
@@ -192,8 +200,6 @@
   static void DocumentEchoShared(RestApiPostCall& call)
   {
     call.GetDocumentation()
-      .SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number,
-                       "Timeout for the C-ECHO command, in seconds", false)
       .SetRequestField(KEY_CHECK_FIND, RestApiCallDocumentation::Type_Boolean,
                        "Issue a dummy C-FIND command after the C-GET SCU, in order to check whether the remote "
                        "modality knows about Orthanc. This field defaults to the value of the `DicomEchoChecksFind` "
@@ -210,7 +216,9 @@
         .SetTag("Networking")
         .SetSummary("Trigger C-ECHO SCU")
         .SetDescription("Trigger C-ECHO SCU command against the DICOM modality whose identifier is provided in URL: "
-                        "https://book.orthanc-server.com/users/rest.html#performing-c-echo")
+                        "https://orthanc.uclouvain.be/book/users/rest.html#performing-c-echo")
+        .SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number,
+                         "Timeout for the C-ECHO command, in seconds", false)
         .SetUriArgument("id", "Identifier of the modality of interest");
       return;
     }
@@ -654,7 +662,7 @@
         .SetTag("Networking")
         .SetSummary("Trigger C-FIND SCU")
         .SetDescription("Trigger C-FIND SCU command against the DICOM modality whose identifier is provided in URL: "
-                        "https://book.orthanc-server.com/users/rest.html#performing-query-retrieve-c-find-and-find-with-rest")
+                        "https://orthanc.uclouvain.be/book/users/rest.html#performing-query-retrieve-c-find-and-find-with-rest")
         .SetUriArgument("id", "Identifier of the modality of interest")
         .SetRequestField(KEY_QUERY, RestApiCallDocumentation::Type_JsonObject,
                          "Associative array containing the filter on the values of the DICOM tags", true)
@@ -757,7 +765,7 @@
         .SetDescription("List the identifiers of all the query/retrieve operations on DICOM modalities, "
                         "as initiated by calls to `/modalities/{id}/query`. The length of this list is bounded "
                         "by the `QueryRetrieveSize` configuration option of Orthanc. "
-                        "https://book.orthanc-server.com/users/rest.html#performing-query-retrieve-c-find-and-find-with-rest")
+                        "https://orthanc.uclouvain.be/book/users/rest.html#performing-query-retrieve-c-find-and-find-with-rest")
         .AddAnswerType(MimeType_Json, "JSON array containing the identifiers");
       return;
     }
@@ -840,7 +848,7 @@
       return;
     }
 
-    const bool expand = call.HasArgument("expand");
+    const bool expand = call.HasArgument("expand") && call.GetBooleanArgument("expand", true);
     const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Full);
     
     QueryAccessor query(call);
@@ -994,7 +1002,7 @@
         .SetSummary("Retrieve one answer")
         .SetDescription("Start a C-MOVE 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://book.orthanc-server.com/users/rest.html#performing-retrieve-c-move")
+                        "https://orthanc.uclouvain.be/book/users/rest.html#performing-retrieve-c-move")
         .SetUriArgument("index", "Index of the answer");
       return;
     }
@@ -1013,7 +1021,7 @@
         .SetSummary("Retrieve all answers")
         .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://book.orthanc-server.com/users/rest.html#performing-retrieve-c-move");
+                        "https://orthanc.uclouvain.be/book/users/rest.html#performing-retrieve-c-move");
       return;
     }
 
@@ -1414,20 +1422,29 @@
         .SetSummary("Trigger C-STORE SCU")
         .SetDescription("Start a C-STORE SCU command as a job, in order to send DICOM resources stored locally "
                         "to some remote DICOM modality whose identifier is provided in the URL: "
-                        "https://book.orthanc-server.com/users/rest.html#rest-store-scu")
+                        "https://orthanc.uclouvain.be/book/users/rest.html#rest-store-scu")
         .AddRequestType(MimeType_PlainText, "The Orthanc identifier of one resource to be sent")
         .SetRequestField(KEY_RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings,
                          "List of the Orthanc identifiers of all the DICOM resources to be sent", 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_CALLED_AET, RestApiCallDocumentation::Type_String,
+                         "Called AET that is used for this commands, defaults to `AET` configuration option. "
+                         "Allows you to overwrite the destination AET for a specific operation.", false)
+        .SetRequestField(KEY_HOST, RestApiCallDocumentation::Type_String,
+                         "Host that is used for this commands, defaults to `Host` configuration option. "
+                         "Allows you to overwrite the destination host for a specific operation.", false)
+        .SetRequestField(KEY_PORT, RestApiCallDocumentation::Type_Number,
+                         "Port that is used for this command, defaults to `Port` configuration option. "
+                         "Allows you to overwrite the destination port for a specific operation.", false)
         .SetRequestField(KEY_MOVE_ORIGINATOR_AET, RestApiCallDocumentation::Type_String,
                          "Move originator AET that is used for this commands, in order to fake a C-MOVE SCU", false)
         .SetRequestField(KEY_MOVE_ORIGINATOR_ID, RestApiCallDocumentation::Type_Number,
                          "Move originator ID that is used for this commands, in order to fake a C-MOVE SCU", false)
         .SetRequestField(KEY_STORAGE_COMMITMENT, RestApiCallDocumentation::Type_Boolean,
                          "Whether to chain C-STORE with DICOM storage commitment to validate the success of the transmission: "
-                         "https://book.orthanc-server.com/users/storage-commitment.html#chaining-c-store-with-storage-commitment", false)
+                         "https://orthanc.uclouvain.be/book/users/storage-commitment.html#chaining-c-store-with-storage-commitment", false)
         .SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number,
                          "Timeout for the C-STORE command, in seconds", false)
         .SetUriArgument("id", "Identifier of the modality of interest");
@@ -1450,8 +1467,17 @@
     int moveOriginatorID = Toolbox::GetJsonIntegerField
       (request, KEY_MOVE_ORIGINATOR_ID, 0 /* By default, not a C-MOVE */);
 
+    RemoteModalityParameters remoteModality = MyGetModalityUsingSymbolicName(remote);
+
+    remoteModality.SetApplicationEntityTitle(Toolbox::GetJsonStringField
+      (request, KEY_CALLED_AET, remoteModality.GetApplicationEntityTitle()));
+    remoteModality.SetHost(Toolbox::GetJsonStringField
+      (request, KEY_HOST, remoteModality.GetHost()));
+    remoteModality.SetPortNumber(static_cast<uint16_t>(Toolbox::GetJsonUnsignedIntegerField
+      (request, KEY_PORT, remoteModality.GetPortNumber())));
+
     job->SetLocalAet(localAet);
-    job->SetRemoteModality(MyGetModalityUsingSymbolicName(remote));
+    job->SetRemoteModality(remoteModality);
 
     if (moveOriginatorID != 0)
     {
@@ -1499,7 +1525,9 @@
 
     std::string sopClassUid, sopInstanceUid;
     connection.Store(sopClassUid, sopInstanceUid, call.GetBodyData(),
-                     call.GetBodySize(), false /* Not a C-MOVE */, "", 0);
+                     call.GetBodySize(), 
+                     false /* Not a C-MOVE */, 
+                     "", 0);
 
     Json::Value answer = Json::objectValue;
     answer[SOP_CLASS_UID] = sopClassUid;
@@ -1523,7 +1551,7 @@
         .SetSummary("Trigger C-MOVE SCU")
         .SetDescription("Start a C-MOVE SCU command as a job, in order to drive the execution of a sequence of "
                         "C-STORE commands by some remote DICOM modality whose identifier is provided in the URL: "
-                        "https://book.orthanc-server.com/users/rest.html#performing-c-move")
+                        "https://orthanc.uclouvain.be/book/users/rest.html#performing-c-move")
         .SetRequestField(KEY_RESOURCES, RestApiCallDocumentation::Type_JsonListOfObjects,
                          "List of queries identifying all the DICOM resources to be sent", true)
         .SetRequestField(KEY_LEVEL, RestApiCallDocumentation::Type_String,
@@ -1628,7 +1656,7 @@
     OrthancRestApi::SetOfStrings peers;
     lock.GetConfiguration().GetListOfOrthancPeers(peers);
 
-    if (call.HasArgument("expand"))
+    if (call.HasArgument("expand") && call.GetBooleanArgument("expand", true))
     {
       Json::Value result = Json::objectValue;
       for (OrthancRestApi::SetOfStrings::const_iterator
@@ -1695,7 +1723,7 @@
         .SetTag("Networking")
         .SetSummary("Send to Orthanc peer")
         .SetDescription("Send DICOM resources stored locally to some remote Orthanc peer whose identifier is provided in the URL: "
-                        "https://book.orthanc-server.com/users/rest.html#sending-one-resource")
+                        "https://orthanc.uclouvain.be/book/users/rest.html#sending-one-resource")
         .AddRequestType(MimeType_PlainText, "The Orthanc identifier of one resource to be sent")
         .SetRequestField(KEY_RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings,
                          "List of the Orthanc identifiers of all the DICOM resources to be sent", true)
@@ -1912,7 +1940,7 @@
     OrthancRestApi::SetOfStrings modalities;
     lock.GetConfiguration().GetListOfDicomModalities(modalities);
 
-    if (call.HasArgument("expand"))
+    if (call.HasArgument("expand") && call.GetBooleanArgument("expand", true))
     {
       Json::Value result = Json::objectValue;
       for (OrthancRestApi::SetOfStrings::const_iterator
@@ -2213,7 +2241,7 @@
         .SetTag("Networking")
         .SetSummary("Trigger storage commitment request")
         .SetDescription("Trigger a storage commitment request to some remote DICOM modality whose identifier is provided "
-                        "in the URL: https://book.orthanc-server.com/users/storage-commitment.html#storage-commitment-scu")
+                        "in the URL: https://orthanc.uclouvain.be/book/users/storage-commitment.html#storage-commitment-scu")
         .SetRequestField(ORTHANC_RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings,
                          "List of the Orthanc identifiers of the DICOM resources to be checked by storage commitment", true)
         .SetRequestField(DICOM_INSTANCES, RestApiCallDocumentation::Type_JsonListOfObjects,
@@ -2402,7 +2430,7 @@
         .SetTag("Networking")
         .SetSummary("Get storage commitment report")
         .SetDescription("Get the storage commitment report whose identifier is provided in the URL: "
-                        "https://book.orthanc-server.com/users/storage-commitment.html#storage-commitment-scu")
+                        "https://orthanc.uclouvain.be/book/users/storage-commitment.html#storage-commitment-scu")
         .SetAnswerField("Status", RestApiCallDocumentation::Type_String,
                         "Can be `Success`, `Failure`, or `Pending` (the latter means that no report has been received yet)")
         .SetAnswerField("RemoteAET", RestApiCallDocumentation::Type_String,
@@ -2450,7 +2478,7 @@
         .SetDescription("Remove out of Orthanc, the DICOM instances that have been reported to have been properly "
                         "received the storage commitment report whose identifier is provided in the URL. This is "
                         "only possible if the `Status` of the storage commitment report is `Success`. "
-                        "https://book.orthanc-server.com/users/storage-commitment.html#removing-the-instances")
+                        "https://orthanc.uclouvain.be/book/users/storage-commitment.html#removing-the-instances")
         .SetUriArgument("id", "Identifier of the storage commitment report");
       return;
     }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -58,6 +59,8 @@
 
 static const char* const IGNORE_LENGTH = "ignore-length";
 static const char* const RECONSTRUCT_FILES = "ReconstructFiles";
+static const char* const LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS = "LimitToThisLevelMainDicomTags";
+static const char* const ARG_WHOLE = "whole";
 
 
 namespace Orthanc
@@ -67,19 +70,19 @@
     switch (type)
     {
       case Orthanc::ResourceType_Instance:
-        return "https://demo.orthanc-server.com/instances/d94d9a03-3003b047-a4affc69-322313b2-680530a2";
+        return "https://orthanc.uclouvain.be/demo/instances/6582b1c0-292ad5ab-ba0f088f-f7a1766f-9a29a54f";
         break;
         
       case Orthanc::ResourceType_Series:
-        return "https://demo.orthanc-server.com/series/37836232-d13a2350-fa1dedc5-962b31aa-010f8e52";
+        return "https://orthanc.uclouvain.be/demo/series/37836232-d13a2350-fa1dedc5-962b31aa-010f8e52";
         break;
         
       case Orthanc::ResourceType_Study:
-        return "https://demo.orthanc-server.com/studies/27f7126f-4f66fb14-03f4081b-f9341db2-53925988";
+        return "https://orthanc.uclouvain.be/demo/studies/27f7126f-4f66fb14-03f4081b-f9341db2-53925988";
         break;
         
       case Orthanc::ResourceType_Patient:
-        return "https://demo.orthanc-server.com/patients/46e6332c-677825b6-202fcf7c-f787bc5f-7b07c382";
+        return "https://orthanc.uclouvain.be/demo/patients/46e6332c-677825b6-202fcf7c-f787bc5f-7b07c382";
         break;
         
       default:
@@ -216,7 +219,7 @@
                             "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://demo.orthanc-server.com/" + resources + "?since=0&limit=2", true);
+        .SetHttpGetSample("https://orthanc.uclouvain.be/demo/" + resources + "?since=0&limit=2", true);
       return;
     }
     
@@ -254,7 +257,7 @@
       index.GetAllUuids(result, resourceType);
     }
 
-    AnswerListOfResources(call.GetOutput(), context, result, resourceType, call.HasArgument("expand"),
+    AnswerListOfResources(call.GetOutput(), context, result, resourceType, call.HasArgument("expand") && call.GetBooleanArgument("expand", true),
                           OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human),
                           requestedTags,
                           true /* allowStorageAccess */);
@@ -342,8 +345,10 @@
     {
       call.GetDocumentation()
         .SetTag("Patients")
-        .SetSummary("Protect one patient against recycling")
-        .SetDescription("Check out configuration options `MaximumStorageSize` and `MaximumPatientCount`")
+        .SetSummary("Protect/Unprotect a patient against recycling")
+        .SetDescription("Protects a patient by sending `1` or `true` in the payload request. "
+                        "Unprotects a patient by sending `0` or `false` in the payload requests. "
+                        "More info: https://orthanc.uclouvain.be/book/faq/features.html#recycling-protection")
         .SetUriArgument("id", "Orthanc identifier of the patient of interest");
       return;
     }
@@ -361,6 +366,8 @@
  
   static void GetInstanceFile(RestApiGetCall& call)
   {
+    static const char* const TRANSCODE = "transcode";
+
     if (call.IsDocumentation())
     {
       call.GetDocumentation()
@@ -369,6 +376,9 @@
         .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,
+                            "If present, the DICOM file will be transcoded to the provided "
+                            "transfer syntax: https://orthanc.uclouvain.be/book/faq/transcoding.html", 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");
@@ -417,7 +427,23 @@
       }
     }
 
-    context.AnswerAttachment(call.GetOutput(), publicId, FileContentType_Dicom);
+    if (call.HasArgument(TRANSCODE))
+    {
+      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, ""))))
+      {
+        call.GetOutput().AnswerBuffer(transcoded, MimeType_Dicom);
+      }
+    }
+    else
+    {
+      // return the attachment without any transcoding
+      context.AnswerAttachment(call.GetOutput(), publicId, FileContentType_Dicom);
+    }
   }
 
 
@@ -428,7 +454,10 @@
       call.GetDocumentation()
         .SetTag("Instances")
         .SetSummary("Write DICOM onto filesystem")
-        .SetDescription("Write the DICOM file onto the filesystem where Orthanc is running")
+        .SetDescription("Write the DICOM file onto the filesystem where Orthanc is running.  This is insecure for "
+                        "Orthanc servers that are remotely accessible since one could overwrite any system file.  "
+                        "Since Orthanc 1.12.0, this route is disabled by default, but can be enabled using "
+                        "the `RestApiWriteToFileSystemEnabled` configuration option.")
         .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest")
         .AddRequestType(MimeType_PlainText, "Target path on the filesystem");
       return;
@@ -436,6 +465,14 @@
 
     ServerContext& context = OrthancRestApi::GetContext(call);
 
+    if (!context.IsRestApiWriteToFileSystemEnabled())
+    {
+      LOG(ERROR) << "The URI /instances/../export is disallowed for security, "
+                 << "check your configuration option `RestApiWriteToFileSystemEnabled`";
+      call.GetOutput().SignalError(HttpStatus_403_Forbidden);
+      return;
+    }
+
     std::string publicId = call.GetUriComponent("id", "");
 
     std::string dicom;
@@ -450,7 +487,8 @@
 
 
   template <DicomToJsonFormat format>
-  static void GetInstanceTagsInternal(RestApiGetCall& call)
+  static void GetInstanceTagsInternal(RestApiGetCall& call,
+                                      bool whole)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
@@ -458,56 +496,90 @@
 
     std::set<DicomTag> ignoreTagLength;
     ParseSetOfTags(ignoreTagLength, call, IGNORE_LENGTH);
-    
-    if (format != DicomToJsonFormat_Full ||
-        !ignoreTagLength.empty())
+
+    if (whole)
     {
-      Json::Value full;
-      context.ReadDicomAsJson(full, publicId, ignoreTagLength);
-      AnswerDicomAsJson(call, full, format);
+      // This is new in Orthanc 1.12.4. Reference:
+      // https://discourse.orthanc-server.org/t/private-tags-with-group-7fe0-are-not-provided-via-rest-api/4744
+      const DicomToJsonFlags flags = static_cast<DicomToJsonFlags>(DicomToJsonFlags_Default & ~DicomToJsonFlags_StopAfterPixelData);
+
+      Json::Value answer;
+
+      {
+        ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), publicId);
+        locker.GetDicom().DatasetToJson(answer, format, flags,
+                                        ORTHANC_MAXIMUM_TAG_LENGTH, ignoreTagLength);
+      }
+
+      call.GetOutput().AnswerJson(answer);
     }
     else
     {
-      // This path allows one to avoid the JSON decoding if no
-      // simplification is asked, and if no "ignore-length" argument
-      // is present
-      Json::Value full;
-      context.ReadDicomAsJson(full, publicId);
-      call.GetOutput().AnswerJson(full);
+      if (format != DicomToJsonFormat_Full ||
+          !ignoreTagLength.empty())
+      {
+        Json::Value full;
+        context.ReadDicomAsJson(full, publicId, ignoreTagLength);
+        AnswerDicomAsJson(call, full, format);
+      }
+      else
+      {
+        // This path allows one to avoid the JSON decoding if no
+        // simplification is asked, and if no "ignore-length" argument
+        // is present
+        Json::Value full;
+        context.ReadDicomAsJson(full, publicId);
+        call.GetOutput().AnswerJson(full);
+      }
     }
   }
 
 
+  static void DocumentGetInstanceTags(RestApiGetCall& call)
+  {
+    call.GetDocumentation()
+      .SetTag("Instances")
+      .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest")
+      .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)
+      .SetHttpGetArgument(
+        ARG_WHOLE, RestApiCallDocumentation::Type_Boolean, "Whether to read the whole DICOM file from the "
+        "storage area (new in Orthanc 1.12.4). If set to \"false\" (default value), the DICOM file is read "
+        "until the pixel data tag (7fe0,0010) to optimize access to storage. Setting the option "
+        "to \"true\" provides access to the DICOM tags stored after the pixel data tag.", false)
+      .AddAnswerType(MimeType_Json, "JSON object containing the DICOM tags and their associated value");
+  }
+
+
   static void GetInstanceTags(RestApiGetCall& call)
   {
     if (call.IsDocumentation())
     {
       OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Full);
+      DocumentGetInstanceTags(call);
       call.GetDocumentation()
-        .SetTag("Instances")
         .SetSummary("Get DICOM tags")
         .SetDescription("Get the DICOM tags in the specified format. By default, the `full` format is used, which "
                         "combines hexadecimal tags with human-readable description.")
-        .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest")
-        .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 containing the DICOM tags and their associated value")
-        .SetTruncatedJsonHttpGetSample("https://demo.orthanc-server.com/instances/7c92ce8e-bbf67ed2-ffa3b8c1-a3b35d94-7ff3ae26/tags", 10);
+        .SetTruncatedJsonHttpGetSample("https://orthanc.uclouvain.be/demo/instances/7c92ce8e-bbf67ed2-ffa3b8c1-a3b35d94-7ff3ae26/tags", 10);
       return;
     }
 
+    const bool whole = call.GetBooleanArgument(ARG_WHOLE, false);
+
     switch (OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Full))
     {
       case DicomToJsonFormat_Human:
-        GetInstanceTagsInternal<DicomToJsonFormat_Human>(call);
+        GetInstanceTagsInternal<DicomToJsonFormat_Human>(call, whole);
         break;
 
       case DicomToJsonFormat_Short:
-        GetInstanceTagsInternal<DicomToJsonFormat_Short>(call);
+        GetInstanceTagsInternal<DicomToJsonFormat_Short>(call, whole);
         break;
 
       case DicomToJsonFormat_Full:
-        GetInstanceTagsInternal<DicomToJsonFormat_Full>(call);
+        GetInstanceTagsInternal<DicomToJsonFormat_Full>(call, whole);
         break;
 
       default:
@@ -520,20 +592,16 @@
   {
     if (call.IsDocumentation())
     {
+      DocumentGetInstanceTags(call);
       call.GetDocumentation()
-        .SetTag("Instances")
         .SetSummary("Get human-readable tags")
         .SetDescription("Get the DICOM tags in human-readable format (same as the `/instances/{id}/tags?simplify` route)")
-        .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest")
-        .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 containing the DICOM tags and their associated value")
-        .SetTruncatedJsonHttpGetSample("https://demo.orthanc-server.com/instances/7c92ce8e-bbf67ed2-ffa3b8c1-a3b35d94-7ff3ae26/simplified-tags", 10);
+        .SetTruncatedJsonHttpGetSample("https://orthanc.uclouvain.be/demo/instances/7c92ce8e-bbf67ed2-ffa3b8c1-a3b35d94-7ff3ae26/simplified-tags", 10);
       return;
     }
     else
     {
-      GetInstanceTagsInternal<DicomToJsonFormat_Human>(call);
+      GetInstanceTagsInternal<DicomToJsonFormat_Human>(call, call.GetBooleanArgument(ARG_WHOLE, false));
     }
   }
 
@@ -548,7 +616,7 @@
         .SetDescription("List the frames that are available in the DICOM instance of interest")
         .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest")
         .AddAnswerType(MimeType_Json, "The list of the indices of the available frames")
-        .SetHttpGetSample("https://demo.orthanc-server.com/instances/7c92ce8e-bbf67ed2-ffa3b8c1-a3b35d94-7ff3ae26/frames", true);      
+        .SetHttpGetSample("https://orthanc.uclouvain.be/demo/instances/7c92ce8e-bbf67ed2-ffa3b8c1-a3b35d94-7ff3ae26/frames", true);      
       return;
     }
 
@@ -628,7 +696,8 @@
       }
 
       virtual void Handle(const std::string& type,
-                          const std::string& subtype) ORTHANC_OVERRIDE
+                          const std::string& subtype,
+                          const HttpContentNegociation::Dictionary& parameters) ORTHANC_OVERRIDE
       {
         assert(type == "image");
         assert(subtype == "png");
@@ -647,7 +716,8 @@
       }
 
       virtual void Handle(const std::string& type,
-                          const std::string& subtype) ORTHANC_OVERRIDE
+                          const std::string& subtype,
+                          const HttpContentNegociation::Dictionary& parameters) ORTHANC_OVERRIDE
       {
         assert(type == "image");
         assert(subtype == "x-portable-arbitrarymap");
@@ -687,7 +757,8 @@
       }
 
       virtual void Handle(const std::string& type,
-                          const std::string& subtype) ORTHANC_OVERRIDE
+                          const std::string& subtype,
+                          const HttpContentNegociation::Dictionary& parameters) ORTHANC_OVERRIDE
       {
         assert(type == "image");
         assert(subtype == "jpeg");
@@ -1446,7 +1517,7 @@
         .SetTag("Instances")
         .SetSummary("Decode frame for Matlab")
         .SetDescription(description + ", and export this frame as a Octave/Matlab matrix to be imported with `eval()`: "
-                        "https://book.orthanc-server.com/faq/matlab.html")
+                        "https://orthanc.uclouvain.be/book/faq/matlab.html")
         .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest")
         .AddAnswerType(MimeType_PlainText, "Octave/Matlab matrix");
       return;
@@ -1666,6 +1737,8 @@
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
         .SetHttpGetArgument("expand", RestApiCallDocumentation::Type_String,
                             "If present, also retrieve the value of the individual metadata", false)
+        .SetHttpGetArgument("numeric", RestApiCallDocumentation::Type_String,
+                            "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);
@@ -1683,15 +1756,26 @@
 
     Json::Value result;
 
-    if (call.HasArgument("expand"))
+    bool isNumeric = call.HasArgument("numeric");
+
+    if (call.HasArgument("expand") && call.GetBooleanArgument("expand", true))
     {
       result = Json::objectValue;
       
       for (Metadata::const_iterator it = metadata.begin(); it != metadata.end(); ++it)
       {
-        std::string key = EnumerationToString(it->first);
+        std::string key;
+        if (isNumeric)
+        {
+          key = boost::lexical_cast<std::string>(it->first);
+        }
+        else
+        {
+          key = EnumerationToString(it->first);
+        }
+
         result[key] = it->second;
-      }      
+      }
     }
     else
     {
@@ -1699,7 +1783,14 @@
       
       for (Metadata::const_iterator it = metadata.begin(); it != metadata.end(); ++it)
       {       
-        result.append(EnumerationToString(it->first));
+        if (isNumeric)
+        {
+          result.append(it->first);
+        }
+        else
+        {
+          result.append(EnumerationToString(it->first));
+        }
       }
     }
 
@@ -1857,7 +1948,8 @@
     std::string name = call.GetUriComponent("name", "");
     MetadataType metadata = StringToMetadata(name);
 
-    if (IsUserMetadata(metadata))  // It is forbidden to modify internal metadata
+    if (IsUserMetadata(metadata) ||  // It is forbidden to delete internal metadata...
+        call.GetRequestOrigin() == RequestOrigin_Plugins)     // ...except for plugins
     {
       bool found;
       int64_t revision;
@@ -1923,7 +2015,8 @@
     std::string value;
     call.BodyToString(value);
 
-    if (IsUserMetadata(metadata))  // It is forbidden to modify internal metadata
+    if (IsUserMetadata(metadata) ||  // It is forbidden to modify internal metadata...
+        call.GetRequestOrigin() == RequestOrigin_Plugins)     // ...except for plugins
     {
       int64_t oldRevision;
       std::string oldMD5;
@@ -1958,6 +2051,129 @@
 
 
 
+  // Handling of labels -------------------------------------------------------
+
+  static void ListLabels(RestApiGetCall& call)
+  {
+    if (call.IsDocumentation())
+    {
+      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
+      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      call.GetDocumentation()
+        .SetTag(GetResourceTypeText(t, 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);
+      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);
+
+    Json::Value result = Json::arrayValue;
+
+    for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
+    {
+      result.append(*it);
+    }
+
+    call.GetOutput().AnswerJson(result);
+  }
+  
+
+  static void GetLabel(RestApiGetCall& call)
+  {
+    if (call.IsDocumentation())
+    {
+      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
+      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      call.GetDocumentation()
+        .SetTag(GetResourceTypeText(t, 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")
+        .SetUriArgument("label", "The label of interest")
+        .AddAnswerType(MimeType_PlainText, "Empty string is returned in the case of presence, error 404 in the case of absence");
+      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", "");
+
+    std::set<std::string> labels;
+    OrthancRestApi::GetIndex(call).ListLabels(labels, publicId, level);
+    
+    if (labels.find(label) != labels.end())
+    {
+      call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+    }
+  }
+
+
+  static void AddLabel(RestApiPutCall& call)
+  {
+    if (call.IsDocumentation())
+    {
+      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
+      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      call.GetDocumentation()
+        .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
+        .SetSummary("Add label")
+        .SetDescription("Associate a label with a " + r)
+        .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
+        .SetUriArgument("label", "The label to be added");
+      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);
+
+    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+  }
+
+
+  static void RemoveLabel(RestApiDeleteCall& call)
+  {
+    if (call.IsDocumentation())
+    {
+      ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
+      std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      call.GetDocumentation()
+        .SetTag(GetResourceTypeText(t, 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")
+        .SetUriArgument("label", "The label to be removed");
+      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);
+
+    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+  }
+  
 
   // Handling of attached files -----------------------------------------------
 
@@ -2070,7 +2286,7 @@
         .SetSummary("List operations on attachments")
         .SetDescription("Get the list of the operations that are available for attachments associated with the given " + r)
         .AddAnswerType(MimeType_Json, "List of the available operations")
-        .SetHttpGetSample("https://demo.orthanc-server.com/instances/d94d9a03-3003b047-a4affc69-322313b2-680530a2/attachments/dicom", true);
+        .SetHttpGetSample("https://orthanc.uclouvain.be/demo/instances/6582b1c0-292ad5ab-ba0f088f-f7a1766f-9a29a54f/attachments/dicom", true);
       return;
     }
 
@@ -2106,8 +2322,6 @@
         operations.append("verify-md5");
       }
 
-      operations.append("uuid");
-
       call.GetOutput().AnswerJson(operations);
     }
   }
@@ -2153,8 +2367,9 @@
       {
         // Return the raw data (possibly compressed), as stored on the filesystem
         std::string content;
+        std::string attachmentId;
         int64_t revision;
-        context.ReadAttachment(content, revision, publicId, type, false, true /* skipCache when you absolutely need the compressed data */);
+        context.ReadAttachment(content, revision, attachmentId, publicId, type, false, true /* skipCache when you absolutely need the compressed data */);
 
         int64_t userRevision;
         std::string userMD5;
@@ -2207,7 +2422,7 @@
         .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")
-        .SetHttpGetSample("https://demo.orthanc-server.com/instances/7c92ce8e-bbf67ed2-ffa3b8c1-a3b35d94-7ff3ae26/attachments/dicom/info", true);
+        .SetHttpGetSample("https://orthanc.uclouvain.be/demo/instances/7c92ce8e-bbf67ed2-ffa3b8c1-a3b35d94-7ff3ae26/attachments/dicom/info", true);
       return;
     }
 
@@ -2336,7 +2551,9 @@
 
     // First check whether the compressed data is correctly stored in the disk
     std::string data;
-    context.ReadAttachment(data, revision, publicId, StringToContentType(name), false, true /* skipCache when you absolutely need the compressed data */);
+    std::string attachmentId;
+
+    context.ReadAttachment(data, revision, attachmentId, publicId, StringToContentType(name), false, true /* skipCache when you absolutely need the compressed data */);
 
     std::string actualMD5;
     Toolbox::ComputeMD5(actualMD5, data);
@@ -2351,7 +2568,7 @@
       }
       else
       {
-        context.ReadAttachment(data, revision, publicId, StringToContentType(name), true, true /* skipCache when you absolutely need the compressed data */);
+        context.ReadAttachment(data, revision, attachmentId, publicId, StringToContentType(name), true, true /* skipCache when you absolutely need the compressed data */);
         Toolbox::ComputeMD5(actualMD5, data);
         ok = (actualMD5 == info.GetUncompressedMD5());
       }
@@ -2396,7 +2613,8 @@
     ResourceType resourceType = StringToResourceType(call.GetFullUri()[0].c_str());
 
     FileContentType contentType = StringToContentType(name);
-    if (IsUserContentType(contentType))  // It is forbidden to modify internal attachments
+    if (IsUserContentType(contentType) ||  // It is forbidden to modify internal attachments...
+        call.GetRequestOrigin() == RequestOrigin_Plugins)   // ...except for plugins
     {
       int64_t oldRevision;
       std::string oldMD5;
@@ -2455,7 +2673,8 @@
     FileContentType contentType = StringToContentType(name);
 
     bool allowed;
-    if (IsUserContentType(contentType))
+    if (IsUserContentType(contentType) ||  // It is forbidden to delete internal attachments...
+        call.GetRequestOrigin() == RequestOrigin_Plugins)   // ...except for plugins
     {
       allowed = true;
     }
@@ -2580,7 +2799,7 @@
         .SetSummary("Get raw tag")
         .SetDescription("Get the raw content of one DICOM tag in the hierarchy of DICOM dataset")
         .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest")
-        .SetUriArgument("...", "Path to the DICOM tag. This is the interleaving of one DICOM tag, possibly followed "
+        .SetUriArgument("path", "Path to the DICOM tag. This is the interleaving of one DICOM tag, possibly followed "
                         "by an index for sequences. Sequences are accessible as, for instance, `/0008-1140/1/0008-1150`")
         .AddAnswerType(MimeType_Binary, "The raw value of the tag of intereset "
                        "(binary data, whose memory layout depends on the underlying transfer syntax), "
@@ -2938,6 +3157,8 @@
     static const char* const KEY_QUERY = "Query";
     static const char* const KEY_REQUESTED_TAGS = "RequestedTags";
     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
 
     if (call.IsDocumentation())
     {
@@ -2948,7 +3169,7 @@
         .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://book.orthanc-server.com/users/rest.html#performing-finds-within-orthanc")
+                        "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,
@@ -2967,6 +3188,10 @@
                          "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`)");
       return;
@@ -2997,25 +3222,37 @@
              request[KEY_CASE_SENSITIVE].type() != Json::booleanValue)
     {
       throw OrthancException(ErrorCode_BadRequest, 
-                             "Field \"" + std::string(KEY_CASE_SENSITIVE) + "\" should be a Boolean");
+                             "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) + "\" should be an integer");
+                             "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) + "\" should be an integer");
+                             "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) + "\" should be an array");
+                             "Field \"" + std::string(KEY_REQUESTED_TAGS) + "\" must be an array");
+    }
+    else if (request.isMember(KEY_LABELS) &&
+             request[KEY_LABELS].type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_LABELS) + "\" must be an array of strings");
+    }
+    else if (request.isMember(KEY_LABELS_CONSTRAINT) &&
+             request[KEY_LABELS_CONSTRAINT].type() != Json::stringValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be an array of strings");
     }
     else
     {
@@ -3038,7 +3275,7 @@
         if (tmp < 0)
         {
           throw OrthancException(ErrorCode_ParameterOutOfRange,
-                                 "Field \"" + std::string(KEY_LIMIT) + "\" should be a positive integer");
+                                 "Field \"" + std::string(KEY_LIMIT) + "\" must be a positive integer");
         }
 
         limit = static_cast<size_t>(tmp);
@@ -3051,7 +3288,7 @@
         if (tmp < 0)
         {
           throw OrthancException(ErrorCode_ParameterOutOfRange,
-                                 "Field \"" + std::string(KEY_SINCE) + "\" should be a positive integer");
+                                 "Field \"" + std::string(KEY_SINCE) + "\" must be a positive integer");
         }
 
         since = static_cast<size_t>(tmp);
@@ -3074,7 +3311,7 @@
         if (request[KEY_QUERY][members[i]].type() != Json::stringValue)
         {
           throw OrthancException(ErrorCode_BadRequest,
-                                 "Tag \"" + members[i] + "\" should be associated with a string");
+                                 "Tag \"" + members[i] + "\" must be associated with a string");
         }
 
         const std::string value = request[KEY_QUERY][members[i]].asString();
@@ -3089,8 +3326,48 @@
         }
       }
 
+      std::set<std::string> labels;
+
+      if (request.isMember(KEY_LABELS))  // New in Orthanc 1.12.0
+      {
+        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
+          {
+            labels.insert(request[KEY_LABELS][i].asString());
+          }
+        }
+      }
+
+      LabelsConstraint labelsConstraint = LabelsConstraint_All;
+      
+      if (request.isMember(KEY_LABELS_CONSTRAINT))
+      {
+        const std::string& s = request[KEY_LABELS_CONSTRAINT].asString();
+        if (s == "All")
+        {
+          labelsConstraint = LabelsConstraint_All;
+        }
+        else if (s == "Any")
+        {
+          labelsConstraint = LabelsConstraint_Any;
+        }
+        else if (s == "None")
+        {
+          labelsConstraint = LabelsConstraint_None;
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be \"All\", \"Any\", or \"None\"");
+        }
+      }
+      
       FindVisitor visitor(OrthancRestApi::GetDicomFormat(request, DicomToJsonFormat_Human), context.GetFindStorageAccessMode());
-      context.Apply(visitor, query, level, since, limit);
+      context.Apply(visitor, query, level, labels, labelsConstraint, since, limit);
       visitor.Answer(call.GetOutput(), context, level, expand, requestedTags);
     }
   }
@@ -3113,12 +3390,15 @@
         .SetDescription("Get detailed information about the child " + children + " of the DICOM " +
                         resource + " whose Orthanc identifier is provided in the URL")
         .SetUriArgument("id", "Orthanc identifier of the " + resource + " of interest")
+        .SetHttpGetArgument("expand", RestApiCallDocumentation::Type_String,
+                            "If false or missing, only retrieve the list of child " + children, false)
         .AddAnswerType(MimeType_Json, "JSON array containing information about the child DICOM " + children)
         .SetTruncatedJsonHttpGetSample(GetDocumentationSampleResource(start) + "/" + children, 5);
       return;
     }
 
     ServerIndex& index = OrthancRestApi::GetIndex(call);
+    ServerContext& context = OrthancRestApi::GetContext(call);
 
     std::set<DicomTag> requestedTags;
     OrthancRestApi::GetRequestedTags(requestedTags, call);
@@ -3144,21 +3424,10 @@
       a.splice(a.begin(), b);
     }
 
-    Json::Value result = Json::arrayValue;
-
-    const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human);
-
-    for (std::list<std::string>::const_iterator
-           it = a.begin(); it != a.end(); ++it)
-    {
-      Json::Value resource;
-      if (OrthancRestApi::GetContext(call).ExpandResource(resource, *it, end, format, requestedTags, true /* allowStorageAccess */))
-      {
-        result.append(resource);
-      }
-    }
-
-    call.GetOutput().AnswerJson(result);
+    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 */);
   }
 
 
@@ -3325,7 +3594,7 @@
                         "Same information as the `Slices` field, but in a compact form")
         .SetAnswerField("Type", RestApiCallDocumentation::Type_String,
                         "Can be `Volume` (for 3D volumes) or `Sequence` (notably for cine images)")
-        .SetTruncatedJsonHttpGetSample("https://demo.orthanc-server.com/series/1e2c125c-411b8e86-3f4fe68e-a7584dd3-c6da78f0/ordered-slices", 10);
+        .SetTruncatedJsonHttpGetSample("https://orthanc.uclouvain.be/demo/series/1e2c125c-411b8e86-3f4fe68e-a7584dd3-c6da78f0/ordered-slices", 10);
       return;
     }
 
@@ -3352,7 +3621,7 @@
                         "combines hexadecimal tags with human-readable description.")
         .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest")
         .AddAnswerType(MimeType_Json, "JSON object containing the DICOM tags and their associated value")
-        .SetHttpGetSample("https://demo.orthanc-server.com/instances/7c92ce8e-bbf67ed2-ffa3b8c1-a3b35d94-7ff3ae26/header", true);
+        .SetHttpGetSample("https://orthanc.uclouvain.be/demo/instances/7c92ce8e-bbf67ed2-ffa3b8c1-a3b35d94-7ff3ae26/header", true);
       return;
     }
 
@@ -3385,7 +3654,7 @@
         .SetDescription("Remove all the attachments of the type \"DICOM-as-JSON\" that are associated will all the "
                         "DICOM instances stored in Orthanc. These summaries will be automatically re-created on the next access. "
                         "This is notably useful after changes to the `Dictionary` configuration option. "
-                        "https://book.orthanc-server.com/faq/orthanc-storage.html#storage-area");
+                        "https://orthanc.uclouvain.be/book/faq/orthanc-storage.html#storage-area");
       return;
     }
 
@@ -3413,12 +3682,21 @@
     call.GetOutput().AnswerBuffer("", MimeType_PlainText);
   }
 
-  void DocumentReconstructFilesField(RestApiPostCall& call)
+  void DocumentReconstructFilesField(RestApiPostCall& call, bool documentLimitField)
   {
     call.GetDocumentation()
       .SetRequestField(RECONSTRUCT_FILES, RestApiCallDocumentation::Type_Boolean,
                        "Also reconstruct the files of the resources (e.g: apply IngestTranscoding, StorageCompression). "
                        "'false' by default. (New in Orthanc 1.11.0)", false);
+    if (documentLimitField)
+    {
+      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 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);
+    }
   }
 
   bool GetReconstructFilesField(const RestApiPostCall& call)
@@ -3440,6 +3718,26 @@
     return reconstructFiles;
   }
 
+  bool GetLimitToThisLevelMainDicomTags(const RestApiPostCall& call)
+  {
+    bool limitToThisLevel = false;
+    Json::Value request;
+
+    if (call.GetBodySize() > 0 && call.ParseJsonRequest(request) && request.isMember(LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS))
+    {
+      if (!request[LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS].isBool())
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "The field " + std::string(LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS) + " must contain a Boolean");
+      }
+
+      limitToThisLevel = request[LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS].asBool();
+    }
+
+    return limitToThisLevel;
+  }
+
+
   template <enum ResourceType type>
   static void ReconstructResource(RestApiPostCall& call)
   {
@@ -3455,13 +3753,13 @@
                         "Beware that this is a time-consuming operation, as all the children DICOM instances will be "
                         "parsed again, and the Orthanc index will be updated accordingly.")
         .SetUriArgument("id", "Orthanc identifier of the " + resource + " of interest");
-        DocumentReconstructFilesField(call);
+        DocumentReconstructFilesField(call, true);
 
       return;
     }
 
     ServerContext& context = OrthancRestApi::GetContext(call);
-    ServerToolbox::ReconstructResource(context, call.GetUriComponent("id", ""), GetReconstructFilesField(call));
+    ServerToolbox::ReconstructResource(context, call.GetUriComponent("id", ""), GetReconstructFilesField(call), GetLimitToThisLevelMainDicomTags(call), type);
     call.GetOutput().AnswerBuffer("", MimeType_PlainText);
   }
 
@@ -3479,7 +3777,7 @@
                         "as all the DICOM instances will be parsed again, and as all the Orthanc index will be regenerated. "
                         "If you have a large database to process, it is advised to use the Housekeeper plugin to perform "
                         "this action resource by resource");
-        DocumentReconstructFilesField(call);
+        DocumentReconstructFilesField(call, false);
 
       return;
     }
@@ -3493,7 +3791,7 @@
     for (std::list<std::string>::const_iterator 
            study = studies.begin(); study != studies.end(); ++study)
     {
-      ServerToolbox::ReconstructResource(context, *study, reconstructFiles);
+      ServerToolbox::ReconstructResource(context, *study, reconstructFiles, false, ResourceType_Study /*  dummy */);
     }
     
     call.GetOutput().AnswerBuffer("", MimeType_PlainText);
@@ -3843,6 +4141,12 @@
       Register("/" + resourceTypes[i] + "/{id}/metadata/{name}", GetMetadata);
       Register("/" + resourceTypes[i] + "/{id}/metadata/{name}", SetMetadata);
 
+      // 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);
+
       Register("/" + resourceTypes[i] + "/{id}/attachments", ListAttachments);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", DeleteAttachment);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", GetAttachmentOperations);
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -50,14 +51,23 @@
 
   static void GetMainDicomTagsConfiguration(Json::Value& result)
   {
-      Json::Value v;
-      
       result["Patient"] = DicomMap::GetMainDicomTagsSignature(ResourceType_Patient);
       result["Study"] = DicomMap::GetMainDicomTagsSignature(ResourceType_Study);
       result["Series"] = DicomMap::GetMainDicomTagsSignature(ResourceType_Series);
       result["Instance"] = DicomMap::GetMainDicomTagsSignature(ResourceType_Instance);
   }
 
+  static void GetUserMetadataConfiguration(Json::Value& result)
+  {
+    std::map<std::string, int> userMetadata;
+    Orthanc::GetRegisteredUserMetadata(userMetadata);
+
+    for (std::map<std::string, int>::const_iterator it = userMetadata.begin(); it != userMetadata.end(); ++it)
+    {
+      result[it->first] = it->second;
+    }
+  }
+
   static void GetSystemInformation(RestApiGetCall& call)
   {
     static const char* const API_VERSION = "ApiVersion";
@@ -77,7 +87,12 @@
     static const char* const STORAGE_COMPRESSION = "StorageCompression";
     static const char* const OVERWRITE_INSTANCES = "OverwriteInstances";
     static const char* const INGEST_TRANSCODING = "IngestTranscoding";
-    
+    static const char* const MAXIMUM_STORAGE_SIZE = "MaximumStorageSize";
+    static const char* const MAXIMUM_PATIENT_COUNT = "MaximumPatientCount";
+    static const char* const MAXIMUM_STORAGE_MODE = "MaximumStorageMode";
+    static const char* const USER_METADATA = "UserMetadata";
+    static const char* const HAS_LABELS = "HasLabels";
+
     if (call.IsDocumentation())
     {
       call.GetDocumentation()
@@ -87,11 +102,11 @@
         .SetAnswerField(API_VERSION, RestApiCallDocumentation::Type_Number, "Version of the REST API")
         .SetAnswerField(VERSION, RestApiCallDocumentation::Type_String, "Version of Orthanc")
         .SetAnswerField(DATABASE_VERSION, RestApiCallDocumentation::Type_Number,
-                        "Version of the database: https://book.orthanc-server.com/developers/db-versioning.html")
+                        "Version of the database: https://orthanc.uclouvain.be/book/developers/db-versioning.html")
         .SetAnswerField(DATABASE_SERVER_IDENTIFIER, RestApiCallDocumentation::Type_String,
                         "ID of the server in the database (when running multiple Orthanc on the same DB)")
         .SetAnswerField(IS_HTTP_SERVER_SECURE, RestApiCallDocumentation::Type_Boolean,
-                        "Whether the REST API is properly secured (assuming no reverse proxy is in use): https://book.orthanc-server.com/faq/security.html#securing-the-http-server")
+                        "Whether the REST API is properly secured (assuming no reverse proxy is in use): https://orthanc.uclouvain.be/book/faq/security.html#securing-the-http-server")
         .SetAnswerField(STORAGE_AREA_PLUGIN, RestApiCallDocumentation::Type_String,
                         "Information about the installed storage area plugin (`null` if no such plugin is installed)")
         .SetAnswerField(DATABASE_BACKEND_PLUGIN, RestApiCallDocumentation::Type_String,
@@ -113,7 +128,17 @@
                         "Whether instances are overwritten when re-ingested (new in Orthanc 1.11.0)")
         .SetAnswerField(INGEST_TRANSCODING, RestApiCallDocumentation::Type_String,
                         "Whether instances are transcoded when ingested into Orthanc (`""` if no transcoding is performed) (new in Orthanc 1.11.0)")
-        .SetHttpGetSample("https://demo.orthanc-server.com/system", true);
+        .SetAnswerField(MAXIMUM_STORAGE_SIZE, RestApiCallDocumentation::Type_Number,
+                        "The configured MaximumStorageSize in MB (new in Orthanc 1.11.3)")
+        .SetAnswerField(MAXIMUM_PATIENT_COUNT, RestApiCallDocumentation::Type_Number,
+                        "The configured MaximumPatientCount (new in Orthanc 1.12.4)")
+        .SetAnswerField(MAXIMUM_STORAGE_MODE, RestApiCallDocumentation::Type_String,
+                        "The configured MaximumStorageMode (new in Orthanc 1.11.3)")
+        .SetAnswerField(USER_METADATA, RestApiCallDocumentation::Type_JsonObject,
+                        "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)")
+        .SetHttpGetSample("https://orthanc.uclouvain.be/demo/system", true);
       return;
     }
 
@@ -137,6 +162,9 @@
       result[OVERWRITE_INSTANCES] = lock.GetConfiguration().GetBooleanParameter(OVERWRITE_INSTANCES, false); // New in Orthanc 1.11.0
       result[INGEST_TRANSCODING] = lock.GetConfiguration().GetStringParameter(INGEST_TRANSCODING, ""); // New in Orthanc 1.11.0
       result[DATABASE_SERVER_IDENTIFIER] = lock.GetConfiguration().GetDatabaseServerIdentifier();
+      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[STORAGE_AREA_PLUGIN] = Json::nullValue;
@@ -164,6 +192,11 @@
     result[MAIN_DICOM_TAGS] = Json::objectValue;
     GetMainDicomTagsConfiguration(result[MAIN_DICOM_TAGS]);
 
+    result[USER_METADATA] = Json::objectValue;
+    GetUserMetadataConfiguration(result[USER_METADATA]);
+
+    result[HAS_LABELS] = OrthancRestApi::GetIndex(call).HasLabelsSupport();
+    
     call.GetOutput().AnswerJson(result);
   }
 
@@ -183,7 +216,7 @@
         .SetAnswerField("TotalDiskSizeMB", RestApiCallDocumentation::Type_Number, "Size of the storage area (in megabytes)")
         .SetAnswerField("TotalUncompressedSize", RestApiCallDocumentation::Type_String, "Total size of all the files once uncompressed (in bytes). This corresponds to `TotalDiskSize` if no compression is enabled, cf. `StorageCompression` configuration option")
         .SetAnswerField("TotalUncompressedSizeMB", RestApiCallDocumentation::Type_Number, "Total size of all the files once uncompressed (in megabytes)")
-        .SetHttpGetSample("https://demo.orthanc-server.com/statistics", true);
+        .SetHttpGetSample("https://orthanc.uclouvain.be/demo/statistics", true);
       return;
     }
 
@@ -237,6 +270,10 @@
     {
       call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance), MimeType_PlainText);
     }
+    else
+    {
+      LOG(ERROR) << "No 'level' or invalid GET argument specified in /tools/generate-uid";
+    }
   }
 
   static void ExecuteScript(RestApiPostCall& call)
@@ -247,7 +284,8 @@
         .SetTag("System")
         .SetSummary("Execute Lua script")
         .SetDescription("Execute the provided Lua script by the Orthanc server. This is very insecure for "
-                        "Orthanc servers that are remotely accessible, cf. configuration option `ExecuteLuaEnabled`")
+                        "Orthanc servers that are remotely accessible.  Since Orthanc 1.5.8, this route "
+                        "is disabled by default and can be enabled thanks to the `ExecuteLuaEnabled` configuration.")
         .AddRequestType(MimeType_PlainText, "The Lua script to be executed")
         .AddAnswerType(MimeType_PlainText, "Output of the Lua script");
       return;
@@ -258,7 +296,7 @@
     if (!context.IsExecuteLuaEnabled())
     {
       LOG(ERROR) << "The URI /tools/execute-script is disallowed for security, "
-                 << "check your configuration file";
+                 << "check your configuration option `ExecuteLuaEnabled`";
       call.GetOutput().SignalError(HttpStatus_403_Forbidden);
       return;
     }
@@ -285,7 +323,7 @@
         .SetTag("System")
         .SetSummary("Get " + type + " time")
         .AddAnswerType(MimeType_PlainText, "The " + type + " time")
-        .SetHttpGetSample("https://demo.orthanc-server.com" + call.FlattenUri(), false);
+        .SetHttpGetSample("https://orthanc.uclouvain.be/demo" + call.FlattenUri(), false);
       return;
     }
 
@@ -475,7 +513,7 @@
         .SetSummary("List plugins")
         .SetDescription("List all the installed plugins")
         .AddAnswerType(MimeType_Json, "JSON array containing the identifiers of the installed plugins")
-        .SetHttpGetSample("https://demo.orthanc-server.com/plugins", true);
+        .SetHttpGetSample("https://orthanc.uclouvain.be/demo/plugins", true);
       return;
     }
 
@@ -511,7 +549,7 @@
         .SetDescription("Get system information about the plugin whose identifier is provided in the URL")
         .SetUriArgument("id", "Identifier of the job of interest")
         .AddAnswerType(MimeType_Json, "JSON object containing information about the plugin")
-        .SetHttpGetSample("https://demo.orthanc-server.com/plugins/dicom-web", true);
+        .SetHttpGetSample("https://orthanc.uclouvain.be/demo/plugins/dicom-web", true);
       return;
     }
 
@@ -620,11 +658,11 @@
                             "If present, retrieve detailed information about the individual jobs", false)
         .AddAnswerType(MimeType_Json, "JSON array containing either the jobs identifiers, or detailed information "
                        "about the reported jobs (if `expand` argument is provided)")
-        .SetTruncatedJsonHttpGetSample("https://demo.orthanc-server.com/jobs", 3);
+        .SetTruncatedJsonHttpGetSample("https://orthanc.uclouvain.be/demo/jobs", 3);
       return;
     }
 
-    bool expand = call.HasArgument("expand");
+    bool expand = call.HasArgument("expand") && call.GetBooleanArgument("expand", true);
 
     Json::Value v = Json::arrayValue;
 
@@ -678,7 +716,7 @@
         .SetTag("Jobs")
         .SetSummary("Get job")
         .SetDescription("Retrieve detailed information about the job whose identifier is provided in the URL: "
-                        "https://book.orthanc-server.com/users/advanced-rest.html#jobs")
+                        "https://orthanc.uclouvain.be/book/users/advanced-rest.html#jobs")
         .SetUriArgument("id", "Identifier of the job of interest")
         .AddAnswerType(MimeType_Json, "JSON object detailing the job")
         .SetSample(sample);
@@ -696,6 +734,34 @@
     }
   }
 
+  static void DeleteJobInfo(RestApiDeleteCall& call)
+  {
+    if (call.IsDocumentation())
+    {
+      call.GetDocumentation()
+        .SetTag("Jobs")
+        .SetSummary("Delete a job from history")
+        .SetDescription("Delete the job from the jobs history.  Only a completed job can be deleted. "
+                        "If the job has not run or not completed yet, you must cancel it first. "
+                        "If the job has outputs, all outputs will be deleted as well. ")
+        .SetUriArgument("id", "Identifier of the job of interest");
+      return;
+    }
+
+    std::string job = call.GetUriComponent("id", "");
+
+    if (OrthancRestApi::GetContext(call).GetJobsEngine().
+        GetRegistry().DeleteJobInfo(job))
+    {
+      call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InexistentItem,
+                             "No job found with this id: " + job);
+    }
+  }
+
 
   static void GetJobOutput(RestApiGetCall& call)
   {
@@ -737,6 +803,36 @@
   }
 
 
+  static void DeleteJobOutput(RestApiDeleteCall& call)
+  {
+    if (call.IsDocumentation())
+    {
+      call.GetDocumentation()
+        .SetTag("Jobs")
+        .SetSummary("Delete a job output")
+        .SetDescription("Delete the output produced by a job. As of Orthanc 1.12.1, only the jobs that generate a "
+                        "DICOMDIR media or a ZIP archive provide such an output (with `key` equals to `archive`).")
+        .SetUriArgument("id", "Identifier of the job of interest")
+        .SetUriArgument("key", "Name of the output of interest");
+      return;
+    }
+
+    std::string job = call.GetUriComponent("id", "");
+    std::string key = call.GetUriComponent("key", "");
+
+    if (OrthancRestApi::GetContext(call).GetJobsEngine().
+        GetRegistry().DeleteJobOutput(job, key))
+    {
+      call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InexistentItem,
+                             "Job has no such output: " + key);
+    }
+  }
+
+
   enum JobAction
   {
     JobAction_Cancel,
@@ -778,7 +874,7 @@
         .SetSummary(verb + " job")
         .SetDescription(verb + " the job whose identifier is provided in the URL. Check out the "
                         "Orthanc Book for more information about the state machine applicable to jobs: "
-                        "https://book.orthanc-server.com/users/advanced-rest.html#jobs")
+                        "https://orthanc.uclouvain.be/book/users/advanced-rest.html#jobs")
         .SetUriArgument("id", "Identifier of the job of interest")
         .AddAnswerType(MimeType_Json, "Empty JSON object in the case of a success");
       return;
@@ -825,8 +921,8 @@
         .SetTag("System")
         .SetSummary("Get usage metrics")
         .SetDescription("Get usage metrics of Orthanc in the Prometheus file format (OpenMetrics): "
-                        "https://book.orthanc-server.com/users/advanced-rest.html#instrumentation-with-prometheus")
-        .SetHttpGetSample("https://demo.orthanc-server.com/tools/metrics-prometheus", false);
+                        "https://orthanc.uclouvain.be/book/users/advanced-rest.html#instrumentation-with-prometheus")
+        .SetHttpGetSample("https://orthanc.uclouvain.be/demo/tools/metrics-prometheus", false);
       return;
     }
 
@@ -845,19 +941,27 @@
     unsigned int jobsPending, jobsRunning, jobsSuccess, jobsFailed;
     context.GetJobsEngine().GetRegistry().GetStatistics(jobsPending, jobsRunning, jobsSuccess, jobsFailed);
 
+    int64_t serverUpTime = context.GetServerUpTime();
+    Json::Value lastChange;
+    context.GetIndex().GetLastChange(lastChange);
+
     MetricsRegistry& registry = context.GetMetricsRegistry();
-    registry.SetValue("orthanc_disk_size_mb", static_cast<float>(diskSize) / MEGA_BYTES);
-    registry.SetValue("orthanc_uncompressed_size_mb", static_cast<float>(diskSize) / MEGA_BYTES);
-    registry.SetValue("orthanc_count_patients", static_cast<unsigned int>(countPatients));
-    registry.SetValue("orthanc_count_studies", static_cast<unsigned int>(countStudies));
-    registry.SetValue("orthanc_count_series", static_cast<unsigned int>(countSeries));
-    registry.SetValue("orthanc_count_instances", static_cast<unsigned int>(countInstances));
-    registry.SetValue("orthanc_jobs_pending", jobsPending);
-    registry.SetValue("orthanc_jobs_running", jobsRunning);
-    registry.SetValue("orthanc_jobs_completed", jobsSuccess + jobsFailed);
-    registry.SetValue("orthanc_jobs_success", jobsSuccess);
-    registry.SetValue("orthanc_jobs_failed", jobsFailed);
-    
+    registry.SetFloatValue("orthanc_disk_size_mb", static_cast<float>(diskSize) / MEGA_BYTES);
+    registry.SetFloatValue("orthanc_uncompressed_size_mb", static_cast<float>(diskSize) / MEGA_BYTES);
+    registry.SetIntegerValue("orthanc_count_patients", static_cast<int64_t>(countPatients));
+    registry.SetIntegerValue("orthanc_count_studies", static_cast<int64_t>(countStudies));
+    registry.SetIntegerValue("orthanc_count_series", static_cast<int64_t>(countSeries));
+    registry.SetIntegerValue("orthanc_count_instances", static_cast<int64_t>(countInstances));
+    registry.SetIntegerValue("orthanc_jobs_pending", jobsPending);
+    registry.SetIntegerValue("orthanc_jobs_running", jobsRunning);
+    registry.SetIntegerValue("orthanc_jobs_completed", jobsSuccess + jobsFailed);
+    registry.SetIntegerValue("orthanc_jobs_success", jobsSuccess);
+    registry.SetIntegerValue("orthanc_jobs_failed", jobsFailed);
+    registry.SetIntegerValue("orthanc_up_time_s", serverUpTime);
+    registry.SetIntegerValue("orthanc_last_change", lastChange["Last"].asInt64());
+
+    context.PublishCacheMetrics();
+
     std::string s;
     registry.ExportPrometheusText(s);
 
@@ -1008,6 +1112,31 @@
   }
 
 
+  static void ListAllLabels(RestApiGetCall& call)
+  {
+    if (call.IsDocumentation())
+    {
+      call.GetDocumentation()
+        .SetTag("System")
+        .SetSummary("Get all the used labels")
+        .SetDescription("List all the labels that are associated with any resource of the Orthanc database")
+        .AddAnswerType(MimeType_Json, "JSON array containing the labels");
+      return;
+    }
+
+    std::set<std::string> labels;
+    OrthancRestApi::GetIndex(call).ListAllLabels(labels);
+
+    Json::Value json = Json::arrayValue;
+    for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
+    {
+      json.append(*it);
+    }
+    
+    call.GetOutput().AnswerJson(json);
+   }
+
+
   void OrthancRestApi::RegisterSystem(bool orthancExplorerEnabled)
   {
     if (orthancExplorerEnabled)
@@ -1044,16 +1173,20 @@
 
     Register("/jobs", ListJobs);
     Register("/jobs/{id}", GetJobInfo);
+    Register("/jobs/{id}", DeleteJobInfo);
     Register("/jobs/{id}/cancel", ApplyJobAction<JobAction_Cancel>);
     Register("/jobs/{id}/pause", ApplyJobAction<JobAction_Pause>);
     Register("/jobs/{id}/resubmit", ApplyJobAction<JobAction_Resubmit>);
     Register("/jobs/{id}/resume", ApplyJobAction<JobAction_Resume>);
     Register("/jobs/{id}/{key}", GetJobOutput);
+    Register("/jobs/{id}/{key}", DeleteJobOutput);
 
     // New in Orthanc 1.9.0
     Register("/tools/accepted-transfer-syntaxes", GetAcceptedTransferSyntaxes);
     Register("/tools/accepted-transfer-syntaxes", SetAcceptedTransferSyntaxes);
     Register("/tools/unknown-sop-class-accepted", GetUnknownSopClassAccepted);
     Register("/tools/unknown-sop-class-accepted", SetUnknownSopClassAccepted);
+
+    Register("/tools/labels", ListAllLabels);  // New in Orthanc 1.12.0
   }
 }
--- a/OrthancServer/Sources/OrthancWebDav.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/OrthancWebDav.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -1221,6 +1222,8 @@
 
   void OrthancWebDav::UploadWorker(OrthancWebDav* that)
   {
+    Logging::SetCurrentThreadName("WEBDAV-UPLOAD");
+
     assert(that != NULL);
 
     boost::posix_time::ptime lastModification = GetNow();
--- a/OrthancServer/Sources/OrthancWebDav.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/OrthancWebDav.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/PrecompiledHeadersServer.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/PrecompiledHeadersServer.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/PrecompiledHeadersServer.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/PrecompiledHeadersServer.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/QueryRetrieveHandler.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/QueryRetrieveHandler.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/QueryRetrieveHandler.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/QueryRetrieveHandler.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/Search/DatabaseConstraint.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Search/DatabaseConstraint.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -244,4 +245,43 @@
     constraint.values = (tmpValues.empty() ? NULL : &tmpValues[0]);
   }
 #endif    
+
+
+  void DatabaseConstraints::Clear()
+  {
+    for (size_t i = 0; i < constraints_.size(); i++)
+    {
+      assert(constraints_[i] != NULL);
+      delete constraints_[i];
+    }
+
+    constraints_.clear();
+  }
+
+
+  void DatabaseConstraints::AddConstraint(DatabaseConstraint* constraint)
+  {
+    if (constraint == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+      constraints_.push_back(constraint);
+    }
+  }
+
+
+  const DatabaseConstraint& DatabaseConstraints::GetConstraint(size_t index) const
+  {
+    if (index >= constraints_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(constraints_[index] != NULL);
+      return *constraints_[index];
+    }
+  }
 }
--- a/OrthancServer/Sources/Search/DatabaseConstraint.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Search/DatabaseConstraint.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -45,6 +46,8 @@
 #  endif
 #endif
 
+#include <deque>
+
 namespace Orthanc
 {
   enum ConstraintType
@@ -77,7 +80,7 @@
 
 
   // This class is also used by the "orthanc-databases" project
-  class DatabaseConstraint
+  class DatabaseConstraint : public boost::noncopyable
   {
   private:
     ResourceType              level_;
@@ -147,4 +150,33 @@
                           std::vector<const char*>& tmpValues) const;
 #endif    
   };
+
+
+  class DatabaseConstraints : public boost::noncopyable
+  {
+  private:
+    std::deque<DatabaseConstraint*>  constraints_;
+
+  public:
+    ~DatabaseConstraints()
+    {
+      Clear();
+    }
+
+    void Clear();
+
+    void AddConstraint(DatabaseConstraint* constraint);  // Takes ownership
+
+    bool IsEmpty() const
+    {
+      return constraints_.empty();
+    }
+
+    size_t GetSize() const
+    {
+      return constraints_.size();
+    }
+
+    const DatabaseConstraint& GetConstraint(size_t index) const;
+  };
 }
--- a/OrthancServer/Sources/Search/DatabaseLookup.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Search/DatabaseLookup.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -281,7 +282,8 @@
 
   bool DatabaseLookup::HasOnlyMainDicomTags() const
   {
-    const std::set<DicomTag>& allMainTags = DicomMap::GetAllMainDicomTags();
+    std::set<DicomTag> allMainTags;
+    DicomMap::GetAllMainDicomTags(allMainTags);
 
     for (size_t i = 0; i < constraints_.size(); i++)
     {
--- a/OrthancServer/Sources/Search/DatabaseLookup.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Search/DatabaseLookup.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -42,10 +43,6 @@
     void AddConstraintInternal(DicomTagConstraint* constraint);  // Takes ownership
 
   public:
-    DatabaseLookup()
-    {
-    }
-
     ~DatabaseLookup();
 
     DatabaseLookup* Clone() const;
--- a/OrthancServer/Sources/Search/DicomTagConstraint.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Search/DicomTagConstraint.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -341,8 +342,8 @@
   }
 
 
-  DatabaseConstraint DicomTagConstraint::ConvertToDatabaseConstraint(ResourceType level,
-                                                                     DicomTagType tagType) const
+  DatabaseConstraint* DicomTagConstraint::ConvertToDatabaseConstraint(ResourceType level,
+                                                                      DicomTagType tagType) const
   {
     bool isIdentifier, caseSensitive;
     
@@ -378,7 +379,7 @@
       }
     }
 
-    return DatabaseConstraint(level, tag_, isIdentifier, constraintType_,
-                              values, caseSensitive, mandatory_);
+    return new DatabaseConstraint(level, tag_, isIdentifier, constraintType_,
+                                  values, caseSensitive, mandatory_);
   }  
 }
--- a/OrthancServer/Sources/Search/DicomTagConstraint.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Search/DicomTagConstraint.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -108,7 +109,7 @@
 
     std::string Format() const;
 
-    DatabaseConstraint ConvertToDatabaseConstraint(ResourceType level,
-                                                   DicomTagType tagType) const;
+    DatabaseConstraint* ConvertToDatabaseConstraint(ResourceType level,
+                                                    DicomTagType tagType) const;
   };
 }
--- a/OrthancServer/Sources/Search/HierarchicalMatcher.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Search/HierarchicalMatcher.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -248,6 +249,8 @@
   {
     std::unique_ptr<DcmDataset> target(new DcmDataset);
 
+    std::string currentPrivateCreator = "";
+
     for (std::set<DicomTag>::const_iterator it = flatTags_.begin();
          it != flatTags_.end(); ++it)
     {
@@ -257,13 +260,19 @@
       if (source.findAndGetElement(tag, element).good() &&
           element != NULL)
       {
-        if (it->IsPrivate())
+        if (tag.isPrivateReservation())
         {
-          throw OrthancException(ErrorCode_NotImplemented,
-                                 "Not applicable to private tags: " + it->Format());
+          OFString privateCreator;
+          element->getOFString(privateCreator, 0, false);
+          currentPrivateCreator = privateCreator.c_str();
         }
-        
-        std::unique_ptr<DcmElement> cloned(FromDcmtkBridge::CreateElementForTag(*it, "" /* no private creator */));
+        else if (!it->IsPrivate())
+        {
+          // reset the private creator as soon as we reach the end of the current private block
+          currentPrivateCreator = "";
+        }
+
+        std::unique_ptr<DcmElement> cloned(FromDcmtkBridge::CreateElementForTag(*it, currentPrivateCreator.c_str()));
         cloned->copyFrom(*element);
         target->insert(cloned.release());
       }
--- a/OrthancServer/Sources/Search/HierarchicalMatcher.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Search/HierarchicalMatcher.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -32,13 +33,17 @@
 
 #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 <cassert>
 #include <boost/lexical_cast.hpp>
+#include <list>
 
 
 namespace Orthanc
@@ -268,24 +273,223 @@
                " AND " + tag + ".tagElement = " +
                boost::lexical_cast<std::string>(constraint.GetTag().GetElement()));
   }
-  
+
+
+  static std::string Join(const std::list<std::string>& values,
+                          const std::string& prefix,
+                          const std::string& separator)
+  {
+    if (values.empty())
+    {
+      return "";
+    }
+    else
+    {
+      std::string s = prefix;
+
+      bool first = true;
+      for (std::list<std::string>::const_iterator it = values.begin(); it != values.end(); ++it)
+      {
+        if (first)
+        {
+          first = false;
+        }
+        else
+        {
+          s += separator;
+        }
+
+        s += *it;
+      }
+
+      return s;
+    }
+  }
+
+  static bool FormatComparison2(std::string& target,
+                                ISqlLookupFormatter& formatter,
+                                const DatabaseConstraint& constraint,
+                                bool escapeBrackets)
+  {
+    std::string comparison;
+    std::string tagFilter = ("tagGroup = " + boost::lexical_cast<std::string>(constraint.GetTag().GetGroup())
+                              + " AND tagElement = " + boost::lexical_cast<std::string>(constraint.GetTag().GetElement()));
+
+    switch (constraint.GetConstraintType())
+    {
+      case ConstraintType_Equal:
+      case ConstraintType_SmallerOrEqual:
+      case ConstraintType_GreaterOrEqual:
+      {
+        std::string op;
+        switch (constraint.GetConstraintType())
+        {
+          case ConstraintType_Equal:
+            op = "=";
+            break;
+          
+          case ConstraintType_SmallerOrEqual:
+            op = "<=";
+            break;
+          
+          case ConstraintType_GreaterOrEqual:
+            op = ">=";
+            break;
+          
+          default:
+            throw OrthancException(ErrorCode_InternalError);
+        }
+
+        std::string parameter = formatter.GenerateParameter(constraint.GetSingleValue());
+
+        if (constraint.IsCaseSensitive())
+        {
+          comparison = " AND value " + op + " " + parameter;
+        }
+        else
+        {
+          comparison = " AND lower(value) " + op + " lower(" + parameter + ")";
+        }
+
+        break;
+      }
+
+      case ConstraintType_List:
+      {
+        std::vector<std::string> comparisonValues;
+        for (size_t i = 0; i < constraint.GetValuesCount(); i++)
+        {
+          std::string parameter = formatter.GenerateParameter(constraint.GetValue(i));
+
+          if (constraint.IsCaseSensitive())
+          {
+            comparisonValues.push_back(parameter);
+          }
+          else
+          {
+            comparisonValues.push_back("lower(" + parameter + ")");
+          }
+        }
+
+        std::string values;
+        Toolbox::JoinStrings(values, comparisonValues, ", ");
 
-  void ISqlLookupFormatter::Apply(std::string& sql,
-                                  ISqlLookupFormatter& formatter,
-                                  const std::vector<DatabaseConstraint>& lookup,
-                                  ResourceType queryLevel,
-                                  size_t limit)
+        if (constraint.IsCaseSensitive())
+        {
+          comparison = " AND value IN (" + values + ")";
+        }
+        else
+        {
+          comparison = " AND lower(value) IN (" + values + ")";
+        }
+            
+        break;
+      }
+
+      case ConstraintType_Wildcard:
+      {
+        const std::string value = constraint.GetSingleValue();
+
+        if (value == "*")
+        {
+          if (!constraint.IsMandatory())
+          {
+            // Universal constraint on an optional tag, ignore it
+            return false;
+          }
+        }
+        else
+        {
+          std::string escaped;
+          escaped.reserve(value.size());
+
+          for (size_t i = 0; i < value.size(); i++)
+          {
+            if (value[i] == '*')
+            {
+              escaped += "%";
+            }
+            else if (value[i] == '?')
+            {
+              escaped += "_";
+            }
+            else if (value[i] == '%')
+            {
+              escaped += "\\%";
+            }
+            else if (value[i] == '_')
+            {
+              escaped += "\\_";
+            }
+            else if (value[i] == '\\')
+            {
+              escaped += "\\\\";
+            }
+            else if (escapeBrackets && value[i] == '[')
+            {
+              escaped += "\\[";
+            }
+            else if (escapeBrackets && value[i] == ']')
+            {
+              escaped += "\\]";
+            }
+            else
+            {
+              escaped += value[i];
+            }               
+          }
+
+          std::string parameter = formatter.GenerateParameter(escaped);
+
+          if (constraint.IsCaseSensitive())
+          {
+            comparison = " AND value LIKE " + parameter + " " + formatter.FormatWildcardEscape();
+          }
+          else
+          {
+            comparison = " AND lower(value) LIKE lower(" + parameter + ") " + formatter.FormatWildcardEscape();
+          }
+        }
+          
+        break;
+      }
+
+      default:
+        return false;
+    }
+
+    if (constraint.IsMandatory())
+    {
+      target = tagFilter + comparison;
+    }
+    else if (comparison.empty())
+    {
+      target = tagFilter + " AND value IS NULL";
+    }
+    else
+    {
+      target = tagFilter + " AND value IS NULL OR " + comparison;
+    }
+
+    return true;
+  }
+
+
+  void ISqlLookupFormatter::GetLookupLevels(ResourceType& lowerLevel,
+                                            ResourceType& upperLevel,
+                                            const ResourceType& queryLevel,
+                                            const DatabaseConstraints& lookup)
   {
     assert(ResourceType_Patient < ResourceType_Study &&
            ResourceType_Study < ResourceType_Series &&
            ResourceType_Series < ResourceType_Instance);
     
-    ResourceType upperLevel = queryLevel;
-    ResourceType lowerLevel = queryLevel;
+    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)
       {
@@ -297,7 +501,20 @@
         lowerLevel = level;
       }
     }
-    
+  }
+  
+
+  void ISqlLookupFormatter::Apply(std::string& sql,
+                                  ISqlLookupFormatter& formatter,
+                                  const DatabaseConstraints& lookup,
+                                  ResourceType queryLevel,
+                                  const std::set<std::string>& labels,
+                                  LabelsConstraint labelsConstraint,
+                                  size_t limit)
+  {
+    ResourceType lowerLevel, upperLevel;
+    GetLookupLevels(lowerLevel, upperLevel, queryLevel, lookup);
+
     assert(upperLevel <= queryLevel &&
            queryLevel <= lowerLevel);
 
@@ -307,14 +524,16 @@
 
     size_t count = 0;
     
-    for (size_t i = 0; i < lookup.size(); i++)
+    for (size_t i = 0; i < lookup.GetSize(); i++)
     {
+      const DatabaseConstraint& 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())
@@ -346,13 +565,170 @@
               FormatLevel(static_cast<ResourceType>(level - 1)) + ".internalId=" +
               FormatLevel(static_cast<ResourceType>(level)) + ".parentId");
     }
+
+    std::list<std::string> where;
+    where.push_back(FormatLevel(queryLevel) + ".resourceType = " +
+                    formatter.FormatResourceType(queryLevel) + comparisons);
+
+    if (!labels.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/
+       **/
+
+      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 (labelsConstraint)
+      {
+        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);
+      }
       
-    sql += (joins + " WHERE " + FormatLevel(queryLevel) + ".resourceType = " +
-            formatter.FormatResourceType(queryLevel) + comparisons);
+      where.push_back("(SELECT COUNT(1) FROM Labels AS selectedLabels WHERE selectedLabels.id = " + FormatLevel(queryLevel) +
+                      ".internalId AND selectedLabels.label IN (" + Join(formattedLabels, "", ", ") + ")) " + condition);
+    }
+
+    sql += joins + Join(where, " WHERE ", " AND ");
 
     if (limit != 0)
     {
       sql += " LIMIT " + boost::lexical_cast<std::string>(limit);
     }
   }
+
+
+  void ISqlLookupFormatter::ApplySingleLevel(std::string& sql,
+                                             ISqlLookupFormatter& formatter,
+                                             const DatabaseConstraints& lookup,
+                                             ResourceType queryLevel,
+                                             const std::set<std::string>& labels,
+                                             LabelsConstraint labelsConstraint,
+                                             size_t limit
+                                             )
+  {
+    ResourceType lowerLevel, upperLevel;
+    GetLookupLevels(lowerLevel, upperLevel, queryLevel, lookup);
+    
+    assert(upperLevel == queryLevel &&
+           queryLevel == lowerLevel);
+
+    const bool escapeBrackets = formatter.IsEscapeBrackets();
+    
+    std::vector<std::string> mainDicomTagsComparisons, dicomIdentifiersComparisons;
+
+    for (size_t i = 0; i < lookup.GetSize(); i++)
+    {
+      const DatabaseConstraint& constraint = lookup.GetConstraint(i);
+
+      std::string comparison;
+      
+      if (FormatComparison2(comparison, formatter, constraint, escapeBrackets))
+      {
+        if (!comparison.empty())
+        {
+          if (constraint.IsIdentifier())
+          {
+            dicomIdentifiersComparisons.push_back(comparison);
+          }
+          else
+          {
+            mainDicomTagsComparisons.push_back(comparison);
+          }
+        }
+      }
+    }
+
+    sql = ("SELECT publicId, internalId "
+           "FROM Resources "
+           "WHERE resourceType = " + formatter.FormatResourceType(queryLevel) 
+            + " ");
+
+    if (dicomIdentifiersComparisons.size() > 0)
+    {
+      for (std::vector<std::string>::const_iterator it = dicomIdentifiersComparisons.begin(); it < dicomIdentifiersComparisons.end(); ++it)
+      {
+        sql += (" AND internalId IN (SELECT id FROM DicomIdentifiers WHERE " + *it + ") ");
+      }
+    }
+
+    if (mainDicomTagsComparisons.size() > 0)
+    {
+      for (std::vector<std::string>::const_iterator it = mainDicomTagsComparisons.begin(); it < mainDicomTagsComparisons.end(); ++it)
+      {
+        sql += (" AND internalId IN (SELECT id FROM MainDicomTags WHERE " + *it + ") ");
+      }
+    }
+
+    if (!labels.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/
+       **/
+
+      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;
+      std::string inOrNotIn;
+      switch (labelsConstraint)
+      {
+        case LabelsConstraint_Any:
+          condition = "> 0";
+          inOrNotIn = "IN";
+          break;
+          
+        case LabelsConstraint_All:
+          condition = "= " + boost::lexical_cast<std::string>(labels.size());
+          inOrNotIn = "IN";
+          break;
+          
+        case LabelsConstraint_None:
+          condition = "> 0";
+          inOrNotIn = "NOT IN";
+          break;
+          
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+      
+      sql += (" AND internalId " + inOrNotIn + " (SELECT id"
+                                 " FROM (SELECT id, COUNT(1) AS labelsCount "
+                                        "FROM Labels "
+                                        "WHERE label IN (" + Join(formattedLabels, "", ", ") + ") GROUP BY id"
+                                        ") AS temp "
+                                 " WHERE labelsCount " + condition + ")");
+    }
+
+    if (limit != 0)
+    {
+      sql += " LIMIT " + boost::lexical_cast<std::string>(limit);
+    }
+  }
+
 }
--- a/OrthancServer/Sources/Search/ISqlLookupFormatter.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -33,8 +34,15 @@
 
 namespace Orthanc
 {
-  class DatabaseConstraint;
+  class DatabaseConstraints;
   
+  enum LabelsConstraint
+  {
+    LabelsConstraint_All,
+    LabelsConstraint_Any,
+    LabelsConstraint_None
+  };
+
   // This class is also used by the "orthanc-databases" project
   class ISqlLookupFormatter : public boost::noncopyable
   {
@@ -52,14 +60,29 @@
     /**
      * Whether to escape '[' and ']', which is only needed for
      * MSSQL. New in Orthanc 1.10.0, from the following changeset:
-     * https://hg.orthanc-server.com/orthanc-databases/rev/389c037387ea
+     * https://orthanc.uclouvain.be/hg/orthanc-databases/rev/389c037387ea
      **/
     virtual bool IsEscapeBrackets() const = 0;
 
+    static void GetLookupLevels(ResourceType& lowerLevel,
+                                ResourceType& upperLevel,
+                                const ResourceType& queryLevel,
+                                const DatabaseConstraints& lookup);
+
     static void Apply(std::string& sql,
                       ISqlLookupFormatter& formatter,
-                      const std::vector<DatabaseConstraint>& lookup,
+                      const DatabaseConstraints& 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 ApplySingleLevel(std::string& sql,
+                                 ISqlLookupFormatter& formatter,
+                                 const DatabaseConstraints& 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);
   };
 }
--- a/OrthancServer/Sources/ServerContext.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -49,6 +50,9 @@
 #include <dcmtk/dcmdata/dcfilefo.h>
 #include <dcmtk/dcmnet/dimse.h>
 
+#if HAVE_MALLOC_TRIM == 1
+#  include <malloc.h>
+#endif
 
 static size_t DICOM_CACHE_SIZE = 128 * 1024 * 1024;  // 128 MB
 
@@ -104,10 +108,43 @@
   {
   }
 
+
+#if HAVE_MALLOC_TRIM == 1
+  void ServerContext::MemoryTrimmingThread(ServerContext* that,
+                                           unsigned int intervalInSeconds)
+  {
+    Logging::SetCurrentThreadName("MEMORY-TRIM");
+
+    boost::posix_time::ptime lastExecution = boost::posix_time::second_clock::universal_time();
+
+    // This thread is started only if malloc_trim is defined
+    while (!that->done_)
+    {
+      boost::posix_time::ptime now = boost::posix_time::second_clock::universal_time();
+      boost::posix_time::time_duration elapsed = now - lastExecution;
+
+      if (elapsed.total_seconds() > intervalInSeconds)
+      {
+        // If possible, gives memory back to the system 
+        // (see OrthancServer/Resources/ImplementationNotes/memory_consumption.txt)
+        {
+          MetricsRegistry::Timer timer(that->GetMetricsRegistry(), "orthanc_memory_trimming_duration_ms");
+          malloc_trim(128*1024);
+        }
+        lastExecution = boost::posix_time::second_clock::universal_time();
+      }
+
+      boost::this_thread::sleep(boost::posix_time::milliseconds(100));
+    }
+  }
+#endif
+
   
   void ServerContext::ChangeThread(ServerContext* that,
                                    unsigned int sleepDelay)
   {
+    Logging::SetCurrentThreadName("CHANGES");
+
     while (!that->done_)
     {
       std::unique_ptr<IDynamicObject> obj(that->pendingChanges_.Dequeue(sleepDelay));
@@ -132,7 +169,7 @@
             }
             catch (...)
             {
-              throw OrthancException(ErrorCode_InternalError);
+              throw OrthancException(ErrorCode_InternalError, "Error while signaling a change");
             }
           }
           catch (OrthancException& e)
@@ -147,9 +184,55 @@
   }
 
 
+  void ServerContext::JobEventsThread(ServerContext* that,
+                                      unsigned int sleepDelay)
+  {
+    Logging::SetCurrentThreadName("JOB-EVENTS");
+
+    while (!that->done_)
+    {
+      std::unique_ptr<IDynamicObject> obj(that->pendingJobEvents_.Dequeue(sleepDelay));
+        
+      if (obj.get() != NULL)
+      {
+        const JobEvent& event = dynamic_cast<const JobEvent&>(*obj.get());
+
+        boost::shared_lock<boost::shared_mutex> lock(that->listenersMutex_);
+        for (ServerListeners::iterator it = that->listeners_.begin(); 
+             it != that->listeners_.end(); ++it)
+        {
+          try
+          {
+            try
+            {
+              it->GetListener().SignalJobEvent(event);
+            }
+            catch (std::bad_alloc&)
+            {
+              LOG(ERROR) << "Not enough memory while signaling a job event";
+            }
+            catch (...)
+            {
+              throw OrthancException(ErrorCode_InternalError, "Error while signaling a job event");
+            }
+          }
+          catch (OrthancException& e)
+          {
+            LOG(ERROR) << "Error in the " << it->GetDescription() 
+                       << " callback while signaling a job event: " << e.What()
+                       << " (code " << e.GetErrorCode() << ")";
+          }
+        }
+      }
+    }
+  }
+
+
   void ServerContext::SaveJobsThread(ServerContext* that,
                                      unsigned int sleepDelay)
   {
+    Logging::SetCurrentThreadName("SAVE-JOBS");
+
     static const boost::posix_time::time_duration PERIODICITY =
       boost::posix_time::seconds(10);
     
@@ -174,42 +257,21 @@
   void ServerContext::SignalJobSubmitted(const std::string& jobId)
   {
     haveJobsChanged_ = true;
-    mainLua_.SignalJobSubmitted(jobId);
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-    if (HasPlugins())
-    {
-      GetPlugins().SignalJobSubmitted(jobId);
-    }
-#endif
+    pendingJobEvents_.Enqueue(new JobEvent(JobEventType_Submitted, jobId));
   }
   
 
   void ServerContext::SignalJobSuccess(const std::string& jobId)
   {
     haveJobsChanged_ = true;
-    mainLua_.SignalJobSuccess(jobId);
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-    if (HasPlugins())
-    {
-      GetPlugins().SignalJobSuccess(jobId);
-    }
-#endif
+    pendingJobEvents_.Enqueue(new JobEvent(JobEventType_Success, jobId));
   }
 
   
   void ServerContext::SignalJobFailure(const std::string& jobId)
   {
     haveJobsChanged_ = true;
-    mainLua_.SignalJobFailure(jobId);
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-    if (HasPlugins())
-    {
-      GetPlugins().SignalJobFailure(jobId);
-    }
-#endif
+    pendingJobEvents_.Enqueue(new JobEvent(JobEventType_Failure, jobId));
   }
 
 
@@ -232,6 +294,14 @@
         {
           LOG(WARNING) << "Cannot unserialize the jobs engine, starting anyway: " << e.What();
         }
+        catch (const std::string& s) 
+        {
+          LOG(WARNING) << "Cannot unserialize the jobs engine, starting anyway: \"" << s << "\"";
+        }
+        catch (...)
+        {
+          LOG(WARNING) << "Cannot unserialize the jobs engine, starting anyway";
+        }
       }
       else
       {
@@ -275,12 +345,15 @@
   }
 
 
-  void ServerContext::PublishDicomCacheMetrics()
+  void ServerContext::PublishCacheMetrics()
   {
-    metricsRegistry_->SetValue("orthanc_dicom_cache_size",
-                               static_cast<float>(dicomCache_.GetCurrentSize()) / static_cast<float>(1024 * 1024));
-    metricsRegistry_->SetValue("orthanc_dicom_cache_count",
-                               static_cast<float>(dicomCache_.GetNumberOfItems()));
+    metricsRegistry_->SetFloatValue("orthanc_dicom_cache_size_mb",
+                                    static_cast<float>(dicomCache_.GetCurrentSize()) / static_cast<float>(1024 * 1024));
+    metricsRegistry_->SetIntegerValue("orthanc_dicom_cache_count", dicomCache_.GetNumberOfItems());
+
+    metricsRegistry_->SetFloatValue("orthanc_storage_cache_size_mb",
+                                    static_cast<float>(storageCache_.GetCurrentSize()) / static_cast<float>(1024 * 1024));
+    metricsRegistry_->SetIntegerValue("orthanc_storage_cache_count", storageCache_.GetNumberOfItems());
   }
 
 
@@ -307,13 +380,15 @@
     metricsRegistry_(new MetricsRegistry),
     isHttpServerSecure_(true),
     isExecuteLuaEnabled_(false),
+    isRestApiWriteToFileSystemEnabled_(false),
     overwriteInstances_(false),
     dcmtkTranscoder_(new DcmtkTranscoder),
     isIngestTranscoding_(false),
     ingestTranscodingOfUncompressed_(true),
     ingestTranscodingOfCompressed_(true),
     preferredTransferSyntax_(DicomTransferSyntax_LittleEndianExplicit),
-    deidentifyLogs_(false)
+    deidentifyLogs_(false),
+    serverStartTimeUtc_(boost::posix_time::second_clock::universal_time())
   {
     try
     {
@@ -384,7 +459,7 @@
           CLOG(INFO, DICOM) << "Deidentification of log contents (notably for DIMSE queries) is enabled";
 
           DicomVersion version = StringToDicomVersion(
-              lock.GetConfiguration().GetStringParameter("DeidentifyLogsDicomVersion", "2021b"));
+              lock.GetConfiguration().GetStringParameter("DeidentifyLogsDicomVersion", "2023b"));
           CLOG(INFO, DICOM) << "Version of DICOM standard used for deidentification is "
                             << EnumerationToString(version);
 
@@ -416,7 +491,15 @@
 
       listeners_.push_back(ServerListener(luaListener_, "Lua"));
       changeThread_ = boost::thread(ChangeThread, this, (unitTesting ? 20 : 100));
-    
+      jobEventsThread_ = boost::thread(JobEventsThread, this, (unitTesting ? 20 : 100));
+      
+#if HAVE_MALLOC_TRIM == 1
+      LOG(INFO) << "Starting memory trimming thread at 30 seconds interval";
+      memoryTrimmingThread_ = boost::thread(MemoryTrimmingThread, this, 30);
+#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&)
@@ -454,11 +537,21 @@
         changeThread_.join();
       }
 
+      if (jobEventsThread_.joinable())
+      {
+        jobEventsThread_.join();
+      }
+
       if (saveJobsThread_.joinable())
       {
         saveJobsThread_.join();
       }
 
+      if (memoryTrimmingThread_.joinable())
+      {
+        memoryTrimmingThread_.join();
+      }
+
       jobsEngine_.GetRegistry().ResetObserver();
 
       if (isJobsEngineUnserialized_)
@@ -489,7 +582,7 @@
                                  FileContentType type,
                                  const std::string& customData)
   {
-    StorageAccessor accessor(area_, &storageCache_, GetMetricsRegistry());
+    StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
     accessor.Remove(fileUuid, type, customData);
   }
 
@@ -520,8 +613,9 @@
 
     bool hasPixelDataOffset;
     uint64_t pixelDataOffset;
+    ValueRepresentation pixelDataVR;
     hasPixelDataOffset = DicomStreamReader::LookupPixelDataOffset(
-      pixelDataOffset, dicom.GetBufferData(), dicom.GetBufferSize());
+      pixelDataOffset, pixelDataVR, dicom.GetBufferData(), dicom.GetBufferSize());
 
     DicomTransferSyntax transferSyntax;
     bool hasTransferSyntax = dicom.LookupTransferSyntax(transferSyntax);
@@ -529,12 +623,13 @@
     DicomMap summary;
     dicom.GetSummary(summary);   // -> from Orthanc 1.11.1, this includes the leaf nodes and sequences
 
-    std::set<DicomTag> allMainDicomTags = DicomMap::GetAllMainDicomTags();
+    std::set<DicomTag> allMainDicomTags;
+    DicomMap::GetAllMainDicomTags(allMainDicomTags);
 
     try
     {
       MetricsRegistry::Timer timer(GetMetricsRegistry(), "orthanc_store_dicom_duration_ms");
-      StorageAccessor accessor(area_, &storageCache_, GetMetricsRegistry());
+      StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
 
       DicomInstanceHasher hasher(summary);
       resultPublicId = hasher.HashInstance();
@@ -595,7 +690,6 @@
       // Remove the file from the DicomCache (useful if
       // "OverwriteInstances" is set to "true")
       dicomCache_.Invalidate(resultPublicId);
-      PublishDicomCacheMetrics();
 
       // TODO Should we use "gzip" instead?
       CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None);
@@ -624,9 +718,33 @@
 
       typedef std::map<MetadataType, std::string>  InstanceMetadata;
       InstanceMetadata  instanceMetadata;
-      result.SetStatus(index_.Store(
-        instanceMetadata, summary, attachments, dicom.GetMetadata(), dicom.GetOrigin(), overwrite,
-        hasTransferSyntax, transferSyntax, hasPixelDataOffset, pixelDataOffset, isReconstruct));
+
+      try 
+      {
+        result.SetStatus(index_.Store(
+          instanceMetadata, summary, attachments, dicom.GetMetadata(), dicom.GetOrigin(), overwrite,
+          hasTransferSyntax, transferSyntax, hasPixelDataOffset, pixelDataOffset, pixelDataVR, isReconstruct));
+      }
+      catch (OrthancException& ex)
+      {
+        if (ex.GetErrorCode() == ErrorCode_DuplicateResource)
+        {
+          LOG(WARNING) << "Duplicate instance, deleting the attachments";
+        }
+        else
+        {
+          LOG(ERROR) << "Unexpected error while storing an instance in DB, cancelling and deleting the attachments: " << ex.GetDetails();
+        }
+
+        accessor.Remove(dicomInfo);
+
+        if (dicomUntilPixelData.IsValid())
+        {
+          accessor.Remove(dicomUntilPixelData);
+        }
+        
+        throw;
+      }
 
       // Only keep the metadata for the "instance" level
       dicom.ClearMetadata();
@@ -653,16 +771,20 @@
         switch (result.GetStatus())
         {
           case StoreStatus_Success:
-            LOG(INFO) << "New instance stored";
+            LOG(INFO) << "New instance stored (" << resultPublicId << ")";
             break;
 
           case StoreStatus_AlreadyStored:
-            LOG(INFO) << "Already stored";
+            LOG(INFO) << "Instance already stored (" << resultPublicId << ")";
             break;
 
           case StoreStatus_Failure:
-            LOG(ERROR) << "Store failure";
-            break;
+            LOG(ERROR) << "Unknown store failure while storing instance " << resultPublicId;
+            throw OrthancException(ErrorCode_InternalError, HttpStatus_500_InternalServerError);
+
+          case StoreStatus_StorageFull:
+            LOG(ERROR) << "Storage full while storing instance " << resultPublicId;
+            throw OrthancException(ErrorCode_FullStorage, HttpStatus_507_InsufficientStorage);
 
           default:
             // This should never happen
@@ -768,7 +890,7 @@
                                                               bool isReconstruct)
   {
 
-    if (!isIngestTranscoding_)
+    if (!isIngestTranscoding_ || dicom->SkipIngestTranscoding())
     {
       // No automated transcoding. This was the only path in Orthanc <= 1.6.1.
       return StoreAfterTranscoding(resultPublicId, *dicom, mode, isReconstruct);
@@ -825,11 +947,7 @@
 
           std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromParsedDicomFile(*tmp));
           toStore->SetOrigin(dicom->GetOrigin());
-
-          if (isReconstruct) // the initial instance to store already has its own metadata
-          {
-            toStore->CopyMetadata(dicom->GetMetadata());
-          }
+          toStore->CopyMetadata(dicom->GetMetadata());
 
           StoreResult result = StoreAfterTranscoding(resultPublicId, *toStore, mode, isReconstruct);
           assert(resultPublicId == tmp->GetHasher().HashInstance());
@@ -858,7 +976,7 @@
     }
     else
     {
-      StorageAccessor accessor(area_, &storageCache_, GetMetricsRegistry());
+      StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
       accessor.AnswerFile(output, attachment, GetFileContentMime(content));
     }
   }
@@ -889,7 +1007,7 @@
 
     std::string content;
 
-    StorageAccessor accessor(area_, &storageCache_, GetMetricsRegistry());
+    StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
     accessor.Read(content, attachment);
 
     std::string newUuid = Toolbox::GenerateUuid();
@@ -962,7 +1080,7 @@
       std::string dicom;
 
       {
-        StorageAccessor accessor(area_, &storageCache_, GetMetricsRegistry());
+        StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
         accessor.Read(dicom, attachment);
       }
 
@@ -1028,8 +1146,8 @@
         std::string dicom;
         
         {
-          StorageAccessor accessor(area_, &storageCache_, GetMetricsRegistry());
-          accessor.ReadStartRange(dicom, attachment.GetUuid(), FileContentType_Dicom, pixelDataOffset, attachment.GetCustomData());
+          StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
+          accessor.ReadStartRange(dicom, attachment, pixelDataOffset);
         }
         
         assert(dicom.size() == pixelDataOffset);
@@ -1051,7 +1169,7 @@
         std::string dicomAsJson;
 
         {
-          StorageAccessor accessor(area_, &storageCache_, GetMetricsRegistry());
+          StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
           accessor.Read(dicomAsJson, attachment);
         }
 
@@ -1085,7 +1203,8 @@
            * Orthanc have failed. Try again this precomputation now
            * for future calls.
            **/
-          if (DicomStreamReader::LookupPixelDataOffset(pixelDataOffset, dicom) &&
+          ValueRepresentation pixelDataVR;
+          if (DicomStreamReader::LookupPixelDataOffset(pixelDataOffset, pixelDataVR, dicom) &&
               pixelDataOffset < dicom.size())
           {
             index_.OverwriteMetadata(instancePublicId, MetadataType_Instance_PixelDataOffset,
@@ -1115,10 +1234,19 @@
 
 
   void ServerContext::ReadDicom(std::string& dicom,
+                                std::string& attachmentId,
                                 const std::string& instancePublicId)
   {
     int64_t revision;
-    ReadAttachment(dicom, revision, instancePublicId, FileContentType_Dicom, true /* uncompress */);
+    ReadAttachment(dicom, revision, attachmentId, instancePublicId, FileContentType_Dicom, true /* uncompress */);
+  }
+
+
+  void ServerContext::ReadDicom(std::string& dicom,
+                                const std::string& instancePublicId)
+  {
+    std::string attachmentId;
+    ReadDicom(dicom, attachmentId, instancePublicId);    
   }
 
   void ServerContext::ReadDicomForHeader(std::string& dicom,
@@ -1133,13 +1261,23 @@
   bool ServerContext::ReadDicomUntilPixelData(std::string& dicom,
                                               const std::string& instancePublicId)
   {
+    FileInfo attachment;
+    int64_t revision;  // Ignored
+    if (index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_DicomUntilPixelData))
+    {
+      StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
+
+      accessor.Read(dicom, attachment);
+      assert(dicom.size() == attachment.GetUncompressedSize());
+
+      return true;
+    }
+
     if (!area_.HasReadRange())
     {
       return false;
     }
     
-    FileInfo attachment;
-    int64_t revision;  // Ignored
     if (!index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_Dicom))
     {
       throw OrthancException(ErrorCode_InternalError,
@@ -1157,9 +1295,9 @@
       {
         uint64_t pixelDataOffset = boost::lexical_cast<uint64_t>(s);
 
-        StorageAccessor accessor(area_, &storageCache_, GetMetricsRegistry());
-
-        accessor.ReadStartRange(dicom, attachment.GetUuid(), attachment.GetContentType(), pixelDataOffset, attachment.GetCustomData());
+        StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
+
+        accessor.ReadStartRange(dicom, attachment, pixelDataOffset);
         assert(dicom.size() == pixelDataOffset);
         
         return true;   // Success
@@ -1176,6 +1314,7 @@
 
   void ServerContext::ReadAttachment(std::string& result,
                                      int64_t& revision,
+                                     std::string& attachmentId,
                                      const std::string& instancePublicId,
                                      FileContentType content,
                                      bool uncompressIfNeeded,
@@ -1190,26 +1329,29 @@
     }
 
     assert(attachment.GetContentType() == content);
-
+    attachmentId = attachment.GetUuid();
+    
     {
-      StorageCache* cache = NULL;
-
-      if (!skipCache)
+      std::unique_ptr<StorageAccessor> accessor;
+      
+      if (skipCache)
       {
-        cache = &storageCache_;
+        accessor.reset(new StorageAccessor(area_, GetMetricsRegistry()));
       }
-
-      StorageAccessor accessor(area_, cache, GetMetricsRegistry());
+      else
+      {
+        accessor.reset(new StorageAccessor(area_, storageCache_, GetMetricsRegistry()));
+      }
 
       if (uncompressIfNeeded)
       {
-        accessor.Read(result, attachment);
+        accessor->Read(result, attachment);
       }
       else
       {
         // Do not uncompress the content of the storage area, return the
         // raw data
-        accessor.ReadRaw(result, attachment);
+        accessor->ReadRaw(result, attachment);
       }
     }
   }
@@ -1229,18 +1371,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 ||
@@ -1255,7 +1396,6 @@
       try
       {
         context_.dicomCache_.Acquire(instancePublicId_, dicom_.release(), dicomSize_);
-        context_.PublishDicomCacheMetrics();
       }
       catch (OrthancException&)
       {
@@ -1277,6 +1417,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)
   {
@@ -1300,8 +1452,8 @@
     // TODO Should we use "gzip" instead?
     CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None);
 
-    StorageAccessor accessor(area_, &storageCache_, GetMetricsRegistry());
-    
+    StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
+
     std::string uuid = Toolbox::GenerateUuid();
     std::string customData;
 
@@ -1340,7 +1492,6 @@
     {
       // remove the file from the DicomCache
       dicomCache_.Invalidate(uuid);
-      PublishDicomCacheMetrics();
     }
 
     return index_.DeleteResource(remainingAncestor, uuid, expectedType);
@@ -1353,7 +1504,6 @@
         change.GetChangeType() == ChangeType_Deleted)
     {
       dicomCache_.Invalidate(change.GetPublicId());
-      PublishDicomCacheMetrics();
     }
     
     pendingChanges_.Enqueue(change.Clone());
@@ -1426,6 +1576,8 @@
   void ServerContext::Apply(ILookupVisitor& visitor,
                             const DatabaseLookup& lookup,
                             ResourceType queryLevel,
+                            const std::set<std::string>& labels,
+                            LabelsConstraint labelsConstraint,
                             size_t since,
                             size_t limit)
   {    
@@ -1448,8 +1600,8 @@
     }
 
     {
-      const size_t lookupLimit = (databaseLimit == 0 ? 0 : databaseLimit + 1);      
-      GetIndex().ApplyLookupResources(resources, &instances, *fastLookup, queryLevel, lookupLimit);
+      const size_t lookupLimit = (databaseLimit == 0 ? 0 : databaseLimit + 1);
+      GetIndex().ApplyLookupResources(resources, &instances, *fastLookup, queryLevel, labels, labelsConstraint, lookupLimit);
     }
 
     bool complete = (databaseLimit == 0 ||
@@ -1542,7 +1694,7 @@
           ComputeStudyTags(resource, *this, resources[i], requestedTags);
 
           std::vector<std::string> modalities;
-          Toolbox::TokenizeString(modalities, resource.tags_.GetValue(DICOM_TAG_MODALITIES_IN_STUDY).GetContent(), '\\');
+          Toolbox::TokenizeString(modalities, resource.GetMainDicomTags().GetValue(DICOM_TAG_MODALITIES_IN_STUDY).GetContent(), '\\');
           bool hasAtLeastOneModalityMatching = false;
           for (size_t m = 0; m < modalities.size(); m++)
           {
@@ -1551,7 +1703,7 @@
 
           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.tags_.GetValue(DICOM_TAG_MODALITIES_IN_STUDY));
+          allMainDicomTagsFromDB.SetValue(DICOM_TAG_MODALITIES_IN_STUDY, resource.GetMainDicomTags().GetValue(DICOM_TAG_MODALITIES_IN_STUDY));
         }
 
         if (isMatch)
@@ -1738,27 +1890,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
-
-      std::unique_ptr<ImageAccessor> decoded;
+      // Use Orthanc's built-in decoder
+
       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
       }
     }
 
@@ -1766,14 +1941,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)
       {
@@ -1793,69 +1963,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();
-      }
-    }
-
-#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;
-    }
+    return DecodeDicomFrame(dicom.GetParsedDicomFile(),
+                            dicom.GetBufferData(),
+                            dicom.GetBufferSize(),
+                            frameIndex);
+
   }
 
 
@@ -1863,8 +2016,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);
   }
   
 
@@ -1877,9 +2030,10 @@
                                            uint16_t moveOriginatorId)
   {
     const void* data = dicom.empty() ? NULL : dicom.c_str();
-    
+    const RemoteModalityParameters& modality = connection.GetParameters().GetRemoteModality();
+
     if (!transcodeDicomProtocol_ ||
-        !connection.GetParameters().GetRemoteModality().IsTranscodingAllowed())
+        !modality.IsTranscodingAllowed())
     {
       connection.Store(sopClassUid, sopInstanceUid, data, dicom.size(),
                        hasMoveOriginator, moveOriginatorAet, moveOriginatorId);
@@ -1892,6 +2046,37 @@
   }
 
 
+  bool ServerContext::TranscodeWithCache(std::string& target,
+                                         const std::string& source,
+                                         const std::string& sourceInstanceId,
+                                         const std::string& attachmentId,
+                                         DicomTransferSyntax targetSyntax)
+  {
+    StorageCache::Accessor cacheAccessor(storageCache_);
+
+    if (!cacheAccessor.FetchTranscodedInstance(target, attachmentId, targetSyntax))
+    {
+      IDicomTranscoder::DicomImage sourceDicom;
+      sourceDicom.SetExternalBuffer(source);
+
+      IDicomTranscoder::DicomImage targetDicom;
+      std::set<DicomTransferSyntax> syntaxes;
+      syntaxes.insert(targetSyntax);
+
+      if (Transcode(targetDicom, sourceDicom, syntaxes, true))
+      {
+        cacheAccessor.AddTranscodedInstance(attachmentId, targetSyntax, reinterpret_cast<const char*>(targetDicom.GetBufferData()), targetDicom.GetBufferSize());
+        target = std::string(reinterpret_cast<const char*>(targetDicom.GetBufferData()), targetDicom.GetBufferSize());
+        return true;
+      }
+
+      return false;
+    }
+
+    return true;
+  }
+
+
   bool ServerContext::Transcode(DicomImage& target,
                                 DicomImage& source /* in, "GetParsed()" possibly modified */,
                                 const std::set<DicomTransferSyntax>& allowedSyntaxes,
@@ -1990,10 +2175,10 @@
   {
     target = Json::objectValue;
 
-    target["Type"] = GetResourceTypeText(resource.type_, false, true);
-    target["ID"] = resource.id_;
-
-    switch (resource.type_)
+    target["Type"] = GetResourceTypeText(resource.GetLevel(), false, true);
+    target["ID"] = resource.GetPublicId();
+
+    switch (resource.GetLevel())
     {
       case ResourceType_Patient:
         break;
@@ -2014,7 +2199,7 @@
         throw OrthancException(ErrorCode_InternalError);
     }
 
-    switch (resource.type_)
+    switch (resource.GetLevel())
     {
       case ResourceType_Patient:
       case ResourceType_Study:
@@ -2028,11 +2213,11 @@
           c.append(*it);
         }
 
-        if (resource.type_ == ResourceType_Patient)
+        if (resource.GetLevel() == ResourceType_Patient)
         {
           target["Studies"] = c;
         }
-        else if (resource.type_ == ResourceType_Study)
+        else if (resource.GetLevel() == ResourceType_Study)
         {
           target["Series"] = c;
         }
@@ -2050,7 +2235,7 @@
         throw OrthancException(ErrorCode_InternalError);
     }
 
-    switch (resource.type_)
+    switch (resource.GetLevel())
     {
       case ResourceType_Patient:
       case ResourceType_Study:
@@ -2099,9 +2284,9 @@
       target["ModifiedFrom"] = resource.modifiedFrom_;
     }
 
-    if (resource.type_ == ResourceType_Patient ||
-        resource.type_ == ResourceType_Study ||
-        resource.type_ == ResourceType_Series)
+    if (resource.GetLevel() == ResourceType_Patient ||
+        resource.GetLevel() == ResourceType_Study ||
+        resource.GetLevel() == ResourceType_Series)
     {
       target["IsStable"] = resource.isStable_;
 
@@ -2117,15 +2302,15 @@
     static const char* const PATIENT_MAIN_DICOM_TAGS = "PatientMainDicomTags";
 
     DicomMap mainDicomTags;
-    resource.tags_.ExtractResourceInformation(mainDicomTags, resource.type_);
+    resource.GetMainDicomTags().ExtractResourceInformation(mainDicomTags, resource.GetLevel());
 
     target[MAIN_DICOM_TAGS] = Json::objectValue;
     FromDcmtkBridge::ToJson(target[MAIN_DICOM_TAGS], mainDicomTags, format);
     
-    if (resource.type_ == ResourceType_Study)
+    if (resource.GetLevel() == ResourceType_Study)
     {
       DicomMap patientMainDicomTags;
-      resource.tags_.ExtractPatientInformation(patientMainDicomTags);
+      resource.GetMainDicomTags().ExtractPatientInformation(patientMainDicomTags);
 
       target[PATIENT_MAIN_DICOM_TAGS] = Json::objectValue;
       FromDcmtkBridge::ToJson(target[PATIENT_MAIN_DICOM_TAGS], patientMainDicomTags, format);
@@ -2136,13 +2321,23 @@
       static const char* const REQUESTED_TAGS = "RequestedTags";
 
       DicomMap tags;
-      resource.tags_.ExtractTags(tags, requestedTags);
+      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;
+    }
   }
 
 
@@ -2153,7 +2348,7 @@
   {
     if (requestedTags.count(DICOM_TAG_INSTANCE_AVAILABILITY) > 0)
     {
-      resource.tags_.SetValue(DICOM_TAG_INSTANCE_AVAILABILITY, "ONLINE", false);
+      resource.GetMainDicomTags().SetValue(DICOM_TAG_INSTANCE_AVAILABILITY, "ONLINE", false);
       resource.missingRequestedTags_.erase(DICOM_TAG_INSTANCE_AVAILABILITY);
     }
   }
@@ -2171,7 +2366,7 @@
 
       index.GetChildren(instances, seriesPublicId);
 
-      resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES,
+      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);
     }
@@ -2216,13 +2411,13 @@
       std::string modalities;
       Toolbox::JoinStrings(modalities, values, "\\");
 
-      resource.tags_.SetValue(DICOM_TAG_MODALITIES_IN_STUDY, modalities, false);
+      resource.GetMainDicomTags().SetValue(DICOM_TAG_MODALITIES_IN_STUDY, modalities, false);
       resource.missingRequestedTags_.erase(DICOM_TAG_MODALITIES_IN_STUDY);
     }
 
     if (hasNbRelatedSeries)
     {
-      resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES,
+      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);
     }
@@ -2240,7 +2435,7 @@
 
       if (hasNbRelatedInstances)
       {
-        resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES,
+        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);
       }
@@ -2264,7 +2459,7 @@
         {
           std::string sopClassUids;
           Toolbox::JoinStrings(sopClassUids, values, "\\");
-          resource.tags_.SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, sopClassUids, false);
+          resource.GetMainDicomTags().SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, sopClassUids, false);
         }
 
         resource.missingRequestedTags_.erase(DICOM_TAG_SOP_CLASSES_IN_STUDY);
@@ -2291,7 +2486,7 @@
 
     if (hasNbRelatedStudies)
     {
-      resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES,
+      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);
     }
@@ -2308,7 +2503,7 @@
 
       if (hasNbRelatedSeries)
       {
-        resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES,
+        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);
       }
@@ -2324,7 +2519,7 @@
         instances.splice(instances.end(), thisInstancesIds);
       }
 
-      resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES,
+      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);
     }
@@ -2388,7 +2583,7 @@
   {
     ExpandedResource resource;
 
-    if (ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, level, requestedTags, ExpandResourceDbFlags_Default, allowStorageAccess))
+    if (ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, level, requestedTags, ExpandResourceFlags_Default, allowStorageAccess))
     {
       SerializeExpandedResource(target, resource, format, requestedTags);
       return true;
@@ -2404,57 +2599,69 @@
                                      const Json::Value* dicomAsJson,   // optional: the dicom-as-json for the resource (if already available)
                                      ResourceType level,
                                      const std::set<DicomTag>& requestedTags,
-                                     ExpandResourceDbFlags expandFlags,
+                                     ExpandResourceFlags expandFlags,
                                      bool allowStorageAccess)
   {
     // first try to get the tags from what is already available
     
-    if ((expandFlags & ExpandResourceDbFlags_IncludeMainDicomTags)
-      && (mainDicomTags.GetSize() > 0)
-      && (dicomAsJson != NULL))
+    if ((expandFlags & ExpandResourceFlags_IncludeMainDicomTags) &&
+        mainDicomTags.GetSize() > 0 &&
+        dicomAsJson != NULL)
     {
       
-      resource.tags_.Merge(mainDicomTags);
+      resource.GetMainDicomTags().Merge(mainDicomTags);
 
       if (dicomAsJson->isObject())
       {
-        resource.tags_.FromDicomAsJson(*dicomAsJson);
+        resource.GetMainDicomTags().FromDicomAsJson(*dicomAsJson);
       }
 
       std::set<DicomTag> retrievedTags;
       std::set<DicomTag> missingTags;
-      resource.tags_.GetTags(retrievedTags);
+      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))
+      if (missingTags.size() > 0 && DicomMap::HasOnlyComputedTags(missingTags))
       {
-        expandFlags = static_cast<ExpandResourceDbFlags>(expandFlags & ~ExpandResourceDbFlags_IncludeMainDicomTags);
+        resource.missingRequestedTags_ = missingTags;
+        ComputeTags(resource, *this, publicId, level, requestedTags);
+        return true;
       }
-
-      if (missingTags.size() == 0 && expandFlags == ExpandResourceDbFlags_None)  // we have already retrieved anything we need
+      else if (missingTags.size() == 0)
+      {
+        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 != ExpandResourceDbFlags_None
-        && GetIndex().ExpandResource(resource, publicId, level, requestedTags, static_cast<ExpandResourceDbFlags>(expandFlags | ExpandResourceDbFlags_IncludeMetadata)))  // we always need the metadata to get the mainDicomTagsSignature
+    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.type_))
+      if (resource.mainDicomTagsSignature_ != DicomMap::GetMainDicomTagsSignature(resource.GetLevel()))
       {
         OrthancConfiguration::ReaderLock lock;
         if (lock.GetConfiguration().IsWarningEnabled(Warnings_002_InconsistentDicomTagsInDb))
         {
-          LOG(WARNING) << "W002: " << Orthanc::GetResourceTypeText(resource.type_, false , false) << " has been stored with another version of Main Dicom Tags list, you should POST to /" << Orthanc::GetResourceTypeText(resource.type_, true, false) << "/" << resource.id_ << "/reconstruct to update the list of tags saved in DB.  Some MainDicomTags might be missing from this answer.";
+          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_))
+      if (allowStorageAccess &&
+          !resource.missingRequestedTags_.empty() &&
+          !DicomMap::HasOnlyComputedTags(resource.missingRequestedTags_))
       {
         OrthancConfiguration::ReaderLock lock;
         if (lock.GetConfiguration().IsWarningEnabled(Warnings_001_TagsBeingReadFromStorage))
@@ -2472,7 +2679,9 @@
           std::string missings;
           FromDcmtkBridge::FormatListOfTags(missings, missingTags);
 
-          LOG(WARNING) << "W001: Accessing Dicom tags from storage when accessing " << Orthanc::GetResourceTypeText(resource.type_, false , false) << " : " << missings;
+          LOG(WARNING) << "W001: Accessing Dicom tags from storage when accessing "
+                       << Orthanc::GetResourceTypeText(resource.GetLevel(), false, false)
+                       << " : " << missings;
         }
 
 
@@ -2508,7 +2717,7 @@
           tagsFromJson.FromDicomAsJson(*dicomAsJson, false /* append */, true /* parseSequences*/);
         }
 
-        resource.tags_.Merge(tagsFromJson);
+        resource.GetMainDicomTags().Merge(tagsFromJson);
       }
 
       // compute the requested tags
@@ -2522,4 +2731,12 @@
     return true;
   }
 
+  int64_t ServerContext::GetServerUpTime() const
+  {
+    boost::posix_time::ptime nowUtc = boost::posix_time::second_clock::universal_time();
+    boost::posix_time::time_duration elapsed = nowUtc - serverStartTimeUtc_;
+
+    return elapsed.total_seconds();
+  }
+
 }
--- a/OrthancServer/Sources/ServerContext.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -35,6 +36,7 @@
 #include "../../OrthancFramework/Sources/FileStorage/StorageCache.h"
 #include "../../OrthancFramework/Sources/MultiThreading/Semaphore.h"
 
+#include <boost/date_time/posix_time/posix_time.hpp>
 
 namespace Orthanc
 {
@@ -138,6 +140,11 @@
         context_.mainLua_.SignalChange(change);
       }
 
+      virtual void SignalJobEvent(const JobEvent& event) ORTHANC_OVERRIDE
+      {
+        context_.mainLua_.SignalJobEvent(event);
+      }
+
       virtual bool FilterIncomingInstance(const DicomInstanceToStore& instance,
                                           const Json::Value& simplified) ORTHANC_OVERRIDE
       {
@@ -183,9 +190,17 @@
     static void ChangeThread(ServerContext* that,
                              unsigned int sleepDelay);
 
+    static void JobEventsThread(ServerContext* that,
+                                unsigned int sleepDelay);
+
     static void SaveJobsThread(ServerContext* that,
                                unsigned int sleepDelay);
 
+#if HAVE_MALLOC_TRIM == 1
+    static void MemoryTrimmingThread(ServerContext* that,
+                                     unsigned int intervalInSeconds);
+#endif
+
     void SaveJobsEngine();
 
     virtual void SignalJobSubmitted(const std::string& jobId) ORTHANC_OVERRIDE;
@@ -227,8 +242,11 @@
     bool haveJobsChanged_;
     bool isJobsEngineUnserialized_;
     SharedMessageQueue  pendingChanges_;
+    SharedMessageQueue  pendingJobEvents_;
     boost::thread  changeThread_;
+    boost::thread  jobEventsThread_;
     boost::thread  saveJobsThread_;
+    boost::thread  memoryTrimmingThread_;
         
     std::unique_ptr<SharedArchive>  queryRetrieveArchive_;
     std::string defaultLocalAet_;
@@ -241,6 +259,7 @@
     std::unique_ptr<MetricsRegistry>  metricsRegistry_;
     bool isHttpServerSecure_;
     bool isExecuteLuaEnabled_;
+    bool isRestApiWriteToFileSystemEnabled_;
     bool overwriteInstances_;
 
     std::unique_ptr<StorageCommitmentReports>  storageCommitmentReports_;
@@ -264,8 +283,6 @@
                                       StoreInstanceMode mode,
                                       bool isReconstruct);
 
-    void PublishDicomCacheMetrics();
-
     // This method must only be called from "ServerIndex"!
     void RemoveFile(const std::string& fileUuid,
                     FileContentType type,
@@ -277,6 +294,8 @@
     DicomModification logsDeidentifierRules_;
     bool              deidentifyLogs_;
 
+    boost::posix_time::ptime serverStartTimeUtc_;
+
   public:
     class DicomCacheLocker : public boost::noncopyable
     {
@@ -287,6 +306,7 @@
       std::unique_ptr<ParsedDicomFile>             dicom_;
       size_t                                       dicomSize_;
       std::unique_ptr<Semaphore::Locker>           largeDicomLocker_;
+      std::string                                  buffer_;
 
     public:
       DicomCacheLocker(ServerContext& context,
@@ -295,6 +315,8 @@
       ~DicomCacheLocker();
 
       ParsedDicomFile& GetDicom() const;
+
+      const std::string& GetBuffer();
     };
 
     ServerContext(IDatabaseWrapper& database,
@@ -362,6 +384,10 @@
     void ReadDicom(std::string& dicom,
                    const std::string& instancePublicId);
 
+    void ReadDicom(std::string& dicom,
+                   std::string& attachmentId,
+                   const std::string& instancePublicId);
+
     void ReadDicomForHeader(std::string& dicom,
                             const std::string& instancePublicId);
 
@@ -371,6 +397,7 @@
     // 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,
                         bool uncompressIfNeeded,
@@ -424,9 +451,20 @@
     void Apply(ILookupVisitor& visitor,
                const DatabaseLookup& lookup,
                ResourceType queryLevel,
+               const std::set<std::string>& labels,
+               LabelsConstraint labelsConstraint,
                size_t since,
                size_t limit);
 
+    void Apply(ILookupVisitor& visitor,
+               const DatabaseLookup& lookup,
+               ResourceType queryLevel,
+               size_t since,
+               size_t limit)
+    {
+      Apply(visitor, lookup, queryLevel, std::set<std::string>(), LabelsConstraint_All, since, limit);
+    }
+
     bool LookupOrReconstructMetadata(std::string& target,
                                      const std::string& publicId,
                                      ResourceType level,
@@ -481,6 +519,16 @@
       return isExecuteLuaEnabled_;
     }
 
+    void SetRestApiWriteToFileSystemEnabled(bool enabled)
+    {
+      isRestApiWriteToFileSystemEnabled_ = enabled;
+    }
+
+    bool IsRestApiWriteToFileSystemEnabled() const
+    {
+      return isRestApiWriteToFileSystemEnabled_;
+    }
+
     void SetOverwriteInstances(bool overwrite)
     {
       overwriteInstances_ = overwrite;
@@ -513,7 +561,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,
@@ -529,6 +582,12 @@
                            const std::set<DicomTransferSyntax>& allowedSyntaxes,
                            bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
 
+    virtual bool TranscodeWithCache(std::string& target,
+                                    const std::string& source,
+                                    const std::string& sourceInstanceId,
+                                    const std::string& attachmentId, // for the storage cache
+                                    DicomTransferSyntax targetSyntax);
+
     bool IsTranscodeDicomProtocol() const
     {
       return transcodeDicomProtocol_;
@@ -568,12 +627,16 @@
                         const Json::Value* dicomAsJson,   // optional: the dicom-as-json for the resource
                         ResourceType level,
                         const std::set<DicomTag>& requestedTags,
-                        ExpandResourceDbFlags expandFlags,
+                        ExpandResourceFlags expandFlags,
                         bool allowStorageAccess);
 
     FindStorageAccessMode GetFindStorageAccessMode() const
     {
       return findStorageAccessMode_;
     }
+
+    int64_t GetServerUpTime() const;
+
+    void PublishCacheMetrics();
   };
 }
--- a/OrthancServer/Sources/ServerEnumerations.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerEnumerations.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -62,6 +63,7 @@
     dictMetadataType_.Add(MetadataType_Instance_PixelDataOffset, "PixelDataOffset");
     dictMetadataType_.Add(MetadataType_MainDicomTagsSignature, "MainDicomTagsSignature");
     dictMetadataType_.Add(MetadataType_MainDicomSequences, "MainDicomSequences");
+    dictMetadataType_.Add(MetadataType_Instance_PixelDataVR, "PixelDataVR");
 
     dictContentType_.Add(FileContentType_Dicom, "dicom");
     dictContentType_.Add(FileContentType_DicomAsJson, "dicom-as-json");
@@ -112,6 +114,23 @@
     return dictMetadataType_.Translate(str);
   }
 
+  void GetRegisteredUserMetadata(std::map<std::string, int>& allEntries)
+  {
+    boost::mutex::scoped_lock lock(enumerationsMutex_);
+
+    allEntries.clear();
+
+    std::map<std::string, MetadataType> allEntriesTyped = dictMetadataType_.GetAllEntries();
+
+    for (std::map<std::string, MetadataType>::const_iterator it = allEntriesTyped.begin(); it != allEntriesTyped.end(); ++it)
+    {
+      if (it->second >= MetadataType_StartUser)
+      {
+        allEntries[it->first] = it->second;
+      }
+    }
+  }
+
   void RegisterUserContentType(int contentType,
                                const std::string& name,
                                const std::string& mime)
@@ -340,6 +359,9 @@
       case StoreStatus_FilteredOut:
         return "FilteredOut";
 
+      case StoreStatus_StorageFull:
+        return "StorageFull";
+
       default:
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
--- a/OrthancServer/Sources/ServerEnumerations.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerEnumerations.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -161,6 +162,7 @@
     MetadataType_Instance_PixelDataOffset = 14,  // New in Orthanc 1.9.0
     MetadataType_MainDicomTagsSignature = 15,    // New in Orthanc 1.11.0
     MetadataType_MainDicomSequences = 16,        // New in Orthanc 1.11.1
+    MetadataType_Instance_PixelDataVR = 17,      // New in Orthanc 1.12.1
     
     // Make sure that the value "65535" can be stored into this enumeration
     MetadataType_StartUser = 1024,
@@ -205,6 +207,7 @@
     Warnings_None,
     Warnings_001_TagsBeingReadFromStorage,
     Warnings_002_InconsistentDicomTagsInDb,
+    Warnings_003_DecoderFailure,              // new in Orthanc 1.12.5
   };
 
 
@@ -217,6 +220,8 @@
 
   std::string EnumerationToString(MetadataType type);
 
+  void GetRegisteredUserMetadata(std::map<std::string, int>& allEntries);
+
   void RegisterUserContentType(int contentType,
                                const std::string& name,
                                const std::string& mime);
--- a/OrthancServer/Sources/ServerIndex.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerIndex.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -36,8 +37,6 @@
 #include "ServerToolbox.h"
 
 
-static const uint64_t MEGA_BYTES = 1024 * 1024;
-
 namespace Orthanc
 {
   class ServerIndex::TransactionContext : public StatelessDatabaseOperations::ITransactionContext
@@ -194,16 +193,17 @@
       }        
     };
 
-    virtual void MarkAsUnstable(int64_t id,
-                                Orthanc::ResourceType type,
+    virtual void MarkAsUnstable(ResourceType type,
+                                int64_t id,
                                 const std::string& publicId) ORTHANC_OVERRIDE
     {
-      context_.GetIndex().MarkAsUnstable(id, type, publicId);
+      context_.GetIndex().MarkAsUnstable(type, id, publicId);
     }
 
-    virtual bool IsUnstableResource(int64_t id) ORTHANC_OVERRIDE
+    virtual bool IsUnstableResource(ResourceType type,
+                                    int64_t id) ORTHANC_OVERRIDE
     {
-      return context_.GetIndex().IsUnstableResource(id);
+      return context_.GetIndex().IsUnstableResource(type, id);
     }
 
     virtual void Commit() ORTHANC_OVERRIDE
@@ -248,18 +248,15 @@
   class ServerIndex::UnstableResourcePayload
   {
   private:
-    ResourceType type_;
     std::string publicId_;
     boost::posix_time::ptime time_;
 
   public:
-    UnstableResourcePayload() : type_(ResourceType_Instance)
+    UnstableResourcePayload()
     {
     }
 
-    UnstableResourcePayload(Orthanc::ResourceType type,
-                            const std::string& publicId) : 
-      type_(type),
+    explicit UnstableResourcePayload(const std::string& publicId) : 
       publicId_(publicId),
       time_(boost::posix_time::second_clock::local_time())
     {
@@ -269,11 +266,6 @@
     {
       return (boost::posix_time::second_clock::local_time() - time_).total_seconds();
     }
-
-    ResourceType GetResourceType() const
-    {
-      return type_;
-    }
     
     const std::string& GetPublicId() const
     {
@@ -281,10 +273,45 @@
     }
   };
 
+  void ServerIndex::UpdateStatisticsThread(ServerIndex* that,
+                                           unsigned int threadSleepGranularityMilliseconds)
+  {
+    Logging::SetCurrentThreadName("DB-STATS");
+
+    static const unsigned int SLEEP_SECONDS = 60;
+
+    if (threadSleepGranularityMilliseconds > 1000)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    LOG(INFO) << "Starting the update statistics thread (sleep = " << SLEEP_SECONDS << " seconds)";
+
+    unsigned int count = 0;
+    unsigned int countThreshold = (1000 * SLEEP_SECONDS) / threadSleepGranularityMilliseconds;
+
+    while (!that->done_)
+    {
+      boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleepGranularityMilliseconds));
+      count++;
+      
+      if (count >= countThreshold)
+      {
+        uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances;
+        that->GetGlobalStatistics(diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances);
+        
+        count = 0;
+      }
+    }
+
+    LOG(INFO) << "Stopping the update statistics thread";
+  }
 
   void ServerIndex::FlushThread(ServerIndex* that,
                                 unsigned int threadSleepGranularityMilliseconds)
   {
+    Logging::SetCurrentThreadName("DB-FLUSH");
+
     // By default, wait for 10 seconds before flushing
     static const unsigned int SLEEP_SECONDS = 10;
 
@@ -316,10 +343,11 @@
   }
 
 
-  bool ServerIndex::IsUnstableResource(int64_t id)
+  bool ServerIndex::IsUnstableResource(ResourceType type,
+                                       int64_t id)
   {
     boost::mutex::scoped_lock lock(monitoringMutex_);
-    return unstableResources_.Contains(id);
+    return unstableResources_.Contains(std::make_pair(type, id));
   }
 
 
@@ -338,11 +366,20 @@
     // execution of Orthanc
     StandaloneRecycling(maximumStorageMode_, maximumStorageSize_, maximumPatients_);
 
-    if (HasFlushToDisk())
+    // 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);
     }
 
+    // For some DB plugins that implements the UpdateAndGetStatistics function, updating 
+    // the statistics can take quite some time if you have not done it for a long time 
+    // -> make sure they are updated at regular interval
+    if (GetDatabaseCapabilities().HasUpdateAndGetStatistics())
+    {
+      updateStatisticsThread_ = boost::thread(UpdateStatisticsThread, this, threadSleepGranularityMilliseconds);
+    }
+
     unstableResourcesMonitorThread_ = boost::thread
       (UnstableResourcesMonitorThread, this, threadSleepGranularityMilliseconds);
   }
@@ -369,6 +406,11 @@
         flushThread_.join();
       }
 
+      if (updateStatisticsThread_.joinable())
+      {
+        updateStatisticsThread_.join();
+      }
+
       if (unstableResourcesMonitorThread_.joinable())
       {
         unstableResourcesMonitorThread_.join();
@@ -409,7 +451,7 @@
       }
       else
       {
-        LOG(WARNING) << "At most " << (size / MEGA_BYTES) << "MB will be used for the storage area";
+        LOG(WARNING) << "At most " << Toolbox::GetHumanFileSize(size) << " will be used for the storage area";
       }
     }
 
@@ -424,11 +466,17 @@
       
       if (mode == MaxStorageMode_Recycle)
       {
-        LOG(WARNING) << "Maximum Storage mode: Recycle";
+        if (maximumStorageSize_ > 0 || maximumPatients_ > 0)
+        {
+          LOG(WARNING) << "Maximum Storage mode: Recycle";
+        }
       }
       else
       {
-        LOG(WARNING) << "Maximum Storage mode: Reject";
+        if (maximumStorageSize_ > 0 || maximumPatients_ > 0)
+        {
+          LOG(WARNING) << "Maximum Storage mode: Reject";
+        }
       }
     }
 
@@ -438,6 +486,8 @@
   void ServerIndex::UnstableResourcesMonitorThread(ServerIndex* that,
                                                    unsigned int threadSleepGranularityMilliseconds)
   {
+    Logging::SetCurrentThreadName("UNSTABLE-MON");
+
     int stableAge;
     
     {
@@ -459,7 +509,8 @@
 
       for (;;)
       {
-        UnstableResourcePayload stableResource;
+        UnstableResourcePayload stablePayload;
+        ResourceType stableLevel;
         int64_t stableId;
 
         {      
@@ -470,8 +521,10 @@
           {
             // This DICOM resource has not received any new instance for
             // some time. It can be considered as stable.
-            stableId = that->unstableResources_.RemoveOldest(stableResource);
-            //LOG(TRACE) << "Stable resource: " << EnumerationToString(stableResource.GetResourceType()) << " " << stableId;
+            std::pair<ResourceType, int64_t> stableResource = that->unstableResources_.RemoveOldest(stablePayload);
+            stableLevel = stableResource.first;
+            stableId = stableResource.second;
+            //LOG(TRACE) << "Stable resource: " << EnumerationToString(stablePayload.GetResourceType()) << " " << stableId;
           }
           else
           {
@@ -489,18 +542,18 @@
            * another thread, which leads to calls to "MarkAsUnstable()",
            * which leads to two lockings of "monitoringMutex_").
            **/
-          switch (stableResource.GetResourceType())
+          switch (stableLevel)
           {
             case ResourceType_Patient:
-              that->LogChange(stableId, ChangeType_StablePatient, stableResource.GetPublicId(), ResourceType_Patient);
+              that->LogChange(stableId, ChangeType_StablePatient, stablePayload.GetPublicId(), ResourceType_Patient);
               break;
             
             case ResourceType_Study:
-              that->LogChange(stableId, ChangeType_StableStudy, stableResource.GetPublicId(), ResourceType_Study);
+              that->LogChange(stableId, ChangeType_StableStudy, stablePayload.GetPublicId(), ResourceType_Study);
               break;
             
             case ResourceType_Series:
-              that->LogChange(stableId, ChangeType_StableSeries, stableResource.GetPublicId(), ResourceType_Series);
+              that->LogChange(stableId, ChangeType_StableSeries, stablePayload.GetPublicId(), ResourceType_Series);
               break;
             
             default:
@@ -518,18 +571,18 @@
   }
   
 
-  void ServerIndex::MarkAsUnstable(int64_t id,
-                                   Orthanc::ResourceType type,
+  void ServerIndex::MarkAsUnstable(ResourceType type,
+                                   int64_t id,
                                    const std::string& publicId)
   {
-    assert(type == Orthanc::ResourceType_Patient ||
-           type == Orthanc::ResourceType_Study ||
-           type == Orthanc::ResourceType_Series);
+    assert(type == ResourceType_Patient ||
+           type == ResourceType_Study ||
+           type == ResourceType_Series);
 
     {
       boost::mutex::scoped_lock lock(monitoringMutex_);
-      UnstableResourcePayload payload(type, publicId);
-      unstableResources_.AddOrMakeMostRecent(id, payload);
+      UnstableResourcePayload payload(publicId);
+      unstableResources_.AddOrMakeMostRecent(std::make_pair(type, id), payload);
       //LOG(INFO) << "Unstable resource: " << EnumerationToString(type) << " " << id;
     }
   }
@@ -545,6 +598,7 @@
                                  DicomTransferSyntax transferSyntax,
                                  bool hasPixelDataOffset,
                                  uint64_t pixelDataOffset,
+                                 ValueRepresentation pixelDataVR,
                                  bool isReconstruct)
   {
     uint64_t maximumStorageSize;
@@ -560,7 +614,8 @@
 
     return StatelessDatabaseOperations::Store(
       instanceMetadata, dicomSummary, attachments, metadata, origin, overwrite, hasTransferSyntax,
-      transferSyntax, hasPixelDataOffset, pixelDataOffset, maximumStorageMode, maximumStorageSize, maximumPatients, isReconstruct);
+      transferSyntax, hasPixelDataOffset, pixelDataOffset, pixelDataVR, maximumStorageMode,
+      maximumStorageSize, maximumPatients, isReconstruct);
   }
 
   
--- a/OrthancServer/Sources/ServerIndex.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerIndex.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -41,9 +42,10 @@
     bool done_;
     boost::mutex monitoringMutex_;
     boost::thread flushThread_;
+    boost::thread updateStatisticsThread_;
     boost::thread unstableResourcesMonitorThread_;
 
-    LeastRecentlyUsedIndex<int64_t, UnstableResourcePayload>  unstableResources_;
+    LeastRecentlyUsedIndex<std::pair<ResourceType, int64_t>, UnstableResourcePayload>  unstableResources_;
 
     MaxStorageMode  maximumStorageMode_;
     uint64_t        maximumStorageSize_;
@@ -52,14 +54,18 @@
     static void FlushThread(ServerIndex* that,
                             unsigned int threadSleep);
 
+    static void UpdateStatisticsThread(ServerIndex* that,
+                                       unsigned int threadSleep);
+
     static void UnstableResourcesMonitorThread(ServerIndex* that,
                                                unsigned int threadSleep);
 
-    void MarkAsUnstable(int64_t id,
-                        Orthanc::ResourceType type,
+    void MarkAsUnstable(ResourceType type,
+                        int64_t id,
                         const std::string& publicId);
 
-    bool IsUnstableResource(int64_t id);
+    bool IsUnstableResource(ResourceType type,
+                            int64_t id);
 
   public:
     ServerIndex(ServerContext& context,
@@ -88,7 +94,8 @@
                       DicomTransferSyntax transferSyntax,
                       bool hasPixelDataOffset,
                       uint64_t pixelDataOffset,
-                      bool isResonstruct);
+                      ValueRepresentation pixelDataVR,
+                      bool isReconstruct);
 
     StoreStatus AddAttachment(int64_t& newRevision /*out*/,
                               const FileInfo& attachment,
--- a/OrthancServer/Sources/ServerIndexChange.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerIndexChange.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -34,6 +35,7 @@
 #include "../ServerContext.h"
 
 #include <stdio.h>
+#include <boost/range/algorithm/count.hpp>
 
 #if defined(_MSC_VER)
 #define snprintf _snprintf
@@ -71,7 +73,7 @@
                           countInstances >= 65535 - FILES_MARGIN);
 
     LOG(INFO) << "Creating a ZIP file with " << countInstances << " files of size "
-              << (uncompressedSize / MEGA_BYTES) << "MB using the "
+              << Toolbox::GetHumanFileSize(uncompressedSize) << " using the "
               << (isZip64 ? "ZIP64" : "ZIP32") << " file format";
 
     return isZip64;
@@ -82,9 +84,13 @@
   {
   protected:
     ServerContext&                        context_;
+    bool                                  transcode_;
+    DicomTransferSyntax                   transferSyntax_;
   public:
-    explicit InstanceLoader(ServerContext& context)
-    : context_(context)
+    explicit InstanceLoader(ServerContext& context, bool transcode, DicomTransferSyntax transferSyntax)
+    : context_(context),
+      transcode_(transcode),
+      transferSyntax_(transferSyntax)
     {
     }
 
@@ -94,7 +100,31 @@
 
     virtual void PrepareDicom(const std::string& instanceId)
     {
+    }
 
+    bool TranscodeDicom(std::string& transcodedBuffer, const std::string& sourceBuffer, const std::string& instanceId)
+    {
+      if (transcode_)
+      {
+        std::set<DicomTransferSyntax> syntaxes;
+        syntaxes.insert(transferSyntax_);
+
+        IDicomTranscoder::DicomImage source, transcoded;
+        source.SetExternalBuffer(sourceBuffer);
+
+        if (context_.Transcode(transcoded, source, syntaxes, true /* allow new SOP instance UID */))
+        {
+          transcodedBuffer.assign(reinterpret_cast<const char*>(transcoded.GetBufferData()), transcoded.GetBufferSize());
+          return true;
+        }
+        else
+        {
+          LOG(INFO) << "Cannot transcode instance " << instanceId
+                    << " to transfer syntax: " << GetTransferSyntaxUid(transferSyntax_);
+        }
+      }
+
+      return false;
     }
 
     virtual void GetDicom(std::string& dicom, const std::string& instanceId) = 0;
@@ -107,14 +137,24 @@
   class ArchiveJob::SynchronousInstanceLoader : public ArchiveJob::InstanceLoader
   {
   public:
-    explicit SynchronousInstanceLoader(ServerContext& context)
-    : InstanceLoader(context)
+    explicit SynchronousInstanceLoader(ServerContext& context, bool transcode, DicomTransferSyntax transferSyntax)
+    : InstanceLoader(context, transcode, transferSyntax)
     {
     }
 
     virtual void GetDicom(std::string& dicom, const std::string& instanceId) ORTHANC_OVERRIDE
     {
       context_.ReadDicom(dicom, instanceId);
+
+      if (transcode_)
+      {
+        std::string transcoded;
+        if (TranscodeDicom(transcoded, dicom, instanceId))
+        {
+          dicom.swap(transcoded);
+        }
+      }
+      
     }
   };
 
@@ -138,6 +178,7 @@
   class ArchiveJob::ThreadedInstanceLoader : public ArchiveJob::InstanceLoader
   {
     Semaphore                           availableInstancesSemaphore_;
+    Semaphore                           bufferedInstancesSemaphore_;
     std::map<std::string, boost::shared_ptr<std::string> >  availableInstances_;
     boost::mutex                        availableInstancesMutex_;
     SharedMessageQueue                  instancesToPreload_;
@@ -145,9 +186,10 @@
 
 
   public:
-    ThreadedInstanceLoader(ServerContext& context, size_t threadCount)
-    : InstanceLoader(context),
-      availableInstancesSemaphore_(0)
+    ThreadedInstanceLoader(ServerContext& context, size_t threadCount, bool transcode, DicomTransferSyntax transferSyntax)
+    : InstanceLoader(context, transcode, transferSyntax),
+      availableInstancesSemaphore_(0),
+      bufferedInstancesSemaphore_(3*threadCount)
     {
       for (size_t i = 0; i < threadCount; i++)
       {
@@ -182,6 +224,9 @@
 
     static void PreloaderWorkerThread(ThreadedInstanceLoader* that)
     {
+      static uint16_t threadCounter = 0;
+      Logging::SetCurrentThreadName(std::string("ARCH-LOAD-") + boost::lexical_cast<std::string>(threadCounter++));
+
       while (true)
       {
         std::unique_ptr<InstanceId> instanceId(dynamic_cast<InstanceId*>(that->instancesToPreload_.Dequeue(0)));
@@ -189,11 +234,24 @@
         {
           return;
         }
+        
+        // wait for the consumers (zip writer), no need to accumulate instances in memory if loaders are faster than writers
+        that->bufferedInstancesSemaphore_.Acquire();
 
         try
         {
           boost::shared_ptr<std::string> dicomContent(new std::string());
           that->context_.ReadDicom(*dicomContent, instanceId->GetId());
+
+          if (that->transcode_)
+          {
+            boost::shared_ptr<std::string> transcodedDicom(new std::string());
+            if (that->TranscodeDicom(*transcodedDicom, *dicomContent, instanceId->GetId()))
+            {
+              dicomContent = transcodedDicom;
+            }
+          }
+
           {
             boost::mutex::scoped_lock lock(that->availableInstancesMutex_);
             that->availableInstances_[instanceId->GetId()] = dicomContent;
@@ -222,6 +280,7 @@
       {
         // wait for an instance to be available but this might not be the one we are waiting for !
         availableInstancesSemaphore_.Acquire();
+        bufferedInstancesSemaphore_.Release(); // unlock the "flow" of loaders
 
         boost::shared_ptr<std::string> dicomContent;
         {
@@ -252,6 +311,91 @@
     }
   };
 
+  // This enum defines specific resource types to be used when exporting the archive.
+  // It defines if we should use the PatientInfo from the Patient or from the Study.
+  enum ArchiveResourceType
+  {
+    ArchiveResourceType_Patient = 0,
+    ArchiveResourceType_PatientInfoFromStudy = 1,
+    ArchiveResourceType_Study = 2,
+    ArchiveResourceType_Series = 3,
+    ArchiveResourceType_Instance = 4
+  };
+
+  ResourceType GetResourceIdType(ArchiveResourceType type)
+  {
+    switch (type)
+    {
+      case ArchiveResourceType_Patient:
+        return ResourceType_Patient;
+      case ArchiveResourceType_PatientInfoFromStudy: // get the Patient tags from the Study id
+        return ResourceType_Study;
+      case ArchiveResourceType_Study:
+        return ResourceType_Study;
+      case ArchiveResourceType_Series:
+        return ResourceType_Series;
+      case ArchiveResourceType_Instance:
+        return ResourceType_Instance;
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  ResourceType GetResourceLevel(ArchiveResourceType type)
+  {
+    switch (type)
+    {
+      case ArchiveResourceType_Patient:
+        return ResourceType_Patient;
+      case ArchiveResourceType_PatientInfoFromStudy: // this is actually the same level as the Patient
+        return ResourceType_Patient;
+      case ArchiveResourceType_Study:
+        return ResourceType_Study;
+      case ArchiveResourceType_Series:
+        return ResourceType_Series;
+      case ArchiveResourceType_Instance:
+        return ResourceType_Instance;
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  ArchiveResourceType GetArchiveResourceType(ResourceType type)
+  {
+    switch (type)
+    {
+      case ResourceType_Patient:
+        return ArchiveResourceType_Patient;
+      case ArchiveResourceType_Study:
+       return ArchiveResourceType_PatientInfoFromStudy;
+      case ResourceType_Series:
+        return ArchiveResourceType_Series;
+      case ResourceType_Instance:
+        return ArchiveResourceType_Instance;
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  ArchiveResourceType GetChildResourceType(ArchiveResourceType type)
+  {
+    switch (type)
+    {
+      case ArchiveResourceType_Patient:
+      case ArchiveResourceType_PatientInfoFromStudy:
+        return ArchiveResourceType_Study;
+
+      case ArchiveResourceType_Study:
+        return ArchiveResourceType_Series;
+        
+      case ArchiveResourceType_Series:
+        return ArchiveResourceType_Instance;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
 
   class ArchiveJob::ResourceIdentifiers : public boost::noncopyable
   {
@@ -356,7 +500,7 @@
     {
     }
 
-    virtual void Open(ResourceType level,
+    virtual void Open(ArchiveResourceType level,
                       const std::string& publicId) = 0;
 
     virtual void Close() = 0;
@@ -385,7 +529,7 @@
     // A "NULL" value for ArchiveIndex indicates a non-expanded node
     typedef std::map<std::string, ArchiveIndex*>   Resources;
 
-    ResourceType         level_;
+    ArchiveResourceType  level_;
     Resources            resources_;   // Only at patient/study/series level
     std::list<Instance>  instances_;   // Only at instance level
 
@@ -393,7 +537,7 @@
     void AddResourceToExpand(ServerIndex& index,
                              const std::string& id)
     {
-      if (level_ == ResourceType_Instance)
+      if (level_ == ArchiveResourceType_Instance)
       {
         FileInfo tmp;
         int64_t revision;  // ignored
@@ -410,7 +554,7 @@
 
 
   public:
-    explicit ArchiveIndex(ResourceType level) :
+    explicit ArchiveIndex(ArchiveResourceType level) :
       level_(level)
     {
     }
@@ -428,14 +572,14 @@
     void Add(ServerIndex& index,
              const ResourceIdentifiers& resource)
     {
-      const std::string& id = resource.GetIdentifier(level_);
+      const std::string& id = resource.GetIdentifier(GetResourceIdType(level_));
       Resources::iterator previous = resources_.find(id);
 
-      if (level_ == ResourceType_Instance)
+      if (level_ == ArchiveResourceType_Instance)
       {
         AddResourceToExpand(index, id);
       }
-      else if (resource.GetLevel() == level_)
+      else if (resource.GetLevel() == GetResourceLevel(level_))
       {
         // Mark this resource for further expansion
         if (previous != resources_.end())
@@ -465,7 +609,7 @@
 
     void Expand(ServerIndex& index)
     {
-      if (level_ == ResourceType_Instance)
+      if (level_ == ArchiveResourceType_Instance)
       {
         // Expanding an instance node makes no sense
         return;
@@ -499,7 +643,7 @@
 
     void Apply(IArchiveVisitor& visitor) const
     {
-      if (level_ == ResourceType_Instance)
+      if (level_ == ArchiveResourceType_Instance)
       {
         for (std::list<Instance>::const_iterator 
                it = instances_.begin(); it != instances_.end(); ++it)
@@ -597,55 +741,20 @@
               return;
             }
 
-            //boost::this_thread::sleep(boost::posix_time::milliseconds(300));
-
             writer.OpenFile(filename_.c_str());
 
-            bool transcodeSuccess = false;
-
             std::unique_ptr<ParsedDicomFile> parsed;
             
-            if (transcode)
-            {
-              // New in Orthanc 1.7.0
-              std::set<DicomTransferSyntax> syntaxes;
-              syntaxes.insert(transferSyntax);
-
-              IDicomTranscoder::DicomImage source, transcoded;
-              source.SetExternalBuffer(content);
+            writer.Write(content);
 
-              if (context.Transcode(transcoded, source, syntaxes, true /* allow new SOP instance UID */))
+            if (dicomDir != NULL)
+            {
+              if (parsed.get() == NULL)
               {
-                writer.Write(transcoded.GetBufferData(), transcoded.GetBufferSize());
-
-                if (dicomDir != NULL)
-                {
-                  std::unique_ptr<ParsedDicomFile> tmp(transcoded.ReleaseAsParsedDicomFile());
-                  dicomDir->Add(dicomDirFolder, filename_, *tmp);
-                }
-                
-                transcodeSuccess = true;
+                parsed.reset(new ParsedDicomFile(content));
               }
-              else
-              {
-                LOG(INFO) << "Cannot transcode instance " << instanceId_
-                          << " to transfer syntax: " << GetTransferSyntaxUid(transferSyntax);
-              }
-            }
 
-            if (!transcodeSuccess)
-            {
-              writer.Write(content);
-
-              if (dicomDir != NULL)
-              {
-                if (parsed.get() == NULL)
-                {
-                  parsed.reset(new ParsedDicomFile(content));
-                }
-
-                dicomDir->Add(dicomDirFolder, filename_, *parsed);
-              }
+              dicomDir->Add(dicomDirFolder, filename_, *parsed);
             }
               
             break;
@@ -804,25 +913,29 @@
       snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%%08d.dcm");
     }
 
-    virtual void Open(ResourceType level,
+    virtual void Open(ArchiveResourceType level,
                       const std::string& publicId) ORTHANC_OVERRIDE
     {
       std::string path;
 
       DicomMap tags;
-      if (context_.GetIndex().GetMainDicomTags(tags, publicId, level, level))
+      ResourceType resourceIdLevel = GetResourceIdType(level);
+      ResourceType interestLevel = (level == ArchiveResourceType_PatientInfoFromStudy ? ResourceType_Patient : resourceIdLevel);
+
+      if (context_.GetIndex().GetMainDicomTags(tags, publicId, resourceIdLevel, interestLevel))
       {
         switch (level)
         {
-          case ResourceType_Patient:
+          case ArchiveResourceType_Patient:
+          case ArchiveResourceType_PatientInfoFromStudy:
             path = GetTag(tags, DICOM_TAG_PATIENT_ID) + " " + GetTag(tags, DICOM_TAG_PATIENT_NAME);
             break;
 
-          case ResourceType_Study:
+          case ArchiveResourceType_Study:
             path = GetTag(tags, DICOM_TAG_ACCESSION_NUMBER) + " " + GetTag(tags, DICOM_TAG_STUDY_DESCRIPTION);
             break;
 
-          case ResourceType_Series:
+          case ArchiveResourceType_Series:
           {
             std::string modality = GetTag(tags, DICOM_TAG_MODALITY);
             path = modality + " " + GetTag(tags, DICOM_TAG_SERIES_DESCRIPTION);
@@ -854,9 +967,10 @@
 
       path = Toolbox::StripSpaces(Toolbox::ConvertToAscii(path));
 
-      if (path.empty())
+      if (path.empty() 
+          || (static_cast<size_t>(boost::count(path, '^')) == path.size()))  // this happens with non ASCII patient names: only the '^' remains and this is not a valid zip folder name
       {
-        path = std::string("Unknown ") + EnumerationToString(level);
+        path = std::string("Unknown ") + EnumerationToString(GetResourceLevel(level));
       }
 
       commands_.AddOpenDirectory(path.c_str());
@@ -892,7 +1006,7 @@
     {
     }
 
-    virtual void Open(ResourceType level,
+    virtual void Open(ArchiveResourceType level,
                       const std::string& publicId) ORTHANC_OVERRIDE
     {
     }
@@ -1083,9 +1197,10 @@
 
   ArchiveJob::ArchiveJob(ServerContext& context,
                          bool isMedia,
-                         bool enableExtendedSopClass) :
+                         bool enableExtendedSopClass,
+                         ResourceType jobLevel) :
     context_(context),
-    archive_(new ArchiveIndex(ResourceType_Patient)),  // root
+    archive_(new ArchiveIndex(GetArchiveResourceType(jobLevel))),  // get patient Info from this level
     isMedia_(isMedia),
     enableExtendedSopClass_(enableExtendedSopClass),
     currentStep_(0),
@@ -1142,7 +1257,9 @@
   }
 
   
-  void ArchiveJob::AddResource(const std::string& publicId)
+  void ArchiveJob::AddResource(const std::string& publicId,
+                               bool mustExist,
+                               ResourceType expectedType)
   {
     if (writer_.get() != NULL)   // Already started
     {
@@ -1150,6 +1267,17 @@
     }
     else
     {
+      if (mustExist)
+      {
+        ResourceType type;
+        if (!context_.GetIndex().LookupResourceType(type, publicId) ||
+            type != expectedType)
+        {
+          throw OrthancException(ErrorCode_InexistentItem,
+                                 "Missing resource while creating an archive: " + publicId);
+        }
+      }
+      
       ResourceIdentifiers resource(context_.GetIndex(), publicId);
       archive_->Add(context_.GetIndex(), resource);
     }
@@ -1195,11 +1323,11 @@
     if (loaderThreads_ == 0)
     {
       // default behaviour before loaderThreads was introducted in 1.10.0
-      instanceLoader_.reset(new SynchronousInstanceLoader(context_));
+      instanceLoader_.reset(new SynchronousInstanceLoader(context_, transcode_, transferSyntax_));
     }
     else
     {
-      instanceLoader_.reset(new ThreadedInstanceLoader(context_, loaderThreads_));
+      instanceLoader_.reset(new ThreadedInstanceLoader(context_, loaderThreads_, transcode_, transferSyntax_));
     }
 
     if (writer_.get() != NULL)
@@ -1427,4 +1555,32 @@
       return false;
     }
   }
+
+  bool ArchiveJob::DeleteOutput(const std::string& key)
+  {   
+    if (key == "archive" &&
+        !mediaArchiveId_.empty())
+    {
+      SharedArchive::Accessor accessor(context_.GetMediaArchive(), mediaArchiveId_);
+
+      if (accessor.IsValid())
+      {
+        context_.GetMediaArchive().Remove(mediaArchiveId_);
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }    
+    else
+    {
+      return false;
+    }
+  }
+
+  void ArchiveJob::DeleteAllOutputs()
+  {
+    DeleteOutput("archive");
+  }
 }
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -76,7 +77,8 @@
   public:
     ArchiveJob(ServerContext& context,
                bool isMedia,
-               bool enableExtendedSopClass);
+               bool enableExtendedSopClass,
+               ResourceType jobLevel);
     
     virtual ~ArchiveJob();
 
@@ -89,7 +91,9 @@
       return description_;
     }
 
-    void AddResource(const std::string& publicId);
+    void AddResource(const std::string& publicId,
+                     bool mustExist,
+                     ResourceType expectedType);
 
     void SetTranscode(DicomTransferSyntax transferSyntax);
 
@@ -118,5 +122,9 @@
                            MimeType& mime,
                            std::string& filename,
                            const std::string& key) ORTHANC_OVERRIDE;
+
+    virtual bool DeleteOutput(const std::string& key) ORTHANC_OVERRIDE;
+
+    virtual void DeleteAllOutputs() ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancServer/Sources/ServerJobs/CleaningInstancesJob.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/CleaningInstancesJob.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/CleaningInstancesJob.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/CleaningInstancesJob.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/IStorageCommitmentFactory.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/IStorageCommitmentFactory.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/LuaJobManager.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/LuaJobManager.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/LuaJobManager.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/LuaJobManager.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/MergeStudyJob.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/MergeStudyJob.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/Operations/DeleteResourceOperation.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/DeleteResourceOperation.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/Operations/DeleteResourceOperation.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/DeleteResourceOperation.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/Operations/DicomInstanceOperationValue.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/DicomInstanceOperationValue.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/Operations/DicomInstanceOperationValue.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/DicomInstanceOperationValue.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/Operations/ModifyInstanceOperation.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/ModifyInstanceOperation.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/Operations/ModifyInstanceOperation.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/ModifyInstanceOperation.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/Operations/StorePeerOperation.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/StorePeerOperation.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/Operations/StorePeerOperation.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/StorePeerOperation.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/Operations/StoreScuOperation.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/StoreScuOperation.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/Operations/StoreScuOperation.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/StoreScuOperation.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/Operations/SystemCallOperation.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/SystemCallOperation.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/Operations/SystemCallOperation.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/Operations/SystemCallOperation.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/OrthancJobUnserializer.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/OrthancJobUnserializer.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/OrthancJobUnserializer.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/OrthancJobUnserializer.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -161,8 +162,36 @@
       return false;
     }
   };
+
+  // Reset is called when resubmitting a failed job
+  void ResourceModificationJob::Reset()
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    // TODO: cleanup the instances that have been generated during the previous run
+    modifiedSeries_.clear();
+    instancesToReconstruct_.clear();
+
+    ThreadedSetOfInstancesJob::Reset();
+  }
+
+  void ResourceModificationJob::PostProcessInstances()
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    // reconstruct the parents MainDicomTags in case one of them has changed
+    if (instancesToReconstruct_.size() > 0)
+    {
+      for (std::set<std::string>::const_iterator it = instancesToReconstruct_.begin(); it != instancesToReconstruct_.end(); ++it)
+      {
+        ServerContext::DicomCacheLocker locker(GetContext(), *it);
+        ParsedDicomFile& modifiedDicom = locker.GetDicom();
+
+        GetContext().GetIndex().ReconstructInstance(modifiedDicom, false, ResourceType_Instance /* dummy */);
+      }
+    }
     
-
+  }
 
   bool ResourceModificationJob::HandleInstance(const std::string& instance)
   {
@@ -176,14 +205,17 @@
       
     LOG(INFO) << "Modifying instance in a job: " << instance;
 
-
     /**
      * Retrieve the original instance from the DICOM cache.
      **/
     
     std::unique_ptr<DicomInstanceHasher> originalHasher;
     std::unique_ptr<ParsedDicomFile> modified;
-
+    std::set<std::string> instanceLabels;
+    std::set<std::string> seriesLabels;
+    std::set<std::string> studyLabels;
+    std::set<std::string> patientLabels;
+ 
     try
     {
       ServerContext::DicomCacheLocker locker(GetContext(), instance);
@@ -195,7 +227,7 @@
     catch (OrthancException& e)
     {
       LOG(WARNING) << "An error occurred while executing a Modification job on instance " << instance << ": " << e.GetDetails();
-      return false;
+      throw;
     }
 
 
@@ -203,7 +235,20 @@
      * Compute the resulting DICOM instance.
      **/
 
-    modification_->Apply(*modified);
+    {
+      boost::recursive_mutex::scoped_lock lock(mutex_);  // DicomModification object is not thread safe, we must protect it from here
+
+      modification_->Apply(*modified);
+
+      if (modification_->AreLabelsKept())
+      {
+        GetContext().GetIndex().ListLabels(instanceLabels, instance, ResourceType_Instance);
+        // we must also save the parent labels.  This instance might currently be the only one in the hierarchy and therefore it might be in charge of restoring all labels of the hierarchy
+        GetContext().GetIndex().ListLabels(seriesLabels, originalHasher->HashSeries(), ResourceType_Series);
+        GetContext().GetIndex().ListLabels(studyLabels, originalHasher->HashStudy(), ResourceType_Study);
+        GetContext().GetIndex().ListLabels(patientLabels, originalHasher->HashPatient(), ResourceType_Patient);
+      }
+    }
 
     const std::string modifiedUid = IDicomTranscoder::GetSopInstanceUid(modified->GetDcmtkObject());
     
@@ -242,6 +287,7 @@
 
     std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromParsedDicomFile(*modified));
     toStore->SetOrigin(origin_);
+    toStore->SetSkipIngestTranscoding(transcode_); // do not apply IngestTranscoding if you have forced the transfer syntax during the modification/anonymization
 
 
     /**
@@ -280,12 +326,25 @@
 
     std::string modifiedInstance;
     ServerContext::StoreResult result = GetContext().Store(modifiedInstance, *toStore, StoreInstanceMode_Default);
-    if (result.GetStatus() != StoreStatus_Success)
+    if (result.GetStatus() != StoreStatus_Success && result.GetStatus() != StoreStatus_AlreadyStored) // when retrying a job, we might save the same data again
     {
       throw OrthancException(ErrorCode_CannotStoreInstance,
                              "Error while storing a modified instance " + instance);
     }
 
+    {
+      boost::recursive_mutex::scoped_lock lock(mutex_);  // DicomModification object is not thread safe, we must protect it from here
+
+      if (modification_->AreLabelsKept())
+      {
+        GetContext().GetIndex().AddLabels(instance, ResourceType_Instance, instanceLabels);
+        GetContext().GetIndex().AddLabels(modifiedHasher.HashSeries(), ResourceType_Series, seriesLabels);
+        GetContext().GetIndex().AddLabels(modifiedHasher.HashStudy(), ResourceType_Study, studyLabels);
+        GetContext().GetIndex().AddLabels(modifiedHasher.HashPatient(), ResourceType_Patient, patientLabels);
+      }
+    }
+
+
     /**
      * The assertion below will fail if automated transcoding to a
      * lossy transfer syntax is enabled in the Orthanc core, and if
@@ -293,14 +352,25 @@
      **/
     // assert(modifiedInstance == modifiedHasher.HashInstance());
 
-    output_->Update(modifiedHasher);
+    {
+      boost::recursive_mutex::scoped_lock lock(outputMutex_);
+
+      output_->Update(modifiedHasher);
+      if (modifiedSeries_.find(modifiedHasher.HashSeries()) == modifiedSeries_.end())
+      {
+        modifiedSeries_.insert(modifiedHasher.HashSeries());
+        // add an instance to reconstruct for each series
+        instancesToReconstruct_.insert(modifiedHasher.HashInstance());
+      }
+      
+    }
 
     return true;
   }
 
 
-  ResourceModificationJob::ResourceModificationJob(ServerContext& context) :
-    CleaningInstancesJob(context, true /* by default, keep source */),
+  ResourceModificationJob::ResourceModificationJob(ServerContext& context, unsigned int workersCount) :
+    ThreadedSetOfInstancesJob(context, true /* post processing step */, true /* by default, keep source */, workersCount),
     isAnonymization_(false),
     transcode_(false),
     transferSyntax_(DicomTransferSyntax_LittleEndianExplicit)  // dummy initialization
@@ -368,6 +438,7 @@
   }
 
 
+#if ORTHANC_BUILD_UNIT_TESTS == 1
   const DicomModification& ResourceModificationJob::GetModification() const
   {
     if (modification_.get() == NULL)
@@ -379,7 +450,7 @@
       return *modification_;
     }
   }
-
+#endif
 
   DicomTransferSyntax ResourceModificationJob::GetTransferSyntax() const
   {
@@ -445,6 +516,8 @@
     }
     else
     {
+      boost::recursive_mutex::scoped_lock lock(outputMutex_);
+
       assert(output_.get() != NULL);
       return output_->IsSingleResource();
     }
@@ -453,6 +526,8 @@
 
   ResourceType ResourceModificationJob::GetOutputLevel() const
   {
+    boost::recursive_mutex::scoped_lock lock(outputMutex_);
+
     if (IsSingleResourceModification())
     {
       assert(modification_.get() != NULL &&
@@ -469,7 +544,9 @@
 
   void ResourceModificationJob::GetPublicContent(Json::Value& value)
   {
-    CleaningInstancesJob::GetPublicContent(value);
+    boost::recursive_mutex::scoped_lock lock(outputMutex_);
+
+    ThreadedSetOfInstancesJob::GetPublicContent(value);
 
     value["IsAnonymization"] = isAnonymization_;
 
@@ -495,7 +572,7 @@
 
   ResourceModificationJob::ResourceModificationJob(ServerContext& context,
                                                    const Json::Value& serialized) :
-    CleaningInstancesJob(context, serialized, true /* by default, keep source */),
+    ThreadedSetOfInstancesJob(context, serialized, false /* no post processing step */, true /* by default, keep source */),
     transferSyntax_(DicomTransferSyntax_LittleEndianExplicit)  // dummy initialization
   {
     assert(serialized.type() == Json::objectValue);
@@ -565,7 +642,7 @@
     {
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
     }
-    else if (!CleaningInstancesJob::Serialize(value))
+    else if (!ThreadedSetOfInstancesJob::Serialize(value))
     {
       return false;
     }
@@ -579,11 +656,17 @@
       {
         value[TRANSCODE] = GetTransferSyntaxUid(transferSyntax_);
       }
-      
+
       origin_.Serialize(value[ORIGIN]);
       
       Json::Value tmp;
-      modification_->Serialize(tmp);
+
+      {
+        boost::recursive_mutex::scoped_lock lock(mutex_);  // DicomModification object is not thread safe, we must protect it from here
+  
+        modification_->Serialize(tmp);
+      }
+
       value[MODIFICATION] = tmp;
 
       // New in Orthanc 1.9.4
@@ -596,4 +679,131 @@
       return true;
     }
   }
+
+  void ResourceModificationJob::PerformSanityChecks()
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);  // because we access the parentResources_
+
+    std::set<DicomTag> emptyRequestedTags;
+
+    if (modification_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    bool replacePatientMainDicomTags = false;
+
+    ResourceType modificationLevel = modification_->GetLevel();
+    std::set<DicomTag> replacedTags;
+    modification_->GetReplacedTags(replacedTags);
+
+    for (std::set<DicomTag>::const_iterator it = replacedTags.begin(); it != replacedTags.end(); ++it)
+    {
+      replacePatientMainDicomTags |= DicomMap::IsMainDicomTag(*it, ResourceType_Patient);
+    }
+
+    if ((modificationLevel == ResourceType_Study ||
+         modificationLevel == ResourceType_Patient) &&
+        !modification_->IsReplaced(DICOM_TAG_PATIENT_ID) &&
+        modification_->IsKept(DICOM_TAG_STUDY_INSTANCE_UID) &&
+        modification_->IsKept(DICOM_TAG_SERIES_INSTANCE_UID) &&
+        modification_->IsKept(DICOM_TAG_SOP_INSTANCE_UID))
+    {
+      // if we keep the SOPInstanceUID, it very likely means that we are modifying existing resources 'in place'
+
+      // we must make sure we do not delete them at the end of the job
+      if (!IsKeepSource()) // note: we can refine this criteria -> this is valid only if all DicomUIDs are kept identical (but this can happen through Keep or Replace options)
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                              "When keeping StudyInstanceUID, SeriesInstanceUID and SOPInstanceUID tag, you must set KeepSource to true to avoid deleting the modified files at the end of the process");
+      }
+
+      // and we must make sure that we overwite them with the modified resources
+      if (IsKeepSource() && !GetContext().IsOverwriteInstances())
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                              "When keeping StudyInstanceUID, SeriesInstanceUID and SOPInstanceUID tag, you must have the 'OverwriteInstances' Orthanc configuration set to true in order to replace the modified resources");
+      }
+    }
+
+    if (modificationLevel == ResourceType_Study && replacePatientMainDicomTags)
+    {
+      for (std::set<std::string>::const_iterator studyId = parentResources_.begin(); studyId != parentResources_.end(); ++studyId)
+      {
+        // When modifying a study, you may not modify patient tags as you wish.
+        // - If this is the patient's only study, you may modify all patient tags. This could be performed in 2 steps (modify the patient and then, the study) but, 
+        //   for many use cases, it's helpful to be able to do it one step (e.g, to modify a name in a study that has just been acquired)
+        // - If the patient already has other studies, you may only 'attach' the study to an existing patient by modifying 
+        //   all patient tags from the study to match those of the target patient.
+        // - Otherwise, you can't modify the patient tags
+        
+        std::string targetPatientId;
+        if (modification_->IsReplaced(DICOM_TAG_PATIENT_ID))
+        {
+          targetPatientId = modification_->GetReplacementAsString(DICOM_TAG_PATIENT_ID);
+        }
+        else
+        {
+          ExpandedResource originalStudy;
+          if (GetContext().GetIndex().ExpandResource(originalStudy, *studyId, ResourceType_Study, emptyRequestedTags, ExpandResourceFlags_IncludeMainDicomTags))
+          {
+            targetPatientId = originalStudy.GetMainDicomTags().GetStringValue(DICOM_TAG_PATIENT_ID, "", false);
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_UnknownResource, "Study not found");
+          }
+        }
+
+        // try to find the targetPatient
+        std::vector<std::string> lookupPatientResult;
+        GetContext().GetIndex().LookupIdentifierExact(lookupPatientResult, ResourceType_Patient, DICOM_TAG_PATIENT_ID, targetPatientId);
+
+        // 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)))
+          {
+            const std::list<std::string> childrenIds = targetPatient.childrenIds_;
+            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
+            }
+
+            if (targetPatientHasOtherStudies)
+            {
+              // this is allowed if all patient replacedTags do match the target patient tags
+              DicomMap targetPatientTags;
+              targetPatient.GetMainDicomTags().ExtractPatientInformation(targetPatientTags);
+
+              std::set<DicomTag> mainPatientTags;
+              DicomMap::GetMainDicomTags(mainPatientTags, ResourceType_Patient);
+              
+              for (std::set<DicomTag>::const_iterator mainPatientTag = mainPatientTags.begin();
+                   mainPatientTag != mainPatientTags.end(); ++mainPatientTag)
+              {
+                if (targetPatientTags.HasTag(*mainPatientTag) &&
+                    (!modification_->IsReplaced(*mainPatientTag) ||
+                     modification_->GetReplacementAsString(*mainPatientTag) != targetPatientTags.GetStringValue(*mainPatientTag, "", false)))
+                {
+                  throw OrthancException(ErrorCode_BadRequest, std::string("Trying to change patient tags in a study.  " 
+                    "The Patient already exists and has other studies.  All the 'Replace' tags should match the existing patient main dicom tags "
+                    "and you should specify all Patient MainDicomTags in your query.  Try using /patients/../modify instead to modify the patient. Failing tag: ") + mainPatientTag->Format());
+                }
+                else if (!targetPatientTags.HasTag(*mainPatientTag) && modification_->IsReplaced(*mainPatientTag) )
+                {
+                  throw OrthancException(ErrorCode_BadRequest, std::string("Trying to change patient tags in a study.  "
+                    "The Patient already exists and has other studies.  You are trying to replace a tag that is not defined yet in this patient. " 
+                    "Try using /patients/../modify instead to modify the patient. Failing tag: ") + mainPatientTag->Format());
+                }
+              }
+            }
+          }
+        }
+      }      
+    }
+  }
 }
--- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -23,14 +24,16 @@
 #pragma once
 
 #include "../../../OrthancFramework/Sources/DicomParsing/DicomModification.h"
+#include "../../../OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.h"
 #include "../DicomInstanceOrigin.h"
-#include "CleaningInstancesJob.h"
+#include "ThreadedSetOfInstancesJob.h"
+#include <boost/thread/recursive_mutex.hpp>
 
 namespace Orthanc
 {
   class ServerContext;
   
-  class ResourceModificationJob : public CleaningInstancesJob
+  class ResourceModificationJob : public ThreadedSetOfInstancesJob
   {
   private:
     class IOutput : public boost::noncopyable
@@ -49,6 +52,8 @@
     
     class SingleOutput;
     class MultipleOutputs;
+
+    mutable boost::recursive_mutex      outputMutex_;
     
     std::unique_ptr<DicomModification>  modification_;
     boost::shared_ptr<IOutput>          output_;
@@ -56,12 +61,16 @@
     DicomInstanceOrigin                 origin_;
     bool                                transcode_;
     DicomTransferSyntax                 transferSyntax_;
+    std::set<std::string>               modifiedSeries_;          // the list of new series ids of the newly generated series
+    std::set<std::string>               instancesToReconstruct_;  // for each new series generated, an instance id that we can use to reconstruct the hierarchy DB model
 
   protected:
     virtual bool HandleInstance(const std::string& instance) ORTHANC_OVERRIDE;
     
+    virtual void PostProcessInstances() ORTHANC_OVERRIDE;
+
   public:
-    explicit ResourceModificationJob(ServerContext& context);
+    explicit ResourceModificationJob(ServerContext& context, unsigned int workersCount);
 
     ResourceModificationJob(ServerContext& context,
                             const Json::Value& serialized);
@@ -79,8 +88,6 @@
 
     void SetOrigin(const RestApiCall& call);
 
-    const DicomModification& GetModification() const;
-
     bool IsAnonymization() const
     {
       return isAnonymization_;
@@ -109,10 +116,6 @@
     // Only possible if "IsSingleResourceModification()"
     ResourceType GetOutputLevel() const;
 
-    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE
-    {
-    }
-
     virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
     {
       target = "ResourceModification";
@@ -121,5 +124,13 @@
     virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
     
     virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE;
+
+    virtual void Reset() ORTHANC_OVERRIDE;
+
+    void PerformSanityChecks();
+
+#if ORTHANC_BUILD_UNIT_TESTS == 1
+    const DicomModification& GetModification() const;
+#endif
   };
 }
--- a/OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/SplitStudyJob.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/SplitStudyJob.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,605 @@
+/**
+ * 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 "ThreadedSetOfInstancesJob.h"
+
+#include "../../../OrthancFramework/Sources/Logging.h"
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+#include "../../../OrthancFramework/Sources/SerializationToolbox.h"
+#include "../ServerContext.h"
+
+#include <boost/lexical_cast.hpp>
+#include <cassert>
+
+namespace Orthanc
+{
+  static const char* EXIT_WORKER_MESSAGE = "exit";
+
+   ThreadedSetOfInstancesJob::ThreadedSetOfInstancesJob(ServerContext& context,
+                                                        bool hasPostProcessing,
+                                                        bool keepSource,
+                                                        size_t workersCount) :
+    hasPostProcessing_(hasPostProcessing),
+    started_(false),
+    stopRequested_(false),
+    permissive_(false),
+    currentStep_(ThreadedJobStep_NotStarted),
+    workersCount_(workersCount),
+    context_(context),
+    keepSource_(keepSource),
+    errorCode_(ErrorCode_Success)
+  {
+  }
+
+
+  ThreadedSetOfInstancesJob::~ThreadedSetOfInstancesJob()
+  {
+    // no need to lock mutex here since we access variables used only by the "master" thread
+
+    StopWorkers();
+    WaitWorkersComplete();
+  }
+
+
+  void ThreadedSetOfInstancesJob::InitWorkers(size_t workersCount)
+  {
+    // no need to lock mutex here since we access variables used only by the "master" thread
+
+    for (size_t i = 0; i < workersCount; i++)
+    {
+      instancesWorkers_.push_back(boost::shared_ptr<boost::thread>(new boost::thread(InstanceWorkerThread, this)));
+    }
+  }
+
+
+  void ThreadedSetOfInstancesJob::WaitWorkersComplete()
+  {
+    // no need to lock mutex here since we access variables used only by the "master" thread
+
+    // send a dummy "exit" message to all workers such that they stop waiting for messages on the queue
+    for (size_t i = 0; i < instancesWorkers_.size(); i++)
+    {
+      instancesToProcessQueue_.Enqueue(new SingleValueObject<std::string>(EXIT_WORKER_MESSAGE));
+    }
+
+    for (size_t i = 0; i < instancesWorkers_.size(); i++)
+    {
+      if (instancesWorkers_[i]->joinable())
+      {
+        instancesWorkers_[i]->join();
+      }
+    }
+
+    instancesWorkers_.clear();
+  }
+
+
+  void ThreadedSetOfInstancesJob::StopWorkers()
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    instancesToProcessQueue_.Clear();
+    stopRequested_ = true;
+  }
+
+
+  void ThreadedSetOfInstancesJob::Stop(JobStopReason reason)
+  {
+    // no need to lock mutex here since we access variables used or set only by the "master" thread
+
+    if (reason == JobStopReason_Canceled ||
+        reason == JobStopReason_Failure ||
+        reason == JobStopReason_Retry)
+    {
+      // deallocate resources
+      StopWorkers();
+      WaitWorkersComplete();
+    }
+    else if (reason == JobStopReason_Paused)
+    {
+      // keep resources allocated.
+      // note that, right now, since all instances are queued from the start, this kind of jobs is not paused while in ProcessingInstances state
+    }
+  }
+
+
+  JobStepResult ThreadedSetOfInstancesJob::Step(const std::string& jobId)
+  {
+    // no need to lock mutex here since we access variables used or set only by the "master" thread
+
+    if (!started_)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    if (GetInstancesCount() == 0)
+    {
+      // No instances to handle: We're done
+      return JobStepResult::Success();
+    }
+    
+    try
+    {
+      if (currentStep_ == ThreadedJobStep_NotStarted)
+      {
+        // create the workers and enqueue all instances
+        for (std::set<std::string>::const_iterator it = instancesToProcess_.begin(); it != instancesToProcess_.end(); ++it)
+        {
+          instancesToProcessQueue_.Enqueue(new SingleValueObject<std::string>(*it));
+        }
+
+        InitWorkers(workersCount_);
+        currentStep_ = ThreadedJobStep_ProcessingInstances;
+      }
+      else if (currentStep_ == ThreadedJobStep_ProcessingInstances)
+      {
+        // wait until all instances are processed by the workers
+        if (instancesToProcessQueue_.GetSize() != 0)
+        {
+          // "slow down" the job main thread, to avoid using 100% of a core simply to check that other threads are done
+          boost::this_thread::sleep(boost::posix_time::milliseconds(5));
+
+          return JobStepResult::Continue();
+        }
+        else
+        {
+          WaitWorkersComplete();
+
+          // check job has really completed !!! it might have been interrupted because of an error
+          if ((processedInstances_.size() != instancesToProcess_.size())
+            || (!IsPermissive() && failedInstances_.size() > 0))
+          {
+            return JobStepResult::Failure(GetErrorCode(), NULL);
+          }
+
+          currentStep_ = ThreadedJobStep_PostProcessingInstances;
+          return JobStepResult::Continue();
+        }
+      }
+      else if (currentStep_ == ThreadedJobStep_PostProcessingInstances)
+      {
+        if (HasPostProcessingStep())
+        {
+          PostProcessInstances();
+        }
+
+        currentStep_ = ThreadedJobStep_Cleanup;
+        return JobStepResult::Continue();
+      }
+      else if (currentStep_ == ThreadedJobStep_Cleanup)
+      {
+        // clean after the post processing step
+        if (HasCleanupStep())
+        {
+          for (std::set<std::string>::const_iterator it = instancesToProcess_.begin(); it != instancesToProcess_.end(); ++it)
+          {
+            Json::Value tmp;
+            context_.DeleteResource(tmp, *it, ResourceType_Instance);
+          }
+        }
+
+        currentStep_ = ThreadedJobStep_Done;
+        return JobStepResult::Success();
+      }
+    }
+    catch (OrthancException& e)
+    {
+      if (permissive_)
+      {
+        LOG(WARNING) << "Ignoring an error in a permissive job: " << e.What();
+      }
+      else
+      {
+        return JobStepResult::Failure(e);
+      }
+    }
+
+    return JobStepResult::Continue();
+  }
+
+
+  bool ThreadedSetOfInstancesJob::HasPostProcessingStep() const
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    return hasPostProcessing_;
+  }
+
+  void ThreadedSetOfInstancesJob::PostProcessInstances()
+  {
+    if (HasPostProcessingStep())
+    {
+      throw OrthancException(ErrorCode_InternalError, "Job with post-processing should override PostProcessInstances");
+    }
+  }
+
+
+  bool ThreadedSetOfInstancesJob::HasCleanupStep() const
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    return !keepSource_;
+  }
+
+
+  void ThreadedSetOfInstancesJob::InstanceWorkerThread(ThreadedSetOfInstancesJob* that)
+  {
+    static uint16_t threadCounter = 0;
+    Logging::SetCurrentThreadName(std::string("JOB-INS-WORK-") + boost::lexical_cast<std::string>(threadCounter++));
+    threadCounter %= 1000;
+
+    while (true)
+    {
+      std::unique_ptr<SingleValueObject<std::string> > instanceId(dynamic_cast<SingleValueObject<std::string>*>(that->instancesToProcessQueue_.Dequeue(0)));
+      if (that->stopRequested_                // no lock(mutex) to access this variable, this is safe since it's just reading a boolean
+        || instanceId->GetValue() == EXIT_WORKER_MESSAGE)
+      {
+        return;
+      }
+
+      bool processed = false;
+      
+      try
+      {
+        processed = that->HandleInstance(instanceId->GetValue());
+      }
+      catch (const Orthanc::OrthancException& e)
+      {
+        if (that->IsPermissive())
+        {
+          LOG(WARNING) << "Ignoring an error in a permissive job: " << e.What();
+        }
+        else
+        {
+          LOG(ERROR) << "Error in a non-permissive job: " << e.What();
+          that->SetErrorCode(e.GetErrorCode());
+          that->StopWorkers();
+          return;
+        }
+      }
+      catch (...)
+      {
+        LOG(ERROR) << "Native exception while executing a job";
+        that->SetErrorCode(ErrorCode_InternalError);
+        that->StopWorkers();
+        return;
+      }
+
+      {
+        boost::recursive_mutex::scoped_lock lock(that->mutex_);
+        
+        that->processedInstances_.insert(instanceId->GetValue());
+
+        if (!processed)
+        {
+          that->failedInstances_.insert(instanceId->GetValue()); 
+        }
+      }
+
+    }
+  }
+
+
+  size_t ThreadedSetOfInstancesJob::GetInstancesCount() const
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+    
+    return instancesToProcess_.size();
+  }
+
+
+  void ThreadedSetOfInstancesJob::GetFailedInstances(std::set<std::string>& target) const
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    target = failedInstances_;
+  }
+
+
+  void ThreadedSetOfInstancesJob::GetInstances(std::set<std::string>& target) const
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    target = instancesToProcess_;
+  }
+
+
+  bool ThreadedSetOfInstancesJob::IsFailedInstance(const std::string &instance) const
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    return failedInstances_.find(instance) != failedInstances_.end();
+  }
+
+
+  void ThreadedSetOfInstancesJob::Start()
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    started_ = true;
+  }
+
+
+  // Reset is called when resubmitting a failed job
+  void ThreadedSetOfInstancesJob::Reset()
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    if (started_)
+    {
+      // We actually cannot clean the instances that would have been generated during a previous run
+      // because the generated instances may or may not have the same orthanc ids as the source
+      // it is too dangerous to guess if they should be deleted or not
+      currentStep_ = ThreadedJobStep_NotStarted;
+      stopRequested_ = false;
+      processedInstances_.clear();
+      failedInstances_.clear();
+      instancesToProcessQueue_.Clear();
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  static const char* KEY_FAILED_INSTANCES = "FailedInstances";
+  static const char* KEY_PARENT_RESOURCES = "ParentResources";
+  static const char* KEY_DESCRIPTION = "Description";
+  static const char* KEY_PERMISSIVE = "Permissive";
+  static const char* KEY_CURRENT_STEP = "CurrentStep";
+  static const char* KEY_TYPE = "Type";
+  static const char* KEY_INSTANCES = "Instances";
+  static const char* KEY_INSTANCES_COUNT = "InstancesCount";
+  static const char* KEY_FAILED_INSTANCES_COUNT = "FailedInstancesCount";
+  static const char* KEY_KEEP_SOURCE = "KeepSource";
+  static const char* KEY_WORKERS_COUNT = "WorkersCount";
+
+
+  void ThreadedSetOfInstancesJob::GetPublicContent(Json::Value& target)
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    target[KEY_DESCRIPTION] = GetDescription();
+    target[KEY_INSTANCES_COUNT] = static_cast<uint32_t>(GetInstancesCount());
+    target[KEY_FAILED_INSTANCES_COUNT] = static_cast<uint32_t>(failedInstances_.size());
+
+    if (!parentResources_.empty())
+    {
+      SerializationToolbox::WriteSetOfStrings(target, parentResources_, KEY_PARENT_RESOURCES);
+    }
+  }
+
+
+  bool ThreadedSetOfInstancesJob::Serialize(Json::Value& target)
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    target = Json::objectValue;
+
+    std::string type;
+    GetJobType(type);
+    target[KEY_TYPE] = type;
+    
+    target[KEY_PERMISSIVE] = permissive_;
+    target[KEY_CURRENT_STEP] = static_cast<unsigned int>(currentStep_);
+    target[KEY_DESCRIPTION] = description_;
+    target[KEY_KEEP_SOURCE] = keepSource_;
+    target[KEY_WORKERS_COUNT] = static_cast<unsigned int>(workersCount_);
+    
+    SerializationToolbox::WriteSetOfStrings(target, instancesToProcess_, KEY_INSTANCES);
+    SerializationToolbox::WriteSetOfStrings(target, failedInstances_, KEY_FAILED_INSTANCES);
+    SerializationToolbox::WriteSetOfStrings(target, parentResources_, KEY_PARENT_RESOURCES);
+
+    return true;
+  }
+
+
+  ThreadedSetOfInstancesJob::ThreadedSetOfInstancesJob(ServerContext& context,
+                                                       const Json::Value& source,
+                                                       bool hasPostProcessing,
+                                                       bool defaultKeepSource) :
+    hasPostProcessing_(hasPostProcessing),
+    started_(false),
+    stopRequested_(false),
+    permissive_(false),
+    currentStep_(ThreadedJobStep_NotStarted),
+    workersCount_(1),
+    context_(context),
+    keepSource_(defaultKeepSource),
+    errorCode_(ErrorCode_Success)
+  {
+    SerializationToolbox::ReadSetOfStrings(failedInstances_, source, KEY_FAILED_INSTANCES);
+
+    if (source.isMember(KEY_PARENT_RESOURCES))
+    {
+      // Backward compatibility with Orthanc <= 1.5.6
+      SerializationToolbox::ReadSetOfStrings(parentResources_, source, KEY_PARENT_RESOURCES);
+    }
+    
+    if (source.isMember(KEY_KEEP_SOURCE))
+    {
+      keepSource_ = SerializationToolbox::ReadBoolean(source, KEY_KEEP_SOURCE);
+    }
+
+    if (source.isMember(KEY_WORKERS_COUNT))
+    {
+      workersCount_ = SerializationToolbox::ReadUnsignedInteger(source, KEY_WORKERS_COUNT);
+    }
+
+    if (source.isMember(KEY_INSTANCES))
+    {
+      SerializationToolbox::ReadSetOfStrings(instancesToProcess_, source, KEY_INSTANCES);
+    }
+
+    if (source.isMember(KEY_CURRENT_STEP))
+    {
+      currentStep_ = static_cast<ThreadedJobStep>(SerializationToolbox::ReadUnsignedInteger(source, KEY_CURRENT_STEP));
+    }
+  }
+
+
+  void ThreadedSetOfInstancesJob::SetKeepSource(bool keep)
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    keepSource_ = keep;
+  }
+
+  bool ThreadedSetOfInstancesJob::IsKeepSource() const
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    return keepSource_;
+  }
+
+  float ThreadedSetOfInstancesJob::GetProgress()
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    if (GetInstancesCount() == 0)
+    {
+      return 1;
+    }
+    else
+    {
+      size_t totalProgress = GetInstancesCount();
+      size_t currentProgress = processedInstances_.size();
+      
+      if (HasPostProcessingStep())
+      {
+        ++totalProgress;
+        if (currentStep_ > ThreadedJobStep_PostProcessingInstances)
+        {
+          ++currentProgress;
+        }
+      }
+
+      if (HasCleanupStep())
+      {
+        ++totalProgress;
+        if (currentStep_ > ThreadedJobStep_Cleanup)
+        {
+          ++currentProgress;
+        }
+      }
+
+      return (static_cast<float>(currentProgress) /
+              static_cast<float>(totalProgress));
+    }
+  }
+
+
+  ThreadedSetOfInstancesJob::ThreadedJobStep ThreadedSetOfInstancesJob::GetCurrentStep() const
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    return currentStep_;
+  }
+
+
+  void ThreadedSetOfInstancesJob::SetDescription(const std::string &description)
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    description_ = description;
+  }
+
+
+  const std::string& ThreadedSetOfInstancesJob::GetDescription() const
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    return description_;
+  }
+
+  void ThreadedSetOfInstancesJob::SetErrorCode(ErrorCode errorCode)
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    errorCode_ = errorCode;
+  }
+
+  ErrorCode ThreadedSetOfInstancesJob::GetErrorCode() const
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    return errorCode_;
+  }
+
+  bool ThreadedSetOfInstancesJob::IsPermissive() const
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    return permissive_;
+  }
+
+
+  void ThreadedSetOfInstancesJob::SetPermissive(bool permissive)
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    if (started_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      permissive_ = permissive;
+    }
+  }
+
+
+  bool ThreadedSetOfInstancesJob::IsStarted() const
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    return started_;
+  }
+
+
+  void ThreadedSetOfInstancesJob::AddInstances(const std::list<std::string>& instances)
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    for (std::list<std::string>::const_iterator
+           it = instances.begin(); it != instances.end(); ++it)
+    {
+      instancesToProcess_.insert(*it);
+    }
+  }
+
+
+  void ThreadedSetOfInstancesJob::AddParentResource(const std::string &resource)
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    parentResources_.insert(resource);
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.h	Tue Sep 24 11:39:52 2024 +0200
@@ -0,0 +1,174 @@
+/**
+ * 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/Compatibility.h"  // For ORTHANC_OVERRIDE
+#include "../../../OrthancFramework/Sources/JobsEngine/IJob.h"
+#include "../../../OrthancFramework/Sources/MultiThreading/SharedMessageQueue.h"
+
+#include <set>
+#include <boost/thread/recursive_mutex.hpp>
+#include <boost/thread.hpp>
+
+namespace Orthanc
+{
+  class ServerContext;
+
+  // This class is a threaded version of SetOfInstancesJob merged with CleaningInstancesJob
+  class ORTHANC_PUBLIC ThreadedSetOfInstancesJob : public IJob
+  {
+  public:
+    enum ThreadedJobStep  // cannot use "Step" since there is a method with this name !
+    {
+      ThreadedJobStep_NotStarted,
+      ThreadedJobStep_ProcessingInstances,
+      ThreadedJobStep_PostProcessingInstances,
+      ThreadedJobStep_Cleanup,
+      ThreadedJobStep_Done
+    };
+
+  private:
+    std::set<std::string>               instancesToProcess_;  // the list of source instances ids to process
+    std::set<std::string>               failedInstances_;     // the list of source instances ids that failed processing
+    std::set<std::string>               processedInstances_;  // the list of source instances ids that have been processed (including failed ones)
+
+    SharedMessageQueue                  instancesToProcessQueue_;
+    std::vector<boost::shared_ptr<boost::thread> >         instancesWorkers_;
+
+    bool                    hasPostProcessing_;  // final step before "KeepSource" cleanup
+    bool                    started_;
+    bool                    stopRequested_;
+    bool                    permissive_;
+    ThreadedJobStep         currentStep_;
+    std::string             description_;
+    size_t                  workersCount_;
+
+    ServerContext&          context_;
+    bool                    keepSource_;
+    ErrorCode               errorCode_;
+  
+  protected:
+    mutable boost::recursive_mutex      mutex_;
+    std::set<std::string>               parentResources_;
+
+  public:
+    ThreadedSetOfInstancesJob(ServerContext& context,
+                              bool hasTrailingStep,
+                              bool keepSource,
+                              size_t workersCount);
+
+    explicit ThreadedSetOfInstancesJob(ServerContext& context,
+                                       const Json::Value& source,
+                                       bool hasTrailingStep,
+                                       bool defaultKeepSource);
+
+    virtual ~ThreadedSetOfInstancesJob();
+
+  protected:
+    virtual bool HandleInstance(const std::string& instance) = 0;
+
+    virtual void PostProcessInstances();
+
+    void InitWorkers(size_t workersCount);
+
+    void StopWorkers();
+
+    void WaitWorkersComplete();
+
+    static void InstanceWorkerThread(ThreadedSetOfInstancesJob* that);
+
+    const std::string& GetInstance(size_t index) const;
+
+    bool HasPostProcessingStep() const;
+
+    bool HasCleanupStep() const;
+
+    void SetErrorCode(ErrorCode errorCode);
+
+    ErrorCode GetErrorCode() const;
+
+  public:
+
+    ThreadedJobStep GetCurrentStep() const;
+
+    void SetDescription(const std::string& description);
+
+    const std::string& GetDescription() const;
+
+    void SetKeepSource(bool keep);
+
+    bool IsKeepSource() const;
+
+    void GetInstances(std::set<std::string>& target) const;
+
+    void GetFailedInstances(std::set<std::string>& target) const;
+
+    size_t GetInstancesCount() const;
+
+    void AddInstances(const std::list<std::string>& instances);
+
+    void AddParentResource(const std::string &resource);
+
+    bool IsPermissive() const;
+
+    void SetPermissive(bool permissive);
+
+    virtual void Reset() ORTHANC_OVERRIDE;
+    
+    virtual void Start() ORTHANC_OVERRIDE;
+    
+    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
+
+    virtual float GetProgress() ORTHANC_OVERRIDE;
+
+    bool IsStarted() const;
+
+    virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE;
+    
+    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    
+    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+
+    virtual bool GetOutput(std::string& output,
+                           MimeType& mime,
+                           std::string& filename,
+                           const std::string& key) ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+
+    virtual bool DeleteOutput(const std::string& key) ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+
+    bool IsFailedInstance(const std::string& instance) const;
+
+    ServerContext& GetContext() const
+    {
+      return context_;
+    }
+
+  };
+}
--- a/OrthancServer/Sources/ServerToolbox.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerToolbox.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -144,7 +145,7 @@
         try
         {
           // Read and parse the content of the DICOM file
-          StorageAccessor accessor(storageArea, NULL);  // no cache
+          StorageAccessor accessor(storageArea);  // no cache
 
           std::string content;
           accessor.Read(content, attachment);
@@ -211,6 +212,31 @@
       std::string t;
       t.reserve(value.size());
 
+#if 0
+      // This version solves some indexing issue (https://discourse.orthanc-server.org/t/postgress-index-effectively-disabled-when-searching-for-greek-names/3371)
+      // and seems functional: I could run the integration tests with both SQLite and PG + the DicomWeb tests with PG.
+      // However, it can not go into production because NormalizeIdentifier is used both at ingest time and at search time;
+      // therefore, if we change it while we have an already populated DB, the searches won't work anymore and, on very large
+      // systems, running the Housekeeper to rebuild the indexes might take months ...
+      // We keep it here because it might be handy once we refactor the DicomIdentifier searches in the future.
+      for (size_t i = 0; i < value.size(); i++)
+      {
+        if (value[i] == '%' ||
+            value[i] == '_')
+        {
+          t.push_back(' ');  // These characters might break wildcard queries in SQL
+        }
+        else if (//isascii(value[i]) &&
+                 !iscntrl(value[i]) &&
+                 (!isspace(value[i]) || value[i] == ' '))
+        {
+          t.push_back(value[i]);
+        }
+      }
+
+      //Toolbox::ToUpperCase(t);
+      t = Toolbox::ToUpperCaseWithAccents(t);
+#else
       for (size_t i = 0; i < value.size(); i++)
       {
         if (value[i] == '%' ||
@@ -227,6 +253,7 @@
       }
 
       Toolbox::ToUpperCase(t);
+#endif
 
       return Toolbox::StripSpaces(t);
     }
@@ -254,44 +281,87 @@
     
     void ReconstructResource(ServerContext& context,
                              const std::string& resource,
-                             bool reconstructFiles)
+                             bool reconstructFiles,
+                             bool limitToThisLevelDicomTags,
+                             ResourceType limitToLevel)
     {
       LOG(WARNING) << "Reconstructing resource " << resource;
       
       std::list<std::string> instances;
       context.GetIndex().GetChildInstances(instances, resource);
 
-      for (std::list<std::string>::const_iterator 
-             it = instances.begin(); it != instances.end(); ++it)
+
+      if (limitToThisLevelDicomTags && instances.size() > 0) // in this case, we only need to rebuild one instance !
       {
-        ServerContext::DicomCacheLocker locker(context, *it);
-
-        // Delay the reconstruction of DICOM-as-JSON to its next access through "ServerContext"
-        context.GetIndex().DeleteAttachment(*it, FileContentType_DicomAsJson, false /* no revision */,
-                                            -1 /* dummy revision */, "" /* dummy MD5 */);
-        
-        context.GetIndex().ReconstructInstance(locker.GetDicom());
-
-        if (reconstructFiles)
+        ServerContext::DicomCacheLocker locker(context, instances.front());
+        context.GetIndex().ReconstructInstance(locker.GetDicom(), true, limitToLevel);
+      }
+      else
+      {
+        for (std::list<std::string>::const_iterator 
+              it = instances.begin(); it != instances.end(); ++it)
         {
-          // preserve metadata from old resource
-          typedef std::map<MetadataType, std::string>  InstanceMetadata;
-          InstanceMetadata  instanceMetadata;
+          ServerContext::DicomCacheLocker locker(context, *it);
 
-          std::string resultPublicId;  // ignored
-          std::unique_ptr<DicomInstanceToStore> dicomInstancetoStore(DicomInstanceToStore::CreateFromParsedDicomFile(locker.GetDicom()));
-
-          context.GetIndex().GetAllMetadata(instanceMetadata, *it, ResourceType_Instance);
+          // Delay the reconstruction of DICOM-as-JSON to its next access through "ServerContext"
+          context.GetIndex().DeleteAttachment(*it, FileContentType_DicomAsJson, false /* no revision */,
+                                              -1 /* dummy revision */, "" /* dummy MD5 */);
           
-          for (InstanceMetadata::const_iterator itm = instanceMetadata.begin();
-              itm != instanceMetadata.end(); ++itm)
+          context.GetIndex().ReconstructInstance(locker.GetDicom(), false, ResourceType_Instance /* dummy */);
+
+          if (reconstructFiles)
           {
-            dicomInstancetoStore->AddMetadata(ResourceType_Instance, itm->first, itm->second);
+            std::string resultPublicId;  // ignored
+            std::unique_ptr<DicomInstanceToStore> dicomInstancetoStore(DicomInstanceToStore::CreateFromParsedDicomFile(locker.GetDicom()));
+
+            // TODO: TranscodeAndStore and specifically ServerIndex::Store have been "poluted" by the isReconstruct parameter
+            // we should very likely refactor it
+            context.TranscodeAndStore(resultPublicId, dicomInstancetoStore.get(), StoreInstanceMode_OverwriteDuplicate, true);
           }
+        }
+      }
+    }
+
+    
+    bool IsValidLabel(const std::string& label)
+    {
+      if (label.empty())
+      {
+        return false;
+      }
 
-          context.TranscodeAndStore(resultPublicId, dicomInstancetoStore.get(), StoreInstanceMode_OverwriteDuplicate, true);
+      if (label.size() > 64)
+      {
+        // This limitation is for MySQL, which cannot use a TEXT
+        // column of undefined length as a primary key
+        return false;
+      }
+      
+      for (size_t i = 0; i < label.size(); i++)
+      {
+        if (!(label[i] == '_' ||
+              label[i] == '-' ||
+              (label[i] >= 'a' && label[i] <= 'z') ||
+              (label[i] >= 'A' && label[i] <= 'Z') ||
+              (label[i] >= '0' && label[i] <= '9')))
+        {
+          return false;
         }
       }
+
+      return true;
+    }
+
+
+    void CheckValidLabel(const std::string& label)
+    {
+      if (!IsValidLabel(label))
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange,
+                               "A label must be a non-empty, alphanumeric string, "
+                               "possibly with '_' or '-' characters, "
+                               "with maximum 64 characters, but got: " + label);
+      }
     }
   }
 }
--- a/OrthancServer/Sources/ServerToolbox.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/ServerToolbox.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -55,6 +56,12 @@
 
     void ReconstructResource(ServerContext& context,
                              const std::string& resource,
-                             bool reconstructFiles);
+                             bool reconstructFiles,
+                             bool limitToThisLevelDicomTags,
+                             ResourceType limitToLevel);
+
+    bool IsValidLabel(const std::string& label);
+
+    void CheckValidLabel(const std::string& label);
   }
 }
--- a/OrthancServer/Sources/SliceOrdering.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/SliceOrdering.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/SliceOrdering.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/SliceOrdering.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/StorageCommitmentReports.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/StorageCommitmentReports.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/StorageCommitmentReports.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/StorageCommitmentReports.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/Sources/main.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/Sources/main.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -58,8 +59,10 @@
 static const char* const KEY_DICOM_TLS_ENABLED = "DicomTlsEnabled";
 static const char* const KEY_DICOM_TLS_CERTIFICATE = "DicomTlsCertificate";
 static const char* const KEY_DICOM_TLS_TRUSTED_CERTIFICATES = "DicomTlsTrustedCertificates";
+static const char* const KEY_DICOM_TLS_REMOTE_CERTIFICATE_REQUIRED = "DicomTlsRemoteCertificateRequired";
+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_DICOM_TLS_REMOTE_CERTIFICATE_REQUIRED = "DicomTlsRemoteCertificateRequired";
 
 
 class OrthancStoreRequestHandler : public IStoreRequestHandler
@@ -682,7 +685,7 @@
     << "case of a directory, all the JSON files it contains will be merged. " << std::endl
     << "If no configuration path is given on the command line, a set of default " << std::endl
     << "parameters is used. Please refer to the Orthanc Book for the full " << std::endl
-    << "instructions about how to use Orthanc <http://book.orthanc-server.com/>." << std::endl
+    << "instructions about how to use Orthanc <https://orthanc.uclouvain.be/book/>." << std::endl
     << std::endl
     << "Pay attention to the fact that the order of the options is important." << std::endl
     << "Options are read left to right. In particular, options such as \"--verbose\" can " << std::endl
@@ -742,8 +745,9 @@
   std::cout
     << path << " " << ORTHANC_VERSION << std::endl
     << "Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics Department, University Hospital of Liege (Belgium)" << std::endl
-    << "Copyright (C) 2017-2022 Osimis S.A. (Belgium)" << std::endl
-    << "Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain (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
     << "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
@@ -819,6 +823,8 @@
     PrintErrorCode(ErrorCode_DatabaseCannotSerialize, "Database could not serialize access due to concurrent update, the transaction should be retried");
     PrintErrorCode(ErrorCode_Revision, "A bad revision number was provided, which might indicate conflict between multiple writers");
     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_SQLiteNotOpened, "SQLite: The database is not opened");
     PrintErrorCode(ErrorCode_SQLiteAlreadyOpened, "SQLite: Connection is already open");
     PrintErrorCode(ErrorCode_SQLiteCannotOpen, "SQLite: Unable to open the database");
@@ -1029,7 +1035,8 @@
       httpServer.SetPortNumber(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpPort", 8042));
       httpServer.SetRemoteAccessAllowed(lock.GetConfiguration().GetBooleanParameter("RemoteAccessAllowed", false));
       httpServer.SetKeepAliveEnabled(lock.GetConfiguration().GetBooleanParameter("KeepAlive", defaultKeepAlive));
-      httpServer.SetHttpCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("HttpCompressionEnabled", true));
+      httpServer.SetKeepAliveTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("KeepAliveTimeout", 1));
+      httpServer.SetHttpCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("HttpCompressionEnabled", false));
       httpServer.SetTcpNoDelay(lock.GetConfiguration().GetBooleanParameter("TcpNoDelay", true));
       httpServer.SetRequestTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpRequestTimeout", 30));
 
@@ -1103,10 +1110,10 @@
         httpServer.SetSslEnabled(true);
         httpServer.SetSslCertificate(certificate.c_str());
         
-        // Default to TLS 1.2 as SSL minimum
+        // Default to TLS 1.2+1.3 as SSL minimum
         // See https://github.com/civetweb/civetweb/blob/master/docs/UserManual.md "ssl_protocol_version" for mapping
-        static const unsigned int TLS_1_2 = 4;
-        unsigned int minimumVersion = lock.GetConfiguration().GetUnsignedIntegerParameter("SslMinimumProtocolVersion", TLS_1_2);
+        static const unsigned int TLS_1_2_AND_1_3 = 4;
+        unsigned int minimumVersion = lock.GetConfiguration().GetUnsignedIntegerParameter("SslMinimumProtocolVersion", TLS_1_2_AND_1_3);
         httpServer.SetSslMinimumVersion(minimumVersion);
 
         static const char* SSL_CIPHERS_ACCEPTED = "SslCiphersAccepted";
@@ -1171,6 +1178,18 @@
         LOG(WARNING) << "Remote LUA script execution is disabled";
       }
 
+      if (lock.GetConfiguration().GetBooleanParameter("RestApiWriteToFileSystemEnabled", false))
+      {
+        context.SetRestApiWriteToFileSystemEnabled(true);
+        LOG(WARNING) << "====> Your REST API can write to the FileSystem.  Review your configuration option \"RestApiWriteToFileSystemEnabled\". "
+                     << "Your setup is POSSIBLY INSECURE <====";
+      }
+      else
+      {
+        context.SetRestApiWriteToFileSystemEnabled(false);
+        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))
       {
         const bool allowDelete = lock.GetConfiguration().GetBooleanParameter("WebDavDeleteAllowed", false);
@@ -1262,6 +1281,12 @@
           lock.GetConfiguration().GetStringParameter(KEY_DICOM_TLS_CERTIFICATE, ""));
         dicomServer.SetTrustedCertificatesPath(
           lock.GetConfiguration().GetStringParameter(KEY_DICOM_TLS_TRUSTED_CERTIFICATES, ""));
+        dicomServer.SetMinimumTlsVersion(
+          lock.GetConfiguration().GetUnsignedIntegerParameter(KEY_DICOM_TLS_MINIMUM_PROTOCOL_VERSION, 0));
+        
+        std::set<std::string> acceptedCiphers;
+        lock.GetConfiguration().GetSetOfStringsParameter(acceptedCiphers, KEY_DICOM_TLS_ACCEPTED_CIPHERS);
+        dicomServer.SetAcceptedCiphers(acceptedCiphers);
       }
 
       dicomServer.SetMaximumPduLength(lock.GetConfiguration().GetUnsignedIntegerParameter(KEY_MAXIMUM_PDU_LENGTH, 16384));
@@ -1417,7 +1442,7 @@
   catch (OrthancException&)
   {
     LOG(ERROR) << "Unable to run the automated upgrade, please use the replication instructions: "
-               << "http://book.orthanc-server.com/users/replication.html";
+               << "https://orthanc.uclouvain.be/book/users/replication.html";
     throw;
   }
     
@@ -1592,6 +1617,7 @@
       lock.GetConfiguration().LoadModalitiesAndPeers();
     }
 
+    // this function exits only when Orthanc stops or resets
     return ConfigureHttpHandler(context, plugins, loadJobsFromDatabase);
   }
 }
@@ -1628,7 +1654,7 @@
     
     if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false))
     {
-      if (database.HasRevisionsSupport())
+      if (database.GetDatabaseCapabilities().HasRevisionsSupport())
       {
         LOG(INFO) << "Handling of revisions is enabled, and the custom database back-end *has* "
                   << "support for revisions of metadata and attachments";
@@ -1651,8 +1677,19 @@
     }
   }
 
-  bool success = ConfigureServerContext
-    (database, storageArea, plugins, loadJobsFromDatabase);
+  if (!database.GetDatabaseCapabilities().HasLabelsSupport())
+  {
+    LOG(WARNING) << "The custom database back-end has *no* support for labels";
+  }
+
+  if (database.GetDatabaseCapabilities().HasMeasureLatency())
+  {
+    uint64_t latency = database.MeasureLatency();
+    LOG(WARNING) << "The DB latency is " << latency << " µs";
+  }
+
+
+  bool success = ConfigureServerContext(database, storageArea, plugins, loadJobsFromDatabase);
 
   database.Close();
 
@@ -1761,6 +1798,7 @@
 int main(int argc, char* argv[]) 
 {
   Logging::Initialize();
+  Logging::SetCurrentThreadName("MAIN");
   SetGlobalVerbosity(Verbosity_Default);
 
   bool upgradeDatabase = false;
@@ -1815,6 +1853,10 @@
     {
       SetGlobalVerbosity(Verbosity_Verbose);
     }
+    else if (argument == "--logs-no-thread")
+    {
+      Logging::EnableThreadNames(false);
+    }
     else if (argument == "--trace")
     {
       SetGlobalVerbosity(Verbosity_Trace);
@@ -1922,15 +1964,15 @@
         openapi["info"]["version"] = ORTHANC_VERSION;
         openapi["info"]["title"] = "Orthanc API";
         openapi["info"]["description"] =
-          "This is the full documentation of the [REST API](https://book.orthanc-server.com/users/rest.html) "
+          "This is the full documentation of the [REST API](https://orthanc.uclouvain.be/book/users/rest.html) "
           "of Orthanc.<p>This reference is automatically generated from the source code of Orthanc. A "
-          "[shorter cheat sheet](https://book.orthanc-server.com/users/rest-cheatsheet.html) is part of "
+          "[shorter cheat sheet](https://orthanc.uclouvain.be/book/users/rest-cheatsheet.html) is part of "
           "the Orthanc Book.<p>An earlier, manually crafted version from August 2019, is [still available]"
           "(2019-08-orthanc-openapi.html), but is not up-to-date anymore ([source]"
           "(https://groups.google.com/g/orthanc-users/c/NUiJTEICSl8/m/xKeqMrbqAAAJ)).";
 
         Json::Value server = Json::objectValue;
-        server["url"] = "https://demo.orthanc-server.com/";
+        server["url"] = "https://orthanc.uclouvain.be/demo/";
         openapi["servers"].append(server);
         
         std::string s;
@@ -1966,7 +2008,7 @@
           MemoryStorageArea inMemoryStorage;
           ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */);
           OrthancRestApi restApi(context, false /* no Orthanc Explorer */);
-          restApi.GenerateReStructuredTextCheatSheet(cheatsheet, "https://api.orthanc-server.com/index.html");
+          restApi.GenerateReStructuredTextCheatSheet(cheatsheet, "https://orthanc.uclouvain.be/api/index.html");
           context.Stop();
         }
 
--- a/OrthancServer/UnitTestsSources/DatabaseLookupTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/UnitTestsSources/DatabaseLookupTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/UnitTestsSources/LuaServerTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/UnitTestsSources/LuaServerTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/UnitTestsSources/PluginsTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/UnitTestsSources/PluginsTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/UnitTestsSources/PrecompiledHeadersUnitTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/UnitTestsSources/PrecompiledHeadersUnitTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/UnitTestsSources/PrecompiledHeadersUnitTests.h	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/UnitTestsSources/PrecompiledHeadersUnitTests.h	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -165,10 +166,11 @@
       
       DicomTagConstraint c(tag, type, value, true, true);
       
-      std::vector<DatabaseConstraint> lookup;
-      lookup.push_back(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
-      
-      transaction_->ApplyLookupResources(result, NULL, lookup, level, 0 /* no limit */);
+      DatabaseConstraints lookup;
+      lookup.AddConstraint(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
+
+      std::set<std::string> noLabel;
+      transaction_->ApplyLookupResources(result, NULL, lookup, level, noLabel, LabelsConstraint_All, 0 /* no limit */);
     }    
 
     void DoLookupIdentifier2(std::list<std::string>& result,
@@ -183,12 +185,13 @@
       
       DicomTagConstraint c1(tag, type1, value1, true, true);
       DicomTagConstraint c2(tag, type2, value2, true, true);
+
+      DatabaseConstraints lookup;
+      lookup.AddConstraint(c1.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
+      lookup.AddConstraint(c2.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
       
-      std::vector<DatabaseConstraint> lookup;
-      lookup.push_back(c1.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
-      lookup.push_back(c2.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
-      
-      transaction_->ApplyLookupResources(result, NULL, lookup, level, 0 /* no limit */);
+      std::set<std::string> noLabel;
+      transaction_->ApplyLookupResources(result, NULL, lookup, level, noLabel, LabelsConstraint_All, 0 /* no limit */);
     }
   };
 }
@@ -739,18 +742,21 @@
       ASSERT_EQ(StoreStatus_Success, index.Store(
                   instanceMetadata, summary, attachments, toStore->GetMetadata(),
                   toStore->GetOrigin(), false /* don't overwrite */,
-                  hasTransferSyntax, transferSyntax, true /* pixel data offset */, 42, false));
+                  hasTransferSyntax, transferSyntax, true /* has pixel data */, 42 /* pixel data offset */,
+                  ValueRepresentation_PersonName /* pixel data VR */, false));
     }
     
-    ASSERT_EQ(7u, instanceMetadata.size());
+    ASSERT_EQ(8u, instanceMetadata.size());
     ASSERT_TRUE(instanceMetadata.find(MetadataType_RemoteAet) != instanceMetadata.end());
     ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_ReceptionDate) != instanceMetadata.end());
     ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_TransferSyntax) != instanceMetadata.end());
     ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_SopClassUid) != instanceMetadata.end());
     ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_PixelDataOffset) != instanceMetadata.end());
     ASSERT_TRUE(instanceMetadata.find(MetadataType_MainDicomTagsSignature) != instanceMetadata.end());
+    ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_PixelDataVR) != instanceMetadata.end());
 
     ASSERT_EQ("42", instanceMetadata[MetadataType_Instance_PixelDataOffset]);
+    ASSERT_EQ("PN", instanceMetadata[MetadataType_Instance_PixelDataVR]);
 
     // The default transfer syntax depends on the OS endianness
     std::string s = instanceMetadata[MetadataType_Instance_TransferSyntax];
@@ -1037,3 +1043,15 @@
     }
   }
 }
+
+
+TEST(ServerToolbox, ValidLabels)
+{
+  ASSERT_TRUE(ServerToolbox::IsValidLabel("abcdefghijklmnopqrstuvwxyz"
+                                          "ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
+  ASSERT_TRUE(ServerToolbox::IsValidLabel("0123456789-_"));
+  ASSERT_FALSE(ServerToolbox::IsValidLabel(""));
+  ASSERT_FALSE(ServerToolbox::IsValidLabel(" "));
+  ASSERT_FALSE(ServerToolbox::IsValidLabel("&"));
+  ASSERT_FALSE(ServerToolbox::IsValidLabel("."));
+}
--- a/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -134,6 +135,11 @@
     {
       return false;
     }
+
+    virtual bool DeleteOutput(const std::string& key) ORTHANC_OVERRIDE
+    {
+      return false;
+    }
   };
 
 
@@ -325,6 +331,34 @@
 }
 
 
+static bool CheckIdempotentSetOfInstances(IJobUnserializer& unserializer,
+                                          ThreadedSetOfInstancesJob& job)
+{
+  Json::Value a = 42;
+  
+  if (!job.Serialize(a))
+  {
+    return false;
+  }
+  else
+  {
+    std::unique_ptr<ThreadedSetOfInstancesJob> unserialized
+      (dynamic_cast<ThreadedSetOfInstancesJob*>(unserializer.UnserializeJob(a)));
+  
+    Json::Value b = 43;
+    if (unserialized->Serialize(b))
+    {    
+      return (CheckSameJson(a, b) &&
+              job.GetCurrentStep() == unserialized->GetCurrentStep() &&
+              job.GetInstancesCount() == unserialized->GetInstancesCount() );
+    }
+    else
+    {
+      return false;
+    }
+  }
+}
+
 static bool CheckIdempotentSerialization(IJobUnserializer& unserializer,
                                          IJobOperation& operation)
 {
@@ -692,7 +726,7 @@
 
   {
     std::unique_ptr<DicomModification> modification(new DicomModification);
-    modification->SetupAnonymization(DicomVersion_2021b);
+    modification->SetupAnonymization(DicomVersion_2023b);
     
     ModifyInstanceOperation operation(GetContext(), RequestOrigin_Lua, modification.release());
 
@@ -717,7 +751,7 @@
   // ArchiveJob
 
   {
-    ArchiveJob job(GetContext(), false, false);
+    ArchiveJob job(GetContext(), false, false, ResourceType_Patient);
     ASSERT_FALSE(job.Serialize(s));  // Cannot serialize this
   }
 
@@ -813,7 +847,7 @@
     modification->SetupAnonymization(DicomVersion_2008);
     modification->SetLevel(ResourceType_Series);
 
-    ResourceModificationJob job(GetContext());
+    ResourceModificationJob job(GetContext(), 1);
     ASSERT_THROW(job.IsSingleResourceModification(), OrthancException);
     job.SetSingleResourceModification(modification.release(), ResourceType_Patient, true);
     job.SetOrigin(DicomInstanceOrigin::FromLua());
@@ -821,7 +855,6 @@
     ASSERT_TRUE(job.IsSingleResourceModification());
     ASSERT_EQ(ResourceType_Patient, job.GetOutputLevel());
 
-    job.AddTrailingStep();  // Necessary since 1.7.0
     ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
     ASSERT_TRUE(job.Serialize(s));
   }
@@ -858,12 +891,11 @@
   }
 
   {
-    ResourceModificationJob job(GetContext());
+    ResourceModificationJob job(GetContext(), 2);
     ASSERT_THROW(job.SetTranscode("nope"), OrthancException);
     job.SetTranscode(DicomTransferSyntax_JPEGProcess1);
     job.SetSingleResourceModification(new DicomModification, ResourceType_Study, false);
 
-    job.AddTrailingStep();  // Necessary since 1.7.0
     ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
     ASSERT_TRUE(job.Serialize(s));
   }
@@ -883,11 +915,12 @@
   }
 
   {
-    ResourceModificationJob job(GetContext());
+    ResourceModificationJob job(GetContext(), 2);
     job.SetMultipleResourcesModification(new DicomModification, true);
-    job.AddInstance("toto");
-    job.AddInstance("tutu");
-    job.AddTrailingStep();  // Necessary since 1.7.0
+    std::list<std::string> instances;
+    instances.push_back("toto");
+    instances.push_back("tutu");
+    job.AddInstances(instances);
     ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
     ASSERT_TRUE(job.Serialize(s));
   }
@@ -899,10 +932,7 @@
     ResourceModificationJob& tmp = dynamic_cast<ResourceModificationJob&>(*job);
 
     std::set<std::string> instances;
-    for (size_t i = 0; i < tmp.GetInstancesCount(); i++)
-    {
-      instances.insert(tmp.GetInstance(i));
-    }
+    tmp.GetInstances(instances);
     
     ASSERT_EQ(2u, instances.size());
     ASSERT_TRUE(instances.find("toto") != instances.end());
--- a/OrthancServer/UnitTestsSources/SizeOfTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/UnitTestsSources/SizeOfTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
--- a/OrthancServer/UnitTestsSources/UnitTestsMain.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/UnitTestsSources/UnitTestsMain.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -241,42 +242,53 @@
   Json::Value dicomAsJson;
   OrthancConfiguration::DefaultDicomDatasetToJson(dicomAsJson, toStore->GetParsedDicomFile());
   
-  DicomMap m;
-  m.FromDicomAsJson(dicomAsJson);
+  { // without parsing sequences
+    DicomMap m;
+    m.FromDicomAsJson(dicomAsJson);
 
-  ASSERT_EQ("ISO_IR 100", m.GetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET).GetContent());
-  
-  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).IsBinary());
-  ASSERT_EQ("Hello", m.GetValue(DICOM_TAG_PATIENT_NAME).GetContent());
-  
-  ASSERT_FALSE(m.GetValue(DICOM_TAG_STUDY_DESCRIPTION).IsBinary());
-  ASSERT_EQ(utf8, m.GetValue(DICOM_TAG_STUDY_DESCRIPTION).GetContent());
+    ASSERT_EQ("ISO_IR 100", m.GetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET).GetContent());
+    
+    ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).IsBinary());
+    ASSERT_EQ("Hello", m.GetValue(DICOM_TAG_PATIENT_NAME).GetContent());
+    
+    ASSERT_FALSE(m.GetValue(DICOM_TAG_STUDY_DESCRIPTION).IsBinary());
+    ASSERT_EQ(utf8, m.GetValue(DICOM_TAG_STUDY_DESCRIPTION).GetContent());
 
-  ASSERT_FALSE(m.HasTag(DICOM_TAG_MANUFACTURER));                // Too long
-  ASSERT_FALSE(m.HasTag(DICOM_TAG_PIXEL_DATA));                  // Pixel data
-  ASSERT_FALSE(m.HasTag(DICOM_TAG_REFERENCED_SERIES_SEQUENCE));  // Sequence
-  ASSERT_EQ(DICOM_TAG_REFERENCED_SERIES_SEQUENCE.GetGroup(), DCM_ReferencedSeriesSequence.getGroup());
-  ASSERT_EQ(DICOM_TAG_REFERENCED_SERIES_SEQUENCE.GetElement(), DCM_ReferencedSeriesSequence.getElement());
+    ASSERT_FALSE(m.HasTag(DICOM_TAG_MANUFACTURER));                // Too long
+    ASSERT_FALSE(m.HasTag(DICOM_TAG_PIXEL_DATA));                  // Pixel data
+    ASSERT_FALSE(m.HasTag(DICOM_TAG_REFERENCED_SERIES_SEQUENCE));  // Sequence
+    ASSERT_EQ(DICOM_TAG_REFERENCED_SERIES_SEQUENCE.GetGroup(), DCM_ReferencedSeriesSequence.getGroup());
+    ASSERT_EQ(DICOM_TAG_REFERENCED_SERIES_SEQUENCE.GetElement(), DCM_ReferencedSeriesSequence.getElement());
+
+    ASSERT_TRUE(m.HasTag(DICOM_TAG_SERIES_DESCRIPTION));  // Maximum length
+    ASSERT_FALSE(m.GetValue(DICOM_TAG_SERIES_DESCRIPTION).IsBinary());
+    ASSERT_EQ(ORTHANC_MAXIMUM_TAG_LENGTH,
+              static_cast<int>(m.GetValue(DICOM_TAG_SERIES_DESCRIPTION).GetContent().length()));
 
-  ASSERT_TRUE(m.HasTag(DICOM_TAG_SERIES_DESCRIPTION));  // Maximum length
-  ASSERT_FALSE(m.GetValue(DICOM_TAG_SERIES_DESCRIPTION).IsBinary());
-  ASSERT_EQ(ORTHANC_MAXIMUM_TAG_LENGTH,
-            static_cast<int>(m.GetValue(DICOM_TAG_SERIES_DESCRIPTION).GetContent().length()));
+    ASSERT_FALSE(m.GetValue(DICOM_TAG_ROWS).IsBinary());
+    ASSERT_EQ("512", m.GetValue(DICOM_TAG_ROWS).GetContent());
 
-  ASSERT_FALSE(m.GetValue(DICOM_TAG_ROWS).IsBinary());
-  ASSERT_EQ("512", m.GetValue(DICOM_TAG_ROWS).GetContent());
+    ASSERT_FALSE(m.GetValue(DICOM_TAG_STUDY_ID).IsNull());
+    ASSERT_FALSE(m.GetValue(DICOM_TAG_STUDY_ID).IsBinary());
+    ASSERT_EQ("", m.GetValue(DICOM_TAG_STUDY_ID).GetContent());
+
+    DicomArray a(m);
+    ASSERT_EQ(6u, a.GetSize());
 
-  ASSERT_FALSE(m.GetValue(DICOM_TAG_STUDY_ID).IsNull());
-  ASSERT_FALSE(m.GetValue(DICOM_TAG_STUDY_ID).IsBinary());
-  ASSERT_EQ("", m.GetValue(DICOM_TAG_STUDY_ID).GetContent());
+    
+    //dicom.SaveToFile("/tmp/test.dcm"); 
+    //std::cout << toStore.GetJson() << std::endl;
+    //a.Print(stdout);
+  }
 
-  DicomArray a(m);
-  ASSERT_EQ(6u, a.GetSize());
+  { // now parses sequences
+    // LOG(INFO) << dicomAsJson.toStyledString();
 
-  
-  //dicom.SaveToFile("/tmp/test.dcm"); 
-  //std::cout << toStore.GetJson() << std::endl;
-  //a.Print(stdout);
+    DicomMap m;
+    m.FromDicomAsJson(dicomAsJson, false, true /* parseSequences */);
+
+    ASSERT_TRUE(m.HasTag(DICOM_TAG_REFERENCED_SERIES_SEQUENCE));
+  }
 }
 
 
--- a/OrthancServer/UnitTestsSources/VersionsTests.cpp	Thu Sep 15 18:13:17 2022 +0200
+++ b/OrthancServer/UnitTestsSources/VersionsTests.cpp	Tue Sep 24 11:39:52 2024 +0200
@@ -2,8 +2,9 @@
  * Orthanc - A Lightweight, RESTful DICOM Store
  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
  * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ * Copyright (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
@@ -107,31 +108,31 @@
 
 TEST(Versions, ZlibStatic)
 {
-  ASSERT_STREQ("1.2.11", zlibVersion());
+  ASSERT_STREQ("1.3.1", zlibVersion());
 }
 
 TEST(Versions, BoostStatic)
 {
-  ASSERT_TRUE(std::string(BOOST_LIB_VERSION) == "1_80" ||
+  ASSERT_TRUE(std::string(BOOST_LIB_VERSION) == "1_85" ||
               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("7.77.0", v->version);
+  ASSERT_STREQ("8.9.0", v->version);
 }
 
 TEST(Versions, PngStatic)
 {
-  ASSERT_EQ(10636u, png_access_version_number());
-  ASSERT_STREQ("1.6.36", PNG_LIBPNG_VER_STRING);
+  ASSERT_EQ(10640u, png_access_version_number());
+  ASSERT_STREQ("1.6.40", PNG_LIBPNG_VER_STRING);
 }
 
 TEST(Versions, JpegStatic)
 {
   ASSERT_EQ(9, JPEG_LIB_VERSION_MAJOR);
-  ASSERT_EQ(3, JPEG_LIB_VERSION_MINOR);
+  ASSERT_EQ(6 /* f */, JPEG_LIB_VERSION_MINOR);
 }
 
 TEST(Versions, CurlSslStatic)
@@ -148,14 +149,14 @@
 #endif
 }
 
-TEST(Version, LuaStatic)
+TEST(Versions, LuaStatic)
 {
   ASSERT_STREQ("Lua 5.3.5", LUA_RELEASE);
 }
 
 
 #if BUILDING_LIBICONV == 1
-TEST(Version, LibIconvStatic)
+TEST(Versions, LibIconvStatic)
 {
   static const int major = 1;
   static const int minor = 15;  
@@ -165,25 +166,25 @@
 
 
 #if ORTHANC_ENABLE_SSL == 1
-TEST(Version, OpenSslStatic)
+TEST(Versions, OpenSslStatic)
 {
-  // openssl-3.0.5
-  ASSERT_EQ(3 * 0x10000000L +
-            0 * 0x00100000L +
-            1 * 0x00000050L +
-            0 * 0x0000000fL, OPENSSL_VERSION_NUMBER);
+  // openssl-3.1.4
+  // https://www.openssl.org/docs/man3.0/man3/OPENSSL_VERSION_NUMBER.html
+  ASSERT_EQ(3 /* major */ * 0x10000000L +
+            1 /* minor */ * 0x00100000L +
+            4 /* patch */ * 0x00000010L, OPENSSL_VERSION_NUMBER);
 }
 #endif
 
 
 #include <json/version.h>
 
-TEST(Version, JsonCpp)
+TEST(Versions, JsonCpp)
 {
 #if ORTHANC_LEGACY_JSONCPP == 1
   ASSERT_STREQ("0.10.6", JSONCPP_VERSION_STRING);
 #elif ORTHANC_LEGACY_JSONCPP == 0
-  ASSERT_STREQ("1.9.4", JSONCPP_VERSION_STRING);
+  ASSERT_STREQ("1.9.5", JSONCPP_VERSION_STRING);
 #else
 #  error Macro ORTHANC_LEGACY_JSONCPP should be set
 #endif
@@ -191,7 +192,7 @@
 
 
 #if ORTHANC_ENABLE_CIVETWEB == 1
-TEST(Version, Civetweb)
+TEST(Versions, Civetweb)
 {
   ASSERT_EQ(1, CIVETWEB_VERSION_MAJOR);
   ASSERT_EQ(14, CIVETWEB_VERSION_MINOR);
@@ -201,9 +202,9 @@
 
 
 #if ORTHANC_ENABLE_PUGIXML == 1
-TEST(Version, Pugixml)
+TEST(Versions, Pugixml)
 {
-  ASSERT_EQ(190, PUGIXML_VERSION);
+  ASSERT_EQ(1140, PUGIXML_VERSION);
 }
 #endif
 
--- a/README	Thu Sep 15 18:13:17 2022 +0200
+++ b/README	Tue Sep 24 11:39:52 2024 +0200
@@ -8,7 +8,7 @@
 General information about this software can be found on its official
 Website, and in the Orthanc Book:
 http://www.orthanc-server.com/
-http://book.orthanc-server.com/
+https://orthanc.uclouvain.be/book/
 
 The instructions for building Orthanc can be found in the "./INSTALL"
 file.
@@ -38,6 +38,14 @@
 * Cross-compilation for Windows under GNU/Linux, with MinGW.
 
 
+Contributing
+------------
+
+Instructions for contributing to the Orthanc project are included in
+the Orthanc Book:
+https://orthanc.uclouvain.be/book/developers/repositories.html
+
+
 Licensing
 ---------
 
@@ -47,7 +55,7 @@
 
 Full information about the licensing of the Orthanc ecosystem is
 available in the Orthanc Book:
-https://book.orthanc-server.com/faq/licensing.html
+https://orthanc.uclouvain.be/book/faq/licensing.html
 
 We also kindly ask scientific works and clinical studies that make
 use of Orthanc to cite Orthanc in their associated publications.
--- a/TODO	Thu Sep 15 18:13:17 2022 +0200
+++ b/TODO	Tue Sep 24 11:39:52 2024 +0200
@@ -37,7 +37,7 @@
 
 For higher-level ideas in the roadmap, please first read the
 "Contributing to Orthanc" section of the Orthanc Book:
-https://book.orthanc-server.com/contributing.html
+https://orthanc.uclouvain.be/book/contributing.html
 
 
 Some features are being funded by an OpenCollective one-time donations.
@@ -54,34 +54,53 @@
 * Support partial file retrieval in Orthanc::HttpClient
 * Support retry counter in Orthanc::HttpClient
 * Option to enable DNS lookups in DICOM:
-  https://hg.orthanc-server.com/orthanc/file/Orthanc-1.9.3/OrthancFramework/Sources/OrthancFramework.cpp#l88
+  https://orthanc.uclouvain.be/hg/orthanc/file/Orthanc-1.9.3/OrthancFramework/Sources/OrthancFramework.cpp#l88
 * Toolbox::ComputeMD5() fails on files larger than 4GB
 * Add an option to run Orthanc in read-only mode both for DICOM and for Rest API.
-* Logging: add some information like Thread-Id to contextualize the logs
+* Logging: add more specific information to contextualize the logs.
+  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)
-* Provide a configuration option related to MaximumStorageSize: instead of 
-  recycling older patients, simply block new ingests (from DICOM/DICOMweb/API)
-  by returning out-of-resource status (HTTP or DIMSE)
 * 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!
-
+* 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.
+  - Generate the events when setting IsStable=true when starting an Orthanc (ok for SQLite) ?
+  - 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.
+* 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)
+  - for job durations (+ have one histogram for each job)
+  - for HTTP request handling
+  - ...
+* Investigate if one could fix KeepAlive race conditions:
+  https://discourse.orthanc-server.org/t/socket-hangup-with-rest-api/4023/3
+* The DICOM file cache shall keep a MD5 of the cached file and compare it with MD5
+  from the DB.  That would allow 2 orthancs in a swarm to realize when the other
+  Orthanc has updated the file:
+  https://discourse.orthanc-server.org/t/instances-id-content-api-results-are-different-in-docker-swarm-replicas-of-orthanc/4582
+* Allow saving PrivateTags in ExtraMainDicomTags.
+  Note: they can actually be stored but they then appear as "Unknown Tag & Data" in the responses.
+  If we try to add the PrivateCreator in the ExtraMainDicomTags, then, the DICOMWeb plugin fails to initialize because the private tags are not known.
 
 ============================
 Documentation (Orthanc Book)
 ============================
 
 * Write a getting started guide (step by step) for each platform to replace
-  https://book.orthanc-server.com/users/cookbook.html :
+  https://orthanc.uclouvain.be/book/users/cookbook.html :
   - Ubuntu/Debian
-  - Windows
+  - Windows (done)
   - OSX
   - Docker on Linux
   Each step by step guide should contain:
@@ -97,7 +116,7 @@
 * How to reproduce issues by replacing Orthanc with storescp or wlmscpfs
 * How to capture TCP traffic of DICOM protocol using tcpdump and Wireshark
 * Add more configurations of viewers (Weasis, Slicer...):
-  https://book.orthanc-server.com/integrations.html
+  https://orthanc.uclouvain.be/book/integrations.html
 * Discuss HL7 in a dedicated page:
   https://groups.google.com/d/msg/orthanc-users/4dt4992O0lQ/opTjTFU2BgAJ
   https://groups.google.com/g/orthanc-users/c/Spjtcj9vSPo/m/ktUArWxUDQAJ
@@ -119,8 +138,6 @@
 Short-term
 ----------
 
-* Add "KeepSource" to "/modify" and "/anonymize" routes
-  https://groups.google.com/g/orthanc-users/c/svS3YOqA2Mo/m/wcoE18IcAAAJ
 
 --------
 Mid-term
@@ -136,11 +153,21 @@
   https://groups.google.com/g/orthanc-users/c/y3-xa_GcdLM/m/m0Kr5G5UPAAJ
 * (1) Specify the transfer syntax in /tools/create-dicom
   https://groups.google.com/g/orthanc-users/c/o15Dekecgds/m/xmPE2y3bAwAJ
+* Support Palette PNG in /tools/create-dicom:
+  https://discourse.orthanc-server.org/t/404-on-tools-create-dicom-endpoint-with-specific-png/3562
+* Support creation of DICOM files from MP4 in /tools/create-dicom.
+  Sample python code: https://github.com/salimkanoun/OrthancGif/blob/new-organisation/python/create_dicom_video.py.
+  We would need to extract frame rate + dimension from the MP4 which would
+  require ffmpeg or a similar library -> can not be done in the Orthanc core.
+  -> keep it for a python plugin
+  -> or require the payload to include rows/columns/cinerate/...
 * (1) In the /studies/{id}/anonymize route, add an option to remove
   secondary captures.  They usually contains Patient info in the
   image. The SOPClassUID might be used to identify such secondary
   captures.
 * Support "/preview" and "/matlab" for LUT color images
+* /preview should be able to display a dose report:
+  https://discourse.orthanc-server.org/t/orthanc-image-preview-shows-empty-image/4459
 * Try to transcode files if a simple decoding fails:
   https://groups.google.com/g/orthanc-users/c/b8168-NkAhA/m/Df3j-CO9CgAJ
 * (2) Ranges of DICOM tags for "Keep" and "Remove" in ".../modify" and ".../anonymize": 
@@ -150,6 +177,17 @@
   https://groups.google.com/g/orthanc-users/c/r20kDb0axms/m/2tzbQzYJAgAJ
 * save more details in jobs e.g: the resources being sent/exported ...
   https://groups.google.com/g/orthanc-users/c/rDDusFG5Lco/m/TzTUjWXLAQAJ
+  https://discourse.orthanc-server.org/t/some-confusion-about-jobs-function/3887
+* allow filtering/ordering on the /jobs route:
+  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 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
+* Implement a 'commit' route to force the Stable status earlier.
+  https://discourse.orthanc-server.org/t/expediting-stability-of-a-dicom-study-new-api-endpoint/1684
 
 ---------
 Long-term
@@ -170,7 +208,7 @@
 
 * Support C-GET SCU (note that C-GET SCP was introduced in Orthanc 1.7.0)
 * Support "Retrieve AE Title" (0008,0054) in C-FIND:
-  - On SCP side: done by https://hg.orthanc-server.com/orthanc/rev/1ec3e1e18f50
+  - On SCP side: done by https://orthanc.uclouvain.be/hg/orthanc/rev/1ec3e1e18f50
   - On SCU side:
     https://groups.google.com/d/msg/orthanc-users/wPl0g5mqZco/5X1Z8tEzBgAJ
 * Check Big Endian transfer syntax in ParsedDicomFile::EmbedImage and
@@ -178,7 +216,12 @@
 * 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
 
 ---------
 Long-term
@@ -188,6 +231,13 @@
   https://groups.google.com/d/msg/orthanc-users/xD4d3mpc6ms/srF7E2goAAAJ
 * Support C-MOVE-CANCEL:
   https://groups.google.com/d/msg/orthanc-users/KnduYBFd06A/o86cl5SeCAAJ
+* Combine StudyDate + StudyTime in C-Find matching (also for the worklist plugin with 
+  ScheduledProcedureStartTime & Date).
+  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
@@ -196,6 +246,7 @@
 * Support multiple specific character sets (cf. "SCSH32" in orthanc-tests)
   - http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.12.html#sect_C.12.1.1.2
   - Japanese test: http://dicom.nema.org/MEDICAL/dicom/2017c/output/chtml/part05/sect_H.3.2.html
+  https://discourse.orthanc-server.org/t/garbled-characters-when-i-uploaded-japanese-patient-name/3204/5
 * Support Supplementary Kanji set (ISO 2022 IR 159)
 * Create DICOM files with multibyte encodings (Korean, JapaneseKanji, SimplifiedChinese)
 
@@ -204,8 +255,6 @@
 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
@@ -221,12 +270,17 @@
   would be compatible with Transcoding.
   Use case: receiving 10 1GB instances in parallel can consume up to 20 GB
   Alternative option 1: write an "external application/plugin" that would take care of these receptions, write the
-  file at the right place and send a signal to Orthanc to "adopt" the file.
+    file at the right place and send a signal to Orthanc to "adopt" the file.
   Alternative option 2: declare a memory resource (X GB) that is available for reception.  Every time
-  Orthanc starts receiving a file, it reserves the memory or twice the memory (through a Semaphore)
-  if no memory is available, it waits and possibly timeouts returning a 503 or DIMSE A700 (out of resources).
-  This would at least protect from "out of memory" crashes.
-
+    Orthanc starts receiving a file, it reserves the memory or twice the memory (through a Semaphore)
+    if no memory is available, it waits and possibly timeouts returning a 503 or DIMSE A700 (out of resources).
+    This would at least protect from "out of memory" crashes.
+  Alternative option 3: Configure DCMTK to "stream" DICOM files on a temporary file on disk.  Pass the file handle
+    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.
+* 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
 
 ========
 Database
@@ -251,11 +305,17 @@
 * Provide access to the Orthanc::DicomUserConnection class in plugins:
   https://groups.google.com/d/msg/orthanc-users/ycDA1xPuTRY/nsT2_GOtEgAJ
 * Provide a C++ callback similar to "ReceivedInstanceFilter()" in Lua
-  https://book.orthanc-server.com/users/lua.html#filtering-incoming-dicom-instances
+  https://orthanc.uclouvain.be/book/users/lua.html#filtering-incoming-dicom-instances
   https://groups.google.com/d/msg/orthanc-users/BtvLTE5Ni8A/vIMhmMgfBAAJ
-* In "OrthancPluginLog[Error|Warning|Info]()", prefix the log line with
-  the name of the plugin, as retrieved by "OrthancPluginGetName()"
 * Update the SDK to handle buffer sizes > 4GB (all sizes are currently coded in uint32_t)
+* Add a C-Get SCP handler: OrthancPluginRegisterGetCallback
+  https://groups.google.com/g/orthanc-users/c/NRhPkYX9IXQ/m/mWS11g0jBwAJ
+* Add a primitive for user authentication (to generate 401 HTTP status, whereas
+  the "RegisterIncomingHttpRequestFilter()" can only generate 403 HTTP status)
+  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
+
 
 ----------------
 Ideas of plugins
@@ -278,14 +338,14 @@
 * Configure HTTP headers from Lua (in RestApiPost(), RestApiPut()
   and RestApiDelete().
   https://groups.google.com/forum/#!msg/orthanc-users/WNnW187OILM/6XX_bm96BwAJ
+* Retrieve HTTP status from calls to HttpGet, HttpPost, ...
+  https://discourse.orthanc-server.org/t/how-to-get-http-status-code-from-httppost/1263/4
 
 
 ================
 Code refactoring
 ================
 
-* Use Semaphore::Locker everywhere (instead of explicit
-  Release() and Acquire())
 * Avoid direct calls to FromDcmtkBridge (make most of its 
   methods private), go through ParsedDicomFile wherever possible
 
@@ -341,6 +401,12 @@
 * Add more complex testing scenarios like data-migration, change of 
   configuration files, multiple orthanc interacting togethers with various 
   config.  This should probably look like the python toolbox tests ...
+  - add a test to validate Modalities and Peers stored in DB are not lost 
+    while upgrading from one version to the other (Sylvain)
+* On Ubuntu 20.04, accesses to unitialized memory are sometimes
+  reported in libgjpeg by valgrind, if running the following command
+  (this is probably unrelated to Orthanc):
+  $ ./Start.sh --force Orthanc.test_bitbucket_issue_141 Orthanc.test_create_pdf Orthanc.test_decode_brainix_as_jpeg
   
 
 ---------------------