changeset 5885:207371ec031e find-refactoring

integration mainline->find-refactoring
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 29 Nov 2024 10:54:20 +0100
parents 92e5579681f2 (current diff) 1e51e6299f7a (diff)
children 9b73ec6a07be
files NEWS OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake OrthancFramework/UnitTestsSources/DicomMapTests.cpp
diffstat 20 files changed, 336 insertions(+), 62 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Fri Nov 29 09:46:30 2024 +0100
+++ b/NEWS	Fri Nov 29 10:54:20 2024 +0100
@@ -22,7 +22,7 @@
 REST API
 --------
 
-* API version upgraded to 25
+* API version upgraded to 26
 * Improved parsing of multiple numerical values in DICOM tags.
   https://discourse.orthanc-server.org/t/qido-includefield-with-sequences/4746/6
 * in /system, added a new field "Capabilities" with new values:
@@ -45,6 +45,12 @@
   is similar to tools/find but only returns the number of resources matching the criteria.
 * With DB backend with "HasExtendedFind" support, usage of 'Limit' and 'Since in /tools/find
   is not allowed if your query includes filtering on DICOM tags that are not stored in DB.
+* In DICOMWeb json, the "DS - Decimal String" values were represented by float numbers
+  and they are now represented as strings to avoid introduction of long float representation
+  (e.g 0.1429999999999 vs "0.143") and be more compliant with the DICOMWeb
+  standard https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.3.html.
+  This has no impact on StoneViewer and OHIF.
+  https://discourse.orthanc-server.org/t/dicomwebplugin-does-not-return-series-metadata-properly/5195
 
 
 Maintenance
@@ -57,14 +63,20 @@
   times in case you don't call /statistics for a long period.
 * Fix C-Find queries not returning computed tags like ModalitiesInStudy, NumberOfStudyRelatedSeries, ...
   in very specific use-cases.
+* Fix C-Find queries not returning private tags in the modality worklist plugin.
 * Fix extremely rare error when 2 threads are trying to create the same folder in the File Storage 
   at the same time.
+* Fix crashes if handling very large images.
+* Fix deadlock when parsing specific invalid DICOM files.
+* Loading plugins:
+  - Orthanc will now fail to start when provided with a plugin path that can not be found.
 * Metrics:
   - fix a few metrics that were not published
   - added 2 metrics: orthanc_storage_cache_miss_count & orthanc_storage_cache_hit_count 
 * Upgraded dependencies for static builds:
   - curl 8.9.0
   - SQLite 3.46
+  - boost 1.86.0
 * Added a new fallback when trying to decode a frame: transcode the file using the plugin
   before decoding the frame.  This solves some issues with JP2K Lossy compression:
   https://discourse.orthanc-server.org/t/decoding-displaying-jpeg2000-lossy-images/5117
@@ -109,7 +121,7 @@
 * Housekeeper plugin: 
   - Added an option "LimitMainDicomTagsReconstructLevel"
     (allowed values: "Patient", "Study", "Series", "Instance"). This can greatly speed
-    up the housekeeper process, e.g. if you have only update the Study level ExtraMainDicomTags.
+    up the housekeeper process, e.g. if you have only updated the Study level ExtraMainDicomTags.
   - Fixed broken /instances/../tags route after running the Housekeeper
     after having changed the "IngestTranscoding".
 * SDK: added OrthancPluginLogMessage() as a new primitive for plugins
--- a/OrthancFramework/Resources/CMake/BoostConfiguration.cmake	Fri Nov 29 09:46:30 2024 +0100
+++ b/OrthancFramework/Resources/CMake/BoostConfiguration.cmake	Fri Nov 29 10:54:20 2024 +0100
@@ -91,10 +91,10 @@
   ## Parameters for static compilation of Boost 
   ##
   
