changeset 4785:61da49321754 openssl-3.x

integration mainline->openssl-3.x
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 30 Aug 2021 22:21:24 +0200
parents b2417ac5055a (current diff) 113afe7b594d (diff)
children 70d2a97ca8cb
files NEWS OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake OrthancServer/CMakeLists.txt OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/main.cpp
diffstat 25 files changed, 983 insertions(+), 206 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/CITATION.cff	Mon Aug 30 22:21:24 2021 +0200
@@ -0,0 +1,14 @@
+cff-version: "1.1.0"
+message: "If you use this software, please cite it using these metadata."
+title: Orthanc
+abstract: "Orthanc is a lightweight open-source DICOM server for medical imaging supporting representational state transfer (REST)."
+authors:
+  -
+    affiliation: UCLouvain
+    family-names: Jodogne
+    given-names: "Sébastien"
+doi: "10.1007/s10278-018-0082-y"
+license: "GPL-3.0-or-later"
+repository-code: "https://hg.orthanc-server.com/orthanc/"
+version: 1.9.7
+date-released: 2021-08-31
--- a/NEWS	Wed Jul 21 10:48:14 2021 +0200
+++ b/NEWS	Mon Aug 30 22:21:24 2021 +0200
@@ -16,6 +16,32 @@
   - openssl 3.0.0-beta1
 
 
+Version 1.9.7 (2021-08-31)
+==========================
+
+General
+-------
+
+* New configuration option "DicomAlwaysAllowMove" to disable verification of
+  the remote modality in C-MOVE SCP
+
+REST API
+--------
+
+* API version upgraded to 15
+* Added "Level" option to POST /tools/bulk-modify
+* Added missing OpenAPI documentation of "KeepSource" in ".../modify" and ".../anonymize"
+
+Maintenance
+-----------
+
+* Added file CITATION.cff
+* Linux Standard Base (LSB) builds of Orthanc can load non-LSB builds of plugins
+* Fix upload of ZIP archives containing a DICOMDIR file
+* Fix computation of the estimated time of arrival in jobs
+* Support detection of windowing and rescale in Philips multiframe images
+
+
 Version 1.9.6 (2021-07-21)
 ==========================
 
--- a/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake	Wed Jul 21 10:48:14 2021 +0200
+++ b/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake	Mon Aug 30 22:21:24 2021 +0200
@@ -136,6 +136,8 @@
         set(ORTHANC_FRAMEWORK_MD5 "10fc64de1254a095e5d3ed3931f0cfbb")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.6")
         set(ORTHANC_FRAMEWORK_MD5 "4b5d05683d747c29b2860ad79d11e62e")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.7")
+        set(ORTHANC_FRAMEWORK_MD5 "c912bbb860d640d3ae3003b5c9698205")
 
       # Below this point are development snapshots that were used to
       # release some plugin, before an official release of the Orthanc
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Wed Jul 21 10:48:14 2021 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Mon Aug 30 22:21:24 2021 +0200
@@ -37,7 +37,7 @@
 # Version of the Orthanc API, can be retrieved from "/system" URI in
 # order to check whether new URI endpoints are available even if using
 # the mainline version of Orthanc
-set(ORTHANC_API_VERSION "14")
+set(ORTHANC_API_VERSION "15")
 
 
 #####################################################################
--- a/OrthancFramework/Sources/DicomFormat/DicomTag.h	Wed Jul 21 10:48:14 2021 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomTag.h	Mon Aug 30 22:21:24 2021 +0200
@@ -172,6 +172,9 @@
   static const DicomTag DICOM_TAG_PATIENT_SPECIES_DESCRIPTION(0x0010, 0x2201);
   static const DicomTag DICOM_TAG_STUDY_COMMENTS(0x0032, 0x4000);
   static const DicomTag DICOM_TAG_OTHER_PATIENT_IDS(0x0010, 0x1000);
+  static const DicomTag DICOM_TAG_PER_FRAME_FUNCTIONAL_GROUP_SEQUENCE(0x5200, 0x9230);
+  static const DicomTag DICOM_TAG_PIXEL_VALUE_TRANSFORMATION_SEQUENCE(0x0028, 0x9145);
+  static const DicomTag DICOM_TAG_FRAME_VOI_LUT_SEQUENCE(0x0028, 0x9132);
 
   // Tags used within the Stone of Orthanc
   static const DicomTag DICOM_TAG_FRAME_INCREMENT_POINTER(0x0028, 0x0009);
--- a/OrthancFramework/Sources/DicomFormat/DicomValue.cpp	Wed Jul 21 10:48:14 2021 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomValue.cpp	Mon Aug 30 22:21:24 2021 +0200
@@ -104,102 +104,110 @@
   }
 #endif
 
-  // same as ParseValue but in case the value actually contains a sequence,
-  // it will return the first value
-  // this has been introduced to support invalid "width/height" DICOM tags in some US
-  // images where the width is stored as "800\0" !
-  template <typename T,
-            bool allowSigned>
-  static bool ParseFirstValue(T& result,
-                              const DicomValue& source)
+  bool DicomValue::ParseInteger32(int32_t& result) const
+  {
+    if (IsBinary() ||
+        IsNull())
+    {
+      return false;
+    }
+    else
+    {
+      return SerializationToolbox::ParseInteger32(result, GetContent());
+    }
+  }
+
+  bool DicomValue::ParseInteger64(int64_t& result) const
   {
-    if (source.IsBinary() ||
-        source.IsNull())
+    if (IsBinary() ||
+        IsNull())
+    {
+      return false;
+    }
+    else
+    {
+      return SerializationToolbox::ParseInteger64(result, GetContent());
+    }
+  }
+
+  bool DicomValue::ParseUnsignedInteger32(uint32_t& result) const
+  {
+    if (IsBinary() ||
+        IsNull())
+    {
+      return false;
+    }
+    else
+    {
+      return SerializationToolbox::ParseUnsignedInteger32(result, GetContent());
+    }
+  }
+
+  bool DicomValue::ParseUnsignedInteger64(uint64_t& result) const
+  {
+    if (IsBinary() ||
+        IsNull())
     {
       return false;
     }
-
-    try
+    else
     {
-      std::string value = Toolbox::StripSpaces(source.GetContent());
-      if (value.empty())
-      {
-        return false;
-      }
-
-      if (!allowSigned &&
-          value[0] == '-')
-      {
-        return false;
-      }
+      return SerializationToolbox::ParseUnsignedInteger64(result, GetContent());
+    }
+  }
 
-      if (value.find("\\") == std::string::npos)
-      {
-        result = boost::lexical_cast<T>(value);
-        return true;
-      }
-      else
-      {
-        std::vector<std::string> tokens;
-        Toolbox::TokenizeString(tokens, value, '\\');
-
-        if (tokens.size() >= 1)
-        {
-          result = boost::lexical_cast<T>(tokens[0]);
-          return true;
-        }
-
-        return false;
-      }
-    }
-    catch (boost::bad_lexical_cast&)
+  bool DicomValue::ParseFloat(float& result) const
+  {
+    if (IsBinary() ||
+        IsNull())
     {
       return false;
     }
+    else
+    {
+      return SerializationToolbox::ParseFloat(result, GetContent());
+    }
   }
 
+  bool DicomValue::ParseDouble(double& result) const
+  {
+    if (IsBinary() ||
+        IsNull())
+    {
+      return false;
+    }
+    else
+    {
+      return SerializationToolbox::ParseDouble(result, GetContent());
+    }
+  }
 
-  template <typename T,
-            bool allowSigned>
-  static bool ParseValue(T& result,
-                         const DicomValue& source)
+  bool DicomValue::ParseFirstFloat(float& result) const
   {
-    if (source.IsBinary() ||
-        source.IsNull())
+    if (IsBinary() ||
+        IsNull())
     {
       return false;
     }
-    
-    try
+    else
     {
-      std::string value = Toolbox::StripSpaces(source.GetContent());
-      if (value.empty())
-      {
-        return false;
-      }
+      return SerializationToolbox::ParseFirstFloat(result, GetContent());
+    }
+  }
 
