changeset 4860:3e9a76464e8a openssl-3.x

integration mainline->openssl-3.x
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 24 Dec 2021 16:52:51 +0100
parents 2e71a08eea15 (current diff) 6f780611fc03 (diff)
children d1aae7c3dd5d
files NEWS OrthancServer/OrthancExplorer/explorer.js OrthancServer/Plugins/Engine/OrthancPlugins.cpp OrthancServer/Plugins/Engine/OrthancPlugins.h OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerEnumerations.h OrthancServer/Sources/main.cpp OrthancServer/UnitTestsSources/PluginsTests.cpp OrthancServer/UnitTestsSources/SizeOfTests.cpp TODO
diffstat 35 files changed, 1263 insertions(+), 112 deletions(-) [+]
line wrap: on
line diff
--- 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
 ---
 
--- 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)
 
--- 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()
--- 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
     )
--- 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<const char*>(data), element.getLength(), true);
             }
+            else if (element.getUint16Array(data16) == EC_Normal)
+            {
+              return new DicomValue(reinterpret_cast<const char*>(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<Sint16>(*decoded)).good();
+          }
+          else
+          {
+            ok = element.putUint16(boost::lexical_cast<Uint16>(*decoded)).good();  
+          }
+          break;
+        }
+
         case EVR_US:  // unsigned short
         {
           ok = element.putUint16(boost::lexical_cast<Uint16>(*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
--- 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<DcmPixelData> 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,
--- 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
--- /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
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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 <boost/lexical_cast.hpp>
+
+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<std::string>(depth) + ", ";
+    }
+    
+    const std::string info = ("{'descr': '" + datatype + "', 'fortran_order': False, " +
+                              "'shape': (" + depthString + boost::lexical_cast<std::string>(height) +
+                              "," + boost::lexical_cast<std::string>(width) +
+                              "," + boost::lexical_cast<std::string>(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_;
+  }
+}
--- /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
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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);
+  };
+}
--- 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);
+    }
+  }
 }
--- 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;
   };
--- 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;
+    }
+  }
 }
--- 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);
   };
 }
--- 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);
+      }
+    }
   }
 
 
--- 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<ParsedDicomFile> dicom(ParsedDicomFile::CreateFromJson(tags, DicomFromJsonFlags_DecodeDataUriScheme, ""));
+  // simply make sure it does not throw !
+}
 
 
 #if ORTHANC_SANDBOXED != 1
--- 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 @@
               <li data-icon="star"><a href="#" id="patient-anonymize">Anonymize</a></li>
             </ul>
 
-            <ul data-role="listview" data-inset="true" data-theme="d" data-divider-theme="c">
+            <ul data-role="listview" data-inset="true" data-theme="d" data-divider-theme="c" id="patient-access">
               <li data-role="list-divider">Access</li>
               <li data-icon="info" data-theme="e" style="display:none">
                 <a href="#" id="patient-anonymized-from">Before anonymization</a>
@@ -306,7 +306,7 @@
               <li data-icon="star"><a href="#" id="study-anonymize">Anonymize</a></li>
             </ul>
 
-            <ul data-role="listview" data-inset="true" data-theme="d" data-divider-theme="c">
+            <ul data-role="listview" data-inset="true" data-theme="d" data-divider-theme="c" id="study-access">
               <li data-role="list-divider">Access</li>
               <li data-icon="info" data-theme="e" style="display:none">
                 <a href="#" id="study-anonymized-from">Before anonymization</a>
@@ -361,7 +361,7 @@
               <li data-icon="star"><a href="#" id="series-anonymize">Anonymize</a></li>
             </ul>
 
-            <ul data-role="listview" data-inset="true" data-theme="d" data-divider-theme="c">
+            <ul data-role="listview" data-inset="true" data-theme="d" data-divider-theme="c" id="series-access">
               <li data-role="list-divider">Access</li>
               <li data-icon="info" data-theme="e" style="display:none">
                 <a href="#" id="series-anonymized-from">Before anonymization</a>
@@ -417,7 +417,7 @@
               <li data-icon="forward"><a href="#" id="instance-store">Send to DICOM modality</a></li>
             </ul>
 
-            <ul data-role="listview" data-inset="true" data-theme="d" data-divider-theme="c">
+            <ul data-role="listview" data-inset="true" data-theme="d" data-divider-theme="c" id="instance-access">
               <li data-role="list-divider">Access</li>
               <li data-icon="info" data-theme="e" style="display:none">
                 <a href="#" id="instance-anonymized-from">Before anonymization</a>
--- 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('<li data-icon="gear" class="' + liClass + '"><a href="#" id="' + attachments[key] + '">Download ' + key + '</a></li>')
+      }
+    }
+    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";
--- 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 <dcmtk/dcmdata/dcdicent.h>
 #include <dcmtk/dcmnet/dimse.h>
 
-#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<OrthancPluginIncomingHttpRequestFilter2>  IncomingHttpRequestFilters2;
     typedef std::list<OrthancPluginIncomingDicomInstanceFilter>  IncomingDicomInstanceFilters;
     typedef std::list<OrthancPluginIncomingCStoreInstanceFilter>  IncomingCStoreInstanceFilters;