-  set(BOOST_NAME boost_1_85_0)
-  set(BOOST_VERSION 1.85.0)
-  set(BOOST_BCP_SUFFIX bcpdigest-1.12.4)
-  set(BOOST_MD5 "1017e9c8383efdea01c059a8d3cc4dda")
+  set(BOOST_NAME boost_1_86_0)
+  set(BOOST_VERSION 1.86.0)
+  set(BOOST_BCP_SUFFIX bcpdigest-1.12.5)
+  set(BOOST_MD5 "20b9c325c0dde830889ee75a9e64ded8")
   set(BOOST_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/${BOOST_NAME}_${BOOST_BCP_SUFFIX}.tar.gz")
   set(BOOST_SOURCES_DIR ${CMAKE_BINARY_DIR}/${BOOST_NAME})
 
@@ -115,7 +115,7 @@
   if (FirstRun)
     execute_process(
       COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
-      ${CMAKE_CURRENT_LIST_DIR}/../Patches/boost-1.85.0-emscripten.patch
+      ${CMAKE_CURRENT_LIST_DIR}/../Patches/boost-1.86.0-emscripten.patch
       WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
       RESULT_VARIABLE Failure
       )
--- a/OrthancFramework/Resources/CMake/BoostConfiguration.sh	Fri Nov 29 09:46:30 2024 +0100
+++ b/OrthancFramework/Resources/CMake/BoostConfiguration.sh	Fri Nov 29 10:54:20 2024 +0100
@@ -27,9 +27,10 @@
 ##   - Orthanc 1.12.2: Boost 1.83.0
 ##   - Orthanc 1.12.3: Boost 1.84.0
 ##   - Orthanc > 1.12.3: Boost 1.85.0
+##   - Orthanc 1.12.5: Boost 1.86.0
 
-BOOST_VERSION=1_85_0
-ORTHANC_VERSION=1.12.4
+BOOST_VERSION=1_86_0
+ORTHANC_VERSION=1.12.5
 
 rm -rf /tmp/boost_${BOOST_VERSION}
 rm -rf /tmp/bcp/boost_${BOOST_VERSION}
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Fri Nov 29 09:46:30 2024 +0100
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Fri Nov 29 10:54:20 2024 +0100
@@ -39,7 +39,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 "25")
+set(ORTHANC_API_VERSION "26")
 
 
 #####################################################################
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/boost-1.86.0-emscripten.patch	Fri Nov 29 10:54:20 2024 +0100
@@ -0,0 +1,127 @@
+diff -urEb boost_1_86_0.orig/libs/locale/src/boost/locale/shared/date_time.cpp boost_1_86_0/libs/locale/src/boost/locale/shared/date_time.cpp
+--- boost_1_86_0.orig/libs/locale/src/boost/locale/shared/date_time.cpp	2024-09-25 15:46:01.000000000 +0200
++++ boost_1_86_0/libs/locale/src/boost/locale/shared/date_time.cpp	2024-09-25 15:58:51.306131987 +0200
+@@ -12,8 +12,10 @@
+ #include <boost/locale/date_time.hpp>
+ #include <boost/locale/formatting.hpp>
+ #include <boost/core/exchange.hpp>
+-#include <boost/thread/locks.hpp>
+-#include <boost/thread/mutex.hpp>
++#if !defined(__EMSCRIPTEN__)
++#  include <boost/thread/locks.hpp>
++#  include <boost/thread/mutex.hpp>
++#endif
+ #include <cmath>
+ 
+ namespace boost { namespace locale {
+@@ -400,6 +402,7 @@
+         return impl_->get_option(abstract_calendar::is_dst) != 0;
+     }
+ 
++#if !defined(__EMSCRIPTEN__)
+     namespace time_zone {
+         boost::mutex& tz_mutex()
+         {
+@@ -422,7 +425,7 @@
+             return boost::exchange(tz_id(), new_id);
+         }
+     } // namespace time_zone
+-
++#endif
+ }} // namespace boost::locale
+ 
+ // boostinspect:nominmax
+diff -urEb boost_1_86_0.orig/libs/locale/src/boost/locale/shared/generator.cpp boost_1_86_0/libs/locale/src/boost/locale/shared/generator.cpp
+--- boost_1_86_0.orig/libs/locale/src/boost/locale/shared/generator.cpp	2024-09-25 15:46:01.000000000 +0200
++++ boost_1_86_0/libs/locale/src/boost/locale/shared/generator.cpp	2024-09-25 16:00:07.756233916 +0200
+@@ -7,8 +7,10 @@
+ #include <boost/locale/encoding.hpp>
+ #include <boost/locale/generator.hpp>
+ #include <boost/locale/localization_backend.hpp>
+-#include <boost/thread/locks.hpp>
+-#include <boost/thread/mutex.hpp>
++#if !defined(__EMSCRIPTEN__)
++#  include <boost/thread/locks.hpp>
++#  include <boost/thread/mutex.hpp>
++#endif
+ #include <algorithm>
+ #include <map>
+ #include <vector>
+@@ -21,8 +23,9 @@
+         {}
+ 
+         mutable std::map<std::string, std::locale> cached;
++#if !defined(__EMSCRIPTEN__)
+         mutable boost::mutex cached_lock;
+-
++#endif
+         category_t cats;
+         char_facet_t chars;
+ 
+@@ -101,7 +104,9 @@
+     std::locale generator::generate(const std::locale& base, const std::string& id) const
+     {
+         if(d->caching_enabled) {
++#if !defined(__EMSCRIPTEN__)
+             boost::unique_lock<boost::mutex> guard(d->cached_lock);
++#endif
+             const auto p = d->cached.find(id);
+             if(p != d->cached.end())
+                 return p->second;
+@@ -126,7 +131,9 @@
+                 result = backend->install(result, facet, char_facet_t::nochar);
+         }
+         if(d->caching_enabled) {
++#if !defined(__EMSCRIPTEN__)
+             boost::unique_lock<boost::mutex> guard(d->cached_lock);
++#endif
+             const auto p = d->cached.find(id);
+             if(p == d->cached.end())
+                 d->cached[id] = result;
+diff -urEb boost_1_86_0.orig/libs/locale/src/boost/locale/shared/localization_backend.cpp boost_1_86_0/libs/locale/src/boost/locale/shared/localization_backend.cpp
+--- boost_1_86_0.orig/libs/locale/src/boost/locale/shared/localization_backend.cpp	2024-09-25 15:46:01.000000000 +0200
++++ boost_1_86_0/libs/locale/src/boost/locale/shared/localization_backend.cpp	2024-09-25 16:01:09.196820495 +0200
+@@ -5,8 +5,10 @@
+ // https://www.boost.org/LICENSE_1_0.txt
+ 
+ #include <boost/locale/localization_backend.hpp>
+-#include <boost/thread/locks.hpp>
+-#include <boost/thread/mutex.hpp>
++#if !defined(__EMSCRIPTEN__)
++#  include <boost/thread/locks.hpp>
++#  include <boost/thread/mutex.hpp>
++#endif
+ #include <functional>
+ #include <memory>
+ #include <vector>
+@@ -211,11 +213,13 @@
+             return mgr;
+         }
+ 
++#if !defined(__EMSCRIPTEN__)
+         boost::mutex& localization_backend_manager_mutex()
+         {
+             static boost::mutex the_mutex;
+             return the_mutex;
+         }
++#endif
+         localization_backend_manager& localization_backend_manager_global()
+         {
+             static localization_backend_manager the_manager = make_default_backend_mgr();
+@@ -225,12 +229,16 @@
+ 
+     localization_backend_manager localization_backend_manager::global()
+     {
++#if !defined(__EMSCRIPTEN__)
+         boost::unique_lock<boost::mutex> lock(localization_backend_manager_mutex());
++#endif
+         return localization_backend_manager_global();
+     }
+     localization_backend_manager localization_backend_manager::global(const localization_backend_manager& in)
+     {
++#if !defined(__EMSCRIPTEN__)
+         boost::unique_lock<boost::mutex> lock(localization_backend_manager_mutex());
++#endif
+         return exchange(localization_backend_manager_global(), in);
+     }
+ 
--- a/OrthancFramework/Resources/Patches/dcmtk-3.6.8.patch	Fri Nov 29 09:46:30 2024 +0100
+++ b/OrthancFramework/Resources/Patches/dcmtk-3.6.8.patch	Fri Nov 29 10:54:20 2024 +0100
@@ -1,6 +1,6 @@
 diff -urEb dcmtk-DCMTK-3.6.8.orig/CMake/GenerateDCMTKConfigure.cmake dcmtk-DCMTK-3.6.8/CMake/GenerateDCMTKConfigure.cmake
---- dcmtk-DCMTK-3.6.8.orig/CMake/GenerateDCMTKConfigure.cmake	2024-01-09 17:13:10.329673608 +0100
-+++ dcmtk-DCMTK-3.6.8/CMake/GenerateDCMTKConfigure.cmake	2024-01-09 18:21:52.568142681 +0100
+--- dcmtk-DCMTK-3.6.8.orig/CMake/GenerateDCMTKConfigure.cmake	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/CMake/GenerateDCMTKConfigure.cmake	2024-11-25 16:54:59.036009112 +0100
 @@ -224,6 +224,8 @@
  
  # Check the sizes of various types
@@ -18,9 +18,11 @@
  
  # Check for include files, libraries, and functions
  include("${DCMTK_CMAKE_INCLUDE}CMake/dcmtkTryCompile.cmake")
+Only in dcmtk-DCMTK-3.6.8/config/include/dcmtk/config: arith.h
+Only in dcmtk-DCMTK-3.6.8/config/include/dcmtk/config: osconfig.h
 diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h dcmtk-DCMTK-3.6.8/dcmdata/include/dcmtk/dcmdata/dcdict.h
---- dcmtk-DCMTK-3.6.8.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h	2024-01-09 17:13:10.337673529 +0100
-+++ dcmtk-DCMTK-3.6.8/dcmdata/include/dcmtk/dcmdata/dcdict.h	2024-01-09 18:21:52.568142681 +0100
+--- dcmtk-DCMTK-3.6.8.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/dcmdata/include/dcmtk/dcmdata/dcdict.h	2024-11-25 16:54:59.036009112 +0100
 @@ -162,6 +162,12 @@
      /// returns an iterator to the end of the repeating tag dictionary
      DcmDictEntryListIterator repeatingEnd() { return repDict.end(); }
@@ -35,17 +37,18 @@
  
      /** private undefined assignment operator
 diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmdata/libsrc/dcdict.cc dcmtk-DCMTK-3.6.8/dcmdata/libsrc/dcdict.cc
---- dcmtk-DCMTK-3.6.8.orig/dcmdata/libsrc/dcdict.cc	2024-01-09 17:13:10.337673529 +0100
-+++ dcmtk-DCMTK-3.6.8/dcmdata/libsrc/dcdict.cc	2024-01-09 18:21:52.568142681 +0100
+--- dcmtk-DCMTK-3.6.8.orig/dcmdata/libsrc/dcdict.cc	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/dcmdata/libsrc/dcdict.cc	2024-11-25 16:54:59.036009112 +0100
 @@ -914,3 +914,5 @@
    wrlock().clear();
    wrunlock();
  }
 +
 +#include "dcdict_orthanc.cc"
+Only in dcmtk-DCMTK-3.6.8/dcmdata/libsrc: dcdict_orthanc.cc
 diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmdata/libsrc/dcpxitem.cc dcmtk-DCMTK-3.6.8/dcmdata/libsrc/dcpxitem.cc
---- dcmtk-DCMTK-3.6.8.orig/dcmdata/libsrc/dcpxitem.cc	2024-01-09 17:13:10.337673529 +0100
-+++ dcmtk-DCMTK-3.6.8/dcmdata/libsrc/dcpxitem.cc	2024-01-09 18:21:52.568142681 +0100
+--- dcmtk-DCMTK-3.6.8.orig/dcmdata/libsrc/dcpxitem.cc	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/dcmdata/libsrc/dcpxitem.cc	2024-11-25 16:54:59.036009112 +0100
 @@ -31,6 +31,9 @@
  #include "dcmtk/dcmdata/dcostrma.h"    /* for class DcmOutputStream */
  #include "dcmtk/dcmdata/dcwcache.h"    /* for class DcmWriteCache */
@@ -57,8 +60,8 @@
  // ********************************
  
 diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmnet/libsrc/scu.cc dcmtk-DCMTK-3.6.8/dcmnet/libsrc/scu.cc
---- dcmtk-DCMTK-3.6.8.orig/dcmnet/libsrc/scu.cc	2024-01-09 17:13:10.349673411 +0100
-+++ dcmtk-DCMTK-3.6.8/dcmnet/libsrc/scu.cc	2024-01-09 18:23:08.723435667 +0100
+--- dcmtk-DCMTK-3.6.8.orig/dcmnet/libsrc/scu.cc	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/dcmnet/libsrc/scu.cc	2024-11-25 16:54:59.036009112 +0100
 @@ -19,6 +19,11 @@
   *
   */
@@ -72,8 +75,8 @@
  
  #include "dcmtk/dcmdata/dcostrmf.h" /* for class DcmOutputFileStream */
 diff -urEb dcmtk-DCMTK-3.6.8.orig/oflog/include/dcmtk/oflog/thread/syncpub.h dcmtk-DCMTK-3.6.8/oflog/include/dcmtk/oflog/thread/syncpub.h
---- dcmtk-DCMTK-3.6.8.orig/oflog/include/dcmtk/oflog/thread/syncpub.h	2024-01-09 17:13:10.389673016 +0100
-+++ dcmtk-DCMTK-3.6.8/oflog/include/dcmtk/oflog/thread/syncpub.h	2024-01-09 18:21:52.568142681 +0100
+--- dcmtk-DCMTK-3.6.8.orig/oflog/include/dcmtk/oflog/thread/syncpub.h	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/oflog/include/dcmtk/oflog/thread/syncpub.h	2024-11-25 16:54:59.037009100 +0100
 @@ -63,7 +63,7 @@
  
  DCMTK_LOG4CPLUS_INLINE_EXPORT
@@ -111,8 +114,8 @@
  
  
 diff -urEb dcmtk-DCMTK-3.6.8.orig/oflog/libsrc/oflog.cc dcmtk-DCMTK-3.6.8/oflog/libsrc/oflog.cc
---- dcmtk-DCMTK-3.6.8.orig/oflog/libsrc/oflog.cc	2024-01-09 17:13:10.389673016 +0100
-+++ dcmtk-DCMTK-3.6.8/oflog/libsrc/oflog.cc	2024-01-09 18:21:52.568142681 +0100
+--- dcmtk-DCMTK-3.6.8.orig/oflog/libsrc/oflog.cc	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/oflog/libsrc/oflog.cc	2024-11-25 16:54:59.037009100 +0100
 @@ -19,6 +19,11 @@
   *
   */
@@ -126,8 +129,8 @@
  #include "dcmtk/oflog/oflog.h"
  
 diff -urEb dcmtk-DCMTK-3.6.8.orig/ofstd/include/dcmtk/ofstd/offile.h dcmtk-DCMTK-3.6.8/ofstd/include/dcmtk/ofstd/offile.h
---- dcmtk-DCMTK-3.6.8.orig/ofstd/include/dcmtk/ofstd/offile.h	2024-01-09 17:13:10.389673016 +0100
-+++ dcmtk-DCMTK-3.6.8/ofstd/include/dcmtk/ofstd/offile.h	2024-01-09 18:21:52.568142681 +0100
+--- dcmtk-DCMTK-3.6.8.orig/ofstd/include/dcmtk/ofstd/offile.h	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/ofstd/include/dcmtk/ofstd/offile.h	2024-11-25 16:54:59.037009100 +0100
 @@ -570,7 +570,7 @@
     */
    void setlinebuf()
@@ -137,3 +140,18 @@
      this->setvbuf(NULL, _IOLBF, 0);
  #else
      :: setlinebuf(file_);
+diff -urEb dcmtk-DCMTK-3.6.8.orig/ofstd/include/dcmtk/ofstd/ofutil.h dcmtk-DCMTK-3.6.8/ofstd/include/dcmtk/ofstd/ofutil.h
+--- dcmtk-DCMTK-3.6.8.orig/ofstd/include/dcmtk/ofstd/ofutil.h	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/ofstd/include/dcmtk/ofstd/ofutil.h	2024-11-25 17:00:27.525244000 +0100
+@@ -75,8 +75,8 @@
+         // copy constructor should be fine for primitive types.
+         inline type(const T& pt)
+         : t( pt ) {}
+-        inline type(const OFrvalue_storage& rhs)
+-        : t( rhs.pt ) {}
++        inline type(const type& rhs)
++        : t( rhs.t ) {}
+ 
+         // automatic conversion to the underlying type
+         inline operator T&() const { return OFconst_cast( T&, t ); }
+Only in dcmtk-DCMTK-3.6.8/ofstd/include/dcmtk/ofstd: ofutil.h~
--- a/OrthancFramework/Sources/DicomFormat/DicomStreamReader.cpp	Fri Nov 29 09:46:30 2024 +0100
+++ b/OrthancFramework/Sources/DicomFormat/DicomStreamReader.cpp	Fri Nov 29 10:54:20 2024 +0100
@@ -247,6 +247,10 @@
           
         pos += length + 12;
       }
+      else
+      {
+        throw OrthancException(ErrorCode_BadFileFormat, "Invalid DICOM File: Unable to parse Meta Header");
+      }
     }
 
     if (pos != block.size())