-      if (!allowSigned &&
-          value[0] == '-')
-      {
-        return false;
-      }
-      
-      result = boost::lexical_cast<T>(value);
-      return true;
-    }
-    catch (boost::bad_lexical_cast&)
+  bool DicomValue::ParseFirstUnsignedInteger(unsigned int& result) const
+  {
+    uint64_t value;
+
+    if (IsBinary() ||
+        IsNull())
     {
       return false;
     }
-  }
-
-  bool DicomValue::ParseInteger32(int32_t& result) const
-  {
-    int64_t tmp;
-    if (ParseValue<int64_t, true>(tmp, *this))
+    else if (SerializationToolbox::ParseFirstUnsignedInteger64(value, GetContent()))
     {
-      result = static_cast<int32_t>(tmp);
-      return (tmp == static_cast<int64_t>(result));  // Check no overflow occurs
+      result = static_cast<unsigned int>(value);
+      return (static_cast<uint64_t>(result) == value);   // Check no overflow
     }
     else
     {
@@ -207,50 +215,6 @@
     }
   }
 
-  bool DicomValue::ParseInteger64(int64_t& result) const
-  {
-    return ParseValue<int64_t, true>(result, *this);
-  }
-
-  bool DicomValue::ParseUnsignedInteger32(uint32_t& result) const
-  {
-    uint64_t tmp;
-    if (ParseValue<uint64_t, false>(tmp, *this))
-    {
-      result = static_cast<uint32_t>(tmp);
-      return (tmp == static_cast<uint64_t>(result));  // Check no overflow occurs
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-  bool DicomValue::ParseUnsignedInteger64(uint64_t& result) const
-  {
-    return ParseValue<uint64_t, false>(result, *this);
-  }
-
-  bool DicomValue::ParseFloat(float& result) const
-  {
-    return ParseValue<float, true>(result, *this);
-  }
-
-  bool DicomValue::ParseDouble(double& result) const
-  {
-    return ParseValue<double, true>(result, *this);
-  }
-
-  bool DicomValue::ParseFirstFloat(float& result) const
-  {
-    return ParseFirstValue<float, true>(result, *this);
-  }
-
-  bool DicomValue::ParseFirstUnsignedInteger(unsigned int& result) const
-  {
-    return ParseFirstValue<unsigned int, true>(result, *this);
-  }
-
   bool DicomValue::CopyToString(std::string& result,
                                 bool allowBinary) const
   {
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Wed Jul 21 10:48:14 2021 +0200
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Mon Aug 30 22:21:24 2021 +0200
@@ -2978,10 +2978,10 @@
   }
 
 
-  static void ApplyInternal(FromDcmtkBridge::IDicomPathVisitor& visitor,
-                            DcmItem& item,
-                            const DicomPath& pattern,
-                            const DicomPath& actualPath)
+  void FromDcmtkBridge::IDicomPathVisitor::ApplyInternal(FromDcmtkBridge::IDicomPathVisitor& visitor,
+                                                         DcmItem& item,
+                                                         const DicomPath& pattern,
+                                                         const DicomPath& actualPath)
   {
     const size_t level = actualPath.GetPrefixLength();
       
@@ -3020,9 +3020,9 @@
   }
 
 
-  void FromDcmtkBridge::Apply(IDicomPathVisitor& visitor,
-                              DcmDataset& dataset,
-                              const DicomPath& path)
+  void FromDcmtkBridge::IDicomPathVisitor::Apply(IDicomPathVisitor& visitor,
+                                                 DcmDataset& dataset,
+                                                 const DicomPath& path)
   {
     DicomPath actualPath(path.GetFinalTag());
     ApplyInternal(visitor, dataset, path, actualPath);
@@ -3044,7 +3044,7 @@
     };
     
     Visitor visitor;
-    Apply(visitor, dataset, path);
+    IDicomPathVisitor::Apply(visitor, dataset, path);
   }
   
 
@@ -3084,7 +3084,7 @@
     };
     
     Visitor visitor(onlyIfExists);
-    Apply(visitor, dataset, path);
+    IDicomPathVisitor::Apply(visitor, dataset, path);
   }
   
 
@@ -3159,9 +3159,59 @@
     else
     {
       Visitor visitor(element, mode);
-      Apply(visitor, dataset, path);
+      IDicomPathVisitor::Apply(visitor, dataset, path);
     }
   }
+
+
+  bool FromDcmtkBridge::LookupSequenceItem(DicomMap& target,
+                                           DcmDataset& dataset,
+                                           const DicomPath& path,
+                                           size_t sequenceIndex)
+  {
+    class Visitor : public FromDcmtkBridge::IDicomPathVisitor
+    {
+    private:
+      bool       found_;
+      DicomMap&  target_;
+      size_t     sequenceIndex_;
+      
+    public:
+      Visitor(DicomMap& target,
+              size_t sequenceIndex) :
+        found_(false),
+        target_(target),
+        sequenceIndex_(sequenceIndex)
+      {
+      }
+      
+      virtual void Visit(DcmItem& item,
+                         const DicomPath& path) ORTHANC_OVERRIDE
+      {
+        DcmTagKey tag(path.GetFinalTag().GetGroup(), path.GetFinalTag().GetElement());
+
+        DcmSequenceOfItems *sequence = NULL;
+        
+        if (item.findAndGetSequence(tag, sequence).good() &&
+            sequence != NULL &&
+            sequenceIndex_ < sequence->card())
+        {
+          std::set<DicomTag> ignoreTagLength;
+          ExtractDicomSummary(target_, *sequence->getItem(sequenceIndex_), 0, ignoreTagLength);
+          found_ = true;
+        }
+      }
+
+      bool HasFound() const
+      {
+        return found_;
+      }
+    };
+
+    Visitor visitor(target, sequenceIndex);
+    IDicomPathVisitor::Apply(visitor, dataset, path);
+    return visitor.HasFound();
+  }
 }
 
 
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h	Wed Jul 21 10:48:14 2021 +0200
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h	Mon Aug 30 22:21:24 2021 +0200
@@ -64,6 +64,12 @@
     // New in Orthanc 1.9.4
     class ORTHANC_PUBLIC IDicomPathVisitor : public boost::noncopyable
     {
+    private:
+      static void ApplyInternal(FromDcmtkBridge::IDicomPathVisitor& visitor,
+                                DcmItem& item,
+                                const DicomPath& pattern,
+                                const DicomPath& actualPath);
+      
     public:
       virtual ~IDicomPathVisitor()
       {
@@ -71,6 +77,10 @@
 
       virtual void Visit(DcmItem& item,
                          const DicomPath& path) = 0;
+
+      static void Apply(IDicomPathVisitor& visitor,
+                        DcmDataset& dataset,
+                        const DicomPath& path);
     };
     
 
@@ -256,10 +266,6 @@
 
     static void LogMissingTagsForStore(DcmDataset& dicom);
 
-    static void Apply(IDicomPathVisitor& visitor,
-                      DcmDataset& dataset,
-                      const DicomPath& path);
-
     static void RemovePath(DcmDataset& dataset,
                            const DicomPath& path);
 
@@ -271,5 +277,10 @@
                             const DicomPath& path,
                             const DcmElement& element,
                             DicomReplaceMode mode);
+
+    static bool LookupSequenceItem(DicomMap& target,
+                                   DcmDataset& dataset,
+                                   const DicomPath& path,
+                                   size_t sequenceIndex);
   };
 }
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp	Wed Jul 21 10:48:14 2021 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp	Mon Aug 30 22:21:24 2021 +0200
@@ -78,6 +78,7 @@
 #include "../Images/PamReader.h"
 #include "../Logging.h"
 #include "../OrthancException.h"
+#include "../SerializationToolbox.h"
 #include "../Toolbox.h"
 
 #if ORTHANC_SANDBOXED == 0
@@ -1777,6 +1778,117 @@
   }
 
 
