# HG changeset patch # User Sebastien Jodogne # Date 1640361171 -3600 # Node ID 3e9a76464e8a3d2d9d94c5070f1a487bea27822b # Parent 2e71a08eea15687f482b49efef614a55a806f07d# Parent 6f780611fc034b020e383b426750ce2b557f7da2 integration mainline->openssl-3.x diff -r 2e71a08eea15 -r 3e9a76464e8a NEWS --- a/NEWS Thu Nov 25 19:02:38 2021 +0100 +++ b/NEWS Fri Dec 24 16:52:51 2021 +0100 @@ -26,6 +26,7 @@ * New configuration option "ZipLoaderThreads" to configure the number of threads used to read instances from storage when createing a Zip archive/media. * Support decoding of black-and-white images (with 1 bit per pixel), notably DICOM SEG +* Added links to download attachments from the Orthanc Explorer Maintenance ----------- @@ -44,7 +45,11 @@ unsupported.png only if the ?returnUnsupportedImage option is specified; otherwise, it raises a 415 error code. * Archive jobs response now contains a header Content-Disposition:filename='archive.zip' - +* "/instances/{...}/frames/{...}/numpy": Download the frame as a Python numpy array +* "/instances/{...}/numpy": Download the instance as a Python numpy array +* "/series/{...}/numpy": Download the series as a Python numpy array +* Added a ?full option to "/patients|studies|series|instances/{...}/attachments route + Lua --- diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake --- a/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake Fri Dec 24 16:52:51 2021 +0100 @@ -72,7 +72,10 @@ if (NOT ORTHANC_FRAMEWORK_MAJOR MATCHES "^[0-9]+$" OR NOT ORTHANC_FRAMEWORK_MINOR MATCHES "^[0-9]+$" OR NOT ORTHANC_FRAMEWORK_REVISION MATCHES "^[0-9]+$") - message("Bad version of the Orthanc framework: ${ORTHANC_FRAMEWORK_VERSION}") + message("Bad version of the Orthanc framework, assuming a pre-release: ${ORTHANC_FRAMEWORK_VERSION}") + set(ORTHANC_FRAMEWORK_MAJOR 999) + set(ORTHANC_FRAMEWORK_MINOR 999) + set(ORTHANC_FRAMEWORK_REVISION 999) endif() if (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.3.1") @@ -159,6 +162,9 @@ elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "23ad1b9c7800") # For "Toolbox::ReadJson()" and "Toolbox::Write{...}Json()" (pre-1.9.0) set(ORTHANC_FRAMEWORK_MD5 "9af92080e57c60dd288eba46ce606c00") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "b2e08d83e21d") + # WSI 1.1 (framework pre-1.9.8), to remove "-std=c++11" + set(ORTHANC_FRAMEWORK_MD5 "2eaa073cbb4b44ffba199ad93393b2b1") endif() endif() endif() @@ -501,35 +507,6 @@ message(FATAL_ERROR "Please install the libjsoncpp-dev package") endif() - # Switch to the C++11 standard if the version of JsonCpp is 1.y.z - # (same as variable JSONCPP_CXX11 in the source code of Orthanc) - if (EXISTS ${JSONCPP_INCLUDE_DIR}/json/version.h) - file(STRINGS - "${JSONCPP_INCLUDE_DIR}/json/version.h" - JSONCPP_VERSION_MAJOR1 REGEX - ".*define JSONCPP_VERSION_MAJOR.*") - - if (NOT JSONCPP_VERSION_MAJOR1) - message(FATAL_ERROR "Unable to extract the major version of JsonCpp") - endif() - - string(REGEX REPLACE - ".*JSONCPP_VERSION_MAJOR.*([0-9]+)$" "\\1" - JSONCPP_VERSION_MAJOR ${JSONCPP_VERSION_MAJOR1}) - message("JsonCpp major version: ${JSONCPP_VERSION_MAJOR}") - - if (JSONCPP_VERSION_MAJOR GREATER 0) - message("Switching to C++11 standard, as version of JsonCpp is >= 1.0.0") - if (CMAKE_COMPILER_IS_GNUCXX) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11") - elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") - endif() - endif() - else() - message("Unable to detect the major version of JsonCpp, assuming < 1.0.0") - endif() - # Look for Orthanc framework shared library include(CheckCXXSymbolExists) diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancFramework/Resources/CMake/JsonCppConfiguration.cmake --- a/OrthancFramework/Resources/CMake/JsonCppConfiguration.cmake Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancFramework/Resources/CMake/JsonCppConfiguration.cmake Fri Dec 24 16:52:51 2021 +0100 @@ -68,7 +68,7 @@ message(FATAL_ERROR "Please install the libjsoncpp-dev package") endif() - # Switch to the C++11 standard if the version of JsonCpp is 1.y.z + # Detect if the version of JsonCpp is >= 1.0.0 if (EXISTS ${JSONCPP_INCLUDE_DIR}/json/version.h) file(STRINGS "${JSONCPP_INCLUDE_DIR}/json/version.h" @@ -99,11 +99,9 @@ # https://gitlab.kitware.com/third-party/jsoncpp/commit/56df2068470241f9043b676bfae415ed62a0c172 add_definitions(-DJSONCPP_DEPRECATED_STACK_LIMIT=5000) - if (CMAKE_COMPILER_IS_GNUCXX) - message("Switching to C++11 standard in gcc, as version of JsonCpp is >= 1.0.0") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11") - elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") - message("Switching to C++11 standard in clang, as version of JsonCpp is >= 1.0.0") + 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() diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake --- a/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake Fri Dec 24 16:52:51 2021 +0100 @@ -183,6 +183,7 @@ ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Images/ImageAccessor.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Images/ImageBuffer.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Images/ImageProcessing.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Images/NumpyWriter.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Images/PamReader.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Images/PamWriter.cpp ) diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp --- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Fri Dec 24 16:52:51 2021 +0100 @@ -734,10 +734,15 @@ if (!(flags & DicomToJsonFlags_ConvertBinaryToNull)) { Uint8* data = NULL; + Uint16* data16 = NULL; if (element.getUint8Array(data) == EC_Normal) { return new DicomValue(reinterpret_cast(data), element.getLength(), true); } + else if (element.getUint16Array(data16) == EC_Normal) + { + return new DicomValue(reinterpret_cast(data16), element.getLength(), true); + } } return new DicomValue; @@ -1851,6 +1856,23 @@ break; } + case EVR_xs: // unsigned short, signed short or multiple values + { + if (decoded->find('\\') != std::string::npos) + { + ok = element.putString(decoded->c_str()).good(); + } + else if (decoded->find('-') != std::string::npos) + { + ok = element.putSint16(boost::lexical_cast(*decoded)).good(); + } + else + { + ok = element.putUint16(boost::lexical_cast(*decoded)).good(); + } + break; + } + case EVR_US: // unsigned short { ok = element.putUint16(boost::lexical_cast(*decoded)).good(); @@ -1902,7 +1924,6 @@ **/ case EVR_ox: // OB or OW depending on context - case EVR_xs: // SS or US depending on context case EVR_lt: // US, SS or OW depending on context, used for LUT Data (thus the name) case EVR_na: // na="not applicable", for data which has no VR case EVR_up: // up="unsigned pointer", used internally for DICOMDIR suppor diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp --- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp Fri Dec 24 16:52:51 2021 +0100 @@ -1195,6 +1195,10 @@ EmbedImage(mime, content); break; + case MimeType_Binary: + EmbedImage(mime, content); + break; + case MimeType_Pdf: EmbedPdf(content); break; @@ -1254,6 +1258,12 @@ break; } + case MimeType_Binary: + { + EmbedRawPixelData(content); + break; + } + default: throw OrthancException(ErrorCode_NotImplemented); } @@ -1407,7 +1417,24 @@ } } - + void ParsedDicomFile::EmbedRawPixelData(const std::string& content) + { + DcmTag key(DICOM_TAG_PIXEL_DATA.GetGroup(), + DICOM_TAG_PIXEL_DATA.GetElement()); + + std::unique_ptr pixels(new DcmPixelData(key)); + + Uint8* target = NULL; + pixels->createUint8Array(content.size(), target); + memcpy(target, content.c_str(), content.size()); + + if (!GetDcmtkObject().getDataset()->insert(pixels.release(), false, false).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + } + + Encoding ParsedDicomFile::DetectEncoding(bool& hasCodeExtensions) const { return FromDcmtkBridge::DetectEncoding(hasCodeExtensions, diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h --- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h Fri Dec 24 16:52:51 2021 +0100 @@ -212,6 +212,8 @@ void EmbedImage(MimeType mime, const std::string& content); + void EmbedRawPixelData(const std::string& content); + Encoding DetectEncoding(bool& hasCodeExtensions) const; // WARNING: This function only sets the encoding, it will not diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancFramework/Sources/Images/NumpyWriter.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/Images/NumpyWriter.cpp Fri Dec 24 16:52:51 2021 +0100 @@ -0,0 +1,242 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2021 Osimis S.A., Belgium + * Copyright (C) 2021-2021 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "NumpyWriter.h" + +#if ORTHANC_ENABLE_ZLIB == 1 +# include "../Compression/ZipWriter.h" +#endif + +#if ORTHANC_SANDBOXED == 0 +# include "../SystemToolbox.h" +#endif + +#include "../OrthancException.h" +#include "../Toolbox.h" + +#include + +namespace Orthanc +{ + void NumpyWriter::WriteHeader(ChunkedBuffer& target, + unsigned int depth, + unsigned int width, + unsigned int height, + PixelFormat format) + { + // https://numpy.org/devdocs/reference/generated/numpy.lib.format.html + static const unsigned char VERSION[] = { + 0x93, 'N', 'U', 'M', 'P', 'Y', + 0x01 /* major version: 1 */, + 0x00 /* minor version: 0 */ + }; + + std::string datatype; + + switch (Toolbox::DetectEndianness()) + { + case Endianness_Little: + datatype = "<"; + break; + + case Endianness_Big: + datatype = ">"; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + unsigned int channels; + + switch (format) + { + case PixelFormat_Grayscale8: + datatype += "u1"; + channels = 1; + break; + + case PixelFormat_Grayscale16: + datatype += "u2"; + channels = 1; + break; + + case PixelFormat_SignedGrayscale16: + datatype += "i2"; + channels = 1; + break; + + case PixelFormat_RGB24: + datatype += "u1"; + channels = 3; + break; + + case PixelFormat_Float32: + datatype += "f4"; + channels = 1; + break; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + + std::string depthString; + if (depth != 0) + { + depthString = boost::lexical_cast(depth) + ", "; + } + + const std::string info = ("{'descr': '" + datatype + "', 'fortran_order': False, " + + "'shape': (" + depthString + boost::lexical_cast(height) + + "," + boost::lexical_cast(width) + + "," + boost::lexical_cast(channels) + "), }"); + + const uint16_t minimumLength = sizeof(VERSION) + sizeof(uint16_t) + info.size() + 1 /* trailing '\n' */; + + // The length of the header must be evenly divisible by 64. This + // loop could be optimized by a "ceil()" operation, but we keep + // the code as simple as possible + uint16_t length = 64; + while (length < minimumLength) + { + length += 64; + } + + uint16_t countZeros = length - minimumLength; + uint16_t headerLength = info.size() + countZeros + 1 /* trailing '\n' */; + uint8_t highByte = headerLength / 256; + uint8_t lowByte = headerLength % 256; + + target.AddChunk(VERSION, sizeof(VERSION)); + target.AddChunk(&lowByte, 1); + target.AddChunk(&highByte, 1); + target.AddChunk(info); + target.AddChunk(std::string(countZeros, ' ')); + target.AddChunk("\n"); + } + + + void NumpyWriter::WritePixels(ChunkedBuffer& target, + const ImageAccessor& image) + { + size_t rowSize = image.GetBytesPerPixel() * image.GetWidth(); + + for (unsigned int y = 0; y < image.GetHeight(); y++) + { + target.AddChunk(image.GetConstRow(y), rowSize); + } + } + + + void NumpyWriter::Finalize(std::string& target, + ChunkedBuffer& source, + bool compress) + { + if (compress) + { +#if ORTHANC_ENABLE_ZLIB == 1 + // This is the default name of the first array if arrays are + // specified as positional arguments in "numpy.savez()" + // https://numpy.org/doc/stable/reference/generated/numpy.savez.html + const char* ARRAY_NAME = "arr_0"; + + std::string uncompressed; + source.Flatten(uncompressed); + + const bool isZip64 = (uncompressed.size() >= 1lu * 1024lu * 1024lu * 1024lu); + + ZipWriter writer; + writer.SetMemoryOutput(target, isZip64); + writer.Open(); + writer.OpenFile(ARRAY_NAME); + writer.Write(uncompressed); + writer.Close(); +#else + throw OrthancException(ErrorCode_InternalError, "Orthanc was compiled without support for zlib"); +#endif + } + else + { + source.Flatten(target); + } + } + + +#if ORTHANC_SANDBOXED == 0 + void NumpyWriter::WriteToFileInternal(const std::string& filename, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) + { + std::string content; + WriteToMemoryInternal(content, width, height, pitch, format, buffer); + + SystemToolbox::WriteFile(content, filename); + } +#endif + + + void NumpyWriter::WriteToMemoryInternal(std::string& content, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) + { + ChunkedBuffer chunks; + WriteHeader(chunks, 0 /* no depth */, width, height, format); + + ImageAccessor image; + image.AssignReadOnly(format, width, height, pitch, buffer); + WritePixels(chunks, image); + + Finalize(content, chunks, compressed_); + } + + + NumpyWriter::NumpyWriter() + { + compressed_ = false; + } + + + void NumpyWriter::SetCompressed(bool compressed) + { +#if ORTHANC_ENABLE_ZLIB == 1 + compressed_ = compressed; +#else + if (compressed) + { + throw OrthancException(ErrorCode_InternalError, "Orthanc was compiled without support for zlib"); + } +#endif + } + + + bool NumpyWriter::IsCompressed() const + { + return compressed_; + } +} diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancFramework/Sources/Images/NumpyWriter.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/Images/NumpyWriter.h Fri Dec 24 16:52:51 2021 +0100 @@ -0,0 +1,78 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2021 Osimis S.A., Belgium + * Copyright (C) 2021-2021 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#if !defined(ORTHANC_ENABLE_ZLIB) +# error The macro ORTHANC_ENABLE_ZLIB must be defined +#endif + +#include "IImageWriter.h" +#include "../ChunkedBuffer.h" +#include "../Compatibility.h" // For ORTHANC_OVERRIDE + +namespace Orthanc +{ + class ORTHANC_PUBLIC NumpyWriter : public IImageWriter + { + protected: +#if ORTHANC_SANDBOXED == 0 + virtual void WriteToFileInternal(const std::string& filename, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) ORTHANC_OVERRIDE; +#endif + + virtual void WriteToMemoryInternal(std::string& content, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) ORTHANC_OVERRIDE; + + private: + bool compressed_; + + public: + NumpyWriter(); + + void SetCompressed(bool compressed); + + bool IsCompressed() const; + + static void WriteHeader(ChunkedBuffer& target, + unsigned int depth, // Must be "0" for 2D images + unsigned int width, + unsigned int height, + PixelFormat format); + + static void WritePixels(ChunkedBuffer& target, + const ImageAccessor& image); + + static void Finalize(std::string& target, + ChunkedBuffer& source, + bool compress); + }; +} diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancFramework/Sources/RestApi/RestApiGetCall.cpp --- a/OrthancFramework/Sources/RestApi/RestApiGetCall.cpp Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancFramework/Sources/RestApi/RestApiGetCall.cpp Fri Dec 24 16:52:51 2021 +0100 @@ -24,6 +24,9 @@ #include "../PrecompiledHeaders.h" #include "RestApiGetCall.h" +#include "../OrthancException.h" +#include "../SerializationToolbox.h" + namespace Orthanc { bool RestApiGetCall::ParseJsonRequest(Json::Value& result) const @@ -38,4 +41,31 @@ return true; } + + + bool RestApiGetCall::GetBooleanArgument(const std::string& name, + bool defaultValue) const + { + HttpToolbox::Arguments::const_iterator found = getArguments_.find(name); + + bool value; + + if (found == getArguments_.end()) + { + return defaultValue; + } + else if (found->second.empty()) + { + return true; + } + else if (SerializationToolbox::ParseBoolean(value, found->second)) + { + return value; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Expected a Boolean for GET argument \"" + + name + "\", found: " + found->second); + } + } } diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancFramework/Sources/RestApi/RestApiGetCall.h --- a/OrthancFramework/Sources/RestApi/RestApiGetCall.h Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancFramework/Sources/RestApi/RestApiGetCall.h Fri Dec 24 16:52:51 2021 +0100 @@ -61,6 +61,9 @@ { return getArguments_.find(name) != getArguments_.end(); } + + bool GetBooleanArgument(const std::string& name, + bool defaultValue) const; virtual bool ParseJsonRequest(Json::Value& result) const ORTHANC_OVERRIDE; }; diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancFramework/Sources/SerializationToolbox.cpp --- a/OrthancFramework/Sources/SerializationToolbox.cpp Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancFramework/Sources/SerializationToolbox.cpp Fri Dec 24 16:52:51 2021 +0100 @@ -648,4 +648,26 @@ return false; } } + + + bool SerializationToolbox::ParseBoolean(bool& result, + const std::string& value) + { + if (value == "0" || + value == "false") + { + result = false; + return true; + } + else if (value == "1" || + value == "true") + { + result = true; + return true; + } + else + { + return false; + } + } } diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancFramework/Sources/SerializationToolbox.h --- a/OrthancFramework/Sources/SerializationToolbox.h Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancFramework/Sources/SerializationToolbox.h Fri Dec 24 16:52:51 2021 +0100 @@ -138,5 +138,8 @@ static bool ParseFirstDouble(double& result, const std::string& value); + + static bool ParseBoolean(bool& result, + const std::string& value); }; } diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancFramework/Sources/WebServiceParameters.cpp --- a/OrthancFramework/Sources/WebServiceParameters.cpp Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancFramework/Sources/WebServiceParameters.cpp Fri Dec 24 16:52:51 2021 +0100 @@ -505,21 +505,19 @@ { return defaultValue; } - else if (found->second == "0" || - found->second == "false") - { - return false; - } - else if (found->second == "1" || - found->second == "true") - { - return true; - } else { - throw OrthancException(ErrorCode_BadFileFormat, "Bad value for a Boolean user property in the parameters " - "of a Web service: Property \"" + key + "\" equals: " + found->second); - } + bool value; + if (SerializationToolbox::ParseBoolean(value, found->second)) + { + return value; + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, "Bad value for a Boolean user property in the parameters " + "of a Web service: Property \"" + key + "\" equals: " + found->second); + } + } } diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancFramework/UnitTestsSources/DicomMapTests.cpp --- a/OrthancFramework/UnitTestsSources/DicomMapTests.cpp Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancFramework/UnitTestsSources/DicomMapTests.cpp Fri Dec 24 16:52:51 2021 +0100 @@ -756,6 +756,15 @@ ASSERT_FALSE(d > d); } +TEST(ParsedDicomFile, canIncludeXsVrTags) +{ + Json::Value tags; + tags["0028,0034"] = "1\\1"; // PixelAspectRatio + tags["0028,1101"] = "256\\0\\16"; // RedPaletteColorLookupTableDescriptor which is declared as xs VR in dicom.dic + + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(tags, DicomFromJsonFlags_DecodeDataUriScheme, "")); + // simply make sure it does not throw ! +} #if ORTHANC_SANDBOXED != 1 diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancServer/OrthancExplorer/explorer.html --- a/OrthancServer/OrthancExplorer/explorer.html Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancServer/OrthancExplorer/explorer.html Fri Dec 24 16:52:51 2021 +0100 @@ -252,7 +252,7 @@
  • Anonymize
  • -
      + -
        + -
          + -
            +
            • Access
            • Before anonymization diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancServer/OrthancExplorer/explorer.js --- a/OrthancServer/OrthancExplorer/explorer.js Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancServer/OrthancExplorer/explorer.js Fri Dec 24 16:52:51 2021 +0100 @@ -689,7 +689,19 @@ } } +function SetupAttachments(accessSelector, liClass, resourceId, resourceType) { + GetResource('/' + resourceType + '/' + resourceId + '/attachments?full', function(attachments) { + target = $(accessSelector); + $('.' + liClass).remove(); + for (var key in attachments) { + if (attachments[key] >= 1024) { + target.append('
            • Download ' + key + '
            • ') + } + } + target.listview('refresh'); + }); +} function RefreshPatient() { @@ -726,6 +738,7 @@ SetupAnonymizedOrModifiedFrom('#patient-anonymized-from', patient, 'patient', ANONYMIZED_FROM); SetupAnonymizedOrModifiedFrom('#patient-modified-from', patient, 'patient', MODIFIED_FROM); + SetupAttachments('#patient-access', 'patient-attachment', pageData.uuid, 'patients'); target.listview('refresh'); @@ -773,6 +786,7 @@ SetupAnonymizedOrModifiedFrom('#study-anonymized-from', study, 'study', ANONYMIZED_FROM); SetupAnonymizedOrModifiedFrom('#study-modified-from', study, 'study', MODIFIED_FROM); + SetupAttachments('#study-access', 'study-attachment', pageData.uuid, 'studies'); target = $('#list-series'); $('li', target).remove(); @@ -790,6 +804,7 @@ } target.listview('refresh'); + currentPage = 'study'; currentUuid = pageData.uuid; }); @@ -827,6 +842,7 @@ SetupAnonymizedOrModifiedFrom('#series-anonymized-from', series, 'series', ANONYMIZED_FROM); SetupAnonymizedOrModifiedFrom('#series-modified-from', series, 'series', MODIFIED_FROM); + SetupAttachments('#series-access', 'series-attachment', pageData.uuid, 'series'); target = $('#list-instances'); $('li', target).remove(); @@ -966,6 +982,8 @@ SetupAnonymizedOrModifiedFrom('#instance-anonymized-from', instance, 'instance', ANONYMIZED_FROM); SetupAnonymizedOrModifiedFrom('#instance-modified-from', instance, 'instance', MODIFIED_FROM); + SetupAttachments('#instance-access', 'instance-attachment', pageData.uuid, 'instances'); + currentPage = 'instance'; currentUuid = pageData.uuid; }); @@ -1335,7 +1353,25 @@ window.location.href = '../series/' + $.mobile.pageData.uuid + '/media'; }); +$('.patient-attachment').live('click', function(e) { + e.preventDefault(); //stop the browser from following + window.location.href = '../patients/' + $.mobile.pageData.uuid + '/attachments/' + e.target.id + '/data'; +}); +$('.study-attachment').live('click', function(e) { + e.preventDefault(); //stop the browser from following + window.location.href = '../studies/' + $.mobile.pageData.uuid + '/attachments/' + e.target.id + '/data'; +}); + +$('.series-attachment').live('click', function(e) { + e.preventDefault(); //stop the browser from following + window.location.href = '../series/' + $.mobile.pageData.uuid + '/attachments/' + e.target.id + '/data'; +}); + +$('.instance-attachment').live('click', function(e) { + e.preventDefault(); //stop the browser from following + window.location.href = '../instances/' + $.mobile.pageData.uuid + '/attachments/' + e.target.id + '/data'; +}); $('#protection').live('change', function(e) { var isProtected = e.target.value == "on"; diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancServer/Plugins/Engine/OrthancPlugins.cpp --- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Fri Dec 24 16:52:51 2021 +0100 @@ -70,7 +70,7 @@ #include #include -#define ERROR_MESSAGE_64BIT "A 64bit version of the Orthanc API is necessary" +#define ERROR_MESSAGE_64BIT "A 64bit version of the Orthanc SDK is necessary to use buffers > 4GB and it is currently not available !" namespace Orthanc @@ -1156,6 +1156,7 @@ typedef std::list IncomingHttpRequestFilters2; typedef std::list IncomingDicomInstanceFilters; typedef std::list IncomingCStoreInstanceFilters; + typedef std::list ReceivedInstanceCallbacks; typedef std::list DecodeImageCallbacks; typedef std::list TranscoderCallbacks; typedef std::list JobsUnserializers; @@ -1179,6 +1180,7 @@ IncomingHttpRequestFilters2 incomingHttpRequestFilters2_; IncomingDicomInstanceFilters incomingDicomInstanceFilters_; IncomingCStoreInstanceFilters incomingCStoreInstanceFilters_; // New in Orthanc 1.9.8 + ReceivedInstanceCallbacks receivedInstanceCallbacks_; // New in Orthanc 1.9.8 RefreshMetricsCallbacks refreshMetricsCallbacks_; StorageCommitmentScpCallbacks storageCommitmentScpCallbacks_; std::unique_ptr storageArea_; @@ -2283,6 +2285,74 @@ } + bool OrthancPlugins::ApplyReceivedInstanceCallbacks(const void* receivedDicom, + size_t receivedDicomSize, + void** modifiedDicomBufferData, + size_t& modifiedDicomBufferSize) + { + uint64_t modifiedDicomSize64 = 0; + *modifiedDicomBufferData = NULL; + + boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_); + + for (PImpl::ReceivedInstanceCallbacks::const_iterator + callback = pimpl_->receivedInstanceCallbacks_.begin(); + callback != pimpl_->receivedInstanceCallbacks_.end(); ++callback) + { + OrthancPluginReceivedInstanceCallbackResult callbackResult = (*callback) (receivedDicom, + receivedDicomSize, + modifiedDicomBufferData, + &modifiedDicomSize64); + + if (callbackResult == OrthancPluginReceivedInstanceCallbackResult_Discard) + { + if (modifiedDicomSize64 > 0 || *modifiedDicomBufferData != NULL) + { + free(modifiedDicomBufferData); + throw OrthancException(ErrorCode_Plugin, "The ReceivedInstanceCallback plugin is returning a modified buffer while it has discarded the instance"); + } + + CLOG(INFO, PLUGINS) << "A plugin has discarded the instance in its ReceivedInstanceCallback"; + return false; + } + else if (callbackResult == OrthancPluginReceivedInstanceCallbackResult_KeepAsIs) + { + if (modifiedDicomSize64 > 0 || *modifiedDicomBufferData != NULL) + { + free(modifiedDicomBufferData); + throw OrthancException(ErrorCode_Plugin, "The ReceivedInstanceCallback plugin is returning a modified buffer while it has not modified the instance"); + } + return true; + } + else if (callbackResult == OrthancPluginReceivedInstanceCallbackResult_Modified) + { + if (modifiedDicomSize64 > 0 && modifiedDicomBufferData != NULL) + { + if (static_cast(modifiedDicomSize64) != modifiedDicomSize64) // Orthanc is running in 32bits and has received a > 4GB buffer + { + free(modifiedDicomBufferData); + throw OrthancException(ErrorCode_Plugin, "The Plugin has returned a > 4GB which is too large for Orthanc running in 32bits"); + } + + modifiedDicomBufferSize = static_cast(modifiedDicomSize64); + + CLOG(INFO, PLUGINS) << "A plugin has modified the instance in its ReceivedInstanceCallback"; + return true; + } + else + { + throw OrthancException(ErrorCode_Plugin, "The ReceivedInstanceCallback plugin is not returning a modified buffer while it has modified the instance"); + } + } + else + { + throw OrthancException(ErrorCode_Plugin, "The ReceivedInstanceCallback has returned an invalid value"); + } + } + + return true; + } + void OrthancPlugins::SignalChangeInternal(OrthancPluginChangeType changeType, OrthancPluginResourceType resourceType, const char* resource) @@ -2509,6 +2579,14 @@ pimpl_->incomingCStoreInstanceFilters_.push_back(p.callback); } + void OrthancPlugins::RegisterReceivedInstanceCallback(const void* parameters) + { + const _OrthancPluginReceivedInstanceCallback& p = + *reinterpret_cast(parameters); + + CLOG(INFO, PLUGINS) << "Plugin has registered a received instance callback"; + pimpl_->receivedInstanceCallbacks_.push_back(p.callback); + } void OrthancPlugins::RegisterRefreshMetricsCallback(const void* parameters) { @@ -4992,6 +5070,10 @@ RegisterIncomingCStoreInstanceFilter(parameters); return true; + case _OrthancPluginService_RegisterReceivedInstanceCallback: + RegisterReceivedInstanceCallback(parameters); + return true; + case _OrthancPluginService_RegisterRefreshMetricsCallback: RegisterRefreshMetricsCallback(parameters); return true; diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancServer/Plugins/Engine/OrthancPlugins.h --- a/OrthancServer/Plugins/Engine/OrthancPlugins.h Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h Fri Dec 24 16:52:51 2021 +0100 @@ -124,6 +124,8 @@ void RegisterIncomingCStoreInstanceFilter(const void* parameters); + void RegisterReceivedInstanceCallback(const void* parameters); + void RegisterRefreshMetricsCallback(const void* parameters); void RegisterStorageCommitmentScpCallback(const void* parameters); @@ -273,6 +275,11 @@ virtual uint16_t FilterIncomingCStoreInstance(const DicomInstanceToStore& instance, const Json::Value& simplified) ORTHANC_OVERRIDE; + virtual bool ApplyReceivedInstanceCallbacks(const void* receivedDicomBuffer, + size_t receivedDicomBufferSize, + void** modifiedDicomBufferData, + size_t& modifiedDicomBufferSize); + bool HasStorageArea() const; IStorageArea* CreateStorageArea(); // To be freed after use diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h --- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Fri Dec 24 16:52:51 2021 +0100 @@ -463,6 +463,7 @@ _OrthancPluginService_RegisterTranscoderCallback = 1015, /* New in Orthanc 1.7.0 */ _OrthancPluginService_RegisterStorageArea2 = 1016, /* New in Orthanc 1.9.0 */ _OrthancPluginService_RegisterIncomingCStoreInstanceFilter = 1017, /* New in Orthanc 1.9.8 */ + _OrthancPluginService_RegisterReceivedInstanceCallback = 1018, /* New in Orthanc 1.9.8 */ /* Sending answers to REST calls */ _OrthancPluginService_AnswerBuffer = 2000, @@ -1001,7 +1002,19 @@ is already in use */ } OrthancPluginStorageCommitmentFailureReason; - + + /** + * The return value of ReceivedInstanceCallback + **/ + typedef enum + { + OrthancPluginReceivedInstanceCallbackResult_KeepAsIs = 1, /*!< Keep the instance as is */ + OrthancPluginReceivedInstanceCallbackResult_Modified = 2, /*!< Modified the instance */ + OrthancPluginReceivedInstanceCallbackResult_Discard = 3, /*!< Tell Orthanc to discard the instance */ + + _OrthancPluginReceivedInstanceCallbackResult_INTERNAL = 0x7fffffff + } OrthancPluginReceivedInstanceCallbackResult; + /** * @brief A 32-bit memory buffer allocated by the core system of Orthanc. @@ -7823,6 +7836,73 @@ } /** + * @brief Callback to possibly modify a DICOM instance received + * by Orthanc through any source (C-Store or Rest API) + * + * Signature of a callback function that is triggered whenever + * Orthanc receives a new DICOM instance (through DICOM protocol or + * Rest API), and that answers a possibly modified version of the + * DICOM that should be stored in Orthanc. + * + * This callback is called immediately after receiption: before + * transcoding and before filtering (FilterIncomingInstance). + * + * @param receivedDicomBuffer A buffer containing the received DICOM (input). + * @param receivedDicomBufferSize The size of the received DICOM (input) + * @param modifiedDicomBuffer A buffer containing the modified DICOM (output). + * This buffer will be freed by the Orthanc Core and must have + * been allocated by malloc in your plugin or by Orthanc core through + * a plugin method. + * @param modifiedDicomBufferSize The size of the modified DICOM (output) + * @return OrthancPluginReceivedInstanceCallbackResult_KeepAsIs to accept the instance as is + * OrthancPluginReceivedInstanceCallbackResult_Modified to store the modified DICOM + * OrthancPluginReceivedInstanceCallbackResult_Discard to tell Orthanc to discard the instance + * @ingroup Callback + **/ + typedef OrthancPluginReceivedInstanceCallbackResult (*OrthancPluginReceivedInstanceCallback) ( + const void* receivedDicomBuffer, + uint64_t receivedDicomBufferSize, + void** modifiedDicomBuffer, + uint64_t* modifiedDicomBufferSize + ); + + + typedef struct + { + OrthancPluginReceivedInstanceCallback callback; + } _OrthancPluginReceivedInstanceCallback; + + /** + * @brief Register a callback to possibly modify a DICOM instance received + * by Orthanc through any source (C-Store or Rest API) + * + * + * @warning Your callback function will be called synchronously with + * the core of Orthanc. This implies that deadlocks might emerge if + * you call other core primitives of Orthanc in your callback (such + * deadlocks are particular visible in the presence of other plugins + * or Lua scripts). It is thus strongly advised to avoid any call to + * the REST API of Orthanc in the callback. If you have to call + * other primitives of Orthanc, you should make these calls in a + * separate thread, passing the pending events to be processed + * through a message queue. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The callback. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterReceivedInstanceCallback( + OrthancPluginContext* context, + OrthancPluginReceivedInstanceCallback callback) + { + _OrthancPluginReceivedInstanceCallback params; + params.callback = callback; + + return context->InvokeService(context, _OrthancPluginService_RegisterReceivedInstanceCallback, ¶ms); + } + + /** * @brief Get the transfer syntax of a DICOM file. * * This function returns a pointer to a newly created string that diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp --- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Fri Dec 24 16:52:51 2021 +0100 @@ -540,6 +540,13 @@ const std::string& password) { Clear(); + + if (body.size() > 0xffffffffu) + { + LogError("Cannot handle body size > 4GB"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + return CheckHttp(OrthancPluginHttpPost(GetGlobalContext(), &buffer_, url.c_str(), body.c_str(), body.size(), username.empty() ? NULL : username.c_str(), @@ -553,6 +560,13 @@ const std::string& password) { Clear(); + + if (body.size() > 0xffffffffu) + { + LogError("Cannot handle body size > 4GB"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + return CheckHttp(OrthancPluginHttpPut(GetGlobalContext(), &buffer_, url.c_str(), body.empty() ? NULL : body.c_str(), body.size(), @@ -1893,6 +1907,12 @@ ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); } + if (body.size() > 0xffffffffu) + { + LogError("Cannot handle body size > 4GB"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + OrthancPlugins::MemoryBuffer answer; uint16_t status; OrthancPluginErrorCode code = OrthancPluginCallPeerApi @@ -1921,6 +1941,12 @@ ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); } + if (body.size() > 0xffffffffu) + { + LogError("Cannot handle body size > 4GB"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + OrthancPlugins::MemoryBuffer answer; uint16_t status; OrthancPluginErrorCode code = OrthancPluginCallPeerApi @@ -2570,8 +2596,8 @@ void HttpClient::ClearCredentials() { - username_.empty(); - password_.empty(); + username_.clear(); + password_.clear(); } @@ -2884,6 +2910,12 @@ MemoryBuffer answerBodyBuffer, answerHeadersBuffer; + if (body.size() > 0xffffffffu) + { + LogError("Cannot handle body size > 4GB"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + OrthancPluginErrorCode error = OrthancPluginHttpClient( GetGlobalContext(), *answerBodyBuffer, diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancServer/Plugins/Samples/Sanitizer/CMakeLists.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Samples/Sanitizer/CMakeLists.txt Fri Dec 24 16:52:51 2021 +0100 @@ -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-2021 Osimis S.A., Belgium +# Copyright (C) 2021-2021 Sebastien Jodogne, ICTEAM UCLouvain, 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 . + + +cmake_minimum_required(VERSION 2.8) + +project(Sanitizer) + +SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)") +SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages") + +SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp") +SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of boost") +SET(ORTHANC_FRAMEWORK_SOURCE path) +SET(ORTHANC_FRAMEWORK_ROOT ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources) + +include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake) +include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/JsonCppConfiguration.cmake) +include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/BoostConfiguration.cmake) + +include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake) +include(${ORTHANC_FRAMEWORK_ROOT}/../Resources/CMake/OrthancFrameworkParameters.cmake) +set(ENABLE_LOCALE ON) +set(ENABLE_DCMTK ON) +include(${ORTHANC_FRAMEWORK_ROOT}/../Resources/CMake/OrthancFrameworkConfiguration.cmake) +include_directories(${ORTHANC_FRAMEWORK_ROOT}) + + +add_library(Sanitizer SHARED + ${CMAKE_SOURCE_DIR}/../Common/OrthancPluginCppWrapper.cpp + ${JSONCPP_SOURCES} + ${BOOST_SOURCES} + Plugin.cpp + ${ORTHANC_CORE_SOURCES} + ${ORTHANC_DICOM_SOURCES} + ) + + +install( + TARGETS Sanitizer + RUNTIME DESTINATION lib # Destination for Windows + LIBRARY DESTINATION share/orthanc/plugins # Destination for Linux + ) diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancServer/Plugins/Samples/Sanitizer/Plugin.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Samples/Sanitizer/Plugin.cpp Fri Dec 24 16:52:51 2021 +0100 @@ -0,0 +1,100 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2021 Osimis S.A., Belgium + * Copyright (C) 2021-2021 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../../../../OrthancFramework/Sources/Compatibility.h" +#include "../../../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h" +#include "../../../../OrthancFramework/Sources/OrthancFramework.h" +#include "../Common/OrthancPluginCppWrapper.h" + +#include +#include +#include +#include + + + + +OrthancPluginReceivedInstanceCallbackResult ReceivedInstanceCallback(const void* receivedDicomBuffer, + uint64_t receivedDicomBufferSize, + void** modifiedDicomBuffer, + uint64_t* modifiedDicomBufferSize) +{ + Orthanc::ParsedDicomFile dicom(receivedDicomBuffer, receivedDicomBufferSize); + std::string institutionName = "My institution"; + + dicom.Replace(Orthanc::DICOM_TAG_INSTITUTION_NAME, institutionName, false, Orthanc::DicomReplaceMode_InsertIfAbsent, ""); + + std::string modifiedDicom; + dicom.SaveToMemoryBuffer(modifiedDicom); + + *modifiedDicomBuffer = malloc(modifiedDicom.size()); + *modifiedDicomBufferSize = modifiedDicom.size(); + memcpy(*modifiedDicomBuffer, modifiedDicom.c_str(), modifiedDicom.size()); + + return OrthancPluginReceivedInstanceCallbackResult_Modified; +} + + +extern "C" +{ + ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c) + { + OrthancPlugins::SetGlobalContext(c); + + Orthanc::InitializeFramework("", true); + + /* Check the version of the Orthanc core */ + // if (OrthancPluginCheckVersion(c) == 0) + // { + // OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, + // ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, + // ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); + // return -1; + // } + + OrthancPlugins::LogWarning("Sanitizer plugin is initializing"); + OrthancPluginSetDescription(c, "Sample plugin to sanitize incoming DICOM instances."); + + OrthancPluginRegisterReceivedInstanceCallback(c, ReceivedInstanceCallback); + + return 0; + } + + + ORTHANC_PLUGINS_API void OrthancPluginFinalize() + { + OrthancPlugins::LogWarning("Sanitizer plugin is finalizing"); + Orthanc::FinalizeFramework(); + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetName() + { + return "sanitizer"; + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() + { + return "0.1"; + } +} diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancServer/Resources/Configuration.json --- a/OrthancServer/Resources/Configuration.json Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancServer/Resources/Configuration.json Fri Dec 24 16:52:51 2021 +0100 @@ -639,10 +639,13 @@ // multiplicity (> 0 with defaults to 1), possibly the maximum // multiplicity (0 means arbitrary multiplicity, defaults to 1), and // possibly the Private Creator (for private tags). + // Note: for private tags, you should only declare the lower 8 bits + // of the element since the higher 8 bits may vary from one file to + // the other. "Dictionary" : { // "0014,1020" : [ "DA", "ValidationExpiryDate", 1, 1 ] - // "00e1,10c2" : [ "UI", "PET-CT Multi Modality Name", 1, 1, "ELSCINT1" ] - // "7053,1003" : [ "ST", "Original Image Filename", 1, 1, "Philips PET Private Group" ] + // "00e1,c2" : [ "UI", "PET-CT Multi Modality Name", 1, 1, "ELSCINT1" ] + // "7053,03" : [ "ST", "Original Image Filename", 1, 1, "Philips PET Private Group" ] // "2001,5f" : [ "SQ", "StackSequence", 1, 1, "Philips Imaging DD 001" ] }, diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancServer/Resources/RunCppCheck.sh --- a/OrthancServer/Resources/RunCppCheck.sh Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancServer/Resources/RunCppCheck.sh Fri Dec 24 16:52:51 2021 +0100 @@ -12,25 +12,25 @@ constParameter:../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp knownArgument:../../OrthancFramework/UnitTestsSources/ImageTests.cpp knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp -nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:320 +nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:321 nullPointerRedundantCheck:../../OrthancFramework/UnitTestsSources/DicomMapTests.cpp -stlFindInsert:../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1194 -stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:164 -stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:72 -stlFindInsert:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:384 -stlFindInsert:../../OrthancServer/Sources/OrthancWebDav.cpp:386 -stlFindInsert:../../OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp:51 -stlFindInsert:../../OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp:201 -syntaxError:../../OrthancFramework/Sources/SQLite/FunctionContext.h:50 -syntaxError:../../OrthancFramework/UnitTestsSources/ZipTests.cpp:131 -syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:321 -uninitMemberVar:../../OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp:427 +stlFindInsert:../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1195 +stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:165 +stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:73 +stlFindInsert:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:385 +stlFindInsert:../../OrthancServer/Sources/OrthancWebDav.cpp:387 +stlFindInsert:../../OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp:52 +stlFindInsert:../../OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp:202 +syntaxError:../../OrthancFramework/Sources/SQLite/FunctionContext.h:52 +syntaxError:../../OrthancFramework/UnitTestsSources/ZipTests.cpp:132 +syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:322 +uninitMemberVar:../../OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp:428 unreadVariable:../../OrthancFramework/Sources/FileStorage/StorageAccessor.cpp -unreadVariable:../../OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp:1124 +unreadVariable:../../OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp:1125 unusedFunction -useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:89 -useInitializationList:../../OrthancFramework/Sources/Images/PngWriter.cpp:97 -useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:285 +useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:90 +useInitializationList:../../OrthancFramework/Sources/Images/PngWriter.cpp:98 +useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:286 EOF ${CPPCHECK} --enable=all --quiet --std=c++11 \ diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancServer/Resources/Samples/Python/ArchiveStudiesInTimeRange.py --- a/OrthancServer/Resources/Samples/Python/ArchiveStudiesInTimeRange.py Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancServer/Resources/Samples/Python/ArchiveStudiesInTimeRange.py Fri Dec 24 16:52:51 2021 +0100 @@ -36,7 +36,7 @@ def CheckIsDate(date): if len(date) != 8 or not date.isdigit(): - print '"%s" is not a valid date!\n' % date + print('"%s" is not a valid date!\n' % date) exit(-1) @@ -83,7 +83,9 @@ GetTag(study, 'StudyDescription')) # Remove any non-ASCII character in the filename - filename = filename.encode('ascii', errors = 'replace').translate(None, r"'\/:*?\"<>|!=").strip() + filename = filename.encode('ascii', errors = 'replace') + filename = filename.translate(None, b"'\/:*?\"<>|!=") + filename = filename.decode('ascii').strip() # Download the ZIP archive of the study print('Downloading %s' % filename) diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancServer/Resources/Samples/Python/ContinuousPatientAnonymization.py --- a/OrthancServer/Resources/Samples/Python/ContinuousPatientAnonymization.py Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancServer/Resources/Samples/Python/ContinuousPatientAnonymization.py Fri Dec 24 16:52:51 2021 +0100 @@ -24,7 +24,7 @@ import time import sys import RestToolbox -import md5 +import hashlib ## @@ -66,13 +66,15 @@ # The PatientID after anonymization is taken as the 8 first # characters from the MD5 hash of the original PatientID - anonymizedID = md5.new(patientID).hexdigest()[:8] + h = hashlib.md5(patientID.encode('ascii')) + anonymizedID = h.hexdigest()[:8] anonymizedName = 'Anonymized patient %d' % COUNT COUNT += 1 RestToolbox.DoPost(URL + path + '/anonymize', { 'Replace' : { 'PatientID' : anonymizedID, - 'PatientName' : anonymizedName } }) + 'PatientName' : anonymizedName }, + 'Force' : True }) # Delete the source patient after the anonymization RestToolbox.DoDelete(URL + change['Path']) diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Fri Dec 24 16:52:51 2021 +0100 @@ -31,6 +31,7 @@ #include "../../../OrthancFramework/Sources/HttpServer/HttpContentNegociation.h" #include "../../../OrthancFramework/Sources/Images/Image.h" #include "../../../OrthancFramework/Sources/Images/ImageProcessing.h" +#include "../../../OrthancFramework/Sources/Images/NumpyWriter.h" #include "../../../OrthancFramework/Sources/Logging.h" #include "../../../OrthancFramework/Sources/MultiThreading/Semaphore.h" #include "../../../OrthancFramework/Sources/SerializationToolbox.h" @@ -1069,9 +1070,9 @@ windowWidth = 1; } - if (std::abs(rescaleSlope) <= 0.1) + if (std::abs(rescaleSlope) <= 0.0001) { - rescaleSlope = 0.1; + rescaleSlope = 0.0001; } const double scaling = 255.0 * rescaleSlope / windowWidth; @@ -1130,6 +1131,247 @@ } + static void DocumentSharedNumpy(RestApiGetCall& call) + { + call.GetDocumentation() + .SetUriArgument("id", "Orthanc identifier of the DICOM resource of interest") + .SetHttpGetArgument("compress", RestApiCallDocumentation::Type_Boolean, "Compress the file as `.npz`", false) + .SetHttpGetArgument("rescale", RestApiCallDocumentation::Type_Boolean, + "On grayscale images, apply the rescaling and return floating-point values", false) + .AddAnswerType(MimeType_PlainText, "Numpy file: https://numpy.org/devdocs/reference/generated/numpy.lib.format.html"); + } + + + namespace + { + class NumpyVisitor : public boost::noncopyable + { + private: + bool rescale_; + unsigned int depth_; + unsigned int currentDepth_; + unsigned int width_; + unsigned int height_; + PixelFormat format_; + ChunkedBuffer buffer_; + + public: + NumpyVisitor(unsigned int depth /* can be zero if 2D frame */, + bool rescale) : + rescale_(rescale), + depth_(depth), + currentDepth_(0), + width_(0), // dummy initialization + height_(0), // dummy initialization + format_(PixelFormat_Grayscale8) // dummy initialization + { + } + + void WriteFrame(const ParsedDicomFile& dicom, + unsigned int frame) + { + std::unique_ptr decoded(dicom.DecodeFrame(frame)); + + if (decoded.get() == NULL) + { + throw OrthancException(ErrorCode_NotImplemented, "Cannot decode DICOM instance"); + } + + if (currentDepth_ == 0) + { + width_ = decoded->GetWidth(); + height_ = decoded->GetHeight(); + format_ = decoded->GetFormat(); + } + else if (width_ != decoded->GetWidth() || + height_ != decoded->GetHeight()) + { + throw OrthancException(ErrorCode_IncompatibleImageSize, "The size of the frames varies across the instance(s)"); + } + else if (format_ != decoded->GetFormat()) + { + throw OrthancException(ErrorCode_IncompatibleImageFormat, "The pixel format of the frames varies across the instance(s)"); + } + + if (rescale_ && + decoded->GetFormat() != PixelFormat_RGB24) + { + if (currentDepth_ == 0) + { + NumpyWriter::WriteHeader(buffer_, depth_, width_, height_, PixelFormat_Float32); + } + + double rescaleIntercept, rescaleSlope; + dicom.GetRescale(rescaleIntercept, rescaleSlope, frame); + + Image converted(PixelFormat_Float32, decoded->GetWidth(), decoded->GetHeight(), false); + ImageProcessing::Convert(converted, *decoded); + ImageProcessing::ShiftScale2(converted, static_cast(rescaleIntercept), static_cast(rescaleSlope), false); + + NumpyWriter::WritePixels(buffer_, converted); + } + else + { + if (currentDepth_ == 0) + { + NumpyWriter::WriteHeader(buffer_, depth_, width_, height_, format_); + } + + NumpyWriter::WritePixels(buffer_, *decoded); + } + + currentDepth_ ++; + } + + void Answer(RestApiOutput& output, + bool compress) + { + if ((depth_ == 0 && currentDepth_ != 1) || + (depth_ != 0 && currentDepth_ != depth_)) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + std::string answer; + NumpyWriter::Finalize(answer, buffer_, compress); + output.AnswerBuffer(answer, MimeType_Binary); + } + } + }; + } + + + static void GetNumpyFrame(RestApiGetCall& call) + { + if (call.IsDocumentation()) + { + DocumentSharedNumpy(call); + call.GetDocumentation() + .SetTag("Instances") + .SetSummary("Decode frame for numpy") + .SetDescription("Decode one frame of interest from the given DICOM instance, for use with numpy in Python. " + "The numpy array has 3 dimensions: (height, width, color channel).") + .SetUriArgument("frame", RestApiCallDocumentation::Type_Number, "Index of the frame (starts at `0`)"); + } + else + { + const std::string instanceId = call.GetUriComponent("id", ""); + const bool compress = call.GetBooleanArgument("compress", false); + const bool rescale = call.GetBooleanArgument("rescale", true); + + uint32_t frame; + if (!SerializationToolbox::ParseUnsignedInteger32(frame, call.GetUriComponent("frame", "0"))) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Expected an unsigned integer for the \"frame\" argument"); + } + + NumpyVisitor visitor(0 /* no depth, 2D frame */, rescale); + + { + Semaphore::Locker throttling(throttlingSemaphore_); + ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), instanceId); + + visitor.WriteFrame(locker.GetDicom(), frame); + } + + visitor.Answer(call.GetOutput(), compress); + } + } + + + static void GetNumpyInstance(RestApiGetCall& call) + { + if (call.IsDocumentation()) + { + DocumentSharedNumpy(call); + call.GetDocumentation() + .SetTag("Instances") + .SetSummary("Decode instance for numpy") + .SetDescription("Decode the given DICOM instance, for use with numpy in Python. " + "The numpy array has 4 dimensions: (frame, height, width, color channel)."); + } + else + { + const std::string instanceId = call.GetUriComponent("id", ""); + const bool compress = call.GetBooleanArgument("compress", false); + const bool rescale = call.GetBooleanArgument("rescale", true); + + { + Semaphore::Locker throttling(throttlingSemaphore_); + ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), instanceId); + + const unsigned int depth = locker.GetDicom().GetFramesCount(); + if (depth == 0) + { + throw OrthancException(ErrorCode_BadFileFormat, "Empty DICOM instance"); + } + + NumpyVisitor visitor(depth, rescale); + + for (unsigned int frame = 0; frame < depth; frame++) + { + visitor.WriteFrame(locker.GetDicom(), frame); + } + + visitor.Answer(call.GetOutput(), compress); + } + } + } + + + static void GetNumpySeries(RestApiGetCall& call) + { + if (call.IsDocumentation()) + { + DocumentSharedNumpy(call); + call.GetDocumentation() + .SetTag("Series") + .SetSummary("Decode series for numpy") + .SetDescription("Decode the given DICOM series, for use with numpy in Python. " + "The numpy array has 4 dimensions: (frame, height, width, color channel)."); + } + else + { + const std::string seriesId = call.GetUriComponent("id", ""); + const bool compress = call.GetBooleanArgument("compress", false); + const bool rescale = call.GetBooleanArgument("rescale", true); + + Semaphore::Locker throttling(throttlingSemaphore_); + + ServerIndex& index = OrthancRestApi::GetIndex(call); + SliceOrdering ordering(index, seriesId); + + unsigned int depth = 0; + for (size_t i = 0; i < ordering.GetInstancesCount(); i++) + { + depth += ordering.GetFramesCount(i); + } + + ServerContext& context = OrthancRestApi::GetContext(call); + + NumpyVisitor visitor(depth, rescale); + + for (size_t i = 0; i < ordering.GetInstancesCount(); i++) + { + const std::string& instanceId = ordering.GetInstanceId(i); + unsigned int framesCount = ordering.GetFramesCount(i); + + { + ServerContext::DicomCacheLocker locker(context, instanceId); + + for (unsigned int frame = 0; frame < framesCount; frame++) + { + visitor.WriteFrame(locker.GetDicom(), frame); + } + } + } + + visitor.Answer(call.GetOutput(), compress); + } + } + + static void GetMatlabImage(RestApiGetCall& call) { if (call.IsDocumentation()) @@ -1677,6 +1919,8 @@ .SetSummary("List attachments") .SetDescription("Get the list of attachments that are associated with the given " + r) .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") + .SetHttpGetArgument("full", RestApiCallDocumentation::Type_String, + "If present, retrieve the attachments list and their numerical ids", false) .AddAnswerType(MimeType_Json, "JSON array containing the names of the attachments") .SetHttpGetSample(GetDocumentationSampleResource(t) + "/attachments", true); return; @@ -1687,12 +1931,28 @@ std::set attachments; OrthancRestApi::GetIndex(call).ListAvailableAttachments(attachments, publicId, StringToResourceType(resourceType.c_str())); - Json::Value result = Json::arrayValue; - - for (std::set::const_iterator - it = attachments.begin(); it != attachments.end(); ++it) + Json::Value result; + + if (call.HasArgument("full")) { - result.append(EnumerationToString(*it)); + result = Json::objectValue; + + for (std::set::const_iterator + it = attachments.begin(); it != attachments.end(); ++it) + { + std::string key = EnumerationToString(*it); + result[key] = static_cast(*it); + } + } + else + { + result = Json::arrayValue; + + for (std::set::const_iterator + it = attachments.begin(); it != attachments.end(); ++it) + { + result.append(EnumerationToString(*it)); + } } call.GetOutput().AnswerJson(result); @@ -3384,6 +3644,7 @@ Register("/instances/{id}/frames/{frame}/matlab", GetMatlabImage); Register("/instances/{id}/frames/{frame}/raw", GetRawFrame); Register("/instances/{id}/frames/{frame}/raw.gz", GetRawFrame); + Register("/instances/{id}/frames/{frame}/numpy", GetNumpyFrame); // New in Orthanc 1.9.8 Register("/instances/{id}/pdf", ExtractPdf); Register("/instances/{id}/preview", GetImage); Register("/instances/{id}/rendered", GetRenderedFrame); @@ -3392,6 +3653,7 @@ Register("/instances/{id}/image-int16", GetImage); Register("/instances/{id}/matlab", GetMatlabImage); Register("/instances/{id}/header", GetInstanceHeader); + Register("/instances/{id}/numpy", GetNumpyInstance); // New in Orthanc 1.9.8 Register("/patients/{id}/protected", IsProtectedPatient); Register("/patients/{id}/protected", SetPatientProtection); @@ -3450,6 +3712,7 @@ Register("/instances/{id}/content/*", GetRawContent); Register("/series/{id}/ordered-slices", OrderSlices); + Register("/series/{id}/numpy", GetNumpySeries); // New in Orthanc 1.9.8 Register("/patients/{id}/reconstruct", ReconstructResource); Register("/studies/{id}/reconstruct", ReconstructResource); diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancServer/Sources/ServerContext.cpp --- a/OrthancServer/Sources/ServerContext.cpp Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancServer/Sources/ServerContext.cpp Fri Dec 24 16:52:51 2021 +0100 @@ -35,6 +35,7 @@ #include "../../OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.h" #include "../../OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h" #include "../../OrthancFramework/Sources/Logging.h" +#include "../../OrthancFramework/Sources/MallocMemoryBuffer.h" #include "../../OrthancFramework/Sources/MetricsRegistry.h" #include "../Plugins/Engine/OrthancPlugins.h" @@ -682,13 +683,46 @@ ServerContext::StoreResult ServerContext::Store(std::string& resultPublicId, - DicomInstanceToStore& dicom, + DicomInstanceToStore& receivedDicom, StoreInstanceMode mode) - { + { + DicomInstanceToStore* dicom = &receivedDicom; + std::unique_ptr modifiedDicom; + + std::unique_ptr raii(new MallocMemoryBuffer); + +#if ORTHANC_ENABLE_PLUGINS == 1 + if (HasPlugins()) + { + void* modifiedDicomBuffer = NULL; + size_t modifiedDicomBufferSize = 0; + + bool store = GetPlugins().ApplyReceivedInstanceCallbacks(receivedDicom.GetBufferData(), + receivedDicom.GetBufferSize(), + &modifiedDicomBuffer, + modifiedDicomBufferSize); + raii->Assign(modifiedDicomBuffer, modifiedDicomBufferSize, ::free); + + if (!store) + { + StoreResult result; + result.SetStatus(StoreStatus_FilteredOut); + return result; + } + + if (modifiedDicomBufferSize > 0 && modifiedDicomBuffer != NULL) + { + modifiedDicom.reset(DicomInstanceToStore::CreateFromBuffer(modifiedDicomBuffer, modifiedDicomBufferSize)); + modifiedDicom->SetOrigin(dicom->GetOrigin()); + dicom = modifiedDicom.get(); + } + } +#endif + if (!isIngestTranscoding_) { // No automated transcoding. This was the only path in Orthanc <= 1.6.1. - return StoreAfterTranscoding(resultPublicId, dicom, mode); + return StoreAfterTranscoding(resultPublicId, *dicom, mode); } else { @@ -697,7 +731,7 @@ bool transcode = false; DicomTransferSyntax sourceSyntax; - if (!dicom.LookupTransferSyntax(sourceSyntax) || + if (!dicom->LookupTransferSyntax(sourceSyntax) || sourceSyntax == ingestTransferSyntax_) { // Don't transcode if the incoming DICOM is already in the proper transfer syntax @@ -724,7 +758,7 @@ if (!transcode) { // No transcoding - return StoreAfterTranscoding(resultPublicId, dicom, mode); + return StoreAfterTranscoding(resultPublicId, *dicom, mode); } else { @@ -733,7 +767,7 @@ syntaxes.insert(ingestTransferSyntax_); IDicomTranscoder::DicomImage source; - source.SetExternalBuffer(dicom.GetBufferData(), dicom.GetBufferSize()); + source.SetExternalBuffer(dicom->GetBufferData(), dicom->GetBufferSize()); IDicomTranscoder::DicomImage transcoded; if (Transcode(transcoded, source, syntaxes, true /* allow new SOP instance UID */)) @@ -741,7 +775,7 @@ std::unique_ptr tmp(transcoded.ReleaseAsParsedDicomFile()); std::unique_ptr toStore(DicomInstanceToStore::CreateFromParsedDicomFile(*tmp)); - toStore->SetOrigin(dicom.GetOrigin()); + toStore->SetOrigin(dicom->GetOrigin()); StoreResult result = StoreAfterTranscoding(resultPublicId, *toStore, mode); assert(resultPublicId == tmp->GetHasher().HashInstance()); @@ -751,7 +785,7 @@ else { // Cannot transcode => store the original file - return StoreAfterTranscoding(resultPublicId, dicom, mode); + return StoreAfterTranscoding(resultPublicId, *dicom, mode); } } } diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancServer/Sources/ServerEnumerations.h --- a/OrthancServer/Sources/ServerEnumerations.h Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancServer/Sources/ServerEnumerations.h Fri Dec 24 16:52:51 2021 +0100 @@ -49,7 +49,7 @@ StoreStatus_Success, StoreStatus_AlreadyStored, StoreStatus_Failure, - StoreStatus_FilteredOut // Removed by NewInstanceFilter + StoreStatus_FilteredOut // Removed by NewInstanceFilter or ReceivedInstanceCallback }; enum DicomTagType diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancServer/Sources/main.cpp --- a/OrthancServer/Sources/main.cpp Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancServer/Sources/main.cpp Fri Dec 24 16:52:51 2021 +0100 @@ -704,6 +704,7 @@ << path << " " << ORTHANC_VERSION << std::endl << "Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics Department, University Hospital of Liege (Belgium)" << std::endl << "Copyright (C) 2017-2021 Osimis S.A. (Belgium)" << std::endl + << "Copyright (C) 2021-2021 Sebastien Jodogne, ICTEAM UCLouvain (Belgium)" << std::endl << "Licensing GPLv3+: GNU GPL version 3 or later , with OpenSSL exception." << std::endl << "This is free software: you are free to change and redistribute it." << std::endl << "There is NO WARRANTY, to the extent permitted by law." << std::endl diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancServer/UnitTestsSources/PluginsTests.cpp --- a/OrthancServer/UnitTestsSources/PluginsTests.cpp Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancServer/UnitTestsSources/PluginsTests.cpp Fri Dec 24 16:52:51 2021 +0100 @@ -76,25 +76,54 @@ //ASSERT_TRUE(l.HasFunction("_init")); #elif defined(__linux__) || defined(__FreeBSD_kernel__) - std::unique_ptr l; + /** + * Since Orthanc 1.9.8, we test the "libdl.so.2" instead of the + * "libdl.so", as discussed here: + * https://groups.google.com/g/orthanc-users/c/I5g1fN6MCvg/m/JVdvRyjJAAAJ + * https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1001305 + * https://salsa.debian.org/med-team/orthanc/-/blob/master/debian/patches/glibc-2.34.patch + **/ + try { - /** - * Since Orthanc 1.9.8, we test the "libdl.so.2" instead of the - * "libdl.so", as discussed here: - * https://groups.google.com/g/orthanc-users/c/I5g1fN6MCvg/m/JVdvRyjJAAAJ - **/ - l.reset(new SharedLibrary("libdl.so.2")); + SharedLibrary l("libdl.so.2"); + ASSERT_THROW(l.GetFunction("world"), OrthancException); + ASSERT_TRUE(l.GetFunction("dlopen") != NULL); + ASSERT_TRUE(l.HasFunction("dlclose")); + ASSERT_FALSE(l.HasFunction("world")); + return; // Success } catch (OrthancException&) { - l.reset(new SharedLibrary("libdl.so")); // Fallback for backward compat + } + + try + { + SharedLibrary l("libdl.so"); // Fallback for backward compat + ASSERT_THROW(l.GetFunction("world"), OrthancException); + ASSERT_TRUE(l.GetFunction("dlopen") != NULL); + ASSERT_TRUE(l.HasFunction("dlclose")); + ASSERT_FALSE(l.HasFunction("world")); + return; // Success + } + catch (OrthancException&) + { } - ASSERT_THROW(l->GetFunction("world"), OrthancException); - ASSERT_TRUE(l->GetFunction("dlopen") != NULL); - ASSERT_TRUE(l->HasFunction("dlclose")); - ASSERT_FALSE(l->HasFunction("world")); + try + { + SharedLibrary l("libmemusage.so"); // Try another common library + ASSERT_THROW(l.GetFunction("world"), OrthancException); + ASSERT_TRUE(l.GetFunction("munmap") != NULL); + ASSERT_TRUE(l.HasFunction("free")); + ASSERT_FALSE(l.HasFunction("world")); + return; // Success + } + catch (OrthancException&) + { + } + + ASSERT_TRUE(0); #elif defined(__FreeBSD__) || defined(__OpenBSD__) // dlopen() in FreeBSD/OpenBSD is supplied by libc, libc.so is diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancServer/UnitTestsSources/SizeOfTests.cpp --- a/OrthancServer/UnitTestsSources/SizeOfTests.cpp Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancServer/UnitTestsSources/SizeOfTests.cpp Fri Dec 24 16:52:51 2021 +0100 @@ -100,6 +100,7 @@ #include "../../OrthancFramework/Sources/FileStorage/IStorageArea.h" #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h" #include "../../OrthancFramework/Sources/FileStorage/StorageAccessor.h" +#include "../../OrthancFramework/Sources/FileStorage/StorageCache.h" #include "../../OrthancFramework/Sources/HttpClient.h" #include "../../OrthancFramework/Sources/HttpServer/BufferHttpSender.h" #include "../../OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.h" @@ -131,6 +132,7 @@ #include "../../OrthancFramework/Sources/Images/ImageTraits.h" #include "../../OrthancFramework/Sources/Images/JpegReader.h" #include "../../OrthancFramework/Sources/Images/JpegWriter.h" +#include "../../OrthancFramework/Sources/Images/NumpyWriter.h" #include "../../OrthancFramework/Sources/Images/PamReader.h" #include "../../OrthancFramework/Sources/Images/PamWriter.h" #include "../../OrthancFramework/Sources/Images/PixelTraits.h" diff -r 2e71a08eea15 -r 3e9a76464e8a OrthancServer/UnitTestsSources/SizeOfTests.impl.h --- a/OrthancServer/UnitTestsSources/SizeOfTests.impl.h Thu Nov 25 19:02:38 2021 +0100 +++ b/OrthancServer/UnitTestsSources/SizeOfTests.impl.h Fri Dec 24 16:52:51 2021 +0100 @@ -72,6 +72,7 @@ printf("sizeof(::Orthanc::MetricsRegistry::Timer) == %d\n", static_cast(sizeof(::Orthanc::MetricsRegistry::Timer))); printf("sizeof(::Orthanc::MultipartStreamReader) == %d\n", static_cast(sizeof(::Orthanc::MultipartStreamReader))); printf("sizeof(::Orthanc::NullOperationValue) == %d\n", static_cast(sizeof(::Orthanc::NullOperationValue))); + printf("sizeof(::Orthanc::NumpyWriter) == %d\n", static_cast(sizeof(::Orthanc::NumpyWriter))); printf("sizeof(::Orthanc::OrthancException) == %d\n", static_cast(sizeof(::Orthanc::OrthancException))); printf("sizeof(::Orthanc::PamReader) == %d\n", static_cast(sizeof(::Orthanc::PamReader))); printf("sizeof(::Orthanc::PamWriter) == %d\n", static_cast(sizeof(::Orthanc::PamWriter))); @@ -103,6 +104,7 @@ printf("sizeof(::Orthanc::SharedLibrary) == %d\n", static_cast(sizeof(::Orthanc::SharedLibrary))); printf("sizeof(::Orthanc::SharedMessageQueue) == %d\n", static_cast(sizeof(::Orthanc::SharedMessageQueue))); printf("sizeof(::Orthanc::StorageAccessor) == %d\n", static_cast(sizeof(::Orthanc::StorageAccessor))); + printf("sizeof(::Orthanc::StorageCache) == %d\n", static_cast(sizeof(::Orthanc::StorageCache))); printf("sizeof(::Orthanc::StreamBlockReader) == %d\n", static_cast(sizeof(::Orthanc::StreamBlockReader))); printf("sizeof(::Orthanc::StringMatcher) == %d\n", static_cast(sizeof(::Orthanc::StringMatcher))); printf("sizeof(::Orthanc::StringOperationValue) == %d\n", static_cast(sizeof(::Orthanc::StringOperationValue))); diff -r 2e71a08eea15 -r 3e9a76464e8a TODO --- a/TODO Thu Nov 25 19:02:38 2021 +0100 +++ b/TODO Fri Dec 24 16:52:51 2021 +0100 @@ -20,7 +20,7 @@ * Option to enable DNS lookups in DICOM: https://hg.orthanc-server.com/orthanc/file/Orthanc-1.9.3/OrthancFramework/Sources/OrthancFramework.cpp#l88 * Toolbox::ComputeMD5() fails on files larger than 4GB - +* Add an option to run Orthanc in read-only mode both for DICOM and for Rest API. ============================ Documentation (Orthanc Book) @@ -167,6 +167,7 @@ https://groups.google.com/d/msg/orthanc-users/BtvLTE5Ni8A/vIMhmMgfBAAJ * In "OrthancPluginLog[Error|Warning|Info]()", prefix the log line with the name of the plugin, as retrieved by "OrthancPluginGetName()" +* update the SDK to handle buffer sizes > 4GB (all sizes are currently coded in uint32_t) ---------------- Ideas of plugins