--- a/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.cpp	Fri Nov 29 09:46:30 2024 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.cpp	Fri Nov 29 10:54:20 2024 +0100
@@ -202,7 +202,7 @@
                                 DicomToJsonFormat format) const
   {
     const ParsedDicomFile& answer = GetAnswer(index);
-    answer.DatasetToJson(target, format, DicomToJsonFlags_None, 0);
+    answer.DatasetToJson(target, format, DicomToJsonFlags_IncludePrivateTags, 0);
   }
 
 
--- a/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.cpp	Fri Nov 29 09:46:30 2024 +0100
+++ b/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.cpp	Fri Nov 29 10:54:20 2024 +0100
@@ -28,6 +28,7 @@
 #include "../Logging.h"
 #include "../OrthancException.h"
 #include "../Toolbox.h"
+#include "../SerializationToolbox.h"
 #include "FromDcmtkBridge.h"
 
 #include <boost/math/special_functions/round.hpp>
@@ -341,6 +342,31 @@
     }
   }
 
+  Json::Value DicomWebJsonVisitor::FormatDecimalString(double value, const std::string& originalString)
+  {
+    try
+    {
+      long long a = boost::math::llround<double>(value);
+
+      double d = fabs(value - static_cast<double>(a));
+
+      if (d <= std::numeric_limits<double>::epsilon() * 100.0)
+      {
+        return FormatInteger(a);  // if the decimal number is an integer, you can represent it as an integer  
+      }
+      else
+      {
+        return Json::Value(originalString);  // keep the original string to avoid rounding errors e.g, transforming "0.143" into 0.14299999999999
+      }
+    }
+    catch (boost::math::rounding_error&)
+    {
+      // Can occur if "long long" is too small to receive this value
+      // (e.g. infinity)
+      return Json::Value(originalString);
+    }
+  }
+
   DicomWebJsonVisitor::DicomWebJsonVisitor() :
     formatter_(NULL)
   {
@@ -677,14 +703,33 @@
                 case ValueRepresentation_DecimalString:
                 {
                   std::string t = Toolbox::StripSpaces(tokens[i]);
+                  boost::replace_all(t, ",", "."); // some invalid files uses "," instead of "."
+                  
+                  // remove invalid/useless trailing decimal separator
+                  if (t.size() > 0 && t[t.size()-1] == '.')
+                  {
+                    t.resize(t.size() -1);
+                  }
+
                   if (t.empty())
                   {
                     node[KEY_VALUE].append(Json::nullValue);
                   }
                   else
                   {
-                    double tmp = boost::lexical_cast<double>(t);
-                    node[KEY_VALUE].append(FormatDouble(tmp));
+                    // https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.3.html
+                    // DS values can be represented as String or Number in Json.
+                    // For IS, DS, SV and UV, a JSON String representation can be used to preserve the original format during transformation of the representation, or if needed to avoid losing precision of a decimal string.
+                    // Since 1.12.5, always use the string repesentation.  Before, decimal numbers were represented as double which led to loss of precision (e.g: 0.143 represented as 0.1429999999)
+                    double tmp;
+                    if (SerializationToolbox::ParseDouble(tmp, t)) // make sure that the string contains a valid decimal number
+                    {
+                      node[KEY_VALUE].append(t);
+                    }
+                    else
+                    {
+                      throw boost::bad_lexical_cast();
+                    }
                   }
 
                   break;
--- a/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h	Fri Nov 29 09:46:30 2024 +0100
+++ b/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h	Fri Nov 29 10:54:20 2024 +0100
@@ -74,6 +74,8 @@
 
     static Json::Value FormatDouble(double value);
 
+    static Json::Value FormatDecimalString(double value, const std::string& originalString);
+
   public:
     DicomWebJsonVisitor();
 
--- a/OrthancFramework/Sources/Images/ImageAccessor.cpp	Fri Nov 29 09:46:30 2024 +0100
+++ b/OrthancFramework/Sources/Images/ImageAccessor.cpp	Fri Nov 29 10:54:20 2024 +0100
@@ -139,7 +139,7 @@
     return pitch_;
   }
 
-  unsigned int ImageAccessor::GetSize() const
+  size_t ImageAccessor::GetSize() const
   {
     return GetHeight() * GetPitch();
   }
@@ -165,7 +165,7 @@
   {
     if (buffer_ != NULL)
     {
-      return buffer_ + y * pitch_;
+      return buffer_ + static_cast<size_t>(y) * static_cast<size_t>(pitch_);
     }
     else
     {
@@ -184,7 +184,7 @@
 
     if (buffer_ != NULL)
     {
-      return buffer_ + y * pitch_;
+      return buffer_ + static_cast<size_t>(y) * static_cast<size_t>(pitch_);
     }
     else
     {
@@ -325,8 +325,8 @@
     else
     {
       uint8_t* p = (buffer_ + 
-                    y * pitch_ + 
-                    x * GetBytesPerPixel());
+                    static_cast<size_t>(y) * static_cast<size_t>(pitch_) +
+                    static_cast<size_t>(x) * static_cast<size_t>(GetBytesPerPixel()));
 
       if (readOnly_)
       {
--- a/OrthancFramework/Sources/Images/ImageAccessor.h	Fri Nov 29 09:46:30 2024 +0100
+++ b/OrthancFramework/Sources/Images/ImageAccessor.h	Fri Nov 29 10:54:20 2024 +0100
@@ -86,7 +86,7 @@
 
     unsigned int GetPitch() const;
 
-    unsigned int GetSize() const;
+    size_t GetSize() const;
 
     const void* GetConstBuffer() const;
 
--- a/OrthancFramework/Sources/Images/ImageBuffer.cpp	Fri Nov 29 09:46:30 2024 +0100
+++ b/OrthancFramework/Sources/Images/ImageBuffer.cpp	Fri Nov 29 10:54:20 2024 +0100
@@ -47,7 +47,7 @@
       */
 
       pitch_ = GetBytesPerPixel() * width_;
-      size_t size = pitch_ * height_;
+      size_t size = static_cast<size_t>(pitch_) * static_cast<size_t>(height_);
 
       if (size == 0)
       {
--- a/OrthancFramework/Sources/Images/ImageProcessing.cpp	Fri Nov 29 09:46:30 2024 +0100
+++ b/OrthancFramework/Sources/Images/ImageProcessing.cpp	Fri Nov 29 10:54:20 2024 +0100
@@ -2888,8 +2888,7 @@
     const unsigned int width = image.GetWidth();
     const unsigned int height = image.GetHeight();
     const unsigned int pitch = image.GetPitch();
-    uint8_t* buffer = reinterpret_cast<uint8_t*>(image.GetBuffer());
-        
+
     if (image.GetFormat() != PixelFormat_RGB24 ||
         pitch < 3 * width)
     {
@@ -2898,7 +2897,7 @@
 
     for (unsigned int y = 0; y < height; y++)
     {
-      uint8_t* p = buffer + y * pitch;
+      uint8_t* p = reinterpret_cast<uint8_t*>(image.GetRow(y));
           
       for (unsigned int x = 0; x < width; x++, p += 3)
       {
--- a/OrthancFramework/Sources/Toolbox.cpp	Fri Nov 29 09:46:30 2024 +0100
+++ b/OrthancFramework/Sources/Toolbox.cpp	Fri Nov 29 10:54:20 2024 +0100
@@ -801,7 +801,6 @@
     return result;
   }
 
-
   void Toolbox::ComputeSHA1(std::string& result,
                             const void* data,
                             size_t size)
@@ -813,11 +812,31 @@
       sha1.process_bytes(data, size);
     }
 
-    unsigned int digest[5];
+#if BOOST_VERSION >= 108600
+    unsigned char digest[20];
 
     // Sanity check for the memory layout: A SHA-1 digest is 160 bits wide
-    assert(sizeof(unsigned int) == 4 && sizeof(digest) == (160 / 8)); 
-    
+    assert(sizeof(digest) == (160 / 8));
+    assert(sizeof(boost::uuids::detail::sha1::digest_type) == 20);
+
+    // From Boost 1.86, digest_type is "unsigned char[20]" while it was "unsigned int[5]"" in previous versions.
+    // Always perform the cast even if it is useless for Boost < 1.86
+    sha1.get_digest(digest);
+
+    result.resize(8 * 5 + 4);
+    sprintf(&result[0], "%02x%02x%02x%02x-%02x%02x%02x%02x-%02x%02x%02x%02x-%02x%02x%02x%02x-%02x%02x%02x%02x",
+            digest[0], digest[1], digest[2], digest[3],
+            digest[4], digest[5], digest[6], digest[7],
+            digest[8], digest[9], digest[10], digest[11],
+            digest[12], digest[13], digest[14], digest[15],
+            digest[16], digest[17], digest[18], digest[19]);
+
+#else
+    unsigned int digest[5];
+    // Sanity check for the memory layout: A SHA-1 digest is 160 bits wide
+    assert(sizeof(unsigned int) == 4 && sizeof(digest) == (160 / 8));
+    assert(sizeof(boost::uuids::detail::sha1::digest_type) == 20);
+
     sha1.get_digest(digest);
 
     result.resize(8 * 5 + 4);
@@ -827,6 +846,9 @@
             digest[2],
             digest[3],
             digest[4]);
+
+#endif
+
   }
 
   void Toolbox::ComputeSHA1(std::string& result,
--- a/OrthancFramework/UnitTestsSources/DicomMapTests.cpp	Fri Nov 29 09:46:30 2024 +0100
+++ b/OrthancFramework/UnitTestsSources/DicomMapTests.cpp	Fri Nov 29 10:54:20 2024 +0100
@@ -806,6 +806,7 @@
   dicom.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "SB1^SB2^SB3^SB4^SB5");
   dicom.ReplacePlainString(DICOM_TAG_IMAGE_ORIENTATION_PATIENT, "1\\2.3\\4");
   dicom.ReplacePlainString(DICOM_TAG_IMAGE_POSITION_PATIENT, "");
+  dicom.ReplacePlainString(DICOM_TAG_PIXEL_SPACING, "0,143\\0,143");  // seen in https://discourse.orthanc-server.org/t/dicomwebplugin-does-not-return-series-metadata-properly/5195
 
   DicomWebJsonVisitor visitor;
   dicom.Apply(visitor);
@@ -817,10 +818,10 @@
     ASSERT_EQ(EnumerationToString(ValueRepresentation_DecimalString), tag["vr"].asString());
     ASSERT_EQ(2u, tag.getMemberNames().size());
     ASSERT_EQ(3u, value.size());
-    ASSERT_EQ(Json::realValue, value[1].type());
-    ASSERT_FLOAT_EQ(1.0f, value[0].asFloat());
-    ASSERT_FLOAT_EQ(2.3f, value[1].asFloat());
-    ASSERT_FLOAT_EQ(4.0f, value[2].asFloat());
+    ASSERT_EQ(Json::stringValue, value[1].type());  // since Orthanc 1.12.5, this is now stored as a string
+    ASSERT_EQ("1", value[0].asString());
+    ASSERT_EQ("2.3", value[1].asString());
+    ASSERT_EQ("4", value[2].asString());
   }
 
   {
@@ -829,13 +830,23 @@
     ASSERT_EQ(1u, tag.getMemberNames().size());
   }
 
+  {
+    const Json::Value& tag = visitor.GetResult() ["00280030"];  // PixelSpacing
+    const Json::Value& value = tag["Value"];
+
+    ASSERT_EQ(EnumerationToString(ValueRepresentation_DecimalString), tag["vr"].asString());
+    ASSERT_EQ(2u, value.size());
+    ASSERT_EQ("0.143", value[0].asString());
+    ASSERT_EQ("0.143", value[1].asString());
+  }
+
   std::string xml;
   visitor.FormatXml(xml);
 
   {
     DicomMap m;
     m.FromDicomWeb(visitor.GetResult());
-    ASSERT_EQ(3u, m.GetSize());
+    ASSERT_EQ(4u, m.GetSize());
 
     std::string s;
     ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, false));
@@ -871,12 +882,12 @@
     ASSERT_EQ(EnumerationToString(ValueRepresentation_DecimalString), tag["vr"].asString());
     ASSERT_EQ(2u, tag.getMemberNames().size());
     ASSERT_EQ(4u, value.size());
-    ASSERT_EQ(Json::realValue, value[0].type());
+    ASSERT_EQ(Json::stringValue, value[0].type());
     ASSERT_EQ(Json::nullValue, value[1].type());
     ASSERT_EQ(Json::nullValue, value[2].type());
-    ASSERT_EQ(Json::realValue, value[3].type());
-    ASSERT_FLOAT_EQ(1.5f, value[0].asFloat());
-    ASSERT_FLOAT_EQ(2.5f, value[3].asFloat());
+    ASSERT_EQ(Json::stringValue, value[3].type());
+    ASSERT_EQ("1.5", value[0].asString());
+    ASSERT_EQ("2.5", value[3].asString());
   }
 
   std::string xml;
@@ -914,8 +925,8 @@
   target.FromDicomWeb(visitor.GetResult());
 
   ASSERT_EQ("DS", visitor.GetResult() ["00280030"]["vr"].asString());
-  ASSERT_FLOAT_EQ(1.5f, visitor.GetResult() ["00280030"]["Value"][0].asFloat());
-  ASSERT_FLOAT_EQ(1.3f, visitor.GetResult() ["00280030"]["Value"][1].asFloat());
+  ASSERT_EQ("1.5", visitor.GetResult() ["00280030"]["Value"][0].asString());
+  ASSERT_EQ("1.3", visitor.GetResult() ["00280030"]["Value"][1].asString());
 
   std::string s;
   ASSERT_TRUE(target.LookupStringValue(s, DICOM_TAG_PIXEL_SPACING, false));
--- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Fri Nov 29 09:46:30 2024 +0100
+++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Fri Nov 29 10:54:20 2024 +0100
@@ -2043,7 +2043,7 @@
   ASSERT_EQ("DA", visitor.GetResult() ["00080012"]["vr"].asString());
   ASSERT_EQ("DA", visitor.GetResult() ["00080012"]["Value"][0].asString());
   ASSERT_EQ("DS", visitor.GetResult() ["00101020"]["vr"].asString());
-  ASSERT_FLOAT_EQ(42.0f, visitor.GetResult() ["00101020"]["Value"][0].asFloat());
+  ASSERT_EQ("42", visitor.GetResult() ["00101020"]["Value"][0].asString());
   ASSERT_EQ("DT", visitor.GetResult() ["0008002A"]["vr"].asString());
   ASSERT_EQ("DT", visitor.GetResult() ["0008002A"]["Value"][0].asString());
   ASSERT_EQ("FL", visitor.GetResult() ["00109431"]["vr"].asString());
--- a/OrthancServer/Plugins/Engine/PluginsManager.cpp	Fri Nov 29 09:46:30 2024 +0100
+++ b/OrthancServer/Plugins/Engine/PluginsManager.cpp	Fri Nov 29 10:54:20 2024 +0100
@@ -244,8 +244,21 @@
   {
     if (!boost::filesystem::exists(path))
     {
-      LOG(ERROR) << "Inexistent path to plugins: " << path;
-      return;
+      boost::filesystem::path p(path);
+      std::string extension = p.extension().string();
+      Toolbox::ToLowerCase(extension);
+
+      if (extension == PLUGIN_EXTENSION)
+      { 
+        // if this is a plugin path, fail to start
+        throw OrthancException(ErrorCode_SharedLibrary, "Inexistent path to plugin: " + path);
+      }
+      else
+      { 
+        // it might be a directory -> just log a warning
+        LOG(WARNING) << "Inexistent path to plugins: " << path;
+        return;
+      }
     }
 
     if (boost::filesystem::is_directory(path))
--- a/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp	Fri Nov 29 09:46:30 2024 +0100
+++ b/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp	Fri Nov 29 10:54:20 2024 +0100
@@ -859,12 +859,15 @@
               "StorageCompressionChange": true,
               "MainDicomTagsChange": true,
               "UnnecessaryDicomAsJsonFiles": true,
+              "IngestTranscodingChange": true,
               "DicomWebCacheChange": true   // new in 1.12.2
             },
 
-            // When rebuilding MainDicomTags, limit to a single level of resource.
-            // Allowed values: "Patient", "Study", "Series", "Instance"
-            "LimitMainDicomTagsReconstructLevel": "Study"
+            // When rebuilding MainDicomTags, limit to a single level of resource
+            // which can greatly improve performances e.g. if you have only updated 
+            // the Study level ExtraMainDicomTags.
+            // Allowed values: "Patient", "Study", "Series", "Instance", "All"
+            "LimitMainDicomTagsReconstructLevel": "All"
 
           }
         }