+  bool ParsedDicomFile::LookupSequenceItem(DicomMap& target,
+                                           const DicomPath& path,
+                                           size_t sequenceIndex) const
+  {
+    DcmDataset& dataset = *const_cast<ParsedDicomFile&>(*this).GetDcmtkObject().getDataset();
+    return FromDcmtkBridge::LookupSequenceItem(target, dataset, path, sequenceIndex);
+  }
+  
+
+  void ParsedDicomFile::GetDefaultWindowing(double& windowCenter,
+                                            double& windowWidth,
+                                            unsigned int frame) const
+  {
+    DcmDataset& dataset = *const_cast<ParsedDicomFile&>(*this).GetDcmtkObject().getDataset();
+
+    const char* wc = NULL;
+    const char* ww = NULL;
+    DcmItem *item1 = NULL;
+    DcmItem *item2 = NULL;
+
+    if (dataset.findAndGetString(DCM_WindowCenter, wc).good() &&
+        dataset.findAndGetString(DCM_WindowWidth, ww).good() &&
+        wc != NULL &&
+        ww != NULL &&
+        SerializationToolbox::ParseFirstDouble(windowCenter, wc) &&
+        SerializationToolbox::ParseFirstDouble(windowWidth, ww))
+    {
+      return;  // OK
+    }
+    else if (dataset.findAndGetSequenceItem(DCM_PerFrameFunctionalGroupsSequence, item1, frame).good() &&
+             item1 != NULL &&
+             item1->findAndGetSequenceItem(DCM_FrameVOILUTSequence, item2, 0).good() &&
+             item2 != NULL &&
+             item2->findAndGetString(DCM_WindowCenter, wc).good() &&
+             item2->findAndGetString(DCM_WindowWidth, ww).good() &&
+             wc != NULL &&
+             ww != NULL &&
+             SerializationToolbox::ParseFirstDouble(windowCenter, wc) &&
+             SerializationToolbox::ParseFirstDouble(windowWidth, ww))
+    {
+      // New in Orthanc 1.9.7, to deal with Philips multiframe images
+      // (cf. private mail from Tomas Kenda on 2021-08-17)
+      return;  // OK
+    }
+    else
+    {
+      Uint16 bitsStored = 0;
+      if (!dataset.findAndGetUint16(DCM_BitsStored, bitsStored).good() ||
+          bitsStored == 0)
+      {
+        bitsStored = 8;  // Rough assumption
+      }
+
+      windowWidth = static_cast<double>(1 << bitsStored);
+      windowCenter = windowWidth / 2.0f;
+    }
+  }
+
+  
+  void ParsedDicomFile::GetRescale(double& rescaleIntercept,
+                                   double& rescaleSlope,
+                                   unsigned int frame) const
+  {
+    DcmDataset& dataset = *const_cast<ParsedDicomFile&>(*this).GetDcmtkObject().getDataset();
+
+    const char* sopClassUid = NULL;
+    const char* intercept = NULL;
+    const char* slope = NULL;
+    DcmItem *item1 = NULL;
+    DcmItem *item2 = NULL;
+
+    if (dataset.findAndGetString(DCM_SOPClassUID, sopClassUid).good() &&
+        sopClassUid != NULL &&
+        std::string(sopClassUid) == std::string(UID_RTDoseStorage))
+    {
+      // We must not take the rescale value into account in the case of doses
+      rescaleIntercept = 0;
+      rescaleSlope = 1;
+    }
+    else if (dataset.findAndGetString(DCM_RescaleIntercept, intercept).good() &&
+             dataset.findAndGetString(DCM_RescaleSlope, slope).good() &&
+             intercept != NULL &&
+             slope != NULL &&
+             SerializationToolbox::ParseDouble(rescaleIntercept, intercept) &&
+             SerializationToolbox::ParseDouble(rescaleSlope, slope))
+    {
+      return;  // OK
+    }
+    else if (dataset.findAndGetSequenceItem(DCM_PerFrameFunctionalGroupsSequence, item1, frame).good() &&
+             item1 != NULL &&
+             item1->findAndGetSequenceItem(DCM_PixelValueTransformationSequence, item2, 0).good() &&
+             item2 != NULL &&
+             item2->findAndGetString(DCM_RescaleIntercept, intercept).good() &&
+             item2->findAndGetString(DCM_RescaleSlope, slope).good() &&
+             intercept != NULL &&
+             slope != NULL &&
+             SerializationToolbox::ParseDouble(rescaleIntercept, intercept) &&
+             SerializationToolbox::ParseDouble(rescaleSlope, slope))
+    {
+      // New in Orthanc 1.9.7, to deal with Philips multiframe images
+      // (cf. private mail from Tomas Kenda on 2021-08-17)
+      return;  // OK
+    }
+    else
+    {
+      rescaleIntercept = 0;
+      rescaleSlope = 1;
+    }
+  }
+
+
 #if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
   // Alias for binary compatibility with Orthanc Framework 1.7.2 => don't use it anymore
   void ParsedDicomFile::DatasetToJson(Json::Value& target,
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h	Wed Jul 21 10:48:14 2021 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h	Mon Aug 30 22:21:24 2021 +0200
@@ -285,5 +285,17 @@
 
     void ClearPath(const DicomPath& path,
                    bool onlyIfExists);
+
+    bool LookupSequenceItem(DicomMap& target,
+                            const DicomPath& path,
+                            size_t sequenceIndex) const;
+
+    void GetDefaultWindowing(double& windowCenter,
+                             double& windowWidth,
+                             unsigned int frame) const;
+
+    void GetRescale(double& rescaleIntercept,
+                    double& rescaleSlope,
+                    unsigned int frame) const;
   };
 }
--- a/OrthancFramework/Sources/Images/ImageProcessing.cpp	Wed Jul 21 10:48:14 2021 +0200
+++ b/OrthancFramework/Sources/Images/ImageProcessing.cpp	Mon Aug 30 22:21:24 2021 +0200
@@ -1405,6 +1405,14 @@
   }
 
 