+    typedef std::list<OrthancPluginReceivedInstanceCallback>  ReceivedInstanceCallbacks;
     typedef std::list<OrthancPluginDecodeImageCallback>  DecodeImageCallbacks;
     typedef std::list<OrthancPluginTranscoderCallback>  TranscoderCallbacks;
     typedef std::list<OrthancPluginJobsUnserializer>  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<StorageAreaFactory>  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<size_t>(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<size_t>(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<const _OrthancPluginReceivedInstanceCallback*>(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;
--- 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
--- 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, &params);
+  }
+
+  /**
    * @brief Get the transfer syntax of a DICOM file.
    *
    * This function returns a pointer to a newly created string that
--- 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,
--- /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 <http://www.gnu.org/licenses/>.
+
+
+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
+  )
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../../../../OrthancFramework/Sources/Compatibility.h"
+#include "../../../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h"
+#include "../../../../OrthancFramework/Sources/OrthancFramework.h"
+#include "../Common/OrthancPluginCppWrapper.h"
+
+#include <boost/filesystem.hpp>
+#include <json/value.h> 
+#include <string.h>
+#include <iostream>
+
+
+
+
+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";
+  }
+}
--- 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" ]
   },
 
--- 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 \
--- 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)
--- 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'])
--- 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<ImageAccessor> 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<float>(rescaleIntercept), static_cast<float>(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<FileContentType> attachments;
     OrthancRestApi::GetIndex(call).ListAvailableAttachments(attachments, publicId, StringToResourceType(resourceType.c_str()));
 
-    Json::Value result = Json::arrayValue;
-
-    for (std::set<FileContentType>::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<FileContentType>::const_iterator 
+            it = attachments.begin(); it != attachments.end(); ++it)
+      {
+        std::string key = EnumerationToString(*it);
+        result[key] = static_cast<uint16_t>(*it);
+      }
+    }
+    else
+    {
+      result = Json::arrayValue;
+      
+      for (std::set<FileContentType>::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<false>);
     Register("/instances/{id}/frames/{frame}/raw.gz", GetRawFrame<true>);
+    Register("/instances/{id}/frames/{frame}/numpy", GetNumpyFrame);  // New in Orthanc 1.9.8
     Register("/instances/{id}/pdf", ExtractPdf);
     Register("/instances/{id}/preview", GetImage<ImageExtractionMode_Preview>);
     Register("/instances/{id}/rendered", GetRenderedFrame);
@@ -3392,6 +3653,7 @@
     Register("/instances/{id}/image-int16", GetImage<ImageExtractionMode_Int16>);
     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<ResourceType_Patient>);
     Register("/studies/{id}/reconstruct", ReconstructResource<ResourceType_Study>);
--- 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<DicomInstanceToStore> modifiedDicom;
+
+    std::unique_ptr<MallocMemoryBuffer> 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<ParsedDicomFile> tmp(transcoded.ReleaseAsParsedDicomFile());
 
           std::unique_ptr<DicomInstanceToStore> 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);
         }
       }
     }
--- 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
--- 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 <http://gnu.org/licenses/gpl.html>, 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
--- 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<SharedLibrary> 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
--- 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"
--- 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<int>(sizeof(::Orthanc::MetricsRegistry::Timer)));
   printf("sizeof(::Orthanc::MultipartStreamReader) == %d\n", static_cast<int>(sizeof(::Orthanc::MultipartStreamReader)));
   printf("sizeof(::Orthanc::NullOperationValue) == %d\n", static_cast<int>(sizeof(::Orthanc::NullOperationValue)));
+  printf("sizeof(::Orthanc::NumpyWriter) == %d\n", static_cast<int>(sizeof(::Orthanc::NumpyWriter)));
   printf("sizeof(::Orthanc::OrthancException) == %d\n", static_cast<int>(sizeof(::Orthanc::OrthancException)));
   printf("sizeof(::Orthanc::PamReader) == %d\n", static_cast<int>(sizeof(::Orthanc::PamReader)));
   printf("sizeof(::Orthanc::PamWriter) == %d\n", static_cast<int>(sizeof(::Orthanc::PamWriter)));
@@ -103,6 +104,7 @@
   printf("sizeof(::Orthanc::SharedLibrary) == %d\n", static_cast<int>(sizeof(::Orthanc::SharedLibrary)));
   printf("sizeof(::Orthanc::SharedMessageQueue) == %d\n", static_cast<int>(sizeof(::Orthanc::SharedMessageQueue)));
   printf("sizeof(::Orthanc::StorageAccessor) == %d\n", static_cast<int>(sizeof(::Orthanc::StorageAccessor)));
+  printf("sizeof(::Orthanc::StorageCache) == %d\n", static_cast<int>(sizeof(::Orthanc::StorageCache)));
   printf("sizeof(::Orthanc::StreamBlockReader) == %d\n", static_cast<int>(sizeof(::Orthanc::StreamBlockReader)));
   printf("sizeof(::Orthanc::StringMatcher) == %d\n", static_cast<int>(sizeof(::Orthanc::StringMatcher)));
   printf("sizeof(::Orthanc::StringOperationValue) == %d\n", static_cast<int>(sizeof(::Orthanc::StringOperationValue)));
--- 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