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