+  static bool IsIdentityRescaling(float offset,
+                                  float scaling)
+  {
+    return (std::abs(offset) <= 10.0f * std::numeric_limits<float>::epsilon() &&
+            std::abs(scaling - 1.0f) <= 10.0f * std::numeric_limits<float>::epsilon());
+  }
+  
+
   void ImageProcessing::ShiftScale2(ImageAccessor& image,
                                     float offset,
                                     float scaling,
@@ -1413,6 +1421,11 @@
     // We compute "a * x + b"
     const float a = scaling;
     const float b = offset;
+
+    if (IsIdentityRescaling(offset, scaling))
+    {
+      return;
+    }
     
     switch (image.GetFormat())
     {
@@ -1477,6 +1490,13 @@
     const float a = scaling;
     const float b = offset;
     
+    if (target.GetFormat() == source.GetFormat() &&
+        IsIdentityRescaling(offset, scaling))
+    {
+      Copy(target, source);
+      return;
+    }
+    
     switch (target.GetFormat())
     {
       case PixelFormat_Grayscale8:
--- a/OrthancFramework/Sources/JobsEngine/JobInfo.cpp	Wed Jul 21 10:48:14 2021 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobInfo.cpp	Mon Aug 30 22:21:24 2021 +0200
@@ -70,8 +70,8 @@
       if (status_.GetProgress() > 0.01f &&
           ms > 0.01f)
       {
-        float ratio = static_cast<float>(1.0 - status_.GetProgress());
-        long long remaining = boost::math::llround(ratio * ms);
+        float progress = status_.GetProgress();
+        long long remaining = boost::math::llround(ms / progress * (1.0f - progress));
         eta_ = timestamp_ + boost::posix_time::milliseconds(remaining);
         hasEta_ = true;
       }
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Wed Jul 21 10:48:14 2021 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Mon Aug 30 22:21:24 2021 +0200
@@ -226,6 +226,11 @@
       return runtime_;
     }
 
+    void ResetRuntime()
+    {
+      runtime_ = boost::posix_time::milliseconds(0);
+    }
+
     const JobStatus& GetLastStatus() const
     {
       return lastStatus_;
@@ -1071,6 +1076,7 @@
       (void) ok;  // Remove warning about unused variable in release builds
       assert(ok);
 
+      found->second->ResetRuntime();
       found->second->SetState(JobState_Pending);
       pendingJobs_.push(found->second);
       pendingJobAvailable_.notify_one();
--- a/OrthancFramework/Sources/SerializationToolbox.cpp	Wed Jul 21 10:48:14 2021 +0200
+++ b/OrthancFramework/Sources/SerializationToolbox.cpp	Mon Aug 30 22:21:24 2021 +0200
@@ -24,11 +24,15 @@
 #include "SerializationToolbox.h"
 
 #include "OrthancException.h"
+#include "Toolbox.h"
 
 #if ORTHANC_ENABLE_DCMTK == 1
 #  include "DicomParsing/FromDcmtkBridge.h"
 #endif
 
+#include <boost/lexical_cast.hpp>
+
+
 namespace Orthanc
 {
   static bool ParseTagInternal(DicomTag& tag,
@@ -445,4 +449,202 @@
       value[it->first.Format()] = it->second;
     }
   }
+
+
+  template <typename T,
+            bool allowSigned>
+  static bool ParseValue(T& target,
+                         const std::string& source)
+  {
+    try
+    {
+      std::string value = Toolbox::StripSpaces(source);
+      if (value.empty())
+      {
+        return false;
+      }
+      else if (!allowSigned &&
+               value[0] == '-')
+      {
+        return false;
+      }
+      else
+      {
+        target = boost::lexical_cast<T>(value);
+        return true;
+      }
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      return false;
+    }
+  }
+
+
+  bool SerializationToolbox::ParseInteger32(int32_t& target,
+                                            const std::string& source)
+  {
+    int64_t tmp;
+    if (ParseValue<int64_t, true>(tmp, source))
+    {
+      target = static_cast<int32_t>(tmp);
+      return (tmp == static_cast<int64_t>(target));  // Check no overflow occurs
+    }
+    else
+    {
+      return false;
+    }
+  }
+  
+
+  bool SerializationToolbox::ParseInteger64(int64_t& target,
+                                            const std::string& source)
+  {
+    return ParseValue<int64_t, true>(target, source);
+  }
+  
+
+  bool SerializationToolbox::ParseUnsignedInteger32(uint32_t& target,
+                                                    const std::string& source)
+  {
+    uint64_t tmp;
+    if (ParseValue<uint64_t, false>(tmp, source))
+    {
+      target = static_cast<uint32_t>(tmp);
+      return (tmp == static_cast<uint64_t>(target));  // Check no overflow occurs
+    }
+    else
+    {
+      return false;
+    }
+  }
+  
+
+  bool SerializationToolbox::ParseUnsignedInteger64(uint64_t& target,
+                                                    const std::string& source)
+  {
+    return ParseValue<uint64_t, false>(target, source);
+  }
+  
+
+  bool SerializationToolbox::ParseFloat(float& target,
+                                        const std::string& source)
+  {
+    return ParseValue<float, true>(target, source);
+  }
+         
+
+  bool SerializationToolbox::ParseDouble(double& target,
+                                         const std::string& source)
+  {
+    return ParseValue<double, true>(target, source);
+  }
+
+
+  static bool GetFirstItem(std::string& target,
+                           const std::string& source)
+  {
+    std::vector<std::string> tokens;
+    Toolbox::TokenizeString(tokens, source, '\\');
+
+    if (tokens.empty())
+    {
+      return false;
+    }
+    else
+    {
+      target = tokens[0];
+      return true;
+    }
+  }
+  
+
+  bool SerializationToolbox::ParseFirstInteger32(int32_t& target,
+                                                 const std::string& source)
+  {
+    std::string first;
+    if (GetFirstItem(first, source))
+    {
+      return ParseInteger32(target, first);
+    }
+    else
+    {
+      return false;
+    }
+  }
+  
+
+  bool SerializationToolbox::ParseFirstInteger64(int64_t& target,
+                                                 const std::string& source)
+  {
+    std::string first;
+    if (GetFirstItem(first, source))
+    {
+      return ParseInteger64(target, first);
+    }
+    else
+    {
+      return false;
+    }
+  }
+  
+
+  bool SerializationToolbox::ParseFirstUnsignedInteger32(uint32_t& target,
+                                                         const std::string& source)
+  {
+    std::string first;
+    if (GetFirstItem(first, source))
+    {
+      return ParseUnsignedInteger32(target, first);
+    }
+    else
+    {
+      return false;
+    }
+  }
+  
+
+  bool SerializationToolbox::ParseFirstUnsignedInteger64(uint64_t& target,
+                                                         const std::string& source)
+  {
+    std::string first;
+    if (GetFirstItem(first, source))
+    {
+      return ParseUnsignedInteger64(target, first);
+    }
+    else
+    {
+      return false;
+    }
+  }
+  
+
+  bool SerializationToolbox::ParseFirstFloat(float& target,
+                                             const std::string& source)
+  {
+    std::string first;
+    if (GetFirstItem(first, source))
+    {
+      return ParseFloat(target, first);
+    }
+    else
+    {
+      return false;
+    }
+  }
+  
+
+  bool SerializationToolbox::ParseFirstDouble(double& target,
+                                              const std::string& source)
+  {
+    std::string first;
+    if (GetFirstItem(first, source))
+    {
+      return ParseDouble(target, first);
+    }
+    else
+    {
+      return false;
+    }
+  }
 }
--- a/OrthancFramework/Sources/SerializationToolbox.h	Wed Jul 21 10:48:14 2021 +0200
+++ b/OrthancFramework/Sources/SerializationToolbox.h	Mon Aug 30 22:21:24 2021 +0200
@@ -101,5 +101,41 @@
     static void WriteMapOfTags(Json::Value& target,
                                const std::map<DicomTag, std::string>& values,
                                const std::string& field);
+
+    static bool ParseInteger32(int32_t& result,
+                               const std::string& value);
+
+    static bool ParseInteger64(int64_t& result,
+                               const std::string& value);
+
+    static bool ParseUnsignedInteger32(uint32_t& result,
+                                       const std::string& value);
+
+    static bool ParseUnsignedInteger64(uint64_t& result,
+                                       const std::string& value);
+
+    static bool ParseFloat(float& result,
+                           const std::string& value);
+
+    static bool ParseDouble(double& result,
+                            const std::string& value);
+
+    static bool ParseFirstInteger32(int32_t& result,
+                                    const std::string& value);
+
+    static bool ParseFirstInteger64(int64_t& result,
+                                    const std::string& value);
+
+    static bool ParseFirstUnsignedInteger32(uint32_t& result,
+                                            const std::string& value);
+
+    static bool ParseFirstUnsignedInteger64(uint64_t& result,
+                                            const std::string& value);
+
+    static bool ParseFirstFloat(float& result,
+                                const std::string& value);
+
+    static bool ParseFirstDouble(double& result,
+                                 const std::string& value);
   };
 }
--- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Wed Jul 21 10:48:14 2021 +0200
+++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Mon Aug 30 22:21:24 2021 +0200
@@ -2739,6 +2739,30 @@
     ASSERT_EQ("1.2.840.113619.2.176.2025.1499492.7040.1171286241.726", vv[REF_IM_SEQ][1][REF_SOP_INSTANCE].asString()); // kept
     ASSERT_EQ("1.2.840.113704.1.111.7016.1342451220.40", vv[REL_SERIES_SEQ][0][STUDY_INSTANCE_UID].asString());  // kept
   }
+
+  {
+    std::unique_ptr<ParsedDicomFile> dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, ""));
+
+    DicomMap m;
+    ASSERT_TRUE(dicom->LookupSequenceItem(m, DicomPath(DICOM_TAG_REFERENCED_IMAGE_SEQUENCE), 0));
+    ASSERT_EQ(2u, m.GetSize());
+    ASSERT_EQ("1.2.840.113619.2.176.2025.1499492.7040.1171286241.719",
+              m.GetStringValue(DICOM_TAG_REFERENCED_SOP_INSTANCE_UID, "", false));
+    
+    ASSERT_TRUE(dicom->LookupSequenceItem(m, DicomPath(DICOM_TAG_REFERENCED_IMAGE_SEQUENCE), 1));
+    ASSERT_EQ(2u, m.GetSize());
+    ASSERT_EQ("1.2.840.113619.2.176.2025.1499492.7040.1171286241.726",
+              m.GetStringValue(DICOM_TAG_REFERENCED_SOP_INSTANCE_UID, "", false));
+    
+    ASSERT_FALSE(dicom->LookupSequenceItem(m, DicomPath(DICOM_TAG_REFERENCED_IMAGE_SEQUENCE), 2));
+    
+    ASSERT_TRUE(dicom->LookupSequenceItem(m, DicomPath(DicomTag(0x0008, 0x1250), 0, DicomTag(0x0040, 0xa170)), 0));
+    ASSERT_EQ(2u, m.GetSize());
+    ASSERT_EQ("122403", m.GetStringValue(DicomTag(0x0008, 0x0100), "", false));
+    ASSERT_EQ("WORLD", m.GetStringValue(DICOM_TAG_SERIES_DESCRIPTION, "", false));
+
+    ASSERT_FALSE(dicom->LookupSequenceItem(m, DicomPath(DicomTag(0x0008, 0x1250), 0, DicomTag(0x0040, 0xa170)), 1));
+  }
 }
 
 