@@ -887,11 +890,12 @@
         triggerOnDicomWebCacheChange_ = triggers.GetBooleanValue("DicomWebCacheChange", true);
       }
 
-      limitMainDicomTagsReconstructLevel_ = housekeeper.GetStringValue("LimitMainDicomTagsReconstructLevel", "");
+      limitMainDicomTagsReconstructLevel_ = housekeeper.GetStringValue("LimitMainDicomTagsReconstructLevel", "All");
       if (limitMainDicomTagsReconstructLevel_ != "Patient" && limitMainDicomTagsReconstructLevel_ != "Study"
-        && limitMainDicomTagsReconstructLevel_ != "Series" && limitMainDicomTagsReconstructLevel_ != "Instance")
+        && limitMainDicomTagsReconstructLevel_ != "Series" && limitMainDicomTagsReconstructLevel_ != "Instance" && limitMainDicomTagsReconstructLevel_ != "All")
       {
         ORTHANC_PLUGINS_LOG_ERROR("Housekeeper invalid value for 'LimitMainDicomTagsReconstructLevel': '" + limitMainDicomTagsReconstructLevel_ + "'");
+        return -1;
       }
       else if (limitMainDicomTagsReconstructLevel_ == "Patient")
       {
--- a/TODO	Fri Nov 29 09:46:30 2024 +0100
+++ b/TODO	Fri Nov 29 10:54:20 2024 +0100
@@ -174,6 +174,8 @@
 --------
 
 * Support C-GET SCU (note that C-GET SCP was introduced in Orthanc 1.7.0)
+* Configure the list of accepted SOP Classes
+  https://discourse.orthanc-server.org/t/can-you-limit-the-sop-classes-accepted-as-store-scp/4606
 * Support "Retrieve AE Title" (0008,0054) in C-FIND:
   - On SCP side: done by https://orthanc.uclouvain.be/hg/orthanc/rev/1ec3e1e18f50
   - On SCU side:
@@ -282,8 +284,22 @@
   https://groups.google.com/g/orthanc-users/c/ymtaAmgSs6Q/m/PqVBactQAQAJ
 * Add an index on the UUID column in the DelayedDeletion plugin:
   https://discourse.orthanc-server.org/t/delayeddeletion-improvement-unique-index-on-pending-uuid-column/4032
+* Orthanc shall refuse to start if one registers 2 storage plugins.
+  Right now, this is not possible because OrthancPluginRegisterStorageArea2 does not return any value
+  and it can not throw an Exception because that's a core function called from a plugin -> the Exception
+  can not cross the C/C++ frontier safely -> we need a OrthancPluginRegisterStorageArea3 with a return value.
+  Ex: install DelayedDeletion + S3 storage.  Right now, the second plugin to load is just ignored with an error
+  message in the logs.
 
 
+-----------
+Housekeeper
+-----------
+
+* The Housekeeper should just refuse to start if you are using a lossy transfer syntax because that would generate 
+  new orthanc ids as well and there is a risk of messing up things.  tools/reconstruct shall return a 400 as well.
+  https://discourse.orthanc-server.org/t/crashes-on-housekeeper-transcode-storing-in-s3/5330/2
+
 ----------------
 Ideas of plugins
 ----------------