changeset 6094:090ef6a37882 attach-custom-data

integration default->attach-custom-data
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 07 Apr 2025 13:23:11 +0200 (6 weeks ago)
parents 26e8abb19d56 (current diff) 3028d158c165 (diff)
children b1764e7248e0
files NEWS OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake OrthancServer/CMakeLists.txt OrthancServer/Sources/OrthancInitialization.cpp
diffstat 52 files changed, 926 insertions(+), 123 deletions(-) [+]
line wrap: on
line diff
--- a/CITATION.cff	Tue Apr 01 16:49:49 2025 +0200
+++ b/CITATION.cff	Mon Apr 07 13:23:11 2025 +0200
@@ -10,5 +10,5 @@
 doi: "10.1007/s10278-018-0082-y"
 license: "GPL-3.0-or-later"
 repository-code: "https://orthanc.uclouvain.be/hg/orthanc/"
-version: 1.12.6
-date-released: 2025-01-22
+version: 1.12.7
+date-released: 2025-04-07
--- a/NEWS	Tue Apr 01 16:49:49 2025 +0200
+++ b/NEWS	Mon Apr 07 13:23:11 2025 +0200
@@ -4,55 +4,63 @@
 General
 -------
 
-* SQLite default DB engine now supports metadata and attachment revisions
+* SQLite default DB engine now supports metadata and attachment revisions.
 * Upgraded the DB to allow plugins to store customData for each attachment.
 * New sample Advanced Storage plugin that allows:
-  - using multiple disk for image storage
-  - use more human friendly storage structure (experimental feature)
-
-REST API
---------
-
-* API version upgraded to 28
-* GET /studies/../archive and sibbling routes now all accept a 'filename' GET argument.
-* POST /studies/../archive and sibbling routes now all accept a 'Filename' query argument.
-* GET /instances/../file and sibbling ../attachments/../data routes now all accept a 'filename' GET argument.
-* All routes accepting a "transcode" url argument or a "Transcode" field in the payload now also
-  accepts a "lossy-quality" url argument or a "LossyQuality" field to define the compression quality factor.
-  If not specified, the "DicomLossyTranscodingQuality" configuration is taken into account.
-* Fix OpenAPI documentation for /modalities/../get
+  - using multiple disk for image storage,
+  - use more human friendly storage structure (experimental feature).
 
 Plugins
 -------
 
 * New database plugin SDK (vX) to handle customData for attachments.
-* New storage plugin SDK (v3) to handle customData for attachments,
+* New storage plugin SDK (v3) to handle customData for attachments.
+
+
+Version 1.12.7 (2025-04-07)
+===========================
+
+REST API
+--------
+
+* API version upgraded to 28
+* POST "/tools/create-dicom" accepts new argument "Encapsulate" to encapsulate a raw JPEG
+  image into a DICOM envelope without transcoding, using 1.2.840.10008.1.2.4.50 transfer syntax.
+* GET "/studies/../archive" and sibling routes now all accept a "filename" GET argument.
+* POST "/studies/../archive" and sibling routes now all accept a "Filename" query argument.
+* GET "/instances/../file" and sibling "../attachments/../data" routes now all accept a "filename" GET argument.
+* All routes accepting a "transcode" URL argument or a "Transcode" field in the payload now also
+  accept a "lossy-quality" URL argument or a "LossyQuality" field to define the compression quality factor.
+  If not specified, the "DicomLossyTranscodingQuality" configuration is taken into account.
+* GET "/series/../study" now also contain LastUpdate field:
+  https://discourse.orthanc-server.org/t/lastupdate-coherency/5524
 
 Maintenance
 -----------
 
 * In the "ExtendedFind" mode:
-  - optimized "tools/find" when "StorageAccessMode" is set to "Never".
-  - "tools/find" is now returning results when e.g, ordering instances against a metadata they don't have.
-  - Get SOPClassUID from metadata when available -> this fixes display of PDF files in Stone when 
+  - optimized "tools/find" if "StorageAccessMode" is set to "Never".
+  - "tools/find" now returns results if e.g, ordering instances against a metadata they don't have.
+  - Get SOPClassUID from metadata if available -> this fixes display of PDF files in Stone if
     "StorageAccessOnFind" is set to "Never".
-* Fixed interpretation of returnUnsupportedImage in /preview route. 
-* GET /series/../study now also contain LastUpdate field:
-  https://discourse.orthanc-server.org/t/lastupdate-coherency/5524
+* Fixed OpenAPI documentation for "/modalities/../get".
+* Fixed interpretation of "returnUnsupportedImage" in "/preview" route.
 * Recovered compatibility with Windows XP that was broken because of DCMTK 3.6.9
 * Enabled support of the 1.2.840.10008.1.2.1.99 transfer syntax
-  (Deflated Explicit VR Little Endian) in static builds + fix length of saved files.
+  (Deflated Explicit VR Little Endian) in static builds, and fix length of saved files.
   https://discourse.orthanc-server.org/t/transcoding-to-deflated-transfer-syntax-fails/5489
-* When anonymizing a resource while forcing some value with the 'Replace' fields, the tag
-  0012,0063 was cleared out because the DICOM Anonymization profile was not strictly followed.
+* When anonymizing a resource while forcing some value with the "Replace" fields, the tag
+  0012,0063 was cleared out because the DICOM anonymization profile was not strictly followed.
   From now on:
-  - 0012,0063 will contain "Orthanc {version} - {Anonymization profile}" if no 'Replace' is used.
-  - 0012,0063 will contain "Orthanc {version}" if 'Replace' is used.
-  (https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=240)
+  - 0012,0063 will contain "Orthanc {version} - {Anonymization profile}" if no "Replace" is used.
+  - 0012,0063 will contain "Orthanc {version}" if "Replace" is used.
+  https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=240
 * Housekeeper plugin:
-  - When encountering an error, the housekeeper now skips the resource and continues processing.
+  - If encountering an error, the housekeeper now skips the resource and continues processing.
 * Orthanc Explorer:
-  - Allow '-' and '_' in labels.
+  - Allow "-" and "_" in labels.
+* Upgraded dependencies for static builds:
+  - lua 5.4.7
 
 
 Version 1.12.6 (2025-01-22)
--- a/OrthancFramework/Resources/CMake/BoostConfiguration.cmake	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancFramework/Resources/CMake/BoostConfiguration.cmake	Mon Apr 07 13:23:11 2025 +0200
@@ -23,7 +23,12 @@
 if (STATIC_BUILD OR NOT USE_SYSTEM_BOOST)
   set(BOOST_STATIC 1)
 else()
-  include(FindBoost)
+  # https://cmake.org/cmake/help/latest/policy/CMP0167.html
+  if (CMAKE_VERSION VERSION_GREATER "3.30")
+    find_package(Boost CONFIG)
+  else()
+    include(FindBoost)
+  endif()
 
   set(BOOST_STATIC 0)
   #set(Boost_DEBUG 1)
--- a/OrthancFramework/Resources/CMake/Compiler.cmake	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancFramework/Resources/CMake/Compiler.cmake	Mon Apr 07 13:23:11 2025 +0200
@@ -22,6 +22,16 @@
 
 # This file sets all the compiler-related flags
 
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")
+  # Since Orthanc 1.12.7 that allows CMake 4.0, builds for macOS
+  # require the C++ standard to be explicitly set to C++11. Do *not*
+  # do this on GNU/Linux, as third-party system libraries could have
+  # been compiled with higher versions of the C++ standard.
+  set(CMAKE_CXX_STANDARD 11)
+  set(CMAKE_CXX_STANDARD_REQUIRED ON)
+  set(CMAKE_CXX_EXTENSIONS OFF)
+endif()
+
 
 # Save the current compiler flags to the cache every time cmake configures the project
 set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS}" CACHE STRING "compiler flags" FORCE)
--- a/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake	Mon Apr 07 13:23:11 2025 +0200
@@ -169,6 +169,8 @@
         set(ORTHANC_FRAMEWORK_MD5 "5bb69f092981fdcfc11dec0a0f9a7db3")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.6")
         set(ORTHANC_FRAMEWORK_MD5 "0e971f32f4f3e4951e0f3b5de49a3da6")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.7")
+        set(ORTHANC_FRAMEWORK_MD5 "f27c27d7a7a694dab1fd7f0a99d9715a")
 
       # Below this point are development snapshots that were used to
       # release some plugin, before an official release of the Orthanc
@@ -501,7 +503,15 @@
   
   include(CheckIncludeFile)
   include(CheckIncludeFileCXX)
-  include(FindPythonInterp)
+  
+  if(CMAKE_VERSION VERSION_GREATER "3.11")
+    find_package(Python REQUIRED COMPONENTS Interpreter)
+    set(PYTHON_EXECUTABLE ${Python_EXECUTABLE})
+  else()
+    include(FindPythonInterp)
+    find_package(PythonInterp REQUIRED)
+  endif()
+  
   include(${CMAKE_CURRENT_LIST_DIR}/Compiler.cmake)
   include(${CMAKE_CURRENT_LIST_DIR}/DownloadPackage.cmake)
   include(${CMAKE_CURRENT_LIST_DIR}/AutoGeneratedCode.cmake)
--- a/OrthancFramework/Resources/CMake/JsonCppConfiguration.cmake	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancFramework/Resources/CMake/JsonCppConfiguration.cmake	Mon Apr 07 13:23:11 2025 +0200
@@ -99,10 +99,4 @@
   # default value (1000), so we increase this limit
   # https://gitlab.kitware.com/third-party/jsoncpp/commit/56df2068470241f9043b676bfae415ed62a0c172
   add_definitions(-DJSONCPP_DEPRECATED_STACK_LIMIT=5000)
-
-  if (APPLE AND
-      "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
-    # Explicitly adding "-std=c++11" is needed on XCode
-    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
-  endif()
 endif()
--- a/OrthancFramework/Resources/CMake/LuaConfiguration.cmake	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancFramework/Resources/CMake/LuaConfiguration.cmake	Mon Apr 07 13:23:11 2025 +0200
@@ -21,9 +21,9 @@
 
 
 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 "https://orthanc.uclouvain.be/downloads/third-party-downloads/lua-5.3.5.tar.gz")
+  SET(LUA_SOURCES_DIR ${CMAKE_BINARY_DIR}/lua-5.4.7)
+  SET(LUA_MD5 "fc3f3291353bbe6ee6dec85ee61331e8")
+  SET(LUA_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/lua-5.4.7.tar.gz")
 
   DownloadPackage(${LUA_MD5} ${LUA_URL} "${LUA_SOURCES_DIR}")
 
@@ -106,7 +106,6 @@
     # Base Lua modules
     ${LUA_SOURCES_DIR}/src/lauxlib.c
     ${LUA_SOURCES_DIR}/src/lbaselib.c
-    ${LUA_SOURCES_DIR}/src/lbitlib.c
     ${LUA_SOURCES_DIR}/src/lcorolib.c
     ${LUA_SOURCES_DIR}/src/ldblib.c
     ${LUA_SOURCES_DIR}/src/liolib.c
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake	Mon Apr 07 13:23:11 2025 +0200
@@ -43,8 +43,15 @@
 include(CheckStructHasMember)
 include(CheckSymbolExists)
 include(CheckTypeSize)
-include(FindPythonInterp)
-  
+
+if(CMAKE_VERSION VERSION_GREATER "3.11")
+  find_package(Python REQUIRED COMPONENTS Interpreter)
+  set(PYTHON_EXECUTABLE ${Python_EXECUTABLE})
+else()
+  include(FindPythonInterp)
+  find_package(PythonInterp REQUIRED)
+endif()
+
 include(${CMAKE_CURRENT_LIST_DIR}/AutoGeneratedCode.cmake)
 include(${CMAKE_CURRENT_LIST_DIR}/DownloadPackage.cmake)
 include(${CMAKE_CURRENT_LIST_DIR}/Compiler.cmake)
@@ -156,8 +163,9 @@
   ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Cache/MemoryCache.cpp
   ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Cache/MemoryObjectCache.cpp
   ${CMAKE_CURRENT_LIST_DIR}/../../Sources/ChunkedBuffer.cpp
+  ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/DicomPath.cpp
   ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/DicomTag.cpp
-  ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/DicomPath.cpp
+  ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/Window.cpp
   ${CMAKE_CURRENT_LIST_DIR}/../../Sources/EnumerationDictionary.h
   ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Enumerations.cpp
   ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/FileInfo.cpp
--- a/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/CMakeLists.txt	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/CMakeLists.txt	Mon Apr 07 13:23:11 2025 +0200
@@ -29,7 +29,7 @@
 # -> Copy the result as "../arith.h"
 
 
-cmake_minimum_required(VERSION 2.8.3)
+cmake_minimum_required(VERSION 2.8.3...4.0)
 
 
 #####################################################################
--- a/OrthancFramework/Resources/DcmtkTools/CMakeLists.txt	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancFramework/Resources/DcmtkTools/CMakeLists.txt	Mon Apr 07 13:23:11 2025 +0200
@@ -22,7 +22,7 @@
 
 #   $ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=../../Resources/Toolchains/LinuxStandardBaseToolchain.cmake -G Ninja
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 
 project(DcmtkTools)
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/curl-8.12.1.patch	Mon Apr 07 13:23:11 2025 +0200
@@ -0,0 +1,12 @@
+diff -urEb curl-8.12.1-orig/CMake/Macros.cmake curl-8.12.1/CMake/Macros.cmake
+--- curl-8.12.1-orig/CMake/Macros.cmake 2025-02-13 08:15:00.000000000 +0100
++++ curl-8.12.1/CMake/Macros.cmake      2025-03-27 10:25:42.119275658 +0100
+@@ -50,7 +50,7 @@
+     message(STATUS "Performing Test ${_curl_test}")
+     try_compile(${_curl_test}
+       ${PROJECT_BINARY_DIR}
+-      "${CMAKE_CURRENT_SOURCE_DIR}/CMake/CurlTests.c"
++      "${CURL_SOURCES_DIR}/CMake/CurlTests.c"
+       CMAKE_FLAGS
+         "-DCOMPILE_DEFINITIONS:STRING=-D${_curl_test} ${CURL_TEST_DEFINES} ${_cmake_required_definitions}"
+         "${_curl_test_add_libraries}"
--- a/OrthancFramework/Resources/ProtocolBuffers/CMakeLists.txt	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancFramework/Resources/ProtocolBuffers/CMakeLists.txt	Mon Apr 07 13:23:11 2025 +0200
@@ -20,7 +20,7 @@
 # <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8.3)
+cmake_minimum_required(VERSION 2.8.3...4.0)
 
 project(ProtocolBuffers)
 
--- a/OrthancFramework/Resources/Samples/MicroService/CMakeLists.txt	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancFramework/Resources/Samples/MicroService/CMakeLists.txt	Mon Apr 07 13:23:11 2025 +0200
@@ -20,7 +20,7 @@
 # <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 
 project(Sample)
 
--- a/OrthancFramework/Resources/ThirdParty/icu/CMakeLists.txt	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancFramework/Resources/ThirdParty/icu/CMakeLists.txt	Mon Apr 07 13:23:11 2025 +0200
@@ -20,7 +20,7 @@
 # <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 project(IcuCodeGeneration)
 
 set(USE_LEGACY_LIBICU OFF CACHE BOOL "Use icu icu4c-58_2, latest version not requiring a C++11 compiler (for LSB and old versions of Visual Studio)")
--- a/OrthancFramework/SharedLibrary/CMakeLists.txt	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancFramework/SharedLibrary/CMakeLists.txt	Mon Apr 07 13:23:11 2025 +0200
@@ -30,7 +30,7 @@
 ##
 
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 cmake_policy(SET CMP0058 NEW)
 
 project(OrthancFramework)
--- a/OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp	Mon Apr 07 13:23:11 2025 +0200
@@ -33,6 +33,7 @@
 #include "../Compatibility.h"
 #include "../Logging.h"
 #include "../OrthancException.h"
+#include "../SerializationToolbox.h"
 #include "../Toolbox.h"
 
 #include <boost/lexical_cast.hpp>
@@ -235,6 +236,54 @@
 
     isPlanar_ = (planarConfiguration != 0 ? true : false);
     isSigned_ = (pixelRepresentation != 0 ? true : false);
+
+    // New in Orthanc 1.12.7
+    double d;
+
+    if (values.ParseDouble(d, DICOM_TAG_RESCALE_SLOPE))
+    {
+      rescaleSlope_ = d;
+    }
+    else
+    {
+      rescaleSlope_ = 1;
+    }
+
+    if (values.ParseDouble(d, DICOM_TAG_RESCALE_INTERCEPT))
+    {
+      rescaleIntercept_ = d;
+    }
+    else
+    {
+      rescaleIntercept_ = 0;
+    }
+
+    if (values.ParseDouble(d, DICOM_TAG_DOSE_GRID_SCALING))
+    {
+      rescaleSlope_ *= d;
+    }
+
+    const std::string centerTag = values.GetStringValue(DICOM_TAG_WINDOW_CENTER, "", false);
+    const std::string widthTag = values.GetStringValue(DICOM_TAG_WINDOW_WIDTH, "", false);
+    if (!centerTag.empty() &&
+        !widthTag.empty())
+    {
+      std::vector<std::string> centers, widths;
+      Toolbox::TokenizeString(centers, centerTag, '\\');
+      Toolbox::TokenizeString(widths, widthTag, '\\');
+      if (centers.size() == widths.size())
+      {
+        for (size_t i = 0; i < centers.size(); i++)
+        {
+          double center, width;
+          if (SerializationToolbox::ParseDouble(center, centers[i]) &&
+              SerializationToolbox::ParseDouble(width, widths[i]))
+          {
+            windows_.push_back(Window(center, width));
+          }
+        }
+      }
+    }
   }
 
   DicomImageInformation* DicomImageInformation::Clone() const
@@ -251,6 +300,9 @@
     target->bitsStored_ = bitsStored_;
     target->highBit_ = highBit_;
     target->photometric_ = photometric_;
+    target->rescaleSlope_ = rescaleSlope_;
+    target->rescaleIntercept_ = rescaleIntercept_;
+    target->windows_ = windows_;
 
     return target.release();
   }
@@ -472,4 +524,73 @@
       return ValueRepresentation_OtherByte;
     }
   }
+
+
+  const Window& DicomImageInformation::GetWindow(size_t index) const
+  {
+    if (index < windows_.size())
+    {
+      return windows_[index];
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  double DicomImageInformation::ApplyRescale(double value) const
+  {
+    return rescaleSlope_ * value + rescaleIntercept_;
+  }
+
+
+  Window DicomImageInformation::GetDefaultWindow() const
+  {
+    if (windows_.empty())
+    {
+      const double width = static_cast<double>(1 << GetBitsStored());
+      const double center = width / 2.0;
+      return Window(center, width);
+    }
+    else
+    {
+      return windows_[0];
+    }
+  }
+
+
+  void DicomImageInformation::ComputeRenderingTransform(double& offset,
+                                                        double& scaling,
+                                                        const Window& window) const
+  {
+    // Check out "../../../OrthancServer/Resources/ImplementationNotes/windowing.py"
+
+    float windowWidth = std::abs(window.GetWidth());
+
+    // Avoid divisions by zero
+    static const double MIN = 0.0001;
+    if (windowWidth <= MIN)
+    {
+      windowWidth = MIN;
+    }
+
+    if (GetPhotometricInterpretation() == PhotometricInterpretation_Monochrome1)
+    {
+      scaling = -255.0 * GetRescaleSlope() / windowWidth;
+      offset = 255.0 * (window.GetCenter() - GetRescaleIntercept()) / windowWidth + 127.5;
+    }
+    else
+    {
+      scaling = 255.0 * GetRescaleSlope() / windowWidth;
+      offset = 255.0 * (GetRescaleIntercept() - window.GetCenter()) / windowWidth + 127.5;
+    }
+  }
+
+
+  void DicomImageInformation::ComputeRenderingTransform(double& offset,
+                                                        double& scaling) const
+  {
+    ComputeRenderingTransform(offset, scaling, GetDefaultWindow());
+  }
 }
--- a/OrthancFramework/Sources/DicomFormat/DicomImageInformation.h	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomImageInformation.h	Mon Apr 07 13:23:11 2025 +0200
@@ -25,6 +25,7 @@
 #pragma once
 
 #include "DicomMap.h"
+#include "Window.h"
 
 #include <stdint.h>
 
@@ -48,6 +49,10 @@
 
     PhotometricInterpretation  photometric_;
 
+    double rescaleSlope_;
+    double rescaleIntercept_;
+    std::vector<Window>  windows_;
+
   protected:
     explicit DicomImageInformation()
     {
@@ -99,5 +104,51 @@
 
     static ValueRepresentation GuessPixelDataValueRepresentation(const DicomTransferSyntax& transferSyntax,
                                                                  unsigned int bitsAllocated);
+
+    double GetRescaleSlope() const
+    {
+      return rescaleSlope_;
+    }
+
+    double GetRescaleIntercept() const
+    {
+      return rescaleIntercept_;
+    }
+
+    bool HasWindows() const
+    {
+      return !windows_.empty();
+    }
+
+    size_t GetWindowsCount() const
+    {
+      return windows_.size();
+    }
+
+    const Window& GetWindow(size_t index) const;
+
+    double ApplyRescale(double value) const;
+
+    Window GetDefaultWindow() const;
+
+    /**
+     * Compute the linear transform "x * scaling + offset" that maps a
+     * window onto the [0,255] range of a grayscale image. The
+     * inversion due to MONOCHROME1 is taken into consideration. This
+     * information can be used in ImageProcessing::ShiftScale2().
+     **/
+    void ComputeRenderingTransform(double& offset,
+                                   double& scaling,
+                                   const Window& window) const;
+
+    void ComputeRenderingTransform(double& offset,
+                                   double& scaling,
+                                   size_t windowIndex) const
+    {
+      ComputeRenderingTransform(offset, scaling, GetWindow(windowIndex));
+    }
+
+    void ComputeRenderingTransform(double& offset,
+                                   double& scaling) const;  // Use the default windowing
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomFormat/Window.cpp	Mon Apr 07 13:23:11 2025 +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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "Window.h"
+
+
+#include <cmath>
+
+namespace Orthanc
+{
+  Window::Window(double center,
+                 double width) :
+    center_(center)
+  {
+    width_ = std::abs(width);
+  }
+
+
+  void Window::GetBounds(double& low,
+                         double& high) const
+  {
+    low = center_ - width_ / 2.0;
+    high = center_ + width_ / 2.0;
+  }
+
+
+  Window Window::FromBounds(double low,
+                            double high)
+  {
+    return Window((low + high) / 2.0, std::abs(high - low));
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomFormat/Window.h	Mon Apr 07 13:23:11 2025 +0200
@@ -0,0 +1,59 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Compatibility.h"
+#include "../OrthancFramework.h"
+
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC Window
+  {
+  private:
+    double center_;
+    double width_;
+
+  public:
+    Window(double center,
+           double width);
+
+    double GetCenter() const
+    {
+      return center_;
+    }
+
+    double GetWidth() const
+    {
+      return width_;
+    }
+
+    void GetBounds(double& low,
+                   double& high) const;
+
+    static Window FromBounds(double low,
+                             double high);
+  };
+}
--- a/OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.cpp	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.cpp	Mon Apr 07 13:23:11 2025 +0200
@@ -25,6 +25,16 @@
 #include "../../PrecompiledHeaders.h"
 #include "DicomTls.h"
 
+
+// This must be *before* the inclusion of "Logging.h"
+#if defined(__ORTHANC_FILE__)
+// Prevents the system-wide DCMTK library from leaking the
+// full path of this source file in "DCMTLS_ERROR()"
+#  undef __FILE__
+#  define __FILE__ __ORTHANC_FILE__
+#endif
+
+
 #include "../../Logging.h"
 #include "../../OrthancException.h"
 #include "../../SystemToolbox.h"
@@ -44,16 +54,6 @@
 #endif
 
 
-#if ORTHANC_ENABLE_PLUGINS == 1
-#  if defined(__ORTHANC_FILE__)
-//   Prevents the system-wide DCMTK library from leaking the
-//   full path of this source file in "DCMTLS_ERROR()"
-#    undef __FILE__
-#    define __FILE__ __ORTHANC_FILE__
-#  endif
-#endif
-
-
 namespace Orthanc
 {
   namespace Internals
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp	Mon Apr 07 13:23:11 2025 +0200
@@ -1312,13 +1312,14 @@
   }
 
 
-  void ParsedDicomFile::EmbedImage(const ImageAccessor& accessor)
+  void ParsedDicomFile::ConfigureTagsForUncompressedImage(unsigned int& bytesPerPixel /* out */,
+                                                          const ImageAccessor& accessor)
   {
     if (accessor.GetFormat() != PixelFormat_Grayscale8 &&
         accessor.GetFormat() != PixelFormat_Grayscale16 &&
         accessor.GetFormat() != PixelFormat_SignedGrayscale16 &&
         accessor.GetFormat() != PixelFormat_RGB24 &&
-        accessor.GetFormat() != PixelFormat_RGBA32 && 
+        accessor.GetFormat() != PixelFormat_RGBA32 &&
         accessor.GetFormat() != PixelFormat_RGBA64)
     {
       throw OrthancException(ErrorCode_NotImplemented);
@@ -1326,7 +1327,7 @@
 
     InvalidateCache();
 
-    if (accessor.GetFormat() == PixelFormat_RGBA32 || 
+    if (accessor.GetFormat() == PixelFormat_RGBA32 ||
         accessor.GetFormat() == PixelFormat_RGBA64)
     {
       LOG(WARNING) << "Getting rid of the alpha channel when embedding a RGBA image inside DICOM";
@@ -1351,7 +1352,7 @@
       ReplacePlainString(DICOM_TAG_PIXEL_REPRESENTATION, "0");  // Unsigned pixels
     }
 
-    unsigned int bytesPerPixel = 0;
+    bytesPerPixel = 0;
 
     switch (accessor.GetFormat())
     {
@@ -1379,7 +1380,7 @@
         ReplacePlainString(DICOM_TAG_PLANAR_CONFIGURATION, "0");  // Color channels are interleaved
 
         break;
-      
+
       case PixelFormat_RGBA64:
         ReplacePlainString(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "RGB");
         ReplacePlainString(DICOM_TAG_SAMPLES_PER_PIXEL, "3");
@@ -1410,6 +1411,12 @@
     }
 
     assert(bytesPerPixel != 0);
+  }
+
+  void ParsedDicomFile::EmbedImage(const ImageAccessor& accessor)
+  {
+    unsigned int bytesPerPixel = 0;
+    ConfigureTagsForUncompressedImage(bytesPerPixel, accessor);
 
     DcmTag key(DICOM_TAG_PIXEL_DATA.GetGroup(), 
                DICOM_TAG_PIXEL_DATA.GetElement());
@@ -1928,7 +1935,7 @@
       }
 
       windowWidth = static_cast<double>(1 << bitsStored);
-      windowCenter = windowWidth / 2.0f;
+      windowCenter = windowWidth / 2.0;
     }
   }
 
@@ -2232,6 +2239,63 @@
   }
 
 
+  void ParsedDicomFile::EncapsulatePixelData(const std::string& dataUriScheme)
+  {
+    std::string mime, content;
+    if (!Toolbox::DecodeDataUriScheme(mime, content, dataUriScheme))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    Remove(DICOM_TAG_PIXEL_DATA);
+
+    if (mime == MIME_JPEG)
+    {
+#if ORTHANC_ENABLE_JPEG == 1
+      JpegReader reader;
+      reader.ReadFromMemory(content);
+      unsigned int bytesPerPixel = 0;
+
+      ConfigureTagsForUncompressedImage(bytesPerPixel, reader);
+
+      if (reader.GetFormat() == PixelFormat_RGB24)
+      {
+        ReplacePlainString(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "YBR_FULL_422");
+      }
+
+      Uint8* raw = const_cast<Uint8*>(reinterpret_cast<const Uint8*>(content.c_str()));
+
+      DcmOffsetList offsetList;
+
+      std::unique_ptr<DcmPixelSequence> pixelSequence(new DcmPixelSequence(DCM_PixelData));
+
+      DcmPixelItem* offsetTable = new DcmPixelItem(DCM_PixelItemTag);
+      if (!pixelSequence->insert(offsetTable).good() ||
+          !pixelSequence->storeCompressedFrame(offsetList, raw, content.size(), 0 /* unlimited fragment size */).good() ||
+          !offsetTable->createOffsetTable(offsetList).good())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      std::unique_ptr<DcmPixelData> pixelData(new DcmPixelData(DCM_PixelData));
+      pixelData->putOriginalRepresentation(EXS_JPEGProcess1, NULL, pixelSequence.release());
+
+      if (!GetDcmtkObject().getDataset()->insert(pixelData.release(), true, false).good() ||
+          !GetDcmtkObject().getDataset()->chooseRepresentation(EXS_JPEGProcess1, NULL).good())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+#else
+      throw OrthancException(ErrorCode_InternalError, "Orthanc was compiled without support for JPEG");
+#endif
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NotImplemented, "Cannot encapsulate pixel data from MIME type: " + mime);
+    }
+  }
+
+
 #if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
   // Alias for binary compatibility with Orthanc Framework 1.7.2 => don't use it anymore
   void ParsedDicomFile::DatasetToJson(Json::Value& target,
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h	Mon Apr 07 13:23:11 2025 +0200
@@ -105,6 +105,9 @@
     // the top of DCMTK API
     DcmFileFormat& GetDcmtkObjectConst() const;
 
+    void ConfigureTagsForUncompressedImage(unsigned int& bytesPerPixel /* out */,
+                                           const ImageAccessor& accessor);
+
     explicit ParsedDicomFile(DcmFileFormat* dicom);  // This takes ownership (no clone)
 
 #if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
@@ -319,5 +322,7 @@
     void RemoveFromPixelData();
 
     ValueRepresentation GuessPixelDataValueRepresentation() const;
+
+    void EncapsulatePixelData(const std::string& dataUriScheme);
   };
 }
--- a/OrthancFramework/Sources/Images/ImageProcessing.cpp	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancFramework/Sources/Images/ImageProcessing.cpp	Mon Apr 07 13:23:11 2025 +0200
@@ -1541,6 +1541,39 @@
 
         switch (source.GetFormat())
         {
+          case PixelFormat_Grayscale8:
+            if (useRound)
+            {
+              ShiftScaleIntegerInternal<uint8_t, uint8_t, true, false>(target, source, a, b);
+            }
+            else
+            {
+              ShiftScaleIntegerInternal<uint8_t, uint8_t, false, false>(target, source, a, b);
+            }
+            return;
+
+          case PixelFormat_Grayscale16:
+            if (useRound)
+            {
+              ShiftScaleIntegerInternal<uint8_t, uint16_t, true, false>(target, source, a, b);
+            }
+            else
+            {
+              ShiftScaleIntegerInternal<uint8_t, uint16_t, false, false>(target, source, a, b);
+            }
+            return;
+
+          case PixelFormat_SignedGrayscale16:
+            if (useRound)
+            {
+              ShiftScaleIntegerInternal<uint8_t, int16_t, true, false>(target, source, a, b);
+            }
+            else
+            {
+              ShiftScaleIntegerInternal<uint8_t, int16_t, false, false>(target, source, a, b);
+            }
+            return;
+
           case PixelFormat_Float32:
             if (useRound)
             {
@@ -3070,4 +3103,74 @@
         throw OrthancException(ErrorCode_NotImplemented);
     }
   }
+
+
+  void ImageProcessing::Render(ImageAccessor& target,
+                               const DicomImageInformation& info,
+                               const ImageAccessor& source,
+                               const Window& window)
+  {
+    if (source.GetFormat() == PixelFormat_RGB24)
+    {
+      Copy(target, source);
+    }
+    else if (source.GetFormat() == PixelFormat_Grayscale8 ||
+             source.GetFormat() == PixelFormat_Grayscale16 ||
+             source.GetFormat() == PixelFormat_SignedGrayscale16)
+    {
+      if (target.GetFormat() != PixelFormat_Grayscale8)
+      {
+        throw OrthancException(ErrorCode_IncompatibleImageFormat);
+      }
+
+      double offset, scaling;
+      info.ComputeRenderingTransform(offset, scaling);
+      ShiftScale2(target, source, static_cast<float>(offset), static_cast<float>(scaling), false);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  void ImageProcessing::RenderDefaultWindow(ImageAccessor& target,
+                                            const DicomImageInformation& info,
+                                            const ImageAccessor& source)
+  {
+    if (source.GetFormat() == PixelFormat_RGB24)
+    {
+      Copy(target, source);
+    }
+    else if (source.GetFormat() == PixelFormat_Grayscale8 ||
+             source.GetFormat() == PixelFormat_Grayscale16 ||
+             source.GetFormat() == PixelFormat_SignedGrayscale16)
+    {
+      if (target.GetFormat() != PixelFormat_Grayscale8)
+      {
+        throw OrthancException(ErrorCode_IncompatibleImageFormat);
+      }
+
+      double offset, scaling;
+      if (info.HasWindows())
+      {
+        info.ComputeRenderingTransform(offset, scaling);  // Use the default windowing
+      }
+      else
+      {
+        // Use the full dynamic range of the image
+        int64_t minValue, maxValue;
+        GetMinMaxIntegerValue(minValue, maxValue, source);
+        double minRescaled = info.ApplyRescale(minValue);
+        double maxRescaled = info.ApplyRescale(maxValue);
+        info.ComputeRenderingTransform(offset, scaling, Window::FromBounds(minRescaled, maxRescaled));
+      }
+
+      ShiftScale2(target, source, static_cast<float>(offset), static_cast<float>(scaling), false);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
 }
--- a/OrthancFramework/Sources/Images/ImageProcessing.h	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancFramework/Sources/Images/ImageProcessing.h	Mon Apr 07 13:23:11 2025 +0200
@@ -26,6 +26,7 @@
 
 #include "../OrthancFramework.h"
 
+#include "../DicomFormat/DicomImageInformation.h"
 #include "ImageAccessor.h"
 
 #include <vector>
@@ -222,5 +223,14 @@
 
     static void Maximum(ImageAccessor& image /* inout */,
                         const ImageAccessor& other);
+
+    static void Render(ImageAccessor& target,
+                       const DicomImageInformation& info,
+                       const ImageAccessor& source,
+                       const Window& window);
+
+    static void RenderDefaultWindow(ImageAccessor& target,
+                                    const DicomImageInformation& info,
+                                    const ImageAccessor& source);
   };
 }
--- a/OrthancFramework/UnitTestsSources/CMakeLists.txt	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancFramework/UnitTestsSources/CMakeLists.txt	Mon Apr 07 13:23:11 2025 +0200
@@ -24,7 +24,7 @@
 ## This file is meant to be used only by ../SharedLibrary/CMakeLists.txt
 ##
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 project(UnitTestsProject)
 
 set(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)")
--- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Mon Apr 07 13:23:11 2025 +0200
@@ -3110,6 +3110,9 @@
 
 TEST(ParsedDicomFile, ImageInformation)
 {
+  // If modifying this test, make sure to reflect the modification in
+  // "TEST(DicomImageInformation, FromDcmtkTests)" in file "ImageProcessingTests.cpp"
+
   double wc, ww;
   double ri, rs;
   PhotometricInterpretation p;
--- a/OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp	Mon Apr 07 13:23:11 2025 +0200
@@ -81,6 +81,169 @@
 }
 
 
+TEST(DicomImageInformation, Windowing)
+{
+  DicomMap m;
+  m.SetValue(DICOM_TAG_ROWS, "24", false);
+  m.SetValue(DICOM_TAG_COLUMNS, "16", false);
+  m.SetValue(DICOM_TAG_BITS_ALLOCATED, "16", false);
+  m.SetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2", false);
+
+  {
+    DicomImageInformation info(m);
+    ASSERT_DOUBLE_EQ(1.0, info.GetRescaleSlope());
+    ASSERT_DOUBLE_EQ(0.0, info.GetRescaleIntercept());
+    ASSERT_EQ(PhotometricInterpretation_Monochrome2, info.GetPhotometricInterpretation());
+    ASSERT_EQ(0, info.GetWindowsCount());
+    ASSERT_DOUBLE_EQ(14.0, info.ApplyRescale(14.0));
+  }
+
+  m.SetValue(DICOM_TAG_RESCALE_SLOPE, "10.25", false);
+  m.SetValue(DICOM_TAG_RESCALE_INTERCEPT, "-1.75", false);
+  m.SetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME1", false);
+
+  {
+    DicomImageInformation info(m);
+    ASSERT_DOUBLE_EQ(10.25, info.GetRescaleSlope());
+    ASSERT_DOUBLE_EQ(-1.75, info.GetRescaleIntercept());
+    ASSERT_EQ(PhotometricInterpretation_Monochrome1, info.GetPhotometricInterpretation());
+    ASSERT_FALSE(info.HasWindows());
+    ASSERT_EQ(0, info.GetWindowsCount());
+    ASSERT_THROW(info.GetWindow(0), OrthancException);
+    ASSERT_DOUBLE_EQ(141.75, info.ApplyRescale(14.0));
+  }
+
+  m.SetValue(Orthanc::DICOM_TAG_WINDOW_CENTER, "10\\100\\1000", false);
+  m.SetValue(Orthanc::DICOM_TAG_WINDOW_WIDTH, "50\\60\\70", false);
+
+  {
+    DicomImageInformation info(m);
+    ASSERT_TRUE(info.HasWindows());
+    ASSERT_EQ(3u, info.GetWindowsCount());
+    ASSERT_DOUBLE_EQ(10.0, info.GetWindow(0).GetCenter());
+    ASSERT_DOUBLE_EQ(50.0, info.GetWindow(0).GetWidth());
+    ASSERT_DOUBLE_EQ(100.0, info.GetWindow(1).GetCenter());
+    ASSERT_DOUBLE_EQ(60.0, info.GetWindow(1).GetWidth());
+    ASSERT_DOUBLE_EQ(1000.0, info.GetWindow(2).GetCenter());
+    ASSERT_DOUBLE_EQ(70.0, info.GetWindow(2).GetWidth());
+    ASSERT_THROW(info.GetWindow(3), OrthancException);
+  }
+}
+
+
+TEST(DicomImageInformation, FromDcmtkTests)
+{
+  // This replicates TEST(ParsedDicomFile, ImageInformation) in
+  // "FromDcmtkTests.cpp", without the handling of frames and sequences
+
+  DicomMap m;
+  m.SetValue(DICOM_TAG_ROWS, "24", false);
+  m.SetValue(DICOM_TAG_COLUMNS, "16", false);
+  m.SetValue(DICOM_TAG_BITS_ALLOCATED, "8", false);
+
+  {
+    DicomImageInformation info(m);
+    Window w = info.GetDefaultWindow();
+    ASSERT_DOUBLE_EQ(128.0, w.GetCenter());
+    ASSERT_DOUBLE_EQ(256.0, w.GetWidth());
+    ASSERT_EQ(PhotometricInterpretation_Unknown, info.GetPhotometricInterpretation());
+    ASSERT_DOUBLE_EQ(0.0, info.GetRescaleIntercept());
+    ASSERT_DOUBLE_EQ(1.0, info.GetRescaleSlope());
+
+    double offset, scaling, x;
+    info.ComputeRenderingTransform(offset, scaling, Window(-100, 200));
+
+    x = -200;  ASSERT_NEAR(0, x * scaling + offset, 0.000001);
+    x = -100;  ASSERT_NEAR(127.5, x * scaling + offset, 0.000001);
+    x = 0;     ASSERT_NEAR(255, x * scaling + offset, 0.000001);
+  }
+
+  m.SetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME1", false);
+
+  {
+    DicomImageInformation info(m);
+    ASSERT_EQ(PhotometricInterpretation_Monochrome1, info.GetPhotometricInterpretation());
+
+    double offset, scaling, x;
+    info.ComputeRenderingTransform(offset, scaling, Window(-100, 200));
+
+    x = -200;  ASSERT_NEAR(255, x * scaling + offset, 0.000001);
+    x = -100;  ASSERT_NEAR(127.5, x * scaling + offset, 0.000001);
+    x = 0;     ASSERT_NEAR(0, x * scaling + offset, 0.000001);
+  }
+
+  m.SetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2", false);
+  m.SetValue(DICOM_TAG_RESCALE_SLOPE, "20", false);
+  m.SetValue(DICOM_TAG_RESCALE_INTERCEPT, "-100", false);
+
+  {
+    DicomImageInformation info(m);
+    ASSERT_EQ(PhotometricInterpretation_Monochrome2, info.GetPhotometricInterpretation());
+
+    double offset, scaling, x;
+    info.ComputeRenderingTransform(offset, scaling, Window(-100, 200));
+
+    x = -5; ASSERT_NEAR(0, x * scaling + offset, 0.000001);
+    x = 0;  ASSERT_NEAR(127.5, x * scaling + offset, 0.000001);
+    x = 5;  ASSERT_NEAR(255, x * scaling + offset, 0.000001);
+  }
+
+  m.SetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME1", false);
+
+  {
+    DicomImageInformation info(m);
+    ASSERT_EQ(PhotometricInterpretation_Monochrome1, info.GetPhotometricInterpretation());
+
+    double offset, scaling, x;
+    info.ComputeRenderingTransform(offset, scaling, Window(-100, 200));
+
+    x = -5; ASSERT_NEAR(255, x * scaling + offset, 0.000001);
+    x = 0;  ASSERT_NEAR(127.5, x * scaling + offset, 0.000001);
+    x = 5;  ASSERT_NEAR(0, x * scaling + offset, 0.000001);
+  }
+
+  m.SetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "RGB", false);
+  m.SetValue(DICOM_TAG_BITS_STORED, "4", false);
+
+  {
+    DicomImageInformation info(m);
+    Window w = info.GetDefaultWindow();
+    ASSERT_DOUBLE_EQ(8.0, w.GetCenter());
+    ASSERT_DOUBLE_EQ(16.0, w.GetWidth());
+    ASSERT_EQ(PhotometricInterpretation_RGB, info.GetPhotometricInterpretation());
+  }
+
+  m.SetValue(DICOM_TAG_WINDOW_CENTER, "12", false);
+  m.SetValue(DICOM_TAG_WINDOW_WIDTH, "-22", false);
+  m.SetValue(DICOM_TAG_RESCALE_INTERCEPT, "-22", false);
+  m.SetValue(DICOM_TAG_RESCALE_SLOPE, "-23", false);
+
+  {
+    DicomImageInformation info(m);
+    Window w = info.GetDefaultWindow();
+    ASSERT_DOUBLE_EQ(12.0, w.GetCenter());
+    ASSERT_DOUBLE_EQ(22.0, w.GetWidth());
+    ASSERT_DOUBLE_EQ(-22.0, info.GetRescaleIntercept());
+    ASSERT_DOUBLE_EQ(-23.0, info.GetRescaleSlope());
+  }
+
+  m.Remove(DICOM_TAG_RESCALE_SLOPE);
+  m.Remove(DICOM_TAG_RESCALE_INTERCEPT);
+  m.SetValue(DICOM_TAG_WINDOW_CENTER, "12\\13\\14", false);
+  m.SetValue(DICOM_TAG_WINDOW_WIDTH, "-22\\-23\\-24", false);
+  m.SetValue(DICOM_TAG_RESCALE_INTERCEPT, "-22\\33\\34", false);
+  m.SetValue(DICOM_TAG_RESCALE_SLOPE, "-23\\-43\\-44", false);
+
+  {
+    DicomImageInformation info(m);
+    Window w = info.GetDefaultWindow();
+    ASSERT_DOUBLE_EQ(12.0, w.GetCenter());
+    ASSERT_DOUBLE_EQ(22.0, w.GetWidth());
+    ASSERT_DOUBLE_EQ(0.0, info.GetRescaleIntercept());
+    ASSERT_DOUBLE_EQ(1.0, info.GetRescaleSlope());
+  }
+}
+
 
 namespace
 {
--- a/OrthancServer/CMakeLists.txt	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancServer/CMakeLists.txt	Mon Apr 07 13:23:11 2025 +0200
@@ -19,7 +19,7 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 cmake_policy(SET CMP0058 NEW)
 
 project(Orthanc)
@@ -533,6 +533,8 @@
 
   DefineSourceBasenameForTarget(PluginsDependencies)
 
+  add_dependencies(PluginsDependencies AutogeneratedTarget)
+
   # Add the "-fPIC" option as this static library must be embedded
   # inside shared libraries (important on UNIX)
   set_target_properties(
--- a/OrthancServer/Plugins/Samples/AutomatedJpeg2kCompression/CMakeLists.txt	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancServer/Plugins/Samples/AutomatedJpeg2kCompression/CMakeLists.txt	Mon Apr 07 13:23:11 2025 +0200
@@ -19,7 +19,7 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 
 project(Basic)
 
--- a/OrthancServer/Plugins/Samples/Basic/CMakeLists.txt	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancServer/Plugins/Samples/Basic/CMakeLists.txt	Mon Apr 07 13:23:11 2025 +0200
@@ -19,7 +19,7 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 
 project(Basic)
 
--- a/OrthancServer/Plugins/Samples/Common/OrthancPlugins.cmake	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPlugins.cmake	Mon Apr 07 13:23:11 2025 +0200
@@ -22,7 +22,16 @@
 include(CheckIncludeFiles)
 include(CheckIncludeFileCXX)
 include(CheckLibraryExists)
-include(FindPythonInterp)
+
+if(CMAKE_VERSION VERSION_GREATER "3.11")
+  find_package(Python REQUIRED COMPONENTS Interpreter)
+  set(PYTHON_EXECUTABLE ${Python_EXECUTABLE})
+else()
+  include(FindPythonInterp)
+  find_package(PythonInterp REQUIRED)
+endif()
+
+
 include(${CMAKE_CURRENT_LIST_DIR}/../../../../OrthancFramework/Resources/CMake/AutoGeneratedCode.cmake)
 include(${CMAKE_CURRENT_LIST_DIR}/../../../../OrthancFramework/Resources/CMake/DownloadPackage.cmake)
 include(${CMAKE_CURRENT_LIST_DIR}/../../../../OrthancFramework/Resources/CMake/Compiler.cmake)
--- a/OrthancServer/Plugins/Samples/ConnectivityChecks/CMakeLists.txt	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancServer/Plugins/Samples/ConnectivityChecks/CMakeLists.txt	Mon Apr 07 13:23:11 2025 +0200
@@ -19,7 +19,7 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 cmake_policy(SET CMP0058 NEW)
 
 project(ConnectivityChecks)
--- a/OrthancServer/Plugins/Samples/CustomImageDecoder/CMakeLists.txt	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancServer/Plugins/Samples/CustomImageDecoder/CMakeLists.txt	Mon Apr 07 13:23:11 2025 +0200
@@ -19,7 +19,7 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 
 project(CustomImageDecoder)
 
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/CMakeLists.txt	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancServer/Plugins/Samples/DelayedDeletion/CMakeLists.txt	Mon Apr 07 13:23:11 2025 +0200
@@ -19,7 +19,7 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 cmake_policy(SET CMP0058 NEW)
 
 project(DelayedDeletion)
--- a/OrthancServer/Plugins/Samples/ModalityWorklists/CMakeLists.txt	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancServer/Plugins/Samples/ModalityWorklists/CMakeLists.txt	Mon Apr 07 13:23:11 2025 +0200
@@ -19,7 +19,7 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 
 project(ModalityWorklists)
 
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/CMakeLists.txt	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/CMakeLists.txt	Mon Apr 07 13:23:11 2025 +0200
@@ -19,7 +19,7 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 
 project(MultitenantDicom)
 
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/OrthancFrameworkDependencies.cpp	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/OrthancFrameworkDependencies.cpp	Mon Apr 07 13:23:11 2025 +0200
@@ -29,6 +29,10 @@
 #  include <winsock2.h>
 #endif
 
+// This must be the first inclusion, as it can modify __FILE__ to
+// avoid the leaking of paths for reproducible builds
+#include "../../../../OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.cpp"
+
 #include "../../../../OrthancFramework/Sources/ChunkedBuffer.cpp"
 #include "../../../../OrthancFramework/Sources/Compression/DeflateBaseCompressor.cpp"
 #include "../../../../OrthancFramework/Sources/Compression/GzipCompressor.cpp"
@@ -42,12 +46,12 @@
 #include "../../../../OrthancFramework/Sources/DicomFormat/DicomPath.cpp"
 #include "../../../../OrthancFramework/Sources/DicomFormat/DicomTag.cpp"
 #include "../../../../OrthancFramework/Sources/DicomFormat/DicomValue.cpp"
+#include "../../../../OrthancFramework/Sources/DicomFormat/Window.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"
--- a/OrthancServer/Plugins/Samples/Sanitizer/CMakeLists.txt	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancServer/Plugins/Samples/Sanitizer/CMakeLists.txt	Mon Apr 07 13:23:11 2025 +0200
@@ -19,7 +19,7 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 
 project(Sanitizer)
 
--- a/OrthancServer/Plugins/Samples/ServeFolders/CMakeLists.txt	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancServer/Plugins/Samples/ServeFolders/CMakeLists.txt	Mon Apr 07 13:23:11 2025 +0200
@@ -19,7 +19,7 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 
 project(ServeFolders)
 
--- a/OrthancServer/Plugins/Samples/StorageArea/CMakeLists.txt	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancServer/Plugins/Samples/StorageArea/CMakeLists.txt	Mon Apr 07 13:23:11 2025 +0200
@@ -19,7 +19,7 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 
 project(Basic)
 
--- a/OrthancServer/Plugins/Samples/StorageCommitmentScp/CMakeLists.txt	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancServer/Plugins/Samples/StorageCommitmentScp/CMakeLists.txt	Mon Apr 07 13:23:11 2025 +0200
@@ -19,7 +19,7 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 
 project(StorageCommitmentScp)
 
--- a/OrthancServer/Plugins/Samples/WebDavFilesystem/CMakeLists.txt	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancServer/Plugins/Samples/WebDavFilesystem/CMakeLists.txt	Mon Apr 07 13:23:11 2025 +0200
@@ -19,7 +19,7 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 
 project(WebDavFilesystem)
 
--- a/OrthancServer/Plugins/Samples/WebSkeleton/CMakeLists.txt	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancServer/Plugins/Samples/WebSkeleton/CMakeLists.txt	Mon Apr 07 13:23:11 2025 +0200
@@ -19,7 +19,7 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 
 project(WebSkeleton)
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/ImplementationNotes/windowing.py	Mon Apr 07 13:23:11 2025 +0200
@@ -0,0 +1,17 @@
+#!/usr/bin/env python3
+
+import sympy
+
+x = sympy.symbols('x')
+(rescaleSlope, rescaleIntercept) = sympy.symbols('rescaleSlope rescaleIntercept')
+(windowCenter, windowWidth) = sympy.symbols('windowCenter windowWidth')
+
+t1 = rescaleSlope * x + rescaleIntercept
+
+# Slide 19 of Session 8 of LSINC1114
+low = windowCenter - windowWidth / 2.0
+high = windowCenter + windowWidth / 2.0
+t2 = 255.0 * (t1 - low) / (high - low)
+
+print('MONOCHROME1:', sympy.expand(255 - t2))
+print('MONOCHROME2:', sympy.expand(t2))
--- a/OrthancServer/Resources/PreventProtobufDirectoryLeaks.py	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancServer/Resources/PreventProtobufDirectoryLeaks.py	Mon Apr 07 13:23:11 2025 +0200
@@ -41,8 +41,10 @@
 else:
     # New version in Orthanc 1.12.5
     s = """
-#undef __FILE__
-#define __FILE__ __ORTHANC_FILE__
+#if defined(__ORTHANC_FILE__)
+#  undef __FILE__
+#  define __FILE__ __ORTHANC_FILE__
+#endif
 """ + s
 
 with open(sys.argv[1], 'w') as f:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Python/EncapsulateJPEG.py	Mon Apr 07 13:23:11 2025 +0200
@@ -0,0 +1,72 @@
+#!/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-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, 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 encapsulate a JPEG
+# image into a DICOM enveloppe, without any transcoding.
+#
+
+import base64
+import json
+import requests
+
+
+##################
+##  Parameters  ##
+##################
+
+JPEG = '/tmp/sample.jpg'
+URL = 'http://localhost:8042/'
+USERNAME = 'orthanc'
+PASSWORD = 'orthanc'
+
+TAGS = {
+    'PatientID' : 'Test',
+    'PatientName' : 'Hello^World',
+    'SOPClassUID' : '1.2.840.10008.5.1.4.1.1.7',  # Secondary capture
+    }
+
+
+
+########################################
+##  Application of the DICOM-ization  ##
+########################################
+
+with open(JPEG, 'rb') as f:
+    jpeg = f.read()
+
+b = base64.b64encode(jpeg)
+content = 'data:image/jpeg;base64,%s' % b.decode()
+
+command = {
+    'Content' : content,
+    'Tags' : TAGS,
+    'Encapsulate' : True,
+}
+
+r = requests.post('%s/tools/create-dicom' % URL, json.dumps(command),
+                  auth = requests.auth.HTTPBasicAuth(USERNAME, PASSWORD))
+r.raise_for_status()
+
+print('URL of the newly created instance: %s/instances/%s/file' % (URL, r.json() ['ID']))
--- a/OrthancServer/Resources/Samples/Tools/CMakeLists.txt	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancServer/Resources/Samples/Tools/CMakeLists.txt	Mon Apr 07 13:23:11 2025 +0200
@@ -19,7 +19,7 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 
 project(OrthancTools)
 
--- a/OrthancServer/Resources/Testing/Issue32/Cpp/CMakeLists.txt	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancServer/Resources/Testing/Issue32/Cpp/CMakeLists.txt	Mon Apr 07 13:23:11 2025 +0200
@@ -19,7 +19,7 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 
 project(Orthanc)
 
--- a/OrthancServer/Sources/OrthancInitialization.cpp	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancServer/Sources/OrthancInitialization.cpp	Mon Apr 07 13:23:11 2025 +0200
@@ -36,6 +36,22 @@
 #  include <malloc.h>
 #endif
 
+
+// This must be *before* the inclusion of "Logging.h"
+#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
+
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+#  include <google/protobuf/stubs/common.h>
+#  include <google/protobuf/any.h>
+#endif
+
+
 #include "OrthancInitialization.h"
 
 #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
@@ -53,16 +69,6 @@
 
 #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/stubs/common.h>
-#  include <google/protobuf/any.h>
-#endif
 
 
 static const char* const STORAGE_DIRECTORY = "StorageDirectory";
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Mon Apr 07 13:23:11 2025 +0200
@@ -59,6 +59,7 @@
 static const char* const TAGS = "Tags";
 static const char* const TRANSCODE = "Transcode";
 static const char* const LOSSY_QUALITY = "LossyQuality";
+static const char* const ENCAPSULATE = "Encapsulate";  // New in Orthanc 1.12.7
 
 
 
@@ -1008,20 +1009,40 @@
     // Inject the content (either an image, a PDF file, or a STL/OBJ/MTL file)
     if (request.isMember(CONTENT))
     {
+      bool encapsulate = false;
+      if (request.isMember(ENCAPSULATE))
+      {
+        encapsulate = request[ENCAPSULATE].asBool();
+      }
+
       const Json::Value& content = request[CONTENT];
 
       if (content.type() == Json::stringValue)
       {
-        dicom.EmbedContent(request[CONTENT].asString());
-
+        if (encapsulate)
+        {
+          // New in Orthanc 1.12.7
+          dicom.EncapsulatePixelData(request[CONTENT].asString());
+        }
+        else
+        {
+          dicom.EmbedContent(request[CONTENT].asString());
+        }
       }
       else if (content.type() == Json::arrayValue)
       {
         if (content.size() > 0)
         {
-          // Let's create a series instead of a single instance
-          CreateSeries(call, dicom, content, decodeBinaryTags, privateCreator, force);
-          return;
+          if (encapsulate)
+          {
+            throw OrthancException(ErrorCode_NotImplemented);
+          }
+          else
+          {
+            // Let's create a series instead of a single instance
+            CreateSeries(call, dicom, content, decodeBinaryTags, privateCreator, force);
+            return;
+          }
         }
       }
       else
@@ -1066,6 +1087,10 @@
                          "Avoid the consistency checks for the DICOM tags that enforce the DICOM model of the real-world. "
                          "You can notably use this flag if you need to manually set the tags `StudyInstanceUID`, "
                          "`SeriesInstanceUID`, or `SOPInstanceUID`. Be careful with this feature.", false)
+        .SetRequestField(ENCAPSULATE, RestApiCallDocumentation::Type_Boolean,
+                         "If set to `true`, encapsulate the binary data of `ContentData` as such, using a compressed transfer syntax. "
+                         "Only applicable if `ContentData` contains a grayscale or color JPEG image in 8bpp, "
+                         "in which case the transfer syntax is set to \"1.2.840.10008.1.2.4.50\". (new in Orthanc 1.12.7)", false)
         .SetAnswerField("ID", RestApiCallDocumentation::Type_String, "Orthanc identifier of the newly created instance")
         .SetAnswerField("Path", RestApiCallDocumentation::Type_String, "Path to access the instance in the REST API");
       return;
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp	Mon Apr 07 13:23:11 2025 +0200
@@ -578,10 +578,7 @@
         .SetDescription("Create a " + m + " containing the DICOM resources (patients, studies, series, or instances) "
                         "whose Orthanc identifiers are provided in the body")
         .SetRequestField(KEY_RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings,
-                         "The list of Orthanc identifiers of interest.", false)
-        .SetRequestField(KEY_FILENAME, RestApiCallDocumentation::Type_String,
-                         "Filename to set in the \"Content-Disposition\" HTTP header "
-                         "(including file extension)", false);
+                         "The list of Orthanc identifiers of interest.", false);
       return;
     }
 
@@ -660,16 +657,6 @@
     }
 
     ServerContext& context = OrthancRestApi::GetContext(call);
-    bool transcode = false;
-    DicomTransferSyntax transferSyntax = DicomTransferSyntax_LittleEndianImplicit;  // Initialize variable to avoid warnings
-    unsigned int lossyQuality;
-
-    if (call.HasArgument(GET_TRANSCODE))
-    {
-      transcode = true;
-      transferSyntax = GetTransferSyntax(call.GetArgument(GET_TRANSCODE, ""));
-      lossyQuality = GetLossyQuality(call);
-    }
     
     if (!call.HasArgument(GET_RESOURCES))
     {
@@ -679,10 +666,10 @@
     std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, DEFAULT_IS_EXTENDED, ResourceType_Patient));
     AddResourcesOfInterestFromString(*job, call.GetArgument(GET_RESOURCES, ""));
 
-    if (transcode)
+    if (call.HasArgument(GET_TRANSCODE))
     {
-      job->SetTranscode(transferSyntax);
-      job->SetLossyQuality(lossyQuality);
+      job->SetTranscode(GetTransferSyntax(call.GetArgument(GET_TRANSCODE, "")));
+      job->SetLossyQuality(GetLossyQuality(call));
     }
 
     const std::string filename = call.GetArgument(GET_FILENAME, "Archive.zip");  // New in Orthanc 1.12.7
--- a/OrthancServer/UnitTestsSources/VersionsTests.cpp	Tue Apr 01 16:49:49 2025 +0200
+++ b/OrthancServer/UnitTestsSources/VersionsTests.cpp	Mon Apr 07 13:23:11 2025 +0200
@@ -151,7 +151,7 @@
 
 TEST(Versions, LuaStatic)
 {
-  ASSERT_STREQ("Lua 5.3.5", LUA_RELEASE);
+  ASSERT_STREQ("Lua 5.4.7", LUA_RELEASE);
 }