@@ -2995,6 +3019,121 @@
 
 
 
+TEST(ParsedDicomFile, ImageInformation)
+{
+  double wc, ww;
+  double ri, rs;
+  PhotometricInterpretation p;
+
+  {
+    ParsedDicomFile dicom(false);
+    dicom.GetDefaultWindowing(wc, ww, 5);
+    dicom.GetRescale(ri, rs, 5);
+    ASSERT_DOUBLE_EQ(128.0, wc);
+    ASSERT_DOUBLE_EQ(256.0, ww);
+    ASSERT_FALSE(dicom.LookupPhotometricInterpretation(p));
+    ASSERT_DOUBLE_EQ(0.0, ri);
+    ASSERT_DOUBLE_EQ(1.0, rs);
+  }
+
+  {
+    ParsedDicomFile dicom(false);
+    ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_BitsStored, "4").good());
+    ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_PhotometricInterpretation, "RGB").good());
+    dicom.GetDefaultWindowing(wc, ww, 5);
+    ASSERT_DOUBLE_EQ(8.0, wc);
+    ASSERT_DOUBLE_EQ(16.0, ww);
+    ASSERT_TRUE(dicom.LookupPhotometricInterpretation(p));
+    ASSERT_EQ(PhotometricInterpretation_RGB, p);
+  }
+
+  {
+    ParsedDicomFile dicom(false);
+    ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_WindowCenter, "12").good());
+    ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_WindowWidth, "-22").good());
+    ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_RescaleIntercept, "-22").good());
+    ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_RescaleSlope, "-23").good());
+    dicom.GetDefaultWindowing(wc, ww, 5);
+    dicom.GetRescale(ri, rs, 5);
+    ASSERT_DOUBLE_EQ(12.0, wc);
+    ASSERT_DOUBLE_EQ(-22.0, ww);
+    ASSERT_DOUBLE_EQ(-22.0, ri);
+    ASSERT_DOUBLE_EQ(-23.0, rs);
+  }
+
+  {
+    ParsedDicomFile dicom(false);
+    ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_WindowCenter, "12\\13\\14").good());
+    ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_WindowWidth, "-22\\-23\\-24").good());
+    ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_RescaleIntercept, "32\\33\\34").good());
+    ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_RescaleSlope, "-42\\-43\\-44").good());
+    dicom.GetDefaultWindowing(wc, ww, 5);
+    dicom.GetRescale(ri, rs, 5);
+    ASSERT_DOUBLE_EQ(12.0, wc);
+    ASSERT_DOUBLE_EQ(-22.0, ww);
+    ASSERT_DOUBLE_EQ(0.0, ri);
+    ASSERT_DOUBLE_EQ(1.0, rs);
+  }
+
+  {
+    // Philips multiframe
+    Json::Value v = Json::objectValue;
+    v["PerFrameFunctionalGroupsSequence"][0]["FrameVOILUTSequence"][0]["WindowCenter"] = "614";
+    v["PerFrameFunctionalGroupsSequence"][0]["FrameVOILUTSequence"][0]["WindowWidth"] = "1067";
+    v["PerFrameFunctionalGroupsSequence"][0]["PixelValueTransformationSequence"][0]["RescaleIntercept"] = "12";
+    v["PerFrameFunctionalGroupsSequence"][0]["PixelValueTransformationSequence"][0]["RescaleSlope"] = "2.551648";
+    v["PerFrameFunctionalGroupsSequence"][1]["FrameVOILUTSequence"][0]["WindowCenter"] = "-61";
+    v["PerFrameFunctionalGroupsSequence"][1]["FrameVOILUTSequence"][0]["WindowWidth"] = "-63";
+    v["PerFrameFunctionalGroupsSequence"][1]["PixelValueTransformationSequence"][0]["RescaleIntercept"] = "13";
+    v["PerFrameFunctionalGroupsSequence"][1]["PixelValueTransformationSequence"][0]["RescaleSlope"] = "-14";
+    std::unique_ptr<ParsedDicomFile> dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, ""));
+    
+    dicom->GetDefaultWindowing(wc, ww, 0);
+    dicom->GetRescale(ri, rs, 0);
+    ASSERT_DOUBLE_EQ(614.0, wc);
+    ASSERT_DOUBLE_EQ(1067.0, ww);
+    ASSERT_DOUBLE_EQ(12.0, ri);
+    ASSERT_DOUBLE_EQ(2.551648, rs);
+    
+    dicom->GetDefaultWindowing(wc, ww, 1);
+    dicom->GetRescale(ri, rs, 1);
+    ASSERT_DOUBLE_EQ(-61.0, wc);
+    ASSERT_DOUBLE_EQ(-63.0, ww);
+    ASSERT_DOUBLE_EQ(13.0, ri);
+    ASSERT_DOUBLE_EQ(-14.0, rs);
+    
+    dicom->GetDefaultWindowing(wc, ww, 2);
+    dicom->GetRescale(ri, rs, 2);
+    ASSERT_DOUBLE_EQ(128.0, wc);
+    ASSERT_DOUBLE_EQ(256.0, ww);
+    ASSERT_DOUBLE_EQ(0.0, ri);
+    ASSERT_DOUBLE_EQ(1.0, rs);
+  }
+
+  {
+    // RT-DOSE
+    Json::Value v = Json::objectValue;
+    v["RescaleIntercept"] = "10";
+    v["RescaleSlope"] = "20";
+    v["PerFrameFunctionalGroupsSequence"][0]["PixelValueTransformationSequence"][0]["RescaleIntercept"] = "30";
+    v["PerFrameFunctionalGroupsSequence"][0]["PixelValueTransformationSequence"][0]["RescaleSlope"] = "40";
+    std::unique_ptr<ParsedDicomFile> dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, ""));
+    
+    dicom->GetRescale(ri, rs, 0);
+    ASSERT_DOUBLE_EQ(10.0, ri);
+    ASSERT_DOUBLE_EQ(20.0, rs);
+
+    v["SOPClassUID"] = "1.2.840.10008.5.1.4.1.1.481.2";
+    dicom.reset(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, ""));
+    dicom->GetRescale(ri, rs, 0);
+    ASSERT_DOUBLE_EQ(0.0, ri);
+    ASSERT_DOUBLE_EQ(1.0, rs);
+  }
+}
+
+
+
+
 #if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
 
 #include "../Sources/DicomNetworking/DicomStoreUserConnection.h"
--- a/OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp	Wed Jul 21 10:48:14 2021 +0200
+++ b/OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp	Mon Aug 30 22:21:24 2021 +0200
@@ -975,6 +975,25 @@
 }
 
 
+TEST(ImageProcessing, Grayscale8_Identity)
+{
+  Image image(PixelFormat_Float32, 5, 1, false);
+  ImageTraits<PixelFormat_Float32>::SetPixel(image, 0, 0, 0);
+  ImageTraits<PixelFormat_Float32>::SetPixel(image, 2.5, 1, 0);
+  ImageTraits<PixelFormat_Float32>::SetPixel(image, 5.5, 2, 0);
+  ImageTraits<PixelFormat_Float32>::SetPixel(image, 10.5, 3, 0);
+  ImageTraits<PixelFormat_Float32>::SetPixel(image, 255.5, 4, 0);
+
+  Image image2(PixelFormat_Grayscale8, 5, 1, false);
+  ImageProcessing::ShiftScale(image2, image, 0, 1, false);
+  ASSERT_TRUE(TestGrayscale8Pixel(image2, 0, 0, 0));
+  ASSERT_TRUE(TestGrayscale8Pixel(image2, 1, 0, 2));
+  ASSERT_TRUE(TestGrayscale8Pixel(image2, 2, 0, 5));
+  ASSERT_TRUE(TestGrayscale8Pixel(image2, 3, 0, 10));
+  ASSERT_TRUE(TestGrayscale8Pixel(image2, 4, 0, 255));
+}
+
+
 TEST(ImageProcessing, ShiftScaleGrayscale16)
 {
   Image image(PixelFormat_Grayscale16, 5, 1, false);
@@ -1011,6 +1030,24 @@
 }
 
 
+TEST(ImageProcessing, ShiftScaleSignedGrayscale16_Identity)
+{
+  Image image(PixelFormat_SignedGrayscale16, 5, 1, false);
+  SetSignedGrayscale16Pixel(image, 0, 0, 0);
+  SetSignedGrayscale16Pixel(image, 1, 0, 2);
+  SetSignedGrayscale16Pixel(image, 2, 0, 5);
+  SetSignedGrayscale16Pixel(image, 3, 0, 10);
+  SetSignedGrayscale16Pixel(image, 4, 0, 255);
+
+  ImageProcessing::ShiftScale(image, 0, 1, true);
+  ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 0, 0, 0));
+  ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 1, 0, 2));
+  ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 2, 0, 5));
+  ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 3, 0, 10));
+  ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 4, 0, 255));
+}
+
+
 TEST(ImageProcessing, ShiftScale2)
 {
   std::vector<float> va;
--- a/OrthancFramework/UnitTestsSources/JobsTests.cpp	Wed Jul 21 10:48:14 2021 +0200
+++ b/OrthancFramework/UnitTestsSources/JobsTests.cpp	Mon Aug 30 22:21:24 2021 +0200
@@ -1575,3 +1575,86 @@
     ASSERT_FALSE(b.IsRemoteCertificateRequired());
   }  
 }
+
+
+TEST(SerializationToolbox, Numbers)
+{
+  {
+    int32_t i;
+    ASSERT_FALSE(SerializationToolbox::ParseInteger32(i, ""));
+    ASSERT_FALSE(SerializationToolbox::ParseInteger32(i, "ee"));
+    ASSERT_TRUE(SerializationToolbox::ParseInteger32(i, "42"));  ASSERT_EQ(42, i);
+    ASSERT_TRUE(SerializationToolbox::ParseInteger32(i, "-42"));  ASSERT_EQ(-42, i);
+    ASSERT_TRUE(SerializationToolbox::ParseInteger32(i, "-2147483648")); ASSERT_EQ(-2147483648l, i);
+    ASSERT_TRUE(SerializationToolbox::ParseInteger32(i, "2147483647")); ASSERT_EQ(2147483647l, i);
+    ASSERT_FALSE(SerializationToolbox::ParseInteger32(i, "-2147483649"));
+    ASSERT_FALSE(SerializationToolbox::ParseInteger32(i, "2147483648"));
+    ASSERT_FALSE(SerializationToolbox::ParseInteger32(i, "-2\\-3\\-4"));
+    ASSERT_TRUE(SerializationToolbox::ParseFirstInteger32(i, "-2\\-3\\-4"));  ASSERT_EQ(-2, i);
+  }
+
+  {
+    uint32_t i;
+    ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger32(i, ""));
+    ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger32(i, "ee"));
+    ASSERT_TRUE(SerializationToolbox::ParseUnsignedInteger32(i, "42"));  ASSERT_EQ(42u, i);
+    ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger32(i, "-42"));
+    ASSERT_TRUE(SerializationToolbox::ParseUnsignedInteger32(i, "4294967295")); ASSERT_EQ(4294967295u, i);
+    ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger32(i, "4294967296"));
+    ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger32(i, "2\\3\\4"));
+    ASSERT_TRUE(SerializationToolbox::ParseFirstUnsignedInteger32(i, "2\\3\\4"));  ASSERT_EQ(2u, i);
+  }
+
+  {
+    int64_t i;
+    ASSERT_FALSE(SerializationToolbox::ParseInteger64(i, ""));
+    ASSERT_FALSE(SerializationToolbox::ParseInteger64(i, "ee"));
+    ASSERT_TRUE(SerializationToolbox::ParseInteger64(i, "42"));  ASSERT_EQ(42, i);
+    ASSERT_TRUE(SerializationToolbox::ParseInteger64(i, "-42"));  ASSERT_EQ(-42, i);
+    ASSERT_TRUE(SerializationToolbox::ParseInteger64(i, "-2147483649")); ASSERT_EQ(-2147483649ll, i);
+    ASSERT_TRUE(SerializationToolbox::ParseInteger64(i, "2147483648")); ASSERT_EQ(2147483648ll, i);
+    ASSERT_FALSE(SerializationToolbox::ParseInteger64(i, "-2\\-3\\-4"));
+    ASSERT_TRUE(SerializationToolbox::ParseFirstInteger64(i, "-2\\-3\\-4"));  ASSERT_EQ(-2, i);
+  }
+
+  {
+    uint64_t i;
+    ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger64(i, ""));
+    ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger64(i, "ee"));
+    ASSERT_TRUE(SerializationToolbox::ParseUnsignedInteger64(i, "42"));  ASSERT_EQ(42u, i);
+    ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger64(i, "-42"));
+    ASSERT_TRUE(SerializationToolbox::ParseUnsignedInteger64(i, "4294967296")); ASSERT_EQ(4294967296lu, i);
+    ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger64(i, "2\\3\\4"));
+    ASSERT_TRUE(SerializationToolbox::ParseFirstUnsignedInteger64(i, "2\\3\\4"));  ASSERT_EQ(2u, i);
+  }
+
+  {
+    float i;
+    ASSERT_FALSE(SerializationToolbox::ParseFloat(i, ""));
+    ASSERT_FALSE(SerializationToolbox::ParseFloat(i, "ee"));
+    ASSERT_TRUE(SerializationToolbox::ParseFloat(i, "42"));  ASSERT_FLOAT_EQ(42.0f, i);
+    ASSERT_TRUE(SerializationToolbox::ParseFloat(i, "-42"));  ASSERT_FLOAT_EQ(-42.0f, i);
+    ASSERT_FALSE(SerializationToolbox::ParseFloat(i, "2\\3\\4"));
+    ASSERT_TRUE(SerializationToolbox::ParseFirstFloat(i, "1.367\\2.367\\3.367"));  ASSERT_FLOAT_EQ(1.367f, i);
+
+    ASSERT_TRUE(SerializationToolbox::ParseFloat(i, "1.2"));  ASSERT_FLOAT_EQ(1.2f, i);
+    ASSERT_TRUE(SerializationToolbox::ParseFloat(i, "-1.2e+2"));  ASSERT_FLOAT_EQ(-120.0f, i);
+    ASSERT_TRUE(SerializationToolbox::ParseFloat(i, "-1e-2"));  ASSERT_FLOAT_EQ(-0.01f, i);
+    ASSERT_TRUE(SerializationToolbox::ParseFloat(i, "1.3671875"));  ASSERT_FLOAT_EQ(1.3671875f, i);
+  }
+
+  {
+    double i;
+    ASSERT_FALSE(SerializationToolbox::ParseDouble(i, ""));
+    ASSERT_FALSE(SerializationToolbox::ParseDouble(i, "ee"));
+    ASSERT_TRUE(SerializationToolbox::ParseDouble(i, "42"));  ASSERT_DOUBLE_EQ(42.0, i);
+    ASSERT_TRUE(SerializationToolbox::ParseDouble(i, "-42"));  ASSERT_DOUBLE_EQ(-42.0, i);
+    ASSERT_FALSE(SerializationToolbox::ParseDouble(i, "2\\3\\4"));
+    ASSERT_TRUE(SerializationToolbox::ParseFirstDouble(i, "1.367\\2.367\\3.367"));  ASSERT_DOUBLE_EQ(1.367, i);
+
+    ASSERT_TRUE(SerializationToolbox::ParseDouble(i, "1.2"));  ASSERT_DOUBLE_EQ(1.2, i);
+    ASSERT_TRUE(SerializationToolbox::ParseDouble(i, "-1.2e+2"));  ASSERT_DOUBLE_EQ(-120.0, i);
+    ASSERT_TRUE(SerializationToolbox::ParseDouble(i, "-1e-2"));  ASSERT_DOUBLE_EQ(-0.01, i);
+    ASSERT_TRUE(SerializationToolbox::ParseDouble(i, "1.3671875"));  ASSERT_DOUBLE_EQ(1.3671875, i);
+  }
+}
--- a/OrthancServer/CMakeLists.txt	Wed Jul 21 10:48:14 2021 +0200
+++ b/OrthancServer/CMakeLists.txt	Mon Aug 30 22:21:24 2021 +0200
@@ -385,6 +385,16 @@
 
 target_link_libraries(Orthanc ServerLibrary CoreLibrary ${DCMTK_LIBRARIES})
 
+if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
+  # The link flag below hides all the global functions so that a Linux
+  # Standard Base (LSB) build of Orthanc can load plugins that are not
+  # built using LSB (new in Orthanc 1.9.7)
+  set_property(
+    TARGET Orthanc
+    PROPERTY LINK_FLAGS "-Wl,--version-script=${CMAKE_SOURCE_DIR}/Resources/VersionScriptOrthanc.map"
+    )
+endif()
+
 install(
   TARGETS Orthanc
   RUNTIME DESTINATION sbin
--- a/OrthancServer/Resources/Configuration.json	Wed Jul 21 10:48:14 2021 +0200
+++ b/OrthancServer/Resources/Configuration.json	Mon Aug 30 22:21:24 2021 +0200
@@ -291,6 +291,12 @@
   // option to "true" implies security risks. (new in Orthanc 1.9.0)
   "DicomAlwaysAllowGet" : false,
 
+  // Whether the Orthanc SCP allows incoming C-MOVE requests, even
+  // from SCU modalities it does not know about (i.e. that are not
+  // listed in the "DicomModalities" option above). Setting this
+  // option to "true" implies security risks. (new in Orthanc 1.9.7)
+  "DicomAlwaysAllowMove" : false,
+
   // Whether Orthanc checks the IP/hostname address of the remote
   // modality initiating a DICOM connection (as listed in the
   // "DicomModalities" option above). If this option is set to
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/VersionScriptOrthanc.map	Mon Aug 30 22:21:24 2021 +0200
@@ -0,0 +1,13 @@
+# This is a version-script for the main Orthanc binary, that hides all
+# the global functions of the executable so that a Linux Standard Base
+# (LSB) build of Orthanc can load plugins that are not built using
+# LSB. Otherwise, the dynamic loader of the plugins will try to use
+# the global functions published by the Orthanc server, which results
+# in a segmentation fault if the data structures don't have the same
+# memory layout (e.g. debug vs. release, or another version of some
+# C/C++ library used both by Orthanc, typically jsoncpp).
+
+{
+local:
+  *;
+};
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Wed Jul 21 10:48:14 2021 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Mon Aug 30 22:21:24 2021 +0200
@@ -46,6 +46,7 @@
 static const char* const KEEP = "Keep";
 static const char* const KEEP_PRIVATE_TAGS = "KeepPrivateTags";
 static const char* const KEEP_SOURCE = "KeepSource";
+static const char* const LEVEL = "Level";
 static const char* const PARENT = "Parent";
 static const char* const PRIVATE_CREATOR = "PrivateCreator";
 static const char* const REMOVE = "Remove";
@@ -68,6 +69,15 @@
   }
 
 
+  static void DocumentKeepSource(RestApiPostCall& call)
+  {
+    call.GetDocumentation()
+      .SetRequestField(KEEP_SOURCE, RestApiCallDocumentation::Type_Boolean,
+                       "If set to `false`, instructs Orthanc to the remove original resources. "
+                       "By default, the original resources are kept in Orthanc.", false);
+  }
+
+
   static void DocumentModifyOptions(RestApiPostCall& call)
   {
     // Check out "DicomModification::ParseModifyRequest()"
@@ -90,6 +100,9 @@
                        "as this breaks the DICOM model of the real world.", false)
       .SetRequestField(PRIVATE_CREATOR, RestApiCallDocumentation::Type_String,
                        "The private creator to be used for private tags in `Replace`", false);
+
+    // This was existing, but undocumented in Orthanc <= 1.9.6
+    DocumentKeepSource(call);
   }
 
 
@@ -113,6 +126,9 @@
                        "List of DICOM tags whose value must not be destroyed by the anonymization. " INFO_SUBSEQUENCES, false)
       .SetRequestField(PRIVATE_CREATOR, RestApiCallDocumentation::Type_String,
                        "The private creator to be used for private tags in `Replace`", false);
+
+    // This was existing, but undocumented in Orthanc <= 1.9.6
+    DocumentKeepSource(call);
   }
 
 
@@ -431,6 +447,11 @@
         .SetSummary("Modify a set of resources")
         .SetRequestField(RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings,
                          "List of the Orthanc identifiers of the patients/studies/series/instances of interest.", true)
+        .SetRequestField(LEVEL, RestApiCallDocumentation::Type_String,
+                         "Level of the modification (`Patient`, `Study`, `Series` or `Instance`). If absent, "
+                         "the level defaults to `Instance`, but is set to `Patient` if `PatientID` is modified, "
+                         "to `Study` if `StudyInstanceUID` is modified, or to `Series` if `SeriesInstancesUID` "
+                         "is modified. (new in Orthanc 1.9.7)", false)
         .SetDescription("Start a job that will modify all the DICOM patients, studies, series or instances "
                         "whose identifiers are provided in the `Resources` field.")
         .AddAnswerType(MimeType_Json, "The list of all the resources that have been altered by this modification");
@@ -442,7 +463,15 @@
     Json::Value body;
     ParseModifyRequest(body, *modification, call);
 
-    modification->SetLevel(DetectModifyLevel(*modification));
+    if (body.isMember(LEVEL))
+    {
+      // This case was introduced in Orthanc 1.9.7
+      modification->SetLevel(StringToResourceType(body[LEVEL].asCString()));
+    }
+    else
+    {
+      modification->SetLevel(DetectModifyLevel(*modification));
+    }
 
     SubmitBulkJob(modification, false /* not an anonymization */, call, body);
   }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Wed Jul 21 10:48:14 2021 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Mon Aug 30 22:21:24 2021 +0200
@@ -199,6 +199,15 @@
             {
               LOG(ERROR) << "Cannot import non-DICOM file from ZIP archive: " << filename;
             }
+            else if (e.GetErrorCode() == ErrorCode_InexistentTag)
+            {
+              /**
+               * Allow upload of ZIP archives containing a DICOMDIR
+               * file (new in Orthanc 1.9.7):
+               * https://groups.google.com/g/orthanc-users/c/sgBU89o4nhU/m/kbRAYiQUAAAJ
+               **/
+              LOG(ERROR) << "Ignoring what is probably a DICOMDIR file within a ZIP archive: \"" << filename << "\"";
+            }
             else
             {
               throw;
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Wed Jul 21 10:48:14 2021 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Mon Aug 30 22:21:24 2021 +0200
@@ -651,9 +651,11 @@
       {
       }
 
+      // "dicom" is non-NULL iff. "RequiresDicomTags() == true"
       virtual void Handle(RestApiGetCall& call,
                           std::unique_ptr<ImageAccessor>& decoded,
-                          const DicomMap& dicom) = 0;
+                          const ParsedDicomFile* dicom,
+                          unsigned int frame) = 0;
 
       virtual bool RequiresDicomTags() const = 0;
 
@@ -764,7 +766,6 @@
           return;
         }
 
-        DicomMap dicom;
         std::unique_ptr<ImageAccessor> decoded;
 
         try
@@ -787,7 +788,11 @@
              * interpretation, and with windowing parameters.
              **/ 
             ServerContext::DicomCacheLocker locker(context, publicId);
-            OrthancConfiguration::DefaultExtractDicomSummary(dicom, locker.GetDicom());
+            handler.Handle(call, decoded, &locker.GetDicom(), frame);
+          }
+          else
+          {
+            handler.Handle(call, decoded, NULL, frame);
           }
         }
         catch (OrthancException& e)
@@ -811,7 +816,6 @@
           return;
         }
 
-        handler.Handle(call, decoded, dicom);
       }
 
 
@@ -853,13 +857,22 @@
 
       virtual void Handle(RestApiGetCall& call,
                           std::unique_ptr<ImageAccessor>& decoded,
-                          const DicomMap& dicom) ORTHANC_OVERRIDE
+                          const ParsedDicomFile* dicom,
+                          unsigned int frame) ORTHANC_OVERRIDE
       {
         bool invert = false;
 
         if (mode_ == ImageExtractionMode_Preview)
         {
-          DicomImageInformation info(dicom);
+          if (dicom == NULL)
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+
+          DicomMap tags;
+          OrthancConfiguration::DefaultExtractDicomSummary(tags, *dicom);
+          
+          DicomImageInformation info(tags);
           invert = (info.GetPhotometricInterpretation() == PhotometricInterpretation_Monochrome1);
         }
 
@@ -876,40 +889,8 @@
     class RenderedFrameHandler : public IDecodedFrameHandler
     {
     private:
-      static void GetDicomParameters(bool& invert,
-                                     float& rescaleSlope,
-                                     float& rescaleIntercept,
-                                     float& windowWidth,
-                                     float& windowCenter,
-                                     const DicomMap& dicom)
-      {
-        DicomImageInformation info(dicom);
-
-        invert = (info.GetPhotometricInterpretation() == PhotometricInterpretation_Monochrome1);
-
-        rescaleSlope = 1.0f;
-        rescaleIntercept = 0.0f;
-
-        if (dicom.HasTag(Orthanc::DICOM_TAG_RESCALE_SLOPE) &&
-            dicom.HasTag(Orthanc::DICOM_TAG_RESCALE_INTERCEPT))
-        {
-          dicom.ParseFloat(rescaleSlope, Orthanc::DICOM_TAG_RESCALE_SLOPE);
-          dicom.ParseFloat(rescaleIntercept, Orthanc::DICOM_TAG_RESCALE_INTERCEPT);
-        }
-
-        windowWidth = static_cast<float>(1 << info.GetBitsStored()) * rescaleSlope;
-        windowCenter = windowWidth / 2.0f + rescaleIntercept;
-
-        if (dicom.HasTag(Orthanc::DICOM_TAG_WINDOW_CENTER) &&
-            dicom.HasTag(Orthanc::DICOM_TAG_WINDOW_WIDTH))
-        {
-          dicom.ParseFirstFloat(windowCenter, Orthanc::DICOM_TAG_WINDOW_CENTER);
-          dicom.ParseFirstFloat(windowWidth, Orthanc::DICOM_TAG_WINDOW_WIDTH);
-        }
-      }
-
-      static void GetUserArguments(float& windowWidth /* inout */,
-                                   float& windowCenter /* inout */,
+      static void GetUserArguments(double& windowWidth /* inout */,
+                                   double& windowCenter /* inout */,
                                    unsigned int& argWidth,
                                    unsigned int& argHeight,
                                    bool& smooth,
@@ -921,30 +902,18 @@
         static const char* ARG_HEIGHT = "height";
         static const char* ARG_SMOOTH = "smooth";
 
-        if (call.HasArgument(ARG_WINDOW_WIDTH))
+        if (call.HasArgument(ARG_WINDOW_WIDTH) &&
+            !SerializationToolbox::ParseDouble(windowWidth, call.GetArgument(ARG_WINDOW_WIDTH, "")))
         {
-          try
-          {
-            windowWidth = boost::lexical_cast<float>(call.GetArgument(ARG_WINDOW_WIDTH, ""));
-          }
-          catch (boost::bad_lexical_cast&)
-          {
-            throw OrthancException(ErrorCode_ParameterOutOfRange,
-                                   "Bad value for argument: " + std::string(ARG_WINDOW_WIDTH));
-          }
+          throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                 "Bad value for argument: " + std::string(ARG_WINDOW_WIDTH));
         }
 
-        if (call.HasArgument(ARG_WINDOW_CENTER))
+        if (call.HasArgument(ARG_WINDOW_CENTER) &&
+            !SerializationToolbox::ParseDouble(windowCenter, call.GetArgument(ARG_WINDOW_CENTER, "")))
         {
-          try
-          {
-            windowCenter = boost::lexical_cast<float>(call.GetArgument(ARG_WINDOW_CENTER, ""));
-          }
-          catch (boost::bad_lexical_cast&)
-          {
-            throw OrthancException(ErrorCode_ParameterOutOfRange,
-                                   "Bad value for argument: " + std::string(ARG_WINDOW_CENTER));
-          }
+          throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                 "Bad value for argument: " + std::string(ARG_WINDOW_CENTER));
         }
 
         argWidth = 0;
@@ -1006,12 +975,22 @@
     public:
       virtual void Handle(RestApiGetCall& call,
                           std::unique_ptr<ImageAccessor>& decoded,
-                          const DicomMap& dicom) ORTHANC_OVERRIDE
+                          const ParsedDicomFile* dicom,
+                          unsigned int frame) ORTHANC_OVERRIDE
       {
-        bool invert;
-        float rescaleSlope, rescaleIntercept, windowWidth, windowCenter;
-        GetDicomParameters(invert, rescaleSlope, rescaleIntercept, windowWidth, windowCenter, dicom);
-
+        if (dicom == NULL)
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+        
+        PhotometricInterpretation photometric;
+        const bool invert = (dicom->LookupPhotometricInterpretation(photometric) &&
+                             photometric == PhotometricInterpretation_Monochrome1);
+          
+        double rescaleIntercept, rescaleSlope, windowCenter, windowWidth;
+        dicom->GetRescale(rescaleIntercept, rescaleSlope, frame);
+        dicom->GetDefaultWindowing(windowCenter, windowWidth, frame);
+        
         unsigned int argWidth, argHeight;
         bool smooth;
         GetUserArguments(windowWidth, windowCenter, argWidth, argHeight, smooth, call);
@@ -1081,16 +1060,16 @@
             windowWidth = 1;
           }
 
-          if (std::abs(rescaleSlope) <= 0.1f)
+          if (std::abs(rescaleSlope) <= 0.1)
           {
-            rescaleSlope = 0.1f;
+            rescaleSlope = 0.1;
           }
 
-          const float scaling = 255.0f * rescaleSlope / windowWidth;
-          const float offset = (rescaleIntercept - windowCenter + windowWidth / 2.0f) / rescaleSlope;
+          const double scaling = 255.0 * rescaleSlope / windowWidth;
+          const double offset = (rescaleIntercept - windowCenter + windowWidth / 2.0) / rescaleSlope;
 
           std::unique_ptr<ImageAccessor> rescaled(new Image(PixelFormat_Grayscale8, decoded->GetWidth(), decoded->GetHeight(), false));
-          ImageProcessing::ShiftScale(*rescaled, converted, offset, scaling, false);
+          ImageProcessing::ShiftScale(*rescaled, converted, static_cast<float>(offset), static_cast<float>(scaling), false);
 
           if (targetWidth == decoded->GetWidth() &&
               targetHeight == decoded->GetHeight())
--- a/OrthancServer/Sources/main.cpp	Wed Jul 21 10:48:14 2021 +0200
+++ b/OrthancServer/Sources/main.cpp	Mon Aug 30 22:21:24 2021 +0200
@@ -267,6 +267,7 @@
   bool            alwaysAllowEcho_;
   bool            alwaysAllowFind_;  // New in Orthanc 1.9.0
   bool            alwaysAllowGet_;   // New in Orthanc 1.9.0
+  bool            alwaysAllowMove_;  // New in Orthanc 1.9.7
   bool            alwaysAllowStore_;
 
 public:
@@ -278,6 +279,7 @@
       alwaysAllowEcho_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowEcho", true);
       alwaysAllowFind_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowFind", false);
       alwaysAllowGet_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowGet", false);
+      alwaysAllowMove_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowMove", false);
       alwaysAllowStore_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowStore", true);
     }
 
@@ -290,6 +292,11 @@
     {
       LOG(WARNING) << "Security risk in DICOM SCP: C-GET requests are always allowed, even from unknown modalities";
     }
+
+    if (alwaysAllowMove_)
+    {
+      LOG(WARNING) << "Security risk in DICOM SCP: C-MOOVE requests are always allowed, even from unknown modalities";
+    }
   }
 
   virtual bool IsAllowedConnection(const std::string& remoteIp,
@@ -302,6 +309,7 @@
     if (alwaysAllowEcho_ ||
         alwaysAllowFind_ ||
         alwaysAllowGet_ ||
+        alwaysAllowMove_ ||
         alwaysAllowStore_)
     {
       return true;
@@ -356,6 +364,12 @@
       // Incoming C-Get requests are always accepted, even from unknown AET
       return true;
     }
+    else if (type == DicomRequestType_Move &&
+             alwaysAllowMove_)
+    {
+      // Incoming C-Move requests are always accepted, even from unknown AET
+      return true;
+    }
     else
     {
       bool checkIp;