changeset 7025:a998c3b34bf4 streaming

integration mainline -> streaming
author Alain Mazy <am@orthanc.team>
date Wed, 15 Jul 2026 11:42:31 +0200
parents e9cebd8402c5 (current diff) a412fcd67ae6 (diff)
children d251cd0ec90a
files NEWS OrthancFramework/Sources/DicomNetworking/DicomServer.cpp OrthancFramework/Sources/JobsEngine/JobsEngine.cpp OrthancFramework/Sources/Logging.cpp OrthancFramework/Sources/Logging.h OrthancFramework/Sources/MultiThreading/ThreadPool.cpp OrthancFramework/Sources/Toolbox.cpp OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp OrthancFramework/UnitTestsSources/ToolboxTests.cpp OrthancServer/Plugins/Engine/OrthancPlugins.cpp OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h OrthancServer/Resources/Configuration.json OrthancServer/Sources/LuaScripting.cpp OrthancServer/Sources/OrthancConfiguration.cpp OrthancServer/Sources/OrthancConfiguration.h OrthancServer/Sources/OrthancGetRequestHandler.cpp OrthancServer/Sources/OrthancMoveRequestHandler.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp OrthancServer/Sources/OrthancWebDav.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h OrthancServer/Sources/ServerIndex.cpp OrthancServer/Sources/ServerTranscoder.cpp OrthancServer/Sources/main.cpp
diffstat 37 files changed, 1822 insertions(+), 436 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Mon Jul 06 15:07:11 2026 +0200
+++ b/NEWS	Wed Jul 15 11:42:31 2026 +0200
@@ -20,6 +20,8 @@
   - "SequentialDicomReaderWindowCapacity"
   And new metrics:
   - TODO list
+* Orthanc will now refuse to start if you have "AuthenticationEnabled" and "RemoteAccessAllowed"
+  set to true and have not defined any users in "RegisteredUsers".
 * New values for "OverwriteInstances" configuration. In previous versions, this configuration
   was a Boolean and it is now a string whose allowed values are "Never", "Always",
   and "IfChanged". For backward compatibility, you may still provide a Boolean value
@@ -46,6 +48,9 @@
     Orthanc now returns the full sequence content when the matched resource contains the requested sequence.
   - When Orthanc was requested to return a private tag in a C-Find query, Orthanc was actually not returning
     the resources that did not contain the private tag although it was not used as a filter in the query.
+* Fix mismatches between the configuration default values and the documentation:
+  - "HttpTimeout" was documented as 60 while its default value is 0 (no timeout).
+  - "Name" was documented as "MyOrthanc" while its default value is "ORTHANC".
 
 REST API
 --------
@@ -76,12 +81,15 @@
 * Added OrthancPluginClearCurrentThreadName() to avoid storing many thread
   names when dynamically creating/killing threads in a plugin.
 * Added OD, OL, UC, UR, OV, SV, and UV values to the OrthancPluginValueRepresentation enumeration
+* Added OrthancPluginDicomWebBinaryMode_ArrayOfValues to generate DICOMweb
+  representations with OW, OL, OV, OF, and OD value representations as array of values
 
 Maintenance
 -----------
 
-* Added internal support for OV, SV, and UV value representations
+* Added support for OV, SV, and UV value representations
 * Added support for multiple values in AT value representation
+* Improved support of OW, OL, OV, OF, and OD value representations in DICOMweb JSON
 * Fix resubmit/pause/cancel and reloading of OrthancPeerStore and DicomModalityStore jobs 
   (bug introduced in 1.12.10)
 * Fix OrthancPeerStore jobs that was using the loader threads only if transcoding
--- a/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.7.0.cmake	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.7.0.cmake	Wed Jul 15 11:42:31 2026 +0200
@@ -110,10 +110,19 @@
 
 # C_CHAR_UNSIGNED *must* be set before calling "GenerateDCMTKConfigure.cmake"
 IF (CMAKE_CROSSCOMPILING)
+  message(STATUS "Cross-compiling to CMAKE_SYSTEM_PROCESSOR: ${CMAKE_SYSTEM_PROCESSOR}")
   if (CMAKE_COMPILER_IS_GNUCXX AND
       CMAKE_SYSTEM_NAME STREQUAL "Windows")  # MinGW
     SET(C_CHAR_UNSIGNED 1 CACHE INTERNAL "Whether char is unsigned.")
 
+  elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
+    if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(mips|mips64|mipsel|mips64el)$")
+      message(STATUS "Target architecture is MIPS, the char type should be unsigned (rough guess)")
+      SET(C_CHAR_UNSIGNED 0 CACHE INTERNAL "")
+    else()
+      SET(C_CHAR_UNSIGNED 1 CACHE INTERNAL "")
+    endif()
+
   elseif(CMAKE_SYSTEM_NAME STREQUAL "Emscripten")  # WebAssembly or asm.js
 
     # Check out "../WebAssembly/ArithmeticTests/" to regenerate the
--- a/OrthancFramework/Resources/CheckValueRepresentations.py	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancFramework/Resources/CheckValueRepresentations.py	Wed Jul 15 11:42:31 2026 +0200
@@ -188,7 +188,7 @@
 
     func = re.search(r'ApplyVisitorToLeaf.*?boost::bad_lexical_cast', content, re.DOTALL).group(0)
     for vr in vrs:
-        if not ('case EVR_%s' % vr) and not ('== EVR_%s' % vr) in func:
+        if not ('case EVR_%s' % vr) in func and not ('== EVR_%s' % vr) in func:
             print('  - Value representation not handled:', vr)
 
 
--- a/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -110,7 +110,7 @@
       if (keyword != std::string(DcmTag_ERROR_TagName))
       {
         node.append_attribute("keyword").set_value(keyword.c_str());
-      }   
+      }
 
       if (content.isMember(KEY_VALUE))
       {
@@ -409,6 +409,77 @@
 #endif
 
 
+  template <typename MemoryType,
+            typename JsonCppType,
+            typename SourceType>
+  static void ApplyBinaryModeToOtherTypes(Json::Value& node,
+                                          DicomWebJsonVisitor::BinaryMode mode,
+                                          const std::string& bulkDataUri,
+                                          const std::vector<SourceType>& values)
+  {
+    static const Endianness endianness = Toolbox::DetectEndianness();
+
+    switch (mode)
+    {
+      case DicomWebJsonVisitor::BinaryMode_Ignore:
+        break;
+
+      case DicomWebJsonVisitor::BinaryMode_BulkDataUri:
+        if (!values.empty())
+        {
+          node[KEY_BULK_DATA_URI] = bulkDataUri;
+        }
+
+        break;
+
+      case DicomWebJsonVisitor::BinaryMode_InlineBinary:
+        if (!values.empty())
+        {
+          std::string raw;
+          raw.resize(values.size() * sizeof(MemoryType));
+
+          if (!raw.empty())
+          {
+            MemoryType *p = reinterpret_cast<MemoryType*>(&raw[0]);
+
+            for (size_t i = 0; i < values.size(); i++, p++)
+            {
+              *p = static_cast<MemoryType>(values[i]);
+            }
+
+            if (endianness == Endianness_Big)
+            {
+              Toolbox::SwapEndianness(raw, sizeof(MemoryType));
+            }
+          }
+
+          std::string base64;
+          Toolbox::EncodeBase64(base64, raw);
+          node[KEY_INLINE_BINARY] = base64;
+        }
+
+        break;
+
+      case DicomWebJsonVisitor::BinaryMode_ArrayOfValues:
+        if (!values.empty())
+        {
+          node[KEY_VALUE] = Json::arrayValue;
+          Json::Value& a = node[KEY_VALUE];
+
+          for (size_t i = 0; i < values.size(); i++)
+          {
+            a.append(static_cast<JsonCppType>(values[i]));
+          }
+        }
+
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
   ITagVisitor::Action
   DicomWebJsonVisitor::VisitNotSupported(const std::vector<DicomTag> &parentTags,
                                          const std::vector<size_t> &parentIndexes,
@@ -452,12 +523,8 @@
                                    const void* data,
                                    size_t size)
   {
-    assert(vr == ValueRepresentation_OtherByte ||
-           vr == ValueRepresentation_OtherDouble ||
-           vr == ValueRepresentation_OtherFloat ||
-           vr == ValueRepresentation_OtherLong ||
-           vr == ValueRepresentation_OtherWord ||
-           vr == ValueRepresentation_OtherVeryLong ||
+    assert((vr == ValueRepresentation_OtherWord && tag == DICOM_TAG_PIXEL_DATA) ||
+           vr == ValueRepresentation_OtherByte ||
            vr == ValueRepresentation_Unknown);
 
     if (tag.GetElement() != 0x0000)
@@ -494,6 +561,7 @@
               node[KEY_BULK_DATA_URI] = bulkDataUri;
               break;
 
+            case BinaryMode_ArrayOfValues:
             case BinaryMode_InlineBinary:
             {
               std::string tmp(static_cast<const char*>(data), size);
@@ -523,21 +591,81 @@
                                      ValueRepresentation vr,
                                      const std::vector<int64_t>& values)
   {
+    /**
+     * "If an attribute is present in DICOM but empty (i.e., Value
+     * Length is 0), it shall be preserved in the DICOM JSON attribute
+     * object containing no "Value", "BulkDataURI" or "InlineBinary"."
+     * https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_f.2.5.html
+     **/
+
+    assert((vr == ValueRepresentation_OtherWord && tag != DICOM_TAG_PIXEL_DATA) ||
+           vr == ValueRepresentation_OtherLong ||
+           vr == ValueRepresentation_OtherVeryLong ||
+           vr == ValueRepresentation_SignedLong ||
+           vr == ValueRepresentation_SignedShort ||
+           vr == ValueRepresentation_SignedVeryLong ||
+           vr == ValueRepresentation_UnsignedLong ||
+           vr == ValueRepresentation_UnsignedShort ||
+           vr == ValueRepresentation_UnsignedVeryLong);
+
     if (tag.GetElement() != 0x0000 &&
         vr != ValueRepresentation_NotSupported)
     {
-      Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
-      node[KEY_VR] = EnumerationToString(vr);
-
-      if (!values.empty())
+      if (vr == ValueRepresentation_OtherWord ||
+          vr == ValueRepresentation_OtherLong ||
+          vr == ValueRepresentation_OtherVeryLong)
       {
-        Json::Value content = Json::arrayValue;
-        for (size_t i = 0; i < values.size(); i++)
+        BinaryMode mode;
+        std::string bulkDataUri;
+
+        if (formatter_ == NULL)
         {
-          content.append(FormatInteger(values[i]));
+          mode = BinaryMode_ArrayOfValues;  // This was the default in Orthanc <= 1.12.11
+        }
+        else
+        {
+          mode = formatter_->Format(bulkDataUri, parentTags, parentIndexes, tag, vr);
         }
 
-        node[KEY_VALUE] = content;
+        if (mode != BinaryMode_Ignore)
+        {
+          Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
+          node[KEY_VR] = EnumerationToString(vr);
+
+          switch (vr)
+          {
+            case ValueRepresentation_OtherWord:
+              ApplyBinaryModeToOtherTypes<uint16_t, Json::Value::UInt, int64_t>(node, mode, bulkDataUri, values);
+              break;
+
+            case ValueRepresentation_OtherLong:
+              ApplyBinaryModeToOtherTypes<uint32_t, Json::Value::UInt, int64_t>(node, mode, bulkDataUri, values);
+              break;
+
+            case ValueRepresentation_OtherVeryLong:
+              ApplyBinaryModeToOtherTypes<uint64_t, Json::Value::UInt64, int64_t>(node, mode, bulkDataUri, values);
+              break;
+
+            default:
+              throw OrthancException(ErrorCode_InternalError);
+          }
+        }
+      }
+      else
+      {
+        Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
+        node[KEY_VR] = EnumerationToString(vr);
+
+        if (!values.empty())
+        {
+          Json::Value content = Json::arrayValue;
+          for (size_t i = 0; i < values.size(); i++)
+          {
+            content.append(FormatInteger(values[i]));
+          }
+
+          node[KEY_VALUE] = content;
+        }
       }
     }
 
@@ -551,21 +679,71 @@
                                     ValueRepresentation vr,
                                     const std::vector<double>& values)
   {
+    /**
+     * "If an attribute is present in DICOM but empty (i.e., Value
+     * Length is 0), it shall be preserved in the DICOM JSON attribute
+     * object containing no "Value", "BulkDataURI" or "InlineBinary"."
+     * https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_f.2.5.html
+     **/
+
+    assert(vr == ValueRepresentation_FloatingPointDouble ||
+           vr == ValueRepresentation_FloatingPointSingle ||
+           vr == ValueRepresentation_OtherDouble ||
+           vr == ValueRepresentation_OtherFloat);
+
     if (tag.GetElement() != 0x0000 &&
         vr != ValueRepresentation_NotSupported)
     {
-      Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
-      node[KEY_VR] = EnumerationToString(vr);
-
-      if (!values.empty())
+      if (vr == ValueRepresentation_OtherDouble ||
+          vr == ValueRepresentation_OtherFloat)
       {
-        Json::Value content = Json::arrayValue;
-        for (size_t i = 0; i < values.size(); i++)
+        BinaryMode mode;
+        std::string bulkDataUri;
+
+        if (formatter_ == NULL)
+        {
+          mode = BinaryMode_ArrayOfValues;  // This was the default in Orthanc <= 1.12.11
+        }
+        else
+        {
+          mode = formatter_->Format(bulkDataUri, parentTags, parentIndexes, tag, vr);
+        }
+
+        if (mode != BinaryMode_Ignore)
         {
-          content.append(FormatDouble(values[i]));
+          Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
+          node[KEY_VR] = EnumerationToString(vr);
+
+          switch (vr)
+          {
+            case ValueRepresentation_OtherDouble:
+              ApplyBinaryModeToOtherTypes<double, double, double>(node, mode, bulkDataUri, values);
+              break;
+
+            case ValueRepresentation_OtherFloat:
+              ApplyBinaryModeToOtherTypes<float, float, double>(node, mode, bulkDataUri, values);
+              break;
+
+            default:
+              throw OrthancException(ErrorCode_InternalError);
+          }
         }
+      }
+      else
+      {
+        Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
+        node[KEY_VR] = EnumerationToString(vr);
+
+        if (!values.empty())
+        {
+          Json::Value content = Json::arrayValue;
+          for (size_t i = 0; i < values.size(); i++)
+          {
+            content.append(FormatDouble(values[i]));
+          }
           
-        node[KEY_VALUE] = content;
+          node[KEY_VALUE] = content;
+        }
       }
     }
 
--- a/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h	Wed Jul 15 11:42:31 2026 +0200
@@ -39,11 +39,14 @@
   class ORTHANC_PUBLIC DicomWebJsonVisitor : public ITagVisitor
   {
   public:
+    // This enumeration specifies how "other" value representations
+    // (OB, OW, OL, OV, OF, and OD) are encoded
     enum BinaryMode
     {
       BinaryMode_Ignore,
       BinaryMode_BulkDataUri,
-      BinaryMode_InlineBinary
+      BinaryMode_InlineBinary,
+      BinaryMode_ArrayOfValues  // New in Orthanc 1.12.12
     };
     
     class IBinaryFormatter : public boost::noncopyable
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -165,13 +165,24 @@
   }
 
 
+  static bool IsBinaryTag(DcmEVR evr,
+                          Uint16 group,
+                          Uint16 element)
+  {
+    return (evr == EVR_OB ||
+            (evr == EVR_OW &&
+             group == DCM_PixelData.getGroup() &&
+             element == DCM_PixelData.getElement()) ||
+            evr == EVR_UN ||
+            evr == EVR_ox);
+  }
+
+
   static bool IsBinaryTag(const DcmTag& key)
   {
+    // TODO - WARNING: This function doesn't inspect the registry of tags, don't use if possible
     return (key.isUnknownVR() ||
-            key.getEVR() == EVR_OB ||
-            key.getEVR() == EVR_OW ||
-            key.getEVR() == EVR_UN ||
-            key.getEVR() == EVR_ox);
+            IsBinaryTag(key.getEVR(), key.getGTag(), key.getETag()));
   }
 
 
@@ -541,6 +552,25 @@
     };
 
 
+    class ValueRepresentationFiller_OW : public ValueRepresentationFillerGeneric<Uint16>
+    {
+    protected:
+      virtual bool PutSingleValue(DcmElement& element,
+                                  const Uint16& value) const ORTHANC_OVERRIDE
+      {
+        // Contrary to US, the underlying type "DcmOtherByteOtherWord"
+        // doesn't implement "putUint16()" for a single value
+        return element.putUint16Array(&value, 1).good();
+      }
+
+      virtual bool PutMultipleValues(DcmElement& element,
+                                     const std::vector<Uint16>& values) const ORTHANC_OVERRIDE
+      {
+        return element.putUint16Array(values.empty() ? NULL : &values[0], values.size()).good();
+      }
+    };
+
+
     class ValueRepresentationFiller_US : public ValueRepresentationFillerGeneric<Uint16>
     {
     protected:
@@ -704,8 +734,7 @@
       {
         DcmAttributeTag& e = dynamic_cast<DcmAttributeTag&>(element);
 
-        if (e.getUint16Array(content_).good() &&
-            content_ != NULL)
+        if (e.getUint16Array(content_).good())
         {
           if (element.getLength() % sizeof(Uint16) != 0)
           {
@@ -713,6 +742,12 @@
           }
 
           size_ = static_cast<uint64_t>(element.getLength()) / sizeof(Uint16);
+          if (size_ != 0 &&
+              content_ == NULL)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat);
+          }
+
           if (size_ % 2 != 0)
           {
             throw OrthancException(ErrorCode_BadFileFormat);
@@ -738,12 +773,34 @@
         return size_;
       }
 
+      DicomTag GetTag(uint64_t index) const
+      {
+        assert(valid_);
+        return DicomTag(content_[2 * index], content_[2 * index + 1]);
+      }
+
       virtual void ReadValue(std::string& value,
                              uint64_t index) const ORTHANC_OVERRIDE
       {
         assert(valid_);
-        DicomTag tag(content_[2 * index], content_[2 * index + 1]);
-        value = tag.Format();
+        assert(index < size_);
+        value = GetTag(index).Format();
+      }
+
+      void ReadArrayOfAttributes(std::vector<DicomTag>& target) const
+      {
+        if (valid_)
+        {
+          target.reserve(size_);
+          for (uint64_t i = 0; i < size_; i++)
+          {
+            target.push_back(GetTag(i));
+          }
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_BadFileFormat);
+        }
       }
     };
 
@@ -768,8 +825,7 @@
          **/
         DcmFloatingPointSingle& e = dynamic_cast<DcmFloatingPointSingle&>(element);
 
-        if (e.getFloat32Array(content_).good() &&
-            content_ != NULL)
+        if (e.getFloat32Array(content_).good())
         {
           if (element.getLength() % sizeof(Float32) != 0)
           {
@@ -777,6 +833,12 @@
           }
 
           size_ = static_cast<uint64_t>(element.getLength()) / sizeof(Float32);
+          if (size_ != 0 &&
+              content_ == NULL)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat);
+          }
+
           valid_ = true;
         }
       }
@@ -796,8 +858,25 @@
                              uint64_t index) const ORTHANC_OVERRIDE
       {
         assert(valid_);
+        assert(index < size_);
         value = boost::lexical_cast<std::string>(content_[index]);
       }
+
+      void ReadArrayOfDoubles(std::vector<double>& target) const
+      {
+        if (valid_)
+        {
+          target.resize(size_);
+          for (uint64_t i = 0; i < size_; i++)
+          {
+            target[i] = content_[i];
+          }
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_BadFileFormat);
+        }
+      }
     };
 
 
@@ -814,15 +893,17 @@
         valid_(false)
       {
         /**
-         * OD stores a binary array of 64-bit IEEE doubles. Similar to OF,
-         * getVM() returns 1 for OD. We must use getFloat64Array() to access
-         * the raw double buffer.
-         * The resulting string is formatted as in ApplyDcmtkToCTypeConverter().
+         * OD stores a binary array of 64-bit IEEE doubles. Unlike FD
+         * where getVM() returns the count of values, for OD getVM()
+         * returns 1 (the entire binary blob is considered one
+         * "value"). But similar to OF, getVM() returns 1 for OD. We
+         * must use getFloat64Array() to access the raw double buffer.
+         * The resulting string is formatted as in
+         * ApplyDcmtkToCTypeConverter().
          **/
         DcmFloatingPointDouble& e = dynamic_cast<DcmFloatingPointDouble&>(element);
 
-        if (e.getFloat64Array(content_).good() &&
-            content_ != NULL)
+        if (e.getFloat64Array(content_).good())
         {
           if (element.getLength() % sizeof(Float64) != 0)
           {
@@ -830,6 +911,12 @@
           }
 
           size_ = static_cast<uint64_t>(element.getLength()) / sizeof(Float64);
+          if (size_ != 0 &&
+              content_ == NULL)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat);
+          }
+
           valid_ = true;
         }
       }
@@ -849,12 +936,97 @@
                              uint64_t index) const ORTHANC_OVERRIDE
       {
         assert(valid_);
+        assert(index < size_);
         value = boost::lexical_cast<std::string>(content_[index]);
       }
+
+      void ReadArrayOfDoubles(std::vector<double>& target) const
+      {
+        if (valid_)
+        {
+          target.resize(size_);
+          for (uint64_t i = 0; i < size_; i++)
+          {
+            target[i] = content_[i];
+          }
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_BadFileFormat);
+        }
+      }
     };
 #endif
 
 
+    class ValueRepresentationReader_OW : public ILeafElementArrayReader
+    {
+    private:
+      bool      valid_;
+      uint64_t  size_;
+      Uint16*   content_;
+
+    public:
+      ValueRepresentationReader_OW(DcmElement& element) :
+        valid_(false)
+      {
+        DcmOtherByteOtherWord& e = dynamic_cast<DcmOtherByteOtherWord&>(element);
+
+        if (e.getUint16Array(content_).good())
+        {
+          if (element.getLength() % sizeof(Uint16) != 0)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat);
+          }
+
+          size_ = static_cast<uint64_t>(element.getLength()) / sizeof(Uint16);
+          if (size_ != 0 &&
+              content_ == NULL)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat);
+          }
+
+          valid_ = true;
+        }
+      }
+
+      virtual bool IsValid() const ORTHANC_OVERRIDE
+      {
+        return valid_;
+      }
+
+      virtual uint64_t GetSize() const ORTHANC_OVERRIDE
+      {
+        assert(valid_);
+        return size_;
+      }
+
+      virtual void ReadValue(std::string& value,
+                             uint64_t index) const ORTHANC_OVERRIDE
+      {
+        assert(valid_);
+        assert(index < size_);
+        value = boost::lexical_cast<std::string>(content_[index]);
+      }
+
+      void ReadArrayOfIntegers(std::vector<int64_t>& target) const
+      {
+        if (valid_)
+        {
+          target.resize(size_);
+          for (uint64_t i = 0; i < size_; i++)
+          {
+            target[i] = content_[i];
+          }
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_BadFileFormat);
+        }
+      }
+    };
+
+
 #if DCMTK_VERSION_NUMBER >= 362
     class ValueRepresentationReader_OL : public ILeafElementArrayReader
     {
@@ -875,8 +1047,7 @@
          **/
         DcmUnsignedLong& e = dynamic_cast<DcmUnsignedLong&>(element);
 
-        if (e.getUint32Array(content_).good() &&
-            content_ != NULL)
+        if (e.getUint32Array(content_).good())
         {
           if (element.getLength() % sizeof(Uint32) != 0)
           {
@@ -884,6 +1055,12 @@
           }
 
           size_ = static_cast<uint64_t>(element.getLength()) / sizeof(Uint32);
+          if (size_ != 0 &&
+              content_ == NULL)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat);
+          }
+
           valid_ = true;
         }
       }
@@ -903,8 +1080,25 @@
                              uint64_t index) const ORTHANC_OVERRIDE
       {
         assert(valid_);
+        assert(index < size_);
         value = boost::lexical_cast<std::string>(content_[index]);
       }
+
+      void ReadArrayOfIntegers(std::vector<int64_t>& target) const
+      {
+        if (valid_)
+        {
+          target.resize(size_);
+          for (uint64_t i = 0; i < size_; i++)
+          {
+            target[i] = content_[i];
+          }
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_BadFileFormat);
+        }
+      }
     };
 #endif
 
@@ -923,8 +1117,7 @@
       {
         DcmUnsigned64bitVeryLong& e = dynamic_cast<DcmOther64bitVeryLong&>(element);
 
-        if (e.getUint64Array(content_).good() &&
-            content_ != NULL)
+        if (e.getUint64Array(content_).good())
         {
           if (element.getLength() % sizeof(Uint64) != 0)
           {
@@ -932,6 +1125,12 @@
           }
 
           size_ = static_cast<uint64_t>(element.getLength()) / sizeof(Uint64);
+          if (size_ != 0 &&
+              content_ == NULL)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat);
+          }
+
           valid_ = true;
         }
       }
@@ -951,8 +1150,38 @@
                              uint64_t index) const ORTHANC_OVERRIDE
       {
         assert(valid_);
+        assert(index < size_);
         value = boost::lexical_cast<std::string>(content_[index]);
       }
+
+      void ReadArrayOfIntegers(std::vector<int64_t>& target) const
+      {
+        if (valid_)
+        {
+          bool truncated = false;
+          target.resize(size_);
+
+          for (uint64_t i = 0; i < size_; i++)
+          {
+            target[i] = static_cast<int64_t>(content_[i]);
+
+            if (static_cast<Uint64>(target[i]) != content_[i])
+            {
+              truncated = true;
+              target[i] = std::numeric_limits<int64_t>::max();
+            }
+          }
+
+          if (truncated)
+          {
+            LOG(WARNING) << "An OV element contains a value that is too large and was truncated";
+          }
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_BadFileFormat);
+        }
+      }
     };
 #endif
   }
@@ -1343,7 +1572,7 @@
       {
         Uint8* data = NULL;
 
-        if (element.getUint8Array(data) == EC_Normal)
+        if (element.getUint8Array(data).good())
         {
           Uint32 length = element.getLength();
 
@@ -1430,11 +1659,11 @@
           {
             Uint8* data = NULL;
             Uint16* data16 = NULL;
-            if (element.getUint8Array(data) == EC_Normal)
+            if (element.getUint8Array(data).good())
             {
               return new DicomValue(reinterpret_cast<const char*>(data), element.getLength(), true);
             }
-            else if (element.getUint16Array(data16) == EC_Normal)
+            else if (element.getUint16Array(data16).good())
             {
               return new DicomValue(reinterpret_cast<const char*>(data16), element.getLength(), true);
             }
@@ -1837,7 +2066,7 @@
         }
       }
 
-      if (IsBinaryTag(element->getTag()))
+      if (IsBinaryTag(element->getTag()))   // TODO - Inspect the registry of tags
       {
         // This is a binary tag
         if ((tag == DICOM_TAG_PIXEL_DATA && !(flags & DicomToJsonFlags_IncludePixelData)) ||
@@ -2670,33 +2899,29 @@
       decoded = &binary;
     }
 
-    if (IsBinaryTag(element.getTag()))
+    if (IsBinaryTag(element.getTag()))   // TODO - Inspect the registry of tags
     {
       bool ok;
 
-      switch (element.getTag().getEVR())
+      if (element.getTag().getEVR() == EVR_OW)
       {
-        case EVR_OW:
-          if (decoded->size() % sizeof(Uint16) != 0)
-          {
-            LOG(ERROR) << "A tag with OW VR must have an even number of bytes";
-            ok = false;
-          }
-          else
-          {
-            ok = element.putUint16Array(reinterpret_cast<const Uint16*>(decoded->c_str()), decoded->size() / sizeof(Uint16)).good();
-          }
-          
-          break;
-      
-        default:
-          ok = element.putUint8Array(reinterpret_cast<const Uint8*>(decoded->c_str()), decoded->size()).good();
-          break;
+        if (decoded->size() % sizeof(Uint16) != 0)
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange, "A tag with OW VR must have an even number of bytes");
+        }
+        else
+        {
+          ok = element.putUint16Array(reinterpret_cast<const Uint16*>(decoded->c_str()), decoded->size() / sizeof(Uint16)).good();
+        }
       }
-      
+      else
+      {
+        ok = element.putUint8Array(reinterpret_cast<const Uint8*>(decoded->c_str()), decoded->size()).good();
+      }
+
       if (ok)
       {
-        return;
+        return;  // We're done
       }
       else
       {
@@ -2713,15 +2938,13 @@
         // http://support.dcmtk.org/docs/dcvr_8h-source.html
 
         /**
-         * TODO.
+         * Cases handled above (cf. "IsBinaryTag()").
          **/
 
         case EVR_OB:  // other byte
-        case EVR_OW:  // other word
-          THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_NotImplemented);
-    
         case EVR_UN:  // unknown value representation
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        case EVR_ox:  // OB or OW depending on context
+          throw OrthancException(ErrorCode_InternalError);
 
 
           /**
@@ -2795,6 +3018,10 @@
           break;
         }
 
+        case EVR_OW:  // other word (was handled as a binary tag in Orthanc <= 1.12.11)
+          ok = IElementFiller::Apply(element, ValueRepresentationFiller_OW(), *decoded);
+          break;
+
         case EVR_US:  // unsigned short
           ok = IElementFiller::Apply(element, ValueRepresentationFiller_US(), *decoded);
           break;
@@ -2844,7 +3071,6 @@
          * Internal to DCMTK.
          **/ 
 
-        case EVR_ox:  // OB or OW depending on context
         case EVR_lt:  // US, SS or OW depending on context, used for LUT Data (thus the name)
         case EVR_na:  // na="not applicable", for data which has no VR
         case EVR_up:  // up="unsigned pointer", used internally for DICOMDIR suppor
@@ -3409,32 +3635,20 @@
      * Deal with binary data (including PixelData).
      **/
 
-    if (evr == EVR_OB ||  // other byte
-        evr == EVR_OW ||  // other word
-        evr == EVR_UN)    // unknown value representation
+    if (IsBinaryTag(evr, element.getTag().getGroup(), element.getTag().getElement()))
     {
       Uint16* data16 = NULL;
       Uint8* data = NULL;
 
       ITagVisitor::Action action;
       
-      if ((element.getTag() == DCM_PixelData ||  // (*) New in Orthanc 1.9.1
-           evr == EVR_OW) &&
-          element.getUint16Array(data16) == EC_Normal)
+      if (evr == EVR_OW &&
+          element.getUint16Array(data16).good())
       {
         action = visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data16, element.getLength());
       }
-      else if (evr != EVR_OW &&
-               element.getUint8Array(data) == EC_Normal)
+      else if (element.getUint8Array(data).good())
       {
-        /**
-         * WARNING: The call to "getUint8Array()" crashes
-         * (segmentation fault) on big-endian architectures if applied
-         * to pixel data, during the call to "swapIfNecessary()" in
-         * "DcmPolymorphOBOW::getUint8Array()" (this method is not
-         * reimplemented in derived class "DcmPixelData"). However,
-         * "getUint16Array()" works correctly, hence (*).
-         **/
         action = visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data, element.getLength());
       }
       else
@@ -3544,7 +3758,7 @@
         {
           Uint8* data = NULL;
           
-          if (element.getUint8Array(data) == EC_Normal)
+          if (element.getUint8Array(data).good())
           {
             const Uint32 length = element.getLength();
             Uint32 l = 0;
@@ -3699,35 +3913,25 @@
           break;
         }
 
+        case EVR_OW:  // other word - binary array of 16-bit unsigned integers (new in Orthanc 1.12.12)
+          if (tag == DICOM_TAG_PIXEL_DATA)
+          {
+            throw OrthancException(ErrorCode_InternalError);  // Should have been handled by "visitor.VisitBinary()"
+          }
+          else
+          {
+            ValueRepresentationReader_OW reader(element);
+            std::vector<int64_t> values;
+            reader.ReadArrayOfIntegers(values);
+            action = visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
+            break;
+          }
+
         case EVR_OF:  // other float - binary array of 32-bit floats (new in Orthanc 1.12.11)
         {
-          /**
-           * OF stores a binary array of 32-bit IEEE floats. Unlike FL where getVM()
-           * returns the count of values, for OF getVM() returns 1 (the entire binary
-           * blob is considered one "value"). We must use getFloat32Array() to access
-           * the raw float buffer, then iterate over all values.
-           **/
-          DcmFloatingPointSingle& content = dynamic_cast<DcmFloatingPointSingle&>(element);
-
+          ValueRepresentationReader_OF reader(element);
           std::vector<double> values;
-
-          Float32* floatArray = NULL;
-          if (content.getFloat32Array(floatArray).good() && floatArray != NULL)
-          {
-            if (element.getLength() % sizeof(Float32) != 0)
-            {
-              throw OrthancException(ErrorCode_BadFileFormat);
-            }
-
-            const unsigned long numFloats = static_cast<unsigned long>(element.getLength() / sizeof(Float32));
-            values.reserve(numFloats);
-
-            for (unsigned long i = 0; i < numFloats; i++)
-            {
-              values.push_back(static_cast<double>(floatArray[i]));
-            }
-          }
-
+          reader.ReadArrayOfDoubles(values);
           action = visitor.VisitDoubles(parentTags, parentIndexes, tag, vr, values);
           break;
         }
@@ -3735,33 +3939,9 @@
 #if DCMTK_VERSION_NUMBER >= 361
         case EVR_OD:  // other double - binary array of 64-bit floats (new in Orthanc 1.12.11)
         {
-          /**
-           * OD stores a binary array of 64-bit IEEE doubles. Unlike FD where getVM()
-           * returns the count of values, for OD getVM() returns 1 (the entire binary
-           * blob is considered one "value"). We must use getFloat64Array() to access
-           * the raw double buffer, then iterate over all values.
-           **/
-          DcmFloatingPointDouble& content = dynamic_cast<DcmFloatingPointDouble&>(element);
-
+          ValueRepresentationReader_OD reader(element);
           std::vector<double> values;
-
-          Float64* doubleArray = NULL;
-          if (content.getFloat64Array(doubleArray).good() && doubleArray != NULL)
-          {
-            if (element.getLength() % sizeof(Float64) != 0)
-            {
-              throw OrthancException(ErrorCode_BadFileFormat);
-            }
-
-            const unsigned long numDoubles = static_cast<unsigned long>(element.getLength() / sizeof(Float64));
-            values.reserve(numDoubles);
-
-            for (unsigned long i = 0; i < numDoubles; i++)
-            {
-              values.push_back(doubleArray[i]);
-            }
-          }
-
+          reader.ReadArrayOfDoubles(values);
           action = visitor.VisitDoubles(parentTags, parentIndexes, tag, vr, values);
           break;
         }
@@ -3770,33 +3950,77 @@
 #if DCMTK_VERSION_NUMBER >= 362
         case EVR_OL:  // other long - binary array of 32-bit unsigned integers (new in Orthanc 1.12.11)
         {
-          /**
-           * OL stores a binary array of 32-bit unsigned integers. Like OF/OD,
-           * getVM() returns 1 for OL (the entire binary blob is one "value").
-           * We must use getUint32Array() to access the raw buffer, then iterate
-           * over all values based on element length.
-           **/
-          DcmUnsignedLong& content = dynamic_cast<DcmUnsignedLong&>(element);
+          ValueRepresentationReader_OL reader(element);
+          std::vector<int64_t> values;
+          reader.ReadArrayOfIntegers(values);
+          action = visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
+          break;
+        }
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 365
+        case EVR_OV:  // other very long - binary array of 64-bit unsigned integers (new in Orthanc 1.12.12)
+        {
+          ValueRepresentationReader_OV reader(element);
+          std::vector<int64_t> values;
+          reader.ReadArrayOfIntegers(values);
+          action = visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
+          break;
+        }
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 365
+        case EVR_SV:  // signed very long
+        {
+          DcmSigned64bitVeryLong& content = dynamic_cast<DcmSigned64bitVeryLong&>(element);
 
           std::vector<int64_t> values;
-
-          Uint32* uint32Array = NULL;
-          if (content.getUint32Array(uint32Array).good() && uint32Array != NULL)
+          values.reserve(content.getVM());
+
+          for (unsigned long i = 0; i < content.getVM(); i++)
           {
-            if (element.getLength() % sizeof(Uint32) != 0)
+            Sint64 f;
+            if (content.getSint64(f, i).good())
             {
-              throw OrthancException(ErrorCode_BadFileFormat);
+              values.push_back(f);
             }
-
-            const unsigned long numValues = static_cast<unsigned long>(element.getLength() / sizeof(Uint32));
-            values.reserve(numValues);
-
-            for (unsigned long i = 0; i < numValues; i++)
+          }
+
+          action = visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
+          break;
+        }
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 365
+        case EVR_UV:  // unsigned very long
+        {
+          DcmUnsigned64bitVeryLong& content = dynamic_cast<DcmUnsigned64bitVeryLong&>(element);
+
+          bool truncated = false;
+
+          std::vector<int64_t> values;
+          values.reserve(content.getVM());
+
+          for (unsigned long i = 0; i < content.getVM(); i++)
+          {
+            Uint64 f;
+            if (content.getUint64(f, i).good())
             {
-              values.push_back(static_cast<int64_t>(uint32Array[i]));
+              values.push_back(f);
+
+              if (static_cast<Uint64>(values.back()) != f)
+              {
+                truncated = true;
+                values.back() = std::numeric_limits<int64_t>::max();
+              }
             }
           }
 
+          if (truncated)
+          {
+            LOG(WARNING) << "An UV element contains a value that is too large and was truncated";
+          }
+
           action = visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
           break;
         }
@@ -3809,22 +4033,12 @@
 
         case EVR_AT:
         {
-          DcmAttributeTag& content = dynamic_cast<DcmAttributeTag&>(element);
-
+          assert(vr == ValueRepresentation_AttributeTag);
+
+          ValueRepresentationReader_AT reader(element);
           std::vector<DicomTag> values;
-          values.reserve(content.getVM());
-
-          for (unsigned long i = 0; i < content.getVM(); i++)
-          {
-            DcmTagKey f;
-            if (content.getTagVal(f, i).good())
-            {
-              DicomTag t(f.getGroup(), f.getElement());
-              values.push_back(t);
-            }
-          }
-
-          assert(vr == ValueRepresentation_AttributeTag);
+          reader.ReadArrayOfAttributes(values);
+
           action = visitor.VisitAttributes(parentTags, parentIndexes, tag, values);
           break;
         }
--- a/OrthancFramework/Sources/DicomParsing/ITagVisitor.h	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ITagVisitor.h	Wed Jul 15 11:42:31 2026 +0200
@@ -58,7 +58,7 @@
                                  const DicomTag& tag,
                                  size_t countItems) = 0;
 
-    // SL, SS, UL, US - can return "Remove" or "None"
+    // SL, SS, UL, US, OW (if not pixel data), OL, OV, SV, UV - can return "Remove" or "None"
     virtual Action VisitIntegers(const std::vector<DicomTag>& parentTags,
                                  const std::vector<size_t>& parentIndexes,
                                  const DicomTag& tag,
@@ -78,7 +78,28 @@
                                    const DicomTag& tag,
                                    const std::vector<DicomTag>& values) = 0;
 
-    // OB, OL, OW, UN - can return "Remove" or "None"
+    /**
+     * "Binary" tags are represented as a raw byte buffer. This
+     * representation is used for:
+     *
+     * OB (Other Byte): always treated as an opaque byte stream, since
+     * its content may be non-numeric, compressed, or otherwise not
+     * meaningfully interpretable as a fixed-size element type.
+     *
+     * OW (Other Word): normally represents an array of 16-bit words
+     * and would map to a typed integer container (uint16_t), but it
+     * is treated as a raw memory buffer only in the specific case of
+     * PixelData, where the true bit depth/interpretation depends on
+     * other attributes (Bits Allocated, Bits Stored, Pixel
+     * Representation) rather than being fixed by the VR itself. Other
+     * OW elements should NOT use this representation.
+     *
+     * WARNING: In Orthanc <= 1.12.11, all OW values were considered
+     * forwarded to "VisitBinary()". Now, OW values that are not pixel
+     * data are forwarded to "VisitIntegers()".
+     **/
+
+    // OW (only if pixel data), OB, and UN - can return "Remove" or "None"
     virtual Action VisitBinary(const std::vector<DicomTag>& parentTags,
                                const std::vector<size_t>& parentIndexes,
                                const DicomTag& tag,
@@ -97,5 +118,5 @@
     // empty sequence element - can return "Remove" or "None"
     virtual Action VisitEmptyElement(const std::vector<DicomTag>& parentTags,
                                      const std::vector<size_t>& parentIndexes) = 0;
-    };
+  };
 }
--- a/OrthancFramework/Sources/Enumerations.h	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancFramework/Sources/Enumerations.h	Wed Jul 15 11:42:31 2026 +0200
@@ -576,8 +576,7 @@
   };
 
   /**
-   * The value representations Orthanc knows about. They correspond to
-   * the DICOM 2016b version of the standard.
+   * The value representations Orthanc knows about.
    * http://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html
    **/
   enum ValueRepresentation
--- a/OrthancFramework/Sources/Logging.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancFramework/Sources/Logging.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -626,6 +626,24 @@
 
 static const size_t THREAD_NAME_MAX_SIZE = 16;   // Thread names are limited to 16 char + a space
 
+
+static std::string FormatThreadName(const std::string& name)
+{
+  if (name.size() < THREAD_NAME_MAX_SIZE)
+  {
+    return std::string(THREAD_NAME_MAX_SIZE - name.size(), ' ') + name;
+  }
+  else if (name.size() == THREAD_NAME_MAX_SIZE)
+  {
+    return name;
+  }
+  else
+  {
+    return name.substr(THREAD_NAME_MAX_SIZE);
+  }
+}
+
+
 namespace
 {
   class ThreadInformation : public boost::noncopyable
@@ -654,19 +672,7 @@
 #endif
 
       hasName_ = true;
-
-      if (name.size() < THREAD_NAME_MAX_SIZE)
-      {
-        name_ = std::string(THREAD_NAME_MAX_SIZE - name.size(), ' ') + name;
-      }
-      else if (name.size() == THREAD_NAME_MAX_SIZE)
-      {
-        name_ = name;
-      }
-      else
-      {
-        name_ = name.substr(THREAD_NAME_MAX_SIZE);
-      }
+      name_ = FormatThreadName(name);
 
       assert(name_.size() == THREAD_NAME_MAX_SIZE);
     }
@@ -786,7 +792,7 @@
         }
         else
         {
-          return boost::lexical_cast<std::string>(threadId_);
+          return FormatThreadName(boost::lexical_cast<std::string>(threadId_));
         }
       }
 
@@ -1092,9 +1098,9 @@
     }    
 
 
-    void PushCurrentThreadContext(const std::string& context)
+    static void PushCurrentThreadContext(const std::string& context)
     {
-      if (context.size() == 0)
+      if (context.empty())
       {
         throw OrthancException(ErrorCode_ParameterOutOfRange);
       }
@@ -1106,7 +1112,7 @@
     }
 
 
-    void PopCurrentThreadContext()
+    static void PopCurrentThreadContext()
     {
       ThreadsInformations::CurrentThreadWriter writer(threadsInformations_);
       writer.PopContext();
@@ -1121,17 +1127,18 @@
 
 
     ThreadContextMemento::ScopedSetter::ScopedSetter(const ThreadContextMemento& memento) :
-      count_(memento.pimpl_->context_.size())
+      count_(0)
     {
       if (logCallerThreadNameInContext && enableThreadNames_ && !memento.pimpl_->threadName_.empty())
       {
+        PushCurrentThreadContext("from " + memento.pimpl_->threadName_);
         count_++;
-        PushCurrentThreadContext("from " + memento.pimpl_->threadName_);
       }
 
       for (std::list<std::string>::const_iterator it = memento.pimpl_->context_.begin(); it != memento.pimpl_->context_.end(); ++it)
       {
         PushCurrentThreadContext(*it);
+        count_++;
       }
     }
 
@@ -1378,6 +1385,18 @@
     }
 
 
+    ScopedCurrentThreadContextSetter::ScopedCurrentThreadContextSetter(const std::string& context)
+    {
+      PushCurrentThreadContext(context);
+    }
+
+
+    ScopedCurrentThreadContextSetter::~ScopedCurrentThreadContextSetter()
+    {
+      PopCurrentThreadContext();
+    }
+
+
     struct InternalLogger::PImpl
     {
       boost::mutex::scoped_lock  lock_;
@@ -1411,6 +1430,16 @@
           // set to "/dev/null"
           return;
         }
+        else
+        {
+          if (enableThreadContexts_)
+          {
+            ThreadsInformations::CurrentThreadReader reader(threadsInformations_);
+            std::string prefix;
+            reader.FormatContext(prefix);
+            messageStream_ << prefix;
+          }
+        }
       }
       else
       {
--- a/OrthancFramework/Sources/Logging.h	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancFramework/Sources/Logging.h	Wed Jul 15 11:42:31 2026 +0200
@@ -147,10 +147,6 @@
 
     ORTHANC_PUBLIC std::string GetCurrentThreadName();
 
-    ORTHANC_PUBLIC void PushCurrentThreadContext(const std::string& context);
-
-    ORTHANC_PUBLIC void PopCurrentThreadContext();
-
     ORTHANC_PUBLIC ThreadContextMemento* CreateCurrentThreadContextMemento();
 
     ORTHANC_PUBLIC void AddLoggingListener(ILoggingListener* listener);
@@ -204,15 +200,9 @@
     class ScopedCurrentThreadContextSetter : public boost::noncopyable
     {
     public:
-      explicit ScopedCurrentThreadContextSetter(const std::string& context)
-      {
-        PushCurrentThreadContext(context);
-      }
+      explicit ScopedCurrentThreadContextSetter(const std::string& context);
 
-      ~ScopedCurrentThreadContextSetter()
-      {
-        PopCurrentThreadContext();
-      }
+      ~ScopedCurrentThreadContextSetter();
     };
 
     struct ORTHANC_LOCAL NullStream : public std::ostream
--- a/OrthancFramework/Sources/Toolbox.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancFramework/Sources/Toolbox.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -313,6 +313,8 @@
     pimpl_->md5_.get_digest(digest);
 
     target.resize(32);
+
+#      if BOOST_ENDIAN_LITTLE_BYTE
     sprintf(&target[0], "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
             (digest[0] >> 0) & 0xFF, 
             (digest[0] >> 8) & 0xFF,
@@ -330,6 +332,25 @@
             (digest[3] >> 8) & 0xFF,
             (digest[3] >> 16) & 0xFF,
             (digest[3] >> 24) & 0xFF);
+#      else
+    sprintf(&target[0], "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
+            (digest[0] >> 24) & 0xFF,
+            (digest[0] >> 16) & 0xFF,
+            (digest[0] >> 8) & 0xFF,
+            (digest[0] >> 0) & 0xFF,
+            (digest[1] >> 24) & 0xFF,
+            (digest[1] >> 16) & 0xFF,
+            (digest[1] >> 8) & 0xFF,
+            (digest[1] >> 0) & 0xFF,
+            (digest[2] >> 24) & 0xFF,
+            (digest[2] >> 16) & 0xFF,
+            (digest[2] >> 8) & 0xFF,
+            (digest[2] >> 0) & 0xFF,
+            (digest[3] >> 24) & 0xFF,
+            (digest[3] >> 16) & 0xFF,
+            (digest[3] >> 8) & 0xFF,
+            (digest[3] >> 0) & 0xFF);
+#      endif
 
 #    endif //BOOST_VERSION >= 108600
   }
@@ -3315,6 +3336,47 @@
       throw OrthancException(ErrorCode_ParameterOutOfRange, "Not a valid version: " + std::string(version));
     }
   }
+
+
+  void Toolbox::SwapEndianness(void* data,
+                               size_t dataSize,
+                               size_t itemSize)
+  {
+    if (itemSize == 0 ||
+        dataSize % itemSize != 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    if (itemSize != 1)
+    {
+      uint8_t* p = reinterpret_cast<uint8_t*>(data);
+      const size_t count = dataSize / itemSize;
+
+      for (size_t i = 0; i < count; i++)
+      {
+        for (size_t b1 = 0; b1 < itemSize / 2; b1++)
+        {
+          size_t b2 = itemSize - 1 - b1;
+          uint8_t tmp = p[b1];
+          p[b1] = p[b2];
+          p[b2] = tmp;
+        }
+
+        p += itemSize;
+      }
+    }
+  }
+
+
+  void Toolbox::SwapEndianness(std::string& buffer,
+                               size_t itemSize)
+  {
+    if (!buffer.empty())
+    {
+      SwapEndianness(&buffer[0], buffer.size(), itemSize);
+    }
+  }
 }
 
 
--- a/OrthancFramework/Sources/Toolbox.h	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancFramework/Sources/Toolbox.h	Wed Jul 15 11:42:31 2026 +0200
@@ -432,6 +432,13 @@
                                unsigned int major,
                                unsigned int minor,
                                unsigned int revision);
+
+    static void SwapEndianness(void* data,
+                               size_t dataSize,
+                               size_t itemSize);
+
+    static void SwapEndianness(std::string& buffer,
+                               size_t itemSize);
   };
 }
 
--- a/OrthancFramework/UnitTestsSources/DicomMapTests.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancFramework/UnitTestsSources/DicomMapTests.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -1188,6 +1188,760 @@
 }
 
 
+// Mock formatter for testing OW/OL/OF/OD binary encoding modes
+namespace
+{
+  class MockBinaryFormatter : public DicomWebJsonVisitor::IBinaryFormatter
+  {
+  private:
+    DicomWebJsonVisitor::BinaryMode  mode_;
+
+  public:
+    explicit MockBinaryFormatter(DicomWebJsonVisitor::BinaryMode mode) :
+      mode_(mode)
+    {
+    }
+
+    virtual DicomWebJsonVisitor::BinaryMode Format(std::string& bulkDataUri,
+                                                   const std::vector<DicomTag>& parentTags,
+                                                   const std::vector<size_t>& parentIndexes,
+                                                   const DicomTag& tag,
+                                                   ValueRepresentation vr) ORTHANC_OVERRIDE
+    {
+      if (mode_ == DicomWebJsonVisitor::BinaryMode_BulkDataUri)
+      {
+        bulkDataUri = "http://bulk/" + tag.Format();
+      }
+      else
+      {
+        bulkDataUri.clear();
+      }
+
+      return mode_;
+    }
+  };
+}
+
+
+TEST(DicomWebJson, VisitIntegers)
+{
+  std::vector<DicomTag> parentTags;
+  std::vector<size_t> parentIndexes;
+  DicomTag tag(0x0066, 0x0041);  // Long Triangle Point Index List (OL, but doesn't matter for this test)
+
+  std::set<ValueRepresentation> vr;
+  vr.insert(ValueRepresentation_OtherWord);
+  vr.insert(ValueRepresentation_OtherLong);
+
+#if DCMTK_VERSION_NUMBER >= 365
+  vr.insert(ValueRepresentation_OtherVeryLong);
+#endif
+
+  // Firstly, test with an empty array of values
+
+  std::vector<int64_t> values;
+
+  for (std::set<ValueRepresentation>::const_iterator it = vr.begin(); it != vr.end(); ++it)
+  {
+    {
+      DicomWebJsonVisitor visitor;
+      MockBinaryFormatter formatter(DicomWebJsonVisitor::BinaryMode_Ignore);
+      visitor.SetFormatter(formatter);
+      visitor.VisitIntegers(parentTags, parentIndexes, tag, *it, values);
+
+      const Json::Value& result = visitor.GetResult();
+      ASSERT_FALSE(result.isMember("00660041"));
+    }
+
+    {
+      DicomWebJsonVisitor visitor;
+      MockBinaryFormatter formatter(DicomWebJsonVisitor::BinaryMode_BulkDataUri);
+      visitor.SetFormatter(formatter);
+      visitor.VisitIntegers(parentTags, parentIndexes, tag, *it, values);
+
+      const Json::Value& result = visitor.GetResult();
+      ASSERT_TRUE(result.isMember("00660041"));
+      ASSERT_EQ(EnumerationToString(*it), result["00660041"]["vr"].asString());
+      ASSERT_FALSE(result["00660041"].isMember("BulkDataURI"));
+      ASSERT_FALSE(result["00660041"].isMember("Value"));
+      ASSERT_FALSE(result["00660041"].isMember("InlineBinary"));
+    }
+
+    {
+      DicomWebJsonVisitor visitor;
+      MockBinaryFormatter formatter(DicomWebJsonVisitor::BinaryMode_ArrayOfValues);
+      visitor.SetFormatter(formatter);
+      visitor.VisitIntegers(parentTags, parentIndexes, tag, *it, values);
+
+      const Json::Value& result = visitor.GetResult();
+      ASSERT_TRUE(result.isMember("00660041"));
+      ASSERT_EQ(EnumerationToString(*it), result["00660041"]["vr"].asString());
+      ASSERT_FALSE(result["00660041"].isMember("BulkDataURI"));
+      ASSERT_FALSE(result["00660041"].isMember("Value"));
+      ASSERT_FALSE(result["00660041"].isMember("InlineBinary"));
+    }
+
+    {
+      DicomWebJsonVisitor visitor;
+      MockBinaryFormatter formatter(DicomWebJsonVisitor::BinaryMode_InlineBinary);
+      visitor.SetFormatter(formatter);
+      visitor.VisitIntegers(parentTags, parentIndexes, tag, *it, values);
+
+      const Json::Value& result = visitor.GetResult();
+      ASSERT_TRUE(result.isMember("00660041"));
+      ASSERT_EQ(EnumerationToString(*it), result["00660041"]["vr"].asString());
+      ASSERT_FALSE(result["00660041"].isMember("InlineBinary"));
+      ASSERT_FALSE(result["00660041"].isMember("Value"));
+      ASSERT_FALSE(result["00660041"].isMember("BulkDataURI"));
+    }
+  }
+
+  // Secondly, test with non-empty array of values
+
+  values.push_back(100);
+  values.push_back(200);
+  values.push_back(300);
+
+  for (std::set<ValueRepresentation>::const_iterator it = vr.begin(); it != vr.end(); ++it)
+  {
+    {
+      DicomWebJsonVisitor visitor;
+      MockBinaryFormatter formatter(DicomWebJsonVisitor::BinaryMode_Ignore);
+      visitor.SetFormatter(formatter);
+      visitor.VisitIntegers(parentTags, parentIndexes, tag, *it, values);
+
+      const Json::Value& result = visitor.GetResult();
+      ASSERT_FALSE(result.isMember("00660041"));
+    }
+
+    {
+      DicomWebJsonVisitor visitor;
+      MockBinaryFormatter formatter(DicomWebJsonVisitor::BinaryMode_BulkDataUri);
+      visitor.SetFormatter(formatter);
+      visitor.VisitIntegers(parentTags, parentIndexes, tag, *it, values);
+
+      const Json::Value& result = visitor.GetResult();
+      ASSERT_TRUE(result.isMember("00660041"));
+      ASSERT_EQ(EnumerationToString(*it), result["00660041"]["vr"].asString());
+      ASSERT_TRUE(result["00660041"].isMember("BulkDataURI"));
+      ASSERT_EQ("http://bulk/0066,0041", result["00660041"]["BulkDataURI"].asString());
+      ASSERT_FALSE(result["00660041"].isMember("Value"));
+      ASSERT_FALSE(result["00660041"].isMember("InlineBinary"));
+    }
+
+    {
+      DicomWebJsonVisitor visitor;
+      MockBinaryFormatter formatter(DicomWebJsonVisitor::BinaryMode_ArrayOfValues);
+      visitor.SetFormatter(formatter);
+      visitor.VisitIntegers(parentTags, parentIndexes, tag, *it, values);
+
+      const Json::Value& result = visitor.GetResult();
+      ASSERT_TRUE(result.isMember("00660041"));
+      ASSERT_EQ(EnumerationToString(*it), result["00660041"]["vr"].asString());
+      ASSERT_FALSE(result["00660041"].isMember("BulkDataURI"));
+      ASSERT_TRUE(result["00660041"].isMember("Value"));
+      ASSERT_FALSE(result["00660041"].isMember("InlineBinary"));
+      ASSERT_EQ(3u, result["00660041"]["Value"].size());
+      ASSERT_EQ(100, result["00660041"]["Value"][0].asInt());
+      ASSERT_EQ(200, result["00660041"]["Value"][1].asInt());
+      ASSERT_EQ(300, result["00660041"]["Value"][2].asInt());
+    }
+  }
+
+  {
+    DicomWebJsonVisitor visitor;
+    MockBinaryFormatter formatter(DicomWebJsonVisitor::BinaryMode_InlineBinary);
+    visitor.SetFormatter(formatter);
+    visitor.VisitIntegers(parentTags, parentIndexes, tag, ValueRepresentation_OtherWord, values);
+
+    const Json::Value& result = visitor.GetResult();
+    ASSERT_TRUE(result.isMember("00660041"));
+    ASSERT_EQ("OW", result["00660041"]["vr"].asString());
+    ASSERT_TRUE(result["00660041"].isMember("InlineBinary"));
+    ASSERT_FALSE(result["00660041"].isMember("Value"));
+    ASSERT_FALSE(result["00660041"].isMember("BulkDataURI"));
+
+    /**
+     * "Other" transfer syntaxes must use Little Endian:
+     * https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_F.2.7
+     * https://dicom.nema.org/medical/dicom/current/output/html/part05.html#sect_7.3
+     **/
+    ASSERT_EQ("ZADIACwB", result["00660041"]["InlineBinary"].asString());
+
+    std::string decoded;
+    Toolbox::DecodeBase64(decoded, result["00660041"]["InlineBinary"].asString());
+    ASSERT_EQ(3 * sizeof(uint16_t), decoded.size());
+
+    if (Toolbox::DetectEndianness() == Endianness_Big)
+    {
+      std::swap(decoded[0], decoded[1]);
+      std::swap(decoded[2], decoded[3]);
+      std::swap(decoded[4], decoded[5]);
+    }
+
+    uint16_t f0, f1, f2;
+    memcpy(&f0, decoded.c_str(), sizeof(uint16_t));
+    memcpy(&f1, decoded.c_str() + sizeof(uint16_t), sizeof(uint16_t));
+    memcpy(&f2, decoded.c_str() + 2 * sizeof(uint16_t), sizeof(uint16_t));
+    ASSERT_EQ(100, f0);
+    ASSERT_EQ(200, f1);
+    ASSERT_EQ(300, f2);
+  }
+
+  {
+    DicomWebJsonVisitor visitor;
+    MockBinaryFormatter formatter(DicomWebJsonVisitor::BinaryMode_InlineBinary);
+    visitor.SetFormatter(formatter);
+    visitor.VisitIntegers(parentTags, parentIndexes, tag, ValueRepresentation_OtherLong, values);
+
+    const Json::Value& result = visitor.GetResult();
+    ASSERT_TRUE(result.isMember("00660041"));
+    ASSERT_EQ("OL", result["00660041"]["vr"].asString());
+    ASSERT_TRUE(result["00660041"].isMember("InlineBinary"));
+    ASSERT_FALSE(result["00660041"].isMember("Value"));
+    ASSERT_FALSE(result["00660041"].isMember("BulkDataURI"));
+
+    ASSERT_EQ("ZAAAAMgAAAAsAQAA", result["00660041"]["InlineBinary"].asString());
+
+    std::string decoded;
+    Toolbox::DecodeBase64(decoded, result["00660041"]["InlineBinary"].asString());
+    ASSERT_EQ(3 * sizeof(uint32_t), decoded.size());
+
+    if (Toolbox::DetectEndianness() == Endianness_Big)
+    {
+      Toolbox::SwapEndianness(decoded, sizeof(uint32_t));
+    }
+
+    uint32_t f0, f1, f2;
+    memcpy(&f0, decoded.c_str(), sizeof(uint32_t));
+    memcpy(&f1, decoded.c_str() + sizeof(uint32_t), sizeof(uint32_t));
+    memcpy(&f2, decoded.c_str() + 2 * sizeof(uint32_t), sizeof(uint32_t));
+    ASSERT_EQ(100, f0);
+    ASSERT_EQ(200, f1);
+    ASSERT_EQ(300, f2);
+  }
+
+#if DCMTK_VERSION_NUMBER >= 365
+  {
+    DicomWebJsonVisitor visitor;
+    MockBinaryFormatter formatter(DicomWebJsonVisitor::BinaryMode_InlineBinary);
+    visitor.SetFormatter(formatter);
+    visitor.VisitIntegers(parentTags, parentIndexes, tag, ValueRepresentation_OtherVeryLong, values);
+
+    const Json::Value& result = visitor.GetResult();
+    ASSERT_TRUE(result.isMember("00660041"));
+    ASSERT_EQ("OV", result["00660041"]["vr"].asString());
+    ASSERT_TRUE(result["00660041"].isMember("InlineBinary"));
+    ASSERT_FALSE(result["00660041"].isMember("Value"));
+    ASSERT_FALSE(result["00660041"].isMember("BulkDataURI"));
+
+    ASSERT_EQ("ZAAAAAAAAADIAAAAAAAAACwBAAAAAAAA", result["00660041"]["InlineBinary"].asString());
+
+    std::string decoded;
+    Toolbox::DecodeBase64(decoded, result["00660041"]["InlineBinary"].asString());
+    ASSERT_EQ(3 * sizeof(uint64_t), decoded.size());
+
+    if (Toolbox::DetectEndianness() == Endianness_Big)
+    {
+      Toolbox::SwapEndianness(decoded, sizeof(uint64_t));
+    }
+
+    uint64_t f0, f1, f2;
+    memcpy(&f0, decoded.c_str(), sizeof(uint64_t));
+    memcpy(&f1, decoded.c_str() + sizeof(uint64_t), sizeof(uint64_t));
+    memcpy(&f2, decoded.c_str() + 2 * sizeof(uint64_t), sizeof(uint64_t));
+    ASSERT_EQ(100, f0);
+    ASSERT_EQ(200, f1);
+    ASSERT_EQ(300, f2);
+  }
+#endif
+}
+
+
+TEST(DicomWebJson, VisitDoubles)
+{
+  std::vector<DicomTag> parentTags;
+  std::vector<size_t> parentIndexes;
+  DicomTag tag(0x0066, 0x0027);  // Triangle Point Index List (OF, but doesn't matter for this test)
+
+  std::set<ValueRepresentation> vr;
+  vr.insert(ValueRepresentation_OtherFloat);
+  vr.insert(ValueRepresentation_OtherDouble);
+
+  // Firstly, test with an empty array of values
+
+  std::vector<double> values;
+
+  for (std::set<ValueRepresentation>::const_iterator it = vr.begin(); it != vr.end(); ++it)
+  {
+    {
+      DicomWebJsonVisitor visitor;
+      MockBinaryFormatter formatter(DicomWebJsonVisitor::BinaryMode_Ignore);
+      visitor.SetFormatter(formatter);
+      visitor.VisitDoubles(parentTags, parentIndexes, tag, *it, values);
+
+      const Json::Value& result = visitor.GetResult();
+      ASSERT_FALSE(result.isMember("00660027"));
+    }
+
+    {
+      DicomWebJsonVisitor visitor;
+      MockBinaryFormatter formatter(DicomWebJsonVisitor::BinaryMode_BulkDataUri);
+      visitor.SetFormatter(formatter);
+      visitor.VisitDoubles(parentTags, parentIndexes, tag, *it, values);
+
+      const Json::Value& result = visitor.GetResult();
+      ASSERT_TRUE(result.isMember("00660027"));
+      ASSERT_EQ(EnumerationToString(*it), result["00660027"]["vr"].asString());
+      ASSERT_FALSE(result["00660027"].isMember("BulkDataURI"));
+      ASSERT_FALSE(result["00660027"].isMember("Value"));
+      ASSERT_FALSE(result["00660027"].isMember("InlineBinary"));
+    }
+
+    {
+      DicomWebJsonVisitor visitor;
+      MockBinaryFormatter formatter(DicomWebJsonVisitor::BinaryMode_ArrayOfValues);
+      visitor.SetFormatter(formatter);
+      visitor.VisitDoubles(parentTags, parentIndexes, tag, *it, values);
+
+      const Json::Value& result = visitor.GetResult();
+      ASSERT_TRUE(result.isMember("00660027"));
+      ASSERT_EQ(EnumerationToString(*it), result["00660027"]["vr"].asString());
+      ASSERT_FALSE(result["00660027"].isMember("BulkDataURI"));
+      ASSERT_FALSE(result["00660027"].isMember("Value"));
+      ASSERT_FALSE(result["00660027"].isMember("InlineBinary"));
+    }
+
+    {
+      DicomWebJsonVisitor visitor;
+      MockBinaryFormatter formatter(DicomWebJsonVisitor::BinaryMode_InlineBinary);
+      visitor.SetFormatter(formatter);
+      visitor.VisitDoubles(parentTags, parentIndexes, tag, *it, values);
+
+      const Json::Value& result = visitor.GetResult();
+      ASSERT_TRUE(result.isMember("00660027"));
+      ASSERT_EQ(EnumerationToString(*it), result["00660027"]["vr"].asString());
+      ASSERT_FALSE(result["00660027"].isMember("InlineBinary"));
+      ASSERT_FALSE(result["00660027"].isMember("Value"));
+      ASSERT_FALSE(result["00660027"].isMember("BulkDataURI"));
+    }
+  }
+
+  // Secondly, test with non-empty array of values
+
+  values.push_back(1.5);
+  values.push_back(2.5);
+
+  for (std::set<ValueRepresentation>::const_iterator it = vr.begin(); it != vr.end(); ++it)
+  {
+    {
+      DicomWebJsonVisitor visitor;
+      MockBinaryFormatter formatter(DicomWebJsonVisitor::BinaryMode_Ignore);
+      visitor.SetFormatter(formatter);
+      visitor.VisitDoubles(parentTags, parentIndexes, tag, *it, values);
+
+      const Json::Value& result = visitor.GetResult();
+      ASSERT_FALSE(result.isMember("00660027"));
+    }
+
+    {
+      DicomWebJsonVisitor visitor;
+      MockBinaryFormatter formatter(DicomWebJsonVisitor::BinaryMode_BulkDataUri);
+      visitor.SetFormatter(formatter);
+      visitor.VisitDoubles(parentTags, parentIndexes, tag, *it, values);
+
+      const Json::Value& result = visitor.GetResult();
+      ASSERT_TRUE(result.isMember("00660027"));
+      ASSERT_EQ(EnumerationToString(*it), result["00660027"]["vr"].asString());
+      ASSERT_TRUE(result["00660027"].isMember("BulkDataURI"));
+      ASSERT_EQ("http://bulk/0066,0027", result["00660027"]["BulkDataURI"].asString());
+      ASSERT_FALSE(result["00660027"].isMember("Value"));
+      ASSERT_FALSE(result["00660027"].isMember("InlineBinary"));
+    }
+
+    {
+      DicomWebJsonVisitor visitor;
+      MockBinaryFormatter formatter(DicomWebJsonVisitor::BinaryMode_ArrayOfValues);
+      visitor.SetFormatter(formatter);
+      visitor.VisitDoubles(parentTags, parentIndexes, tag, *it, values);
+
+      const Json::Value& result = visitor.GetResult();
+      ASSERT_TRUE(result.isMember("00660027"));
+      ASSERT_EQ(EnumerationToString(*it), result["00660027"]["vr"].asString());
+      ASSERT_FALSE(result["00660027"].isMember("BulkDataURI"));
+      ASSERT_TRUE(result["00660027"].isMember("Value"));
+      ASSERT_FALSE(result["00660027"].isMember("InlineBinary"));
+      ASSERT_EQ(2u, result["00660027"]["Value"].size());
+      ASSERT_FLOAT_EQ(1.5f, result["00660027"]["Value"][0].asFloat());
+      ASSERT_FLOAT_EQ(2.5f, result["00660027"]["Value"][1].asFloat());
+    }
+  }
+
+  {
+    DicomWebJsonVisitor visitor;
+    MockBinaryFormatter formatter(DicomWebJsonVisitor::BinaryMode_InlineBinary);
+    visitor.SetFormatter(formatter);
+    visitor.VisitDoubles(parentTags, parentIndexes, tag, ValueRepresentation_OtherFloat, values);
+
+    const Json::Value& result = visitor.GetResult();
+    ASSERT_TRUE(result.isMember("00660027"));
+    ASSERT_EQ("OF", result["00660027"]["vr"].asString());
+    ASSERT_TRUE(result["00660027"].isMember("InlineBinary"));
+    ASSERT_FALSE(result["00660027"].isMember("Value"));
+    ASSERT_FALSE(result["00660027"].isMember("BulkDataURI"));
+
+    /**
+     * "Other" transfer syntaxes must use Little Endian:
+     * https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_F.2.7
+     * https://dicom.nema.org/medical/dicom/current/output/html/part05.html#sect_7.3
+     **/
+    ASSERT_EQ("AADAPwAAIEA=", result["00660027"]["InlineBinary"].asString());
+
+    std::string decoded;
+    Toolbox::DecodeBase64(decoded, result["00660027"]["InlineBinary"].asString());
+    ASSERT_EQ(2 * sizeof(float), decoded.size());
+
+    if (Toolbox::DetectEndianness() == Endianness_Big)
+    {
+      Toolbox::SwapEndianness(decoded, sizeof(float));
+    }
+
+    float f0, f1;
+    memcpy(&f0, decoded.c_str(), sizeof(float));
+    memcpy(&f1, decoded.c_str() + sizeof(float), sizeof(float));
+    ASSERT_FLOAT_EQ(1.5f, f0);
+    ASSERT_FLOAT_EQ(2.5f, f1);
+  }
+
+  {
+    DicomWebJsonVisitor visitor;
+    MockBinaryFormatter formatter(DicomWebJsonVisitor::BinaryMode_InlineBinary);
+    visitor.SetFormatter(formatter);
+    visitor.VisitDoubles(parentTags, parentIndexes, tag, ValueRepresentation_OtherDouble, values);
+
+    const Json::Value& result = visitor.GetResult();
+    ASSERT_TRUE(result.isMember("00660027"));
+    ASSERT_EQ("OD", result["00660027"]["vr"].asString());
+    ASSERT_TRUE(result["00660027"].isMember("InlineBinary"));
+    ASSERT_FALSE(result["00660027"].isMember("Value"));
+    ASSERT_FALSE(result["00660027"].isMember("BulkDataURI"));
+
+    ASSERT_EQ("AAAAAAAA+D8AAAAAAAAEQA==", result["00660027"]["InlineBinary"].asString());
+
+    std::string decoded;
+    Toolbox::DecodeBase64(decoded, result["00660027"]["InlineBinary"].asString());
+    ASSERT_EQ(2 * sizeof(double), decoded.size());
+
+    if (Toolbox::DetectEndianness() == Endianness_Big)
+    {
+      Toolbox::SwapEndianness(decoded, sizeof(double));
+    }
+
+    double f0, f1;
+    memcpy(&f0, decoded.c_str(), sizeof(double));
+    memcpy(&f1, decoded.c_str() + sizeof(double), sizeof(double));
+    ASSERT_DOUBLE_EQ(1.5f, f0);
+    ASSERT_DOUBLE_EQ(2.5f, f1);
+  }
+}
+
+
+TEST(DicomWebJson, OnlyOtherAffectedByIntegersFormatter)
+{
+  std::vector<DicomTag> parentTags;
+  std::vector<size_t> parentIndexes;
+  DicomTag tag(0x0028, 0x0008);  // NumberOfFrames (IS, but doesn't matter for this test)
+
+  std::vector<int64_t> values;
+  values.push_back(10);
+
+  std::set<DicomWebJsonVisitor::BinaryMode> modes;
+  modes.insert(DicomWebJsonVisitor::BinaryMode_Ignore);
+  modes.insert(DicomWebJsonVisitor::BinaryMode_BulkDataUri);
+  modes.insert(DicomWebJsonVisitor::BinaryMode_ArrayOfValues);
+  modes.insert(DicomWebJsonVisitor::BinaryMode_InlineBinary);
+
+  std::set<ValueRepresentation> vrs;
+  vrs.insert(ValueRepresentation_SignedLong);
+  vrs.insert(ValueRepresentation_SignedShort);
+  vrs.insert(ValueRepresentation_UnsignedLong);
+  vrs.insert(ValueRepresentation_UnsignedShort);
+
+#if DCMTK_VERSION_NUMBER >= 365
+  vrs.insert(ValueRepresentation_SignedVeryLong);
+  vrs.insert(ValueRepresentation_UnsignedVeryLong);
+#endif
+
+  for (std::set<ValueRepresentation>::const_iterator vr = vrs.begin(); vr != vrs.end(); ++vr)
+  {
+    for (std::set<DicomWebJsonVisitor::BinaryMode>::const_iterator mode = modes.begin(); mode != modes.end(); ++mode)
+    {
+      DicomWebJsonVisitor visitor;
+      MockBinaryFormatter formatter(*mode);
+      visitor.SetFormatter(formatter);
+
+      visitor.VisitIntegers(parentTags, parentIndexes, tag, *vr, values);
+
+      const Json::Value& result = visitor.GetResult();
+      ASSERT_TRUE(result.isMember("00280008"));
+      ASSERT_EQ(EnumerationToString(*vr), result["00280008"]["vr"].asString());
+      ASSERT_TRUE(result["00280008"].isMember("Value"));
+      ASSERT_FALSE(result["00280008"].isMember("BulkDataURI"));
+      ASSERT_FALSE(result["00280008"].isMember("InlineBinary"));
+      ASSERT_EQ(1u, result["00280008"]["Value"].size());
+      ASSERT_EQ(10, result["00280008"]["Value"][0].asInt());
+    }
+  }
+}
+
+
+TEST(DicomWebJson, OnlyOtherAffectedByDoublesFormatter)
+{
+  std::vector<DicomTag> parentTags;
+  std::vector<size_t> parentIndexes;
+  DicomTag tag(0x0066, 0x0027);  // Triangle Point Index List (OF, but doesn't matter for this test)
+
+  std::vector<double> values;
+  values.push_back(21.5);
+
+  std::set<DicomWebJsonVisitor::BinaryMode> modes;
+  modes.insert(DicomWebJsonVisitor::BinaryMode_Ignore);
+  modes.insert(DicomWebJsonVisitor::BinaryMode_BulkDataUri);
+  modes.insert(DicomWebJsonVisitor::BinaryMode_ArrayOfValues);
+  modes.insert(DicomWebJsonVisitor::BinaryMode_InlineBinary);
+
+  std::set<ValueRepresentation> vrs;
+  vrs.insert(ValueRepresentation_FloatingPointSingle);
+  vrs.insert(ValueRepresentation_FloatingPointDouble);
+
+  for (std::set<ValueRepresentation>::const_iterator vr = vrs.begin(); vr != vrs.end(); ++vr)
+  {
+    for (std::set<DicomWebJsonVisitor::BinaryMode>::const_iterator mode = modes.begin(); mode != modes.end(); ++mode)
+    {
+      DicomWebJsonVisitor visitor;
+      MockBinaryFormatter formatter(*mode);
+      visitor.SetFormatter(formatter);
+
+      visitor.VisitDoubles(parentTags, parentIndexes, tag, *vr, values);
+
+      const Json::Value& result = visitor.GetResult();
+      ASSERT_TRUE(result.isMember("00660027"));
+      ASSERT_EQ(EnumerationToString(*vr), result["00660027"]["vr"].asString());
+      ASSERT_TRUE(result["00660027"].isMember("Value"));
+      ASSERT_FALSE(result["00660027"].isMember("BulkDataURI"));
+      ASSERT_FALSE(result["00660027"].isMember("InlineBinary"));
+      ASSERT_EQ(1u, result["00660027"]["Value"].size());
+      ASSERT_FLOAT_EQ(21.5, result["00660027"]["Value"][0].asFloat());
+    }
+  }
+}
+
+
+TEST(DicomWebJson, BinaryModes)
+{
+  ParsedDicomFile dicom(false);
+  dicom.ReplacePlainString(DicomTag(0x0010, 0x9431), "43.5");  // FL
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x1163), "44.5");  // FD
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x1160), "45");    // IS
+  dicom.ReplacePlainString(DicomTag(0x0028, 0x1201), "57");    // OW
+  dicom.ReplacePlainString(DicomTag(0x7fe0, 0x0009), "3.5");   // OD (other double)
+  dicom.ReplacePlainString(DicomTag(0x0064, 0x0009), "2.5");   // OF (other float)
+  dicom.ReplacePlainString(DicomTag(0x0066, 0x0040), "46");    // OL (other long)
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x1161), "128");   // UL
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x0301), "17");    // US
+  dicom.ReplacePlainString(DicomTag(0x0002, 0x0102), "MyPrivateInformation");   // OB
+
+  {
+    DicomWebJsonVisitor visitor;
+    dicom.Apply(visitor);
+
+    const Json::Value& result = visitor.GetResult();
+
+    ASSERT_EQ("FL", result["00109431"]["vr"].asString());
+    ASSERT_FLOAT_EQ(43.5f, result["00109431"]["Value"][0].asFloat());
+    ASSERT_EQ("FD", result["00081163"]["vr"].asString());
+    ASSERT_FLOAT_EQ(44.5f, result["00081163"]["Value"][0].asFloat());
+    ASSERT_EQ("IS", result["00081160"]["vr"].asString());
+    ASSERT_FLOAT_EQ(45.0f, result["00081160"]["Value"][0].asFloat());
+    ASSERT_EQ("UL", result["00081161"]["vr"].asString());
+    ASSERT_EQ(128u, result["00081161"]["Value"][0].asUInt());
+
+#if DCMTK_VERSION_NUMBER >= 361
+    ASSERT_EQ("US", result["00080301"]["vr"].asString());
+    ASSERT_EQ(17u, result["00080301"]["Value"][0].asUInt());
+
+    ASSERT_EQ("OD", result["7FE00009"]["vr"].asString());
+    ASSERT_FLOAT_EQ(3.5f, result["7FE00009"]["Value"][0].asFloat());
+
+#if DCMTK_VERSION_NUMBER > 361
+    ASSERT_EQ("OL", result["00660040"]["vr"].asString());
+#else
+    // DCMTK 3.6.1 confused "OL" with "UL", so it was handled as an array of values
+    ASSERT_EQ("UL", result["00660040"]["vr"].asString());
+#endif
+
+    ASSERT_EQ(46, result["00660040"]["Value"][0].asInt());
+#endif
+
+    ASSERT_EQ("OW", result["00281201"]["vr"].asString());
+    ASSERT_EQ(57, result["00281201"]["Value"][0].asInt());
+
+    ASSERT_EQ("OF", result["00640009"]["vr"].asString());
+    ASSERT_FLOAT_EQ(2.5f, result["00640009"]["Value"][0].asFloat());
+
+    ASSERT_EQ("OB", result["00020102"]["vr"].asString());
+
+    std::string decoded;
+    Toolbox::DecodeBase64(decoded, result["00020102"]["InlineBinary"].asString());
+    ASSERT_EQ("MyPrivateInformation", decoded);
+  }
+
+  std::set<DicomWebJsonVisitor::BinaryMode> modes;
+  modes.insert(DicomWebJsonVisitor::BinaryMode_Ignore);
+  modes.insert(DicomWebJsonVisitor::BinaryMode_BulkDataUri);
+  modes.insert(DicomWebJsonVisitor::BinaryMode_ArrayOfValues);
+  modes.insert(DicomWebJsonVisitor::BinaryMode_InlineBinary);
+
+  for (std::set<DicomWebJsonVisitor::BinaryMode>::const_iterator mode = modes.begin(); mode != modes.end(); ++mode)
+  {
+    DicomWebJsonVisitor visitor;
+    MockBinaryFormatter formatter(*mode);
+    visitor.SetFormatter(formatter);
+    dicom.Apply(visitor);
+
+    const Json::Value& result = visitor.GetResult();
+
+    ASSERT_EQ("FL", result["00109431"]["vr"].asString());
+    ASSERT_FLOAT_EQ(43.5f, result["00109431"]["Value"][0].asFloat());
+    ASSERT_EQ("FD", result["00081163"]["vr"].asString());
+    ASSERT_FLOAT_EQ(44.5f, result["00081163"]["Value"][0].asFloat());
+    ASSERT_EQ("IS", result["00081160"]["vr"].asString());
+    ASSERT_FLOAT_EQ(45.0f, result["00081160"]["Value"][0].asFloat());
+    ASSERT_EQ("UL", result["00081161"]["vr"].asString());
+    ASSERT_EQ(128u, result["00081161"]["Value"][0].asUInt());
+
+#if DCMTK_VERSION_NUMBER >= 361
+    ASSERT_EQ("US", result["00080301"]["vr"].asString());
+    ASSERT_EQ(17u, result["00080301"]["Value"][0].asUInt());
+#endif
+
+    switch (*mode)
+    {
+      case DicomWebJsonVisitor::BinaryMode_Ignore:
+#if DCMTK_VERSION_NUMBER >= 361
+        ASSERT_FALSE(result.isMember("7FE00009"));
+#endif
+
+#if DCMTK_VERSION_NUMBER > 361
+        ASSERT_FALSE(result.isMember("00660040"));  // DCMTK 3.6.1 considered OL as a non-other VR
+#endif
+
+        ASSERT_FALSE(result.isMember("00281201"));
+        ASSERT_FALSE(result.isMember("00640009"));
+        ASSERT_FALSE(result.isMember("00020102"));
+        break;
+
+      case DicomWebJsonVisitor::BinaryMode_BulkDataUri:
+#if DCMTK_VERSION_NUMBER >= 361
+        ASSERT_EQ("OD", result["7FE00009"]["vr"].asString());
+#endif
+
+#if DCMTK_VERSION_NUMBER > 361
+        ASSERT_EQ("OL", result["00660040"]["vr"].asString());
+        ASSERT_EQ("http://bulk/0066,0040", result["00660040"]["BulkDataURI"].asString());
+#endif
+
+        ASSERT_EQ("OW", result["00281201"]["vr"].asString());
+        ASSERT_EQ("OF", result["00640009"]["vr"].asString());
+        ASSERT_EQ("OB", result["00020102"]["vr"].asString());
+
+        ASSERT_FALSE(result["00281201"].isMember("Value"));
+        ASSERT_TRUE(result["00281201"].isMember("BulkDataURI"));
+        ASSERT_FALSE(result["00281201"].isMember("InlineBinary"));
+
+        ASSERT_EQ("http://bulk/0028,1201", result["00281201"]["BulkDataURI"].asString());
+        ASSERT_EQ("http://bulk/0064,0009", result["00640009"]["BulkDataURI"].asString());
+        ASSERT_EQ("http://bulk/7fe0,0009", result["7FE00009"]["BulkDataURI"].asString());
+        ASSERT_EQ("http://bulk/0002,0102", result["00020102"]["BulkDataURI"].asString());
+        break;
+
+      case DicomWebJsonVisitor::BinaryMode_ArrayOfValues:
+      {
+#if DCMTK_VERSION_NUMBER >= 361
+        ASSERT_EQ("OD", result["7FE00009"]["vr"].asString());
+#endif
+
+#if DCMTK_VERSION_NUMBER > 361
+        ASSERT_EQ("OL", result["00660040"]["vr"].asString());
+#endif
+
+        ASSERT_EQ("OW", result["00281201"]["vr"].asString());
+        ASSERT_EQ("OF", result["00640009"]["vr"].asString());
+        ASSERT_EQ("OB", result["00020102"]["vr"].asString());
+
+        ASSERT_TRUE(result["00281201"].isMember("Value"));
+        ASSERT_FALSE(result["00281201"].isMember("BulkDataURI"));
+        ASSERT_FALSE(result["00281201"].isMember("InlineBinary"));
+
+        ASSERT_EQ(57, result["00281201"]["Value"][0].asInt());
+        ASSERT_FLOAT_EQ(2.5f, result["00640009"]["Value"][0].asFloat());
+
+#if DCMTK_VERSION_NUMBER >= 361
+        ASSERT_FLOAT_EQ(3.5f, result["7FE00009"]["Value"][0].asFloat());
+        ASSERT_EQ(46, result["00660040"]["Value"][0].asInt());
+#endif
+
+        // Special case: OB, UN, and OW for PixelData are always inlined
+        std::string decoded;
+        Toolbox::DecodeBase64(decoded, result["00020102"]["InlineBinary"].asString());
+        ASSERT_EQ("MyPrivateInformation", decoded);
+
+        break;
+      }
+
+      case DicomWebJsonVisitor::BinaryMode_InlineBinary:
+      {
+#if DCMTK_VERSION_NUMBER >= 361
+        ASSERT_EQ("OD", result["7FE00009"]["vr"].asString());
+#endif
+
+#if DCMTK_VERSION_NUMBER > 361
+        ASSERT_EQ("OL", result["00660040"]["vr"].asString());
+#endif
+
+        ASSERT_EQ("OW", result["00281201"]["vr"].asString());
+        ASSERT_EQ("OF", result["00640009"]["vr"].asString());
+        ASSERT_EQ("OB", result["00020102"]["vr"].asString());
+
+        ASSERT_FALSE(result["00281201"].isMember("Value"));
+        ASSERT_FALSE(result["00281201"].isMember("BulkDataURI"));
+        ASSERT_TRUE(result["00281201"].isMember("InlineBinary"));
+
+        ASSERT_EQ("OQA=", result["00281201"]["InlineBinary"].asString());
+        ASSERT_EQ("AAAgQA==", result["00640009"]["InlineBinary"].asString());
+
+#if DCMTK_VERSION_NUMBER >= 361
+        ASSERT_EQ("AAAAAAAADEA=", result["7FE00009"]["InlineBinary"].asString());
+#endif
+
+#if DCMTK_VERSION_NUMBER > 361
+        ASSERT_EQ("LgAAAA==", result["00660040"]["InlineBinary"].asString());
+#endif
+
+        std::string decoded;
+        Toolbox::DecodeBase64(decoded, result["00020102"]["InlineBinary"].asString());
+        ASSERT_EQ("MyPrivateInformation", decoded);
+
+        break;
+      }
+
+      default:
+        break;
+    }
+  }
+}
+
+
 #if ORTHANC_SANDBOXED != 1
 
 #include "../Sources/SystemToolbox.h"
--- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -2015,6 +2015,12 @@
 {
   // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.3.html
 
+#if DCMTK_VERSION_NUMBER >= 365
+  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7056, 0x1000), ValueRepresentation_OtherVeryLong, "Tag1", 1, 1, "");
+  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7056, 0x1001), ValueRepresentation_SignedVeryLong, "Tag2", 1, 1, "");
+  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7056, 0x1002), ValueRepresentation_UnsignedVeryLong, "Tag3", 1, 1, "");
+#endif
+
   ParsedDicomFile dicom(false);
   dicom.ReplacePlainString(DicomTag(0x0040, 0x0241), "AE");
   dicom.ReplacePlainString(DicomTag(0x0010, 0x1010), "AS");
@@ -2033,7 +2039,7 @@
   dicom.ReplacePlainString(DicomTag(0x0064, 0x0009), "2.71828");  // OF (other float)
   dicom.ReplacePlainString(DicomTag(0x0066, 0x0040), "46");  // OL (other long)
   ASSERT_THROW(dicom.ReplacePlainString(DicomTag(0x0028, 0x1201), "O"), OrthancException);
-  dicom.ReplacePlainString(DicomTag(0x0028, 0x1201), "OWOW");
+  dicom.ReplacePlainString(DicomTag(0x0028, 0x1201), "57");  // OW
   dicom.ReplacePlainString(DicomTag(0x0010, 0x0010), "PN");
   dicom.ReplacePlainString(DicomTag(0x0008, 0x0050), "SH");
   dicom.ReplacePlainString(DicomTag(0x0018, 0x6020), "-15");  // SL
@@ -2048,6 +2054,12 @@
   dicom.ReplacePlainString(DicomTag(0x0008, 0x0301), "17");   // US
   dicom.ReplacePlainString(DicomTag(0x0040, 0x0031), "UT");  
 
+#if DCMTK_VERSION_NUMBER >= 365
+  dicom.ReplacePlainString(DicomTag(0x7056, 0x1000), "17");  // OV
+  dicom.ReplacePlainString(DicomTag(0x7056, 0x1001), "-18");  // SV
+  dicom.ReplacePlainString(DicomTag(0x7056, 0x1002), "19");  // UV
+#endif
+
   DicomWebJsonVisitor visitor;
   dicom.Apply(visitor);
 
@@ -2113,8 +2125,7 @@
 #endif
 
   ASSERT_EQ("OW", visitor.GetResult() ["00281201"]["vr"].asString());
-  Toolbox::DecodeBase64(s, visitor.GetResult() ["00281201"]["InlineBinary"].asString());
-  ASSERT_EQ("OWOW", s);
+  ASSERT_EQ(57, visitor.GetResult() ["00281201"]["Value"][0].asInt());
 
   ASSERT_EQ("PN", visitor.GetResult() ["00100010"]["vr"].asString());
   ASSERT_EQ("PN", visitor.GetResult() ["00100010"]["Value"][0]["Alphabetic"].asString());
@@ -2174,13 +2185,27 @@
   ASSERT_EQ("UT", visitor.GetResult() ["00400031"]["vr"].asString());
   ASSERT_EQ("UT", visitor.GetResult() ["00400031"]["Value"][0].asString());
 
+#if DCMTK_VERSION_NUMBER >= 365
+  ASSERT_EQ("OV", visitor.GetResult() ["70561000"]["vr"].asString());
+  ASSERT_EQ(17, visitor.GetResult() ["70561000"]["Value"][0].asInt());
+  ASSERT_EQ("SV", visitor.GetResult() ["70561001"]["vr"].asString());
+  ASSERT_EQ(-18, visitor.GetResult() ["70561001"]["Value"][0].asInt());
+  ASSERT_EQ("UV", visitor.GetResult() ["70561002"]["vr"].asString());
+  ASSERT_EQ(19, visitor.GetResult() ["70561002"]["Value"][0].asInt());
+#endif
+
   std::string xml;
   visitor.FormatXml(xml);
   
   {
     DicomMap m;
     m.FromDicomWeb(visitor.GetResult());
+
+#if DCMTK_VERSION_NUMBER >= 365
+    ASSERT_EQ(34u, m.GetSize());
+#else
     ASSERT_EQ(31u, m.GetSize());
+#endif
     
     ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0002, 0x0002), false));  ASSERT_EQ("UI", s);
     ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0040, 0x0241), false));  ASSERT_EQ("AE", s);
@@ -2208,7 +2233,7 @@
 
     ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0064, 0x0009), true));
     ASSERT_FLOAT_EQ(2.71828f, boost::lexical_cast<float>(s));
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0028, 0x1201), true));   ASSERT_EQ("OWOW", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0028, 0x1201), false));  ASSERT_EQ("57", s);
     ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0010, 0x0010), false));  ASSERT_EQ("PN", s);
     ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0050), false));  ASSERT_EQ("SH", s);
     ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0018, 0x6020), false));  ASSERT_EQ("-15", s);
@@ -2231,6 +2256,12 @@
     ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0120), true));  ASSERT_EQ("UR", s);
     ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0301), true));  ASSERT_EQ("17", s);  // US (but tag unknown to DCMTK 3.6.0)
 #endif    
+
+#if DCMTK_VERSION_NUMBER >= 365
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x7056, 0x1000), true));  ASSERT_EQ("17", s);  // OV
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x7056, 0x1001), true));  ASSERT_EQ("-18", s);  // SV
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x7056, 0x1002), true));  ASSERT_EQ("19", s);  // UV
+#endif
   }
 }
 
--- a/OrthancFramework/UnitTestsSources/LoggingTests.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancFramework/UnitTestsSources/LoggingTests.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -132,11 +132,24 @@
 
 
 #if ORTHANC_ENABLE_LOGGING_STDIO == 0
+namespace
+{
+  class BasicTestRestore : public boost::noncopyable
+  {
+  public:
+    ~BasicTestRestore()
+    {
+      Orthanc::Logging::EnableTraceLevel(false);  // Back to normal
+    }
+  };
+}
+
 TEST(FuncStreamBuf, BasicTest)
 {
   LoggingMementoScope loggingConfiguration;
 
   Orthanc::Logging::EnableTraceLevel(true);
+  BasicTestRestore restore;
 
   typedef void(*LoggingFunctionFunc)(const char*);
 
@@ -199,8 +212,6 @@
     ASSERT_TRUE(ok);
     ASSERT_STREQ(payload.c_str(), text);
   }
-
-  Orthanc::Logging::EnableTraceLevel(false);  // Back to normal
 }
 #endif
 
--- a/OrthancFramework/UnitTestsSources/ToolboxTests.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancFramework/UnitTestsSources/ToolboxTests.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -492,3 +492,33 @@
   ASSERT_EQ(static_cast<int32_t>(16 * 1024 * 1024) * KILOBYTE, 16ull * 1024ull * 1024ull * 1024ull);
   ASSERT_EQ(KILOBYTE * static_cast<int32_t>(16 * 1024 * 1024), 16ull * 1024ull * 1024ull * 1024ull);
 }
+
+
+TEST(Toolbox, SwapEndianness)
+{
+  const std::string s = "abcdefgh";
+
+  {
+    std::string t = s;
+    Toolbox::SwapEndianness(t, 1);
+    ASSERT_EQ("abcdefgh", t);
+  }
+
+  {
+    std::string t = s;
+    Toolbox::SwapEndianness(t, 2);
+    ASSERT_EQ("badcfehg", t);
+  }
+
+  {
+    std::string t = s;
+    Toolbox::SwapEndianness(t, 4);
+    ASSERT_EQ("dcbahgfe", t);
+  }
+
+  {
+    std::string t = s;
+    Toolbox::SwapEndianness(t, 8);
+    ASSERT_EQ("hgfedcba", t);
+  }
+}
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -1128,6 +1128,10 @@
             that.currentMode_ = DicomWebJsonVisitor::BinaryMode_BulkDataUri;
             break;
 
+          case OrthancPluginDicomWebBinaryMode_ArrayOfValues:
+            that.currentMode_ = DicomWebJsonVisitor::BinaryMode_ArrayOfValues;
+            break;
+
           default:
             throw OrthancException(ErrorCode_ParameterOutOfRange);
         }
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Wed Jul 15 11:42:31 2026 +0200
@@ -126,7 +126,7 @@
 
 #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     1
 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     12
-#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  10
+#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  12
 
 
 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
@@ -1070,13 +1070,15 @@
 
   /**
    * The available modes to export a binary DICOM tag into a DICOMweb
-   * JSON or XML document.
+   * JSON or XML document. Binary DICOM tags correspond to the tags
+   * whose value representation is "other" (OB, OD, OF, OL, OV, or OW).
    **/
   typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.5.4")
   {
     OrthancPluginDicomWebBinaryMode_Ignore = 0,        /*!< Don't include binary tags */
     OrthancPluginDicomWebBinaryMode_InlineBinary = 1,  /*!< Inline encoding using Base64 */
-    OrthancPluginDicomWebBinaryMode_BulkDataUri = 2    /*!< Use a bulk data URI field */
+    OrthancPluginDicomWebBinaryMode_BulkDataUri = 2,   /*!< Use a bulk data URI field */
+    OrthancPluginDicomWebBinaryMode_ArrayOfValues ORTHANC_PLUGIN_SINCE_SDK("1.12.12") = 3  /*!< Encode the values as an array (not available for OB, new in Orthanc 1.12.12) */
   } OrthancPluginDicomWebBinaryMode;
 
 
--- a/OrthancServer/Resources/Configuration.json	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancServer/Resources/Configuration.json	Wed Jul 15 11:42:31 2026 +0200
@@ -10,7 +10,7 @@
 
   // The logical name of this instance of Orthanc. This one is
   // displayed in Orthanc Explorer and at the URI "/system".
-  "Name" : "MyOrthanc",
+  "Name" : "ORTHANC",
 
   // Path to the directory that holds the heavyweight files (i.e. the
   // raw DICOM instances). Backslashes must be either escaped by
@@ -644,7 +644,8 @@
   "HttpVerbose" : false,
 
   // Set the timeout for HTTP requests issued by Orthanc (in seconds).
-  "HttpTimeout" : 60,
+  // 0 means no timeout.
+  "HttpTimeout" : 0,
 
   // Enable the verification of the peers certificates during HTTPS
   // requests. Setting this option to "false" is equivalent to the
--- a/OrthancServer/Sources/LuaScripting.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancServer/Sources/LuaScripting.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -1311,7 +1311,7 @@
 
     std::list<std::string> luaScripts;
     configLock.GetConfiguration().GetListOfStringsParameter(luaScripts, "LuaScripts");
-    heartBeatPeriod_ = configLock.GetConfiguration().GetIntegerParameter("LuaHeartBeatPeriod", 0);
+    heartBeatPeriod_ = configLock.GetConfiguration().GetUnsignedIntegerParameter("LuaHeartBeatPeriod");
 
     LuaScripting::Lock lock(*this);
 
--- a/OrthancServer/Sources/OrthancConfiguration.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancServer/Sources/OrthancConfiguration.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -50,6 +50,7 @@
 static const char* const CONFIG_LOADER_THREADS = "LoaderThreads";
 static const char* const CONFIG_ZIP_LOADER_THREADS = "ZipLoaderThreads"; // for backward compatibility only
 
+static Json::Value defaultConfiguration;
 
 namespace Orthanc
 {
@@ -134,6 +135,22 @@
   static void ReadConfiguration(Json::Value& target, 
                                 const boost::filesystem::path &configurationFile)
   {
+    // lazy loading of the default configuration
+    if (defaultConfiguration.empty())
+    {
+      std::string defaultConfigurationContent;
+      GetFileResource(defaultConfigurationContent, ServerResources::CONFIGURATION_SAMPLE);
+
+      Json::Value tmp;
+      if (!Toolbox::ReadJson(tmp, defaultConfigurationContent) ||
+          tmp.type() != Json::objectValue)
+      {
+        throw OrthancException(ErrorCode_InternalError, "The default configuration file does not follow the JSON syntax !!");
+      }
+
+      Toolbox::CopyJsonWithoutComments(defaultConfiguration, tmp);
+    }
+
     target = Json::objectValue;
 
     if (!configurationFile.empty())
@@ -242,7 +259,7 @@
 
   void OrthancConfiguration::LoadModalities()
   {
-    if (GetBooleanParameter(DICOM_MODALITIES_IN_DB, false))
+    if (GetBooleanParameter(DICOM_MODALITIES_IN_DB))
     {
       // Modalities are stored in the database
       if (serverIndex_ == NULL)
@@ -330,7 +347,7 @@
 
   void OrthancConfiguration::LoadPeers()
   {
-    if (GetBooleanParameter(ORTHANC_PEERS_IN_DB, false))
+    if (GetBooleanParameter(ORTHANC_PEERS_IN_DB))
     {
       // Peers are stored in the database
       if (serverIndex_ == NULL)
@@ -400,7 +417,7 @@
     
   void OrthancConfiguration::SaveModalities()
   {
-    if (GetBooleanParameter(DICOM_MODALITIES_IN_DB, false))
+    if (GetBooleanParameter(DICOM_MODALITIES_IN_DB))
     {
       // Modalities are stored in the database
       if (serverIndex_ == NULL)
@@ -432,7 +449,7 @@
 
   void OrthancConfiguration::SavePeers()
   {
-    if (GetBooleanParameter(ORTHANC_PEERS_IN_DB, false))
+    if (GetBooleanParameter(ORTHANC_PEERS_IN_DB))
     {
       // Peers are stored in the database
       if (serverIndex_ == NULL)
@@ -492,17 +509,20 @@
   }
 
 
-  std::string OrthancConfiguration::GetStringParameter(const std::string& parameter,
-                                                       const std::string& defaultValue) const
+  std::string OrthancConfiguration::GetStringParameter(const std::string& parameter) const
   {
     std::string value;
     if (LookupStringParameter(value, parameter))
     {
       return value;
     }
+    else if (defaultConfiguration.isMember(parameter) && defaultConfiguration[parameter].type() == Json::stringValue)
+    {
+      return defaultConfiguration[parameter].asString();
+    }
     else
     {
-      return defaultValue;
+      throw OrthancException(ErrorCode_InternalError, std::string("No or invalid default parameter found in the default configuration for '") + parameter + "'");
     }
   }
 
@@ -530,20 +550,6 @@
   }
 
 
-  int OrthancConfiguration::GetIntegerParameter(const std::string& parameter,
-                                                int defaultValue) const
-  {
-    int v;
-    if (LookupIntegerParameter(v, parameter))
-    {
-      return v;
-    }
-    else
-    {
-      return defaultValue;
-    }
-  }
-
     
   bool OrthancConfiguration::LookupUnsignedIntegerParameter(unsigned int& target,
                                                             const std::string& parameter) const
@@ -569,17 +575,20 @@
   }
 
 
-  unsigned int OrthancConfiguration::GetUnsignedIntegerParameter(const std::string& parameter,
-                                                                 unsigned int defaultValue) const
+  unsigned int OrthancConfiguration::GetUnsignedIntegerParameter(const std::string& parameter) const
   {
     unsigned int v;
     if (LookupUnsignedIntegerParameter(v, parameter))
     {
       return v;
     }
+    else if (defaultConfiguration.isMember(parameter) && defaultConfiguration[parameter].type() == Json::intValue)
+    {
+      return static_cast<unsigned int>(defaultConfiguration[parameter].asInt());
+    }
     else
     {
-      return defaultValue;
+      throw OrthancException(ErrorCode_InternalError, std::string("No or invalid default parameter found in the default configuration for '") + parameter + "'");
     }
   }
 
@@ -608,17 +617,20 @@
   }
 
 
-  bool OrthancConfiguration::GetBooleanParameter(const std::string& parameter,
-                                                 bool defaultValue) const
+  bool OrthancConfiguration::GetBooleanParameter(const std::string& parameter) const
   {
     bool value;
     if (LookupBooleanParameter(value, parameter))
     {
       return value;
     }
+    else if (defaultConfiguration.isMember(parameter) && defaultConfiguration[parameter].type() == Json::booleanValue)
+    {
+      return defaultConfiguration[parameter].asBool();
+    }
     else
     {
-      return defaultValue;
+      throw OrthancException(ErrorCode_InternalError, std::string("No or invalid default parameter found in the default configuration for '") + parameter + "'");
     }
   }
 
@@ -743,7 +755,7 @@
 
   unsigned int OrthancConfiguration::GetDicomLossyTranscodingQuality() const
   {
-    return GetUnsignedIntegerParameter(DICOM_LOSSY_TRANSCODING_QUALITY, 90);
+    return GetUnsignedIntegerParameter(DICOM_LOSSY_TRANSCODING_QUALITY);
   }
 
 
@@ -848,7 +860,7 @@
   bool OrthancConfiguration::IsSameAETitle(const std::string& aet1,
                                            const std::string& aet2) const
   {
-    if (GetBooleanParameter("StrictAetComparison", false))
+    if (GetBooleanParameter("StrictAetComparison"))
     {
       // Case-sensitive matching
       return aet1 == aet2;
@@ -907,7 +919,7 @@
                    << "\" is not listed in the \"DicomModalities\" configuration option";
       return false;
     }
-    else if (!GetBooleanParameter("DicomCheckModalityHost", false) ||
+    else if (!GetBooleanParameter("DicomCheckModalityHost") ||
              ip == modality.GetHost())
     {
       return true;
@@ -1045,9 +1057,11 @@
   
   TemporaryFile* OrthancConfiguration::CreateTemporaryFile() const
   {
-    if (json_.isMember(TEMPORARY_DIRECTORY))
+    std::string temporaryDirectory = ".";
+
+    if (LookupStringParameter(temporaryDirectory, TEMPORARY_DIRECTORY))
     {
-      return new TemporaryFile(InterpretStringParameterAsPath(GetStringParameter(TEMPORARY_DIRECTORY, ".")), "");
+      return new TemporaryFile(InterpretStringParameterAsPath(temporaryDirectory), "");
     }
     else
     {
@@ -1059,7 +1073,7 @@
   std::string OrthancConfiguration::GetDefaultPrivateCreator() const
   {
     // New configuration option in Orthanc 1.6.0
-    return GetStringParameter("DefaultPrivateCreator", "");
+    return GetStringParameter("DefaultPrivateCreator");
   }
 
 
@@ -1354,7 +1368,12 @@
   unsigned int OrthancConfiguration::GetLoaderThreads() const
   {
     // from 1.10.0 to 1.12.10, only CONFIG_ZIP_LOADER_THREADS was available -> read from it if CONFIG_LOADER_THREADS is not specified.
-    unsigned int loaderThreads = GetUnsignedIntegerParameter(CONFIG_LOADER_THREADS, GetUnsignedIntegerParameter(CONFIG_ZIP_LOADER_THREADS, 1));
+    unsigned int loaderThreads = 1; // old ZipLoaderThreads default value
+
+    if (!LookupUnsignedIntegerParameter(loaderThreads, CONFIG_LOADER_THREADS))
+    {
+      LookupUnsignedIntegerParameter(loaderThreads, CONFIG_ZIP_LOADER_THREADS); // we cannot use GetUnsignedIntegerParameter() because there is no default value for this old configuration
+    }
 
     if (loaderThreads <= 1)
     {
@@ -1368,16 +1387,16 @@
 
   unsigned int OrthancConfiguration::GetConcurrentJobs() const
   {
-    return GetUnsignedIntegerParameter(ORTHANC_CONFIG_CONCURRENT_JOBS, 2);
+    return GetUnsignedIntegerParameter(ORTHANC_CONFIG_CONCURRENT_JOBS);
   }
 
   unsigned int OrthancConfiguration::GetHttpThreadsCount() const
   {
-    return GetUnsignedIntegerParameter(ORTHANC_CONFIG_HTTP_THREADS_COUNT, 50);
+    return GetUnsignedIntegerParameter(ORTHANC_CONFIG_HTTP_THREADS_COUNT);
   }
 
   unsigned int OrthancConfiguration::GetDicomThreadsCount() const
   {
-    return GetUnsignedIntegerParameter(ORTHANC_CONFIG_DICOM_THREADS_COUNT, 4);
+    return GetUnsignedIntegerParameter(ORTHANC_CONFIG_DICOM_THREADS_COUNT);
   }
 }
--- a/OrthancServer/Sources/OrthancConfiguration.h	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancServer/Sources/OrthancConfiguration.h	Wed Jul 15 11:42:31 2026 +0200
@@ -56,6 +56,7 @@
 #define ORTHANC_CONFIG_CONCURRENT_JOBS "ConcurrentJobs"
 #define ORTHANC_CONFIG_HTTP_THREADS_COUNT "HttpThreadsCount"
 #define ORTHANC_CONFIG_DICOM_THREADS_COUNT "DicomThreadsCount"
+#define ORTHANC_CONFIG_STORAGE_DIRECTORY "StorageDirectory"
 
 #define ORTHANC_CONFIG_STORAGE_LOADER_THREADS "StorageLoaderThreads"
 #define ORTHANC_CONFIG_STORAGE_MEMORY_CAPACITY "StorageMemoryCapacity"
@@ -71,6 +72,7 @@
 #define ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_WINDOW_CAPACITY "SequentialDicomReaderWindowCapacity"
 
 
+
 namespace Orthanc
 {
   class DicomMap;
@@ -213,26 +215,20 @@
     bool LookupStringParameter(std::string& target,
                                const std::string& parameter) const;
 
-    std::string GetStringParameter(const std::string& parameter,
-                                   const std::string& defaultValue) const;
+    std::string GetStringParameter(const std::string& parameter) const;
     
     bool LookupIntegerParameter(int& target,
                                 const std::string& parameter) const;
 
-    int GetIntegerParameter(const std::string& parameter,
-                            int defaultValue) const;
-
     bool LookupUnsignedIntegerParameter(unsigned int& target,
                                         const std::string& parameter) const;
 
-    unsigned int GetUnsignedIntegerParameter(const std::string& parameter,
-                                             unsigned int defaultValue) const;
+    unsigned int GetUnsignedIntegerParameter(const std::string& parameter) const;
 
     bool LookupBooleanParameter(bool& target,
                                 const std::string& parameter) const;
 
-    bool GetBooleanParameter(const std::string& parameter,
-                             bool defaultValue) const;
+    bool GetBooleanParameter(const std::string& parameter) const;
 
     void GetDicomModalityUsingSymbolicName(RemoteModalityParameters& modality,
                                            const std::string& name) const;
@@ -315,71 +311,66 @@
 
     std::string GetOrthancAET() const
     {
-      return GetStringParameter(ORTHANC_CONFIG_DICOM_AET, "ORTHANC");
+      return GetStringParameter(ORTHANC_CONFIG_DICOM_AET);
     }
 
     std::string GetOrthancName() const
     {
-      return GetStringParameter(ORTHANC_CONFIG_NAME, "ORTHANC");
-    }
-
-    std::string GetIngestTranscoding() const
-    {
-      return GetStringParameter(ORTHANC_CONFIG_INGEST_TRANSCODING, "");
+      return GetStringParameter(ORTHANC_CONFIG_NAME);
     }
 
     std::string GetMaximumStorageMode() const
     {
-      return GetStringParameter(ORTHANC_CONFIG_MAXIMUM_STORAGE_MODE, "Recycle");
+      return GetStringParameter(ORTHANC_CONFIG_MAXIMUM_STORAGE_MODE);
     }
 
     std::string GetDicomDefaultRetrieveMethod() const
     {
-      return GetStringParameter(ORTHANC_CONFIG_DICOM_DEFAULT_RETRIEVE_METHOD, "C-MOVE");
+      return GetStringParameter(ORTHANC_CONFIG_DICOM_DEFAULT_RETRIEVE_METHOD);
     }
     unsigned int GetMaximumStorageCacheSize() const
     {
-      return GetUnsignedIntegerParameter(ORTHANC_CONFIG_MAXIMUM_STORAGE_CACHE_SIZE, 128);
+      return GetUnsignedIntegerParameter(ORTHANC_CONFIG_MAXIMUM_STORAGE_CACHE_SIZE);
     }
 
     unsigned int GetMaximumStorageSize() const
     {
-      return GetUnsignedIntegerParameter(ORTHANC_CONFIG_MAXIMUM_STORAGE_SIZE, 0);
+      return GetUnsignedIntegerParameter(ORTHANC_CONFIG_MAXIMUM_STORAGE_SIZE);
     }
 
     unsigned int GetMaximumPatientCount() const
     {
-      return GetUnsignedIntegerParameter(ORTHANC_CONFIG_MAXIMUM_PATIENT_COUNT, 0);
+      return GetUnsignedIntegerParameter(ORTHANC_CONFIG_MAXIMUM_PATIENT_COUNT);
     }
     
     unsigned int GetDicomPort() const
     {
-      return GetUnsignedIntegerParameter(ORTHANC_CONFIG_DICOM_PORT, 4242);
+      return GetUnsignedIntegerParameter(ORTHANC_CONFIG_DICOM_PORT);
     }
 
     unsigned int GetHttpPort() const
     {
-      return GetUnsignedIntegerParameter(ORTHANC_CONFIG_HTTP_PORT, 8042);
+      return GetUnsignedIntegerParameter(ORTHANC_CONFIG_HTTP_PORT);
     }
 
     bool HasCheckRevisions() const
     {
-      return GetBooleanParameter(ORTHANC_CONFIG_CHECK_REVISIONS, false);
+      return GetBooleanParameter(ORTHANC_CONFIG_CHECK_REVISIONS);
     }
 
     bool HasStoreMD5ForAttachments() const
     {
-      return GetBooleanParameter(ORTHANC_CONFIG_STORE_MD5_FOR_ATTACHMENTS, true);
+      return GetBooleanParameter(ORTHANC_CONFIG_STORE_MD5_FOR_ATTACHMENTS);
     }
 
     bool HasStorageCompression() const
     {
-      return GetBooleanParameter(ORTHANC_CONFIG_STORAGE_COMPRESSION, false);
+      return GetBooleanParameter(ORTHANC_CONFIG_STORAGE_COMPRESSION);
     }
 
     bool HasPatientLevelEnabled() const
     {
-      return GetBooleanParameter(ORTHANC_CONFIG_PATIENT_LEVEL_ENABLED, true);
+      return GetBooleanParameter(ORTHANC_CONFIG_PATIENT_LEVEL_ENABLED);
     }
 
     static void DefaultExtractDicomSummary(DicomMap& target,
--- a/OrthancServer/Sources/OrthancFindRequestHandler.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancServer/Sources/OrthancFindRequestHandler.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -280,12 +280,12 @@
 
     {
       OrthancConfiguration::ReaderLock lock;
-      caseSensitivePN = lock.GetConfiguration().GetBooleanParameter("CaseSensitivePN", false);
+      caseSensitivePN = lock.GetConfiguration().GetBooleanParameter("CaseSensitivePN");
 
       RemoteModalityParameters remote;
       if (!lock.GetConfiguration().LookupDicomModalityUsingAETitle(remote, connection.GetRemoteAet()))
       {
-        if (lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowFind", false))
+        if (lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowFind"))
         {
           CLOG(INFO, DICOM) << "C-FIND: Allowing SCU request from unknown modality with AET: " << connection.GetRemoteAet();
         }
--- a/OrthancServer/Sources/OrthancGetRequestHandler.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancServer/Sources/OrthancGetRequestHandler.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -593,7 +593,7 @@
         allowTranscoding_ = (context_.IsTranscodeDicomProtocol() &&
                              remote.IsTranscodingAllowed());
       }
-      else if (lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowGet", false))
+      else if (lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowGet"))
       {
         CLOG(INFO, DICOM) << "C-GET: Allowing SCU request from unknown modality with AET: " << originatorAet;
         allowTranscoding_ = context_.IsTranscodeDicomProtocol();
--- a/OrthancServer/Sources/OrthancInitialization.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancServer/Sources/OrthancInitialization.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -72,10 +72,6 @@
 
 
 
-static const char* const STORAGE_DIRECTORY = "StorageDirectory";
-static const char* const ORTHANC_STORAGE = "OrthancStorage";
-
-
 namespace Orthanc
 {
   static void RegisterUserMetadata(const Json::Value& config)
@@ -356,27 +352,16 @@
 
     {
       std::string locale;
-      
-      if (lock.GetJson().isMember(LOCALE))
-      {
-        locale = lock.GetConfiguration().GetStringParameter(LOCALE, "");
-      }
-      
-      bool loadPrivate = lock.GetConfiguration().GetBooleanParameter(LOAD_PRIVATE_DICTIONARY, true);
+      lock.GetConfiguration().LookupStringParameter(locale, LOCALE);
+
+      bool loadPrivate = lock.GetConfiguration().GetBooleanParameter(LOAD_PRIVATE_DICTIONARY);
       Orthanc::InitializeFramework(locale, loadPrivate);
     }
 
     // The Orthanc framework is now initialized
 
-    if (lock.GetJson().isMember(DEFAULT_ENCODING))
-    {
-      std::string encoding = lock.GetConfiguration().GetStringParameter(DEFAULT_ENCODING, "");
-      SetDefaultDicomEncoding(StringToEncoding(encoding.c_str()));
-    }
-    else
-    {
-      SetDefaultDicomEncoding(ORTHANC_DEFAULT_DICOM_ENCODING);
-    }
+    std::string encoding = lock.GetConfiguration().GetStringParameter(DEFAULT_ENCODING);
+    SetDefaultDicomEncoding(StringToEncoding(encoding.c_str()));
 
     if (lock.GetJson().isMember(PKCS11))
     {
@@ -399,7 +384,7 @@
 #if HAVE_MALLOPT == 1
     // New in Orthanc 1.8.2
     // https://orthanc.uclouvain.be/book/faq/scalability.html#controlling-memory-usage
-    unsigned int maxArena = lock.GetConfiguration().GetUnsignedIntegerParameter(MALLOC_ARENA_MAX, 5);
+    unsigned int maxArena = lock.GetConfiguration().GetUnsignedIntegerParameter(MALLOC_ARENA_MAX);
     if (maxArena != 0)
     {
       // https://man7.org/linux/man-pages/man3/mallopt.3.html
@@ -437,11 +422,17 @@
     OrthancConfiguration::ReaderLock lock;
 
     std::string storageDirectoryStr = 
-      lock.GetConfiguration().GetStringParameter(STORAGE_DIRECTORY, ORTHANC_STORAGE);
+      lock.GetConfiguration().GetStringParameter(ORTHANC_CONFIG_STORAGE_DIRECTORY);
+
+    std::string indexDirectoryStr;
+    if (!lock.GetConfiguration().LookupStringParameter(indexDirectoryStr, "IndexDirectory"))
+    {
+      indexDirectoryStr = storageDirectoryStr;
+    }
 
     // Open the database
     boost::filesystem::path indexDirectory = lock.GetConfiguration().InterpretStringParameterAsPath(
-      lock.GetConfiguration().GetStringParameter("IndexDirectory", storageDirectoryStr));
+      indexDirectoryStr);
 
     LOG(WARNING) << "SQLite index directory: " << SystemToolbox::PathToUtf8(indexDirectory);
 
@@ -525,7 +516,7 @@
     OrthancConfiguration::ReaderLock lock;
 
     std::string storageDirectoryStr = 
-      lock.GetConfiguration().GetStringParameter(STORAGE_DIRECTORY, ORTHANC_STORAGE);
+      lock.GetConfiguration().GetStringParameter(ORTHANC_CONFIG_STORAGE_DIRECTORY);
 
     boost::filesystem::path storageDirectory = 
       lock.GetConfiguration().InterpretStringParameterAsPath(storageDirectoryStr);
@@ -533,9 +524,9 @@
     LOG(WARNING) << "Storage directory: " << SystemToolbox::PathToUtf8(storageDirectory);
 
     // New in Orthanc 1.7.4
-    bool fsyncOnWrite = lock.GetConfiguration().GetBooleanParameter(SYNC_STORAGE_AREA, true);
+    bool fsyncOnWrite = lock.GetConfiguration().GetBooleanParameter(SYNC_STORAGE_AREA);
 
-    if (lock.GetConfiguration().GetBooleanParameter(STORE_DICOM, true))
+    if (lock.GetConfiguration().GetBooleanParameter(STORE_DICOM))
     {
       return new PluginStorageAreaAdapter(new FilesystemStorage(storageDirectory, fsyncOnWrite));
     }
--- a/OrthancServer/Sources/OrthancMoveRequestHandler.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancServer/Sources/OrthancMoveRequestHandler.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -362,7 +362,7 @@
 
     {
       OrthancConfiguration::ReaderLock lock;
-      synchronous = lock.GetConfiguration().GetBooleanParameter("SynchronousCMove", true);
+      synchronous = lock.GetConfiguration().GetBooleanParameter("SynchronousCMove");
     }
 
     if (synchronous)
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -495,7 +495,7 @@
       
       {
         OrthancConfiguration::ReaderLock lock;
-        streaming = lock.GetConfiguration().GetBooleanParameter("SynchronousZipStream", true);  // New in Orthanc 1.9.4
+        streaming = lock.GetConfiguration().GetBooleanParameter("SynchronousZipStream");  // New in Orthanc 1.9.4
       }
 
       if (streaming)
@@ -624,7 +624,7 @@
 
       {
         OrthancConfiguration::ReaderLock lock;
-        defaultUtf8 = lock.GetConfiguration().GetBooleanParameter(CONFIG_ALLOW_UTF8, false);  // New in Orthanc 1.12.11
+        defaultUtf8 = lock.GetConfiguration().GetBooleanParameter(CONFIG_ALLOW_UTF8);  // New in Orthanc 1.12.11
       }
 
       bool synchronous, extended, transcode;
@@ -826,7 +826,7 @@
 
       {
         OrthancConfiguration::ReaderLock lock;
-        defaultUtf8 = lock.GetConfiguration().GetBooleanParameter(CONFIG_ALLOW_UTF8, false);  // New in Orthanc 1.12.11
+        defaultUtf8 = lock.GetConfiguration().GetBooleanParameter(CONFIG_ALLOW_UTF8);  // New in Orthanc 1.12.11
       }
 
       bool synchronous, extended, transcode;
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -167,7 +167,7 @@
     else
     {
       OrthancConfiguration::ReaderLock lock;
-      checkFind = lock.GetConfiguration().GetBooleanParameter("DicomEchoChecksFind", false);
+      checkFind = lock.GetConfiguration().GetBooleanParameter("DicomEchoChecksFind");
     }
 
     ScuOperationFlags operations = ScuOperationFlags_Echo;
@@ -1429,7 +1429,7 @@
 
     {
       OrthancConfiguration::ReaderLock lock;
-      logExportedResources = lock.GetConfiguration().GetBooleanParameter("LogExportedResources", false);
+      logExportedResources = lock.GetConfiguration().GetBooleanParameter("LogExportedResources");
     }
 
     for (Json::Value::ArrayIndex i = 0; i < resources->size(); i++)
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -2692,7 +2692,7 @@
     {
       OrthancConfiguration::ReaderLock lock;
 
-      if (lock.GetConfiguration().GetBooleanParameter("StoreDicom", true) &&
+      if (lock.GetConfiguration().GetBooleanParameter("StoreDicom") &&
           contentType == FileContentType_DicomAsJson)
       {
         allowed = true;
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -171,7 +171,6 @@
       result[ORTHANC_CONFIG_CHECK_REVISIONS] = lock.GetConfiguration().HasCheckRevisions();  // New in Orthanc 1.9.2
       result[ORTHANC_CONFIG_STORE_MD5_FOR_ATTACHMENTS] = lock.GetConfiguration().HasStoreMD5ForAttachments(); // New in Orthanc 1.12.12
       result[ORTHANC_CONFIG_STORAGE_COMPRESSION] = lock.GetConfiguration().HasStorageCompression(); // New in Orthanc 1.11.0
-      result[ORTHANC_CONFIG_INGEST_TRANSCODING] = lock.GetConfiguration().GetIngestTranscoding(); // New in Orthanc 1.11.0
       result[ORTHANC_CONFIG_DATABASE_SERVER_IDENTIFIER] = lock.GetConfiguration().GetDatabaseServerIdentifier();
       result[ORTHANC_CONFIG_MAXIMUM_STORAGE_SIZE] = lock.GetConfiguration().GetMaximumStorageSize(); // New in Orthanc 1.11.3
       result[ORTHANC_CONFIG_MAXIMUM_PATIENT_COUNT] = lock.GetConfiguration().GetMaximumPatientCount(); // New in Orthanc 1.12.4
@@ -202,6 +201,16 @@
       result[ORTHANC_CONFIG_DICOM_PARSER_SOURCE_THREADS] = dicomParserThreadsCount;
     }
 
+    DicomTransferSyntax ingestTransferSyntax;
+    if (context.LookupIngestTranscoding(ingestTransferSyntax))
+    {
+      result[ORTHANC_CONFIG_INGEST_TRANSCODING] = GetTransferSyntaxUid(ingestTransferSyntax); // New in Orthanc 1.11.0
+    }
+    else
+    {
+      result[ORTHANC_CONFIG_INGEST_TRANSCODING] = "";
+    }
+
     result[ORTHANC_CONFIG_OVERWRITE_INSTANCES] = context.IsOverwriteInstances(); // New in Orthanc 1.11.0
     result[OVERWRITE_INSTANCES_MODE] = EnumerationToString(context.GetOverwriteInstances()); // New in Orthanc 1.12.12
     result[ORTHANC_CONFIG_PATIENT_LEVEL_ENABLED] = context.IsPatientLevelEnabled(); // New in Orthanc 1.12.11
--- a/OrthancServer/Sources/Search/HierarchicalMatcher.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancServer/Sources/Search/HierarchicalMatcher.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -40,7 +40,7 @@
 
     {
       OrthancConfiguration::ReaderLock lock;
-      caseSensitivePN = lock.GetConfiguration().GetBooleanParameter("CaseSensitivePN", false);
+      caseSensitivePN = lock.GetConfiguration().GetBooleanParameter("CaseSensitivePN");
     }
 
     bool hasCodeExtensions;
--- a/OrthancServer/Sources/ServerContext.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -500,12 +500,11 @@
 
   static void GetMemoryConfiguration(unsigned int& resultMb, 
                                      OrthancConfiguration::ReaderLock& lock,
-                                     const char* configurationName,
-                                     unsigned int defaultValueMb)
+                                     const char* configurationName)
   {
     if (!lock.GetConfiguration().LookupUnsignedIntegerParameter(resultMb, configurationName))
     {
-      resultMb = defaultValueMb;
+      resultMb = lock.GetConfiguration().GetUnsignedIntegerParameter(configurationName);
       LOG(WARNING) << "====> '" << configurationName << "' is not defined in your configuration, setting it to " << resultMb << ".  Depending on the available memory on the system, you might want to adapt this value.";
     }
     else
@@ -558,31 +557,31 @@
         OrthancConfiguration::ReaderLock lock;
 
         queryRetrieveArchive_.reset(
-          new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("QueryRetrieveSize", 100)));
+          new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("QueryRetrieveSize")));
         mediaArchive_.reset(
-          new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("MediaArchiveSize", 1)));
+          new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("MediaArchiveSize")));
         defaultLocalAet_ = lock.GetConfiguration().GetOrthancAET();
         jobsEngine_.SetWorkersCount(lock.GetConfiguration().GetConcurrentJobs());
 
-        saveJobs_ = lock.GetConfiguration().GetBooleanParameter("SaveJobs", true);
+        saveJobs_ = lock.GetConfiguration().GetBooleanParameter("SaveJobs");
         if (readOnly_ && saveJobs_)
         {
           LOG(WARNING) << "READ-ONLY SYSTEM: SaveJobs = true is incompatible with a ReadOnly system, ignoring this configuration";
           saveJobs_ = false;
         }
 
-        metricsRegistry_->SetEnabled(lock.GetConfiguration().GetBooleanParameter("MetricsEnabled", true));
+        metricsRegistry_->SetEnabled(lock.GetConfiguration().GetBooleanParameter("MetricsEnabled"));
 
         // New configuration options in Orthanc 1.5.1
-        findStorageAccessMode_ = StringToFindStorageAccessMode(lock.GetConfiguration().GetStringParameter("StorageAccessOnFind", "Always"));
-        limitFindInstances_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances", 0);
-        limitFindResults_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults", 0);
+        findStorageAccessMode_ = StringToFindStorageAccessMode(lock.GetConfiguration().GetStringParameter("StorageAccessOnFind"));
+        limitFindInstances_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances");
+        limitFindResults_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults");
 
         // New configuration option in Orthanc 1.6.0
-        storageCommitmentReports_.reset(new StorageCommitmentReports(lock.GetConfiguration().GetUnsignedIntegerParameter("StorageCommitmentReportsSize", 100)));
+        storageCommitmentReports_.reset(new StorageCommitmentReports(lock.GetConfiguration().GetUnsignedIntegerParameter("StorageCommitmentReportsSize")));
 
         // New options in Orthanc 1.7.0
-        transcodeDicomProtocol_ = lock.GetConfiguration().GetBooleanParameter("TranscodeDicomProtocol", true);
+        transcodeDicomProtocol_ = lock.GetConfiguration().GetBooleanParameter("TranscodeDicomProtocol");
 
         std::string s;
         if (lock.GetConfiguration().LookupStringParameter(s, ORTHANC_CONFIG_INGEST_TRANSCODING))
@@ -594,8 +593,8 @@
                          << "transfer syntax: " << GetTransferSyntaxUid(ingestTransferSyntax_);
 
             // New options in Orthanc 1.8.2
-            ingestTranscodingOfUncompressed_ = lock.GetConfiguration().GetBooleanParameter("IngestTranscodingOfUncompressed", true);
-            ingestTranscodingOfCompressed_ = lock.GetConfiguration().GetBooleanParameter("IngestTranscodingOfCompressed", true);
+            ingestTranscodingOfUncompressed_ = lock.GetConfiguration().GetBooleanParameter("IngestTranscodingOfUncompressed");
+            ingestTranscodingOfCompressed_ = lock.GetConfiguration().GetBooleanParameter("IngestTranscodingOfCompressed");
 
             LOG(WARNING) << "  Ingest transcoding will "
                          << (ingestTranscodingOfUncompressed_ ? "be applied" : "*not* be applied")
@@ -618,13 +617,13 @@
         }
 
         // New options in Orthanc 1.8.2
-        if (lock.GetConfiguration().GetBooleanParameter("DeidentifyLogs", true))
+        if (lock.GetConfiguration().GetBooleanParameter("DeidentifyLogs"))
         {
           deidentifyLogs_ = true;
           CLOG(INFO, DICOM) << "Deidentification of log contents (notably for DIMSE queries) is enabled";
 
           DicomVersion version = StringToDicomVersion(
-              lock.GetConfiguration().GetStringParameter("DeidentifyLogsDicomVersion", "2023b"));
+              lock.GetConfiguration().GetStringParameter("DeidentifyLogsDicomVersion"));
           CLOG(INFO, DICOM) << "Version of DICOM standard used for deidentification is "
                             << EnumerationToString(version);
 
@@ -649,7 +648,7 @@
 
         lock.GetConfiguration().GetAcceptedTransferSyntaxes(acceptedTransferSyntaxes_);
 
-        isUnknownSopClassAccepted_ = lock.GetConfiguration().GetBooleanParameter("UnknownSopClassAccepted", false);
+        isUnknownSopClassAccepted_ = lock.GetConfiguration().GetBooleanParameter("UnknownSopClassAccepted");
 
         // New options in Orthanc 1.12.6
         std::list<std::string> acceptedSopClasses;
@@ -725,10 +724,10 @@
         }
 
         // ----> IF you update values here, you must also update them in Configuration.json <-----
-        transcoderThreads = lock.GetConfiguration().GetUnsignedIntegerParameter(ORTHANC_CONFIG_TRANSCODER_THREADS, 4);
+        transcoderThreads = lock.GetConfiguration().GetUnsignedIntegerParameter(ORTHANC_CONFIG_TRANSCODER_THREADS);
         LOG(WARNING) << "'" << ORTHANC_CONFIG_TRANSCODER_THREADS << "' is set to " << transcoderThreads;
 
-        dicomParserThreads = lock.GetConfiguration().GetUnsignedIntegerParameter(ORTHANC_CONFIG_DICOM_PARSER_SOURCE_THREADS, 2);
+        dicomParserThreads = lock.GetConfiguration().GetUnsignedIntegerParameter(ORTHANC_CONFIG_DICOM_PARSER_SOURCE_THREADS);
         LOG(WARNING) << "'" << ORTHANC_CONFIG_DICOM_PARSER_SOURCE_THREADS << "' is set to " << dicomParserThreads;
 
         if (!lock.GetConfiguration().LookupUnsignedIntegerParameter(sequentialReaderThreads, ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_THREADS))
@@ -742,14 +741,14 @@
         }
 
         // ----> IF you update values here, you must also update them in Configuration.json <-----
-        GetMemoryConfiguration(storageMemoryCapacityMb, lock, ORTHANC_CONFIG_STORAGE_MEMORY_CAPACITY, 512);
-        GetMemoryConfiguration(dicomParserMemoryCapacityMb, lock, ORTHANC_CONFIG_DICOM_PARSER_MEMORY_CAPACITY, 256);
-        GetMemoryConfiguration(dicomParserCacheSizeMb, lock, ORTHANC_CONFIG_DICOM_PARSER_CACHE_SIZE, 128);
-        GetMemoryConfiguration(transcoderMemoryCapacityMb, lock, ORTHANC_CONFIG_TRANSCODER_MEMORY_CAPACITY, 256);
-        GetMemoryConfiguration(transcoderCacheSizeMb, lock, ORTHANC_CONFIG_TRANSCODER_CACHE_SIZE, 128);
-        GetMemoryConfiguration(sequentialReaderWindowCapacityMb, lock, ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_WINDOW_CAPACITY, 64);
+        GetMemoryConfiguration(storageMemoryCapacityMb, lock, ORTHANC_CONFIG_STORAGE_MEMORY_CAPACITY);
+        GetMemoryConfiguration(dicomParserMemoryCapacityMb, lock, ORTHANC_CONFIG_DICOM_PARSER_MEMORY_CAPACITY);
+        GetMemoryConfiguration(dicomParserCacheSizeMb, lock, ORTHANC_CONFIG_DICOM_PARSER_CACHE_SIZE);
+        GetMemoryConfiguration(transcoderMemoryCapacityMb, lock, ORTHANC_CONFIG_TRANSCODER_MEMORY_CAPACITY);
+        GetMemoryConfiguration(transcoderCacheSizeMb, lock, ORTHANC_CONFIG_TRANSCODER_CACHE_SIZE);
+        GetMemoryConfiguration(sequentialReaderWindowCapacityMb, lock, ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_WINDOW_CAPACITY);
 
-        sequentialReaderWindowSize = lock.GetConfiguration().GetUnsignedIntegerParameter(ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_WINDOW_SIZE, 4);
+        sequentialReaderWindowSize = lock.GetConfiguration().GetUnsignedIntegerParameter(ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_WINDOW_SIZE);
         LOG(WARNING) << "'" << ORTHANC_CONFIG_SEQUENTIAL_DICOM_READER_WINDOW_SIZE << "' is set to " << sequentialReaderWindowSize;
       }
 
@@ -927,6 +926,20 @@
   }
 
 
+  bool ServerContext::LookupIngestTranscoding(DicomTransferSyntax& target) const
+  {
+    if (isIngestTranscoding_)
+    {
+      target = ingestTransferSyntax_;
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
   void ServerContext::SetCompressionEnabled(bool enabled)
   {
     if (enabled)
--- a/OrthancServer/Sources/ServerContext.h	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Wed Jul 15 11:42:31 2026 +0200
@@ -329,6 +329,8 @@
       return patientLevelEnabled_;
     }
 
+    bool LookupIngestTranscoding(DicomTransferSyntax& target) const;
+
     void SetCompressionEnabled(bool enabled);
 
     bool IsCompressionEnabled() const
--- a/OrthancServer/Sources/ServerIndex.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancServer/Sources/ServerIndex.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -464,7 +464,7 @@
     
     {
       OrthancConfiguration::ReaderLock lock;
-      stableAge = lock.GetConfiguration().GetUnsignedIntegerParameter("StableAge", 60);
+      stableAge = lock.GetConfiguration().GetUnsignedIntegerParameter("StableAge");
     }
 
     if (stableAge < 1)
--- a/OrthancServer/Sources/ServerJobs/LuaJobManager.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/LuaJobManager.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -62,7 +62,7 @@
     
     {
       OrthancConfiguration::ReaderLock lock;
-      dicomTimeout = lock.GetConfiguration().GetUnsignedIntegerParameter("DicomAssociationCloseDelay", 5);
+      dicomTimeout = lock.GetConfiguration().GetUnsignedIntegerParameter("DicomAssociationCloseDelay");
     }
 
     connectionManager_.SetInactivityTimeout(dicomTimeout * 1000);  // Milliseconds expected
--- a/OrthancServer/Sources/ServerTranscoder.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancServer/Sources/ServerTranscoder.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -116,7 +116,7 @@
     OrthancConfiguration::ReaderLock lock;
 
     // New option in Orthanc 1.7.0
-    builtinDecoderTranscoderOrder_ = StringToBuiltinDecoderTranscoderOrder(lock.GetConfiguration().GetStringParameter("BuiltinDecoderTranscoderOrder", "After"));
+    builtinDecoderTranscoderOrder_ = StringToBuiltinDecoderTranscoderOrder(lock.GetConfiguration().GetStringParameter("BuiltinDecoderTranscoderOrder"));
 
     // New option in Orthanc 1.12.6
     dynamic_cast<DcmtkTranscoder&>(*dcmtkTranscoder_).SetDefaultLossyQuality(lock.GetConfiguration().GetDicomLossyTranscodingQuality());
--- a/OrthancServer/Sources/main.cpp	Mon Jul 06 15:07:11 2026 +0200
+++ b/OrthancServer/Sources/main.cpp	Wed Jul 15 11:42:31 2026 +0200
@@ -236,8 +236,8 @@
 
     {
       OrthancConfiguration::ReaderLock lock;
-      result->SetMaxResults(lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults", 0));
-      result->SetMaxInstances(lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances", 0));
+      result->SetMaxResults(lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults"));
+      result->SetMaxInstances(lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances"));
     }
 
     if (result->GetMaxResults() == 0)
@@ -302,12 +302,12 @@
   {
     {
       OrthancConfiguration::ReaderLock lock;
-      alwaysAllowEcho_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowEcho", true);
-      alwaysAllowFind_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowFind", false);
-      alwaysAllowFindWorklist_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowFindWorklist", false);
-      alwaysAllowGet_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowGet", false);
-      alwaysAllowMove_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowMove", false);
-      alwaysAllowStore_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowStore", true);
+      alwaysAllowEcho_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowEcho");
+      alwaysAllowFind_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowFind");
+      alwaysAllowFindWorklist_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowFindWorklist");
+      alwaysAllowGet_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowGet");
+      alwaysAllowMove_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowMove");
+      alwaysAllowStore_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowStore");
     }
 
     if (alwaysAllowFind_)
@@ -417,7 +417,7 @@
       {
         OrthancConfiguration::ReaderLock lock;
         lock.GetConfiguration().LookupDicomModalitiesUsingAETitle(modalities, remoteAet);
-        checkIp = lock.GetConfiguration().GetBooleanParameter("DicomCheckModalityHost", false);
+        checkIp = lock.GetConfiguration().GetBooleanParameter("DicomCheckModalityHost");
       }
       
       if (modalities.empty())
@@ -1088,7 +1088,7 @@
 
   {
     OrthancConfiguration::ReaderLock lock;
-    httpServerEnabled = lock.GetConfiguration().GetBooleanParameter("HttpServerEnabled", true);
+    httpServerEnabled = lock.GetConfiguration().GetBooleanParameter("HttpServerEnabled");
   }
 
   if (!httpServerEnabled)
@@ -1103,9 +1103,9 @@
     bool httpDescribeErrors;
 
 #if ORTHANC_ENABLE_MONGOOSE == 1
-    const bool defaultKeepAlive = false;
+    bool keepAlive = false;
 #elif ORTHANC_ENABLE_CIVETWEB == 1
-    const bool defaultKeepAlive = true;
+    bool keepAlive = true;
 #else
 #  error "Either Mongoose or Civetweb must be enabled to compile this file"
 #endif
@@ -1115,7 +1115,7 @@
     {
       OrthancConfiguration::ReaderLock lock;
       
-      httpDescribeErrors = lock.GetConfiguration().GetBooleanParameter("HttpDescribeErrors", true);
+      httpDescribeErrors = lock.GetConfiguration().GetBooleanParameter("HttpDescribeErrors");
   
       // HTTP server
       httpServer.SetThreadsCount(lock.GetConfiguration().GetHttpThreadsCount());
@@ -1123,15 +1123,17 @@
       std::set<std::string> httpBindAddresses;
       lock.GetConfiguration().GetSetOfStringsParameter(httpBindAddresses, "HttpBindAddresses");
       httpServer.SetBindAddresses(httpBindAddresses);
-      httpServer.SetRemoteAccessAllowed(lock.GetConfiguration().GetBooleanParameter("RemoteAccessAllowed", false));
-      httpServer.SetKeepAliveEnabled(lock.GetConfiguration().GetBooleanParameter("KeepAlive", defaultKeepAlive));
-      httpServer.SetKeepAliveTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("KeepAliveTimeout", 1));
-      httpServer.SetHttpCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("HttpCompressionEnabled", false));
-      httpServer.SetTcpNoDelay(lock.GetConfiguration().GetBooleanParameter("TcpNoDelay", true));
-      httpServer.SetRequestTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpRequestTimeout", 30));
+      httpServer.SetRemoteAccessAllowed(lock.GetConfiguration().GetBooleanParameter("RemoteAccessAllowed"));
+      
+      lock.GetConfiguration().LookupBooleanParameter(keepAlive, "KeepAlive"); // we cannot use GetBooleanParameter because the default value depends on the HttpServer lib that is used
+      httpServer.SetKeepAliveEnabled(keepAlive);
+      httpServer.SetKeepAliveTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("KeepAliveTimeout"));
+      httpServer.SetHttpCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("HttpCompressionEnabled"));
+      httpServer.SetTcpNoDelay(lock.GetConfiguration().GetBooleanParameter("TcpNoDelay"));
+      httpServer.SetRequestTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpRequestTimeout"));
 
       // New in Orthanc 1.12.11
-      const unsigned int maxBodySize = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumRequestBodySizeMB", 8192);
+      const unsigned int maxBodySize = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumRequestBodySizeMB");
       if (maxBodySize != 0)
       {
         const uint64_t size = Toolbox::BoundMemorySizeToCurrentArchitecture(maxBodySize * MEGABYTE);
@@ -1143,7 +1145,7 @@
         LOG(WARNING) << "No limit on the maximum body size in HTTP requests";
       }
 
-      const unsigned int maxSizeInArchive = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumFileSizeInArchiveMB", 4096);
+      const unsigned int maxSizeInArchive = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumFileSizeInArchiveMB");
       if (maxSizeInArchive != 0)
       {
         const uint64_t size = Toolbox::BoundMemorySizeToCurrentArchitecture(maxSizeInArchive * MEGABYTE);
@@ -1200,21 +1202,15 @@
             status == OrthancConfiguration::RegisteredUsersStatus_NoConfiguration)
         {
           /**
-           * Starting with Orthanc 1.5.8, if no user is explicitly
-           * defined while remote access is allowed, we create a
-           * default user, and Orthanc Explorer shows a warning
-           * message about an "Insecure setup". This convention is
-           * used in Docker images "jodogne/orthanc",
-           * "jodogne/orthanc-plugins" and "osimis/orthanc".
+           * Starting with Orthanc 1.13.0, if no user is explicitly
+           * defined while remote access is allowed, Orthanc refuses to start
+           * and asks the user to create at least one user.
            **/
-          LOG(WARNING) << "====> HTTP authentication is enabled, but no user is declared. "
-                       << "Creating a default user: Review your configuration option \"RegisteredUsers\". "
-                       << "Your setup is INSECURE <====";
+          std::string errorMessage = "HTTP authentication is enabled and Remote Access is allowed, but no user is declared.  "
+                                     "Starting with Orthanc 1.13.0, you must at least explicitly declare one user in the configuration option \"RegisteredUsers\".";
+          LOG(ERROR) << "====> " << errorMessage << " Orthanc will refuse to start. <====";
 
-          context.SetHttpServerSecure(false);
-
-          // This is the username/password of the default user in Orthanc.
-          httpServer.RegisterUser("orthanc", "orthanc");
+          throw OrthancException(ErrorCode_IncompatibleConfigurations, errorMessage);
         }
         else
         {
@@ -1224,17 +1220,16 @@
         }
       }
       
-      if (lock.GetConfiguration().GetBooleanParameter("SslEnabled", false))
+      if (lock.GetConfiguration().GetBooleanParameter("SslEnabled"))
       {
         boost::filesystem::path certificate = lock.GetConfiguration().InterpretStringParameterAsPath(
-          lock.GetConfiguration().GetStringParameter("SslCertificate", "certificate.pem"));
+          lock.GetConfiguration().GetStringParameter("SslCertificate"));
         httpServer.SetSslEnabled(true);
         httpServer.SetSslCertificate(certificate.c_str());
         
         // Default to TLS 1.2+1.3 as SSL minimum
         // See https://github.com/civetweb/civetweb/blob/master/docs/UserManual.md "ssl_protocol_version" for mapping
-        static const unsigned int TLS_1_2_AND_1_3 = 4;
-        unsigned int minimumVersion = lock.GetConfiguration().GetUnsignedIntegerParameter("SslMinimumProtocolVersion", TLS_1_2_AND_1_3);
+        unsigned int minimumVersion = lock.GetConfiguration().GetUnsignedIntegerParameter("SslMinimumProtocolVersion");
         httpServer.SetSslMinimumVersion(minimumVersion);
 
         static const char* SSL_CIPHERS_ACCEPTED = "SslCiphersAccepted";
@@ -1273,10 +1268,10 @@
         httpServer.SetSslEnabled(false);
       }
 
-      if (lock.GetConfiguration().GetBooleanParameter("SslVerifyPeers", false))
+      if (lock.GetConfiguration().GetBooleanParameter("SslVerifyPeers"))
       {
         boost::filesystem::path trustedClientCertificates = lock.GetConfiguration().InterpretStringParameterAsPath(
-          lock.GetConfiguration().GetStringParameter("SslTrustedClientCertificates", "trustedCertificates.pem"));
+          lock.GetConfiguration().GetStringParameter("SslTrustedClientCertificates"));
         httpServer.SetSslVerifyPeers(true);
         httpServer.SetSslTrustedClientCertificates(trustedClientCertificates);
       }
@@ -1287,7 +1282,7 @@
 
       LOG(INFO) << "Version of Lua: " << LUA_VERSION;
 
-      if (lock.GetConfiguration().GetBooleanParameter("ExecuteLuaEnabled", false))
+      if (lock.GetConfiguration().GetBooleanParameter("ExecuteLuaEnabled"))
       {
         context.SetExecuteLuaEnabled(true);
         LOG(WARNING) << "====> Remote LUA script execution is enabled.  Review your configuration option \"ExecuteLuaEnabled\". "
@@ -1299,7 +1294,7 @@
         LOG(WARNING) << "Remote LUA script execution is disabled";
       }
 
-      if (lock.GetConfiguration().GetBooleanParameter("RestApiWriteToFileSystemEnabled", false))
+      if (lock.GetConfiguration().GetBooleanParameter("RestApiWriteToFileSystemEnabled"))
       {
         context.SetRestApiWriteToFileSystemEnabled(true);
         LOG(WARNING) << "====> Your REST API can write to the FileSystem.  Review your configuration option \"RestApiWriteToFileSystemEnabled\". "
@@ -1311,10 +1306,10 @@
         LOG(WARNING) << "REST API cannot write to the file system because the \"RestApiWriteToFileSystemEnabled\" configuration is set to false.  The URI /instances/../export is disabled.  This is the most secure configuration.";
       }
 
-      if (lock.GetConfiguration().GetBooleanParameter("WebDavEnabled", true))
+      if (lock.GetConfiguration().GetBooleanParameter("WebDavEnabled"))
       {
-        const bool allowDelete = lock.GetConfiguration().GetBooleanParameter("WebDavDeleteAllowed", false);
-        const bool allowUpload = lock.GetConfiguration().GetBooleanParameter("WebDavUploadAllowed", true);
+        const bool allowDelete = lock.GetConfiguration().GetBooleanParameter("WebDavDeleteAllowed");
+        const bool allowUpload = lock.GetConfiguration().GetBooleanParameter("WebDavUploadAllowed");
         
         UriComponents root;
         root.push_back("webdav");
@@ -1367,7 +1362,7 @@
 
   {
     OrthancConfiguration::ReaderLock lock;
-    dicomServerEnabled = lock.GetConfiguration().GetBooleanParameter("DicomServerEnabled", true);
+    dicomServerEnabled = lock.GetConfiguration().GetBooleanParameter("DicomServerEnabled");
   }
 
   if (!dicomServerEnabled)
@@ -1393,34 +1388,41 @@
 
     {
       OrthancConfiguration::ReaderLock lock;
-      dicomServer.SetCalledApplicationEntityTitleCheck(lock.GetConfiguration().GetBooleanParameter("DicomCheckCalledAet", false));
-      dicomServer.SetAssociationTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomScpTimeout", 30));
+      dicomServer.SetCalledApplicationEntityTitleCheck(lock.GetConfiguration().GetBooleanParameter("DicomCheckCalledAet"));
+      dicomServer.SetAssociationTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomScpTimeout"));
       dicomServer.SetPortNumber(lock.GetConfiguration().GetDicomPort());
       dicomServer.SetThreadsCount(lock.GetConfiguration().GetDicomThreadsCount());
       dicomServer.SetApplicationEntityTitle(lock.GetConfiguration().GetOrthancAET());
 
       // Configuration of DICOM TLS for Orthanc SCP (since Orthanc 1.9.0)
-      dicomServer.SetDicomTlsEnabled(lock.GetConfiguration().GetBooleanParameter(KEY_DICOM_TLS_ENABLED, false));
+      dicomServer.SetDicomTlsEnabled(lock.GetConfiguration().GetBooleanParameter(KEY_DICOM_TLS_ENABLED));
       if (dicomServer.IsDicomTlsEnabled())
       {
-        dicomServer.SetOwnCertificatePath(
-          lock.GetConfiguration().GetStringParameter(KEY_DICOM_TLS_PRIVATE_KEY, ""),
-          lock.GetConfiguration().GetStringParameter(KEY_DICOM_TLS_CERTIFICATE, ""));
-        dicomServer.SetTrustedCertificatesPath(
-          lock.GetConfiguration().GetStringParameter(KEY_DICOM_TLS_TRUSTED_CERTIFICATES, ""));
+        std::string tlsPrivateKey, tlsCertificate;
+        if (lock.GetConfiguration().LookupStringParameter(tlsPrivateKey, KEY_DICOM_TLS_PRIVATE_KEY)
+            && lock.GetConfiguration().LookupStringParameter(tlsCertificate, KEY_DICOM_TLS_CERTIFICATE))
+        {
+          dicomServer.SetOwnCertificatePath(tlsPrivateKey, tlsCertificate);
+        }
+
+        std::string tlsTrustedCertificates;
+        if (lock.GetConfiguration().LookupStringParameter(tlsTrustedCertificates, KEY_DICOM_TLS_TRUSTED_CERTIFICATES))
+        {
+          dicomServer.SetTrustedCertificatesPath(tlsTrustedCertificates);
+        }
         dicomServer.SetMinimumTlsVersion(
-          lock.GetConfiguration().GetUnsignedIntegerParameter(KEY_DICOM_TLS_MINIMUM_PROTOCOL_VERSION, 0));
+          lock.GetConfiguration().GetUnsignedIntegerParameter(KEY_DICOM_TLS_MINIMUM_PROTOCOL_VERSION));
         
         std::set<std::string> acceptedCiphers;
         lock.GetConfiguration().GetSetOfStringsParameter(acceptedCiphers, KEY_DICOM_TLS_ACCEPTED_CIPHERS);
         dicomServer.SetAcceptedCiphers(acceptedCiphers);
       }
 
-      dicomServer.SetMaximumPduLength(lock.GetConfiguration().GetUnsignedIntegerParameter(KEY_MAXIMUM_PDU_LENGTH, 16384));
+      dicomServer.SetMaximumPduLength(lock.GetConfiguration().GetUnsignedIntegerParameter(KEY_MAXIMUM_PDU_LENGTH));
 
       // New option in Orthanc 1.9.3
       dicomServer.SetRemoteCertificateRequired(
-        lock.GetConfiguration().GetBooleanParameter(KEY_DICOM_TLS_REMOTE_CERTIFICATE_REQUIRED, true));
+        lock.GetConfiguration().GetBooleanParameter(KEY_DICOM_TLS_REMOTE_CERTIFICATE_REQUIRED));
     }
 
 #if ORTHANC_ENABLE_PLUGINS == 1
@@ -1458,6 +1460,7 @@
 
     bool restart = false;
     ErrorCode error = ErrorCode_Success;
+    std::string errorDetails;
 
     try
     {
@@ -1466,6 +1469,7 @@
     catch (OrthancException& e)
     {
       error = e.GetErrorCode();
+      errorDetails = e.GetDetails();
     }
 
     dicomServer.Stop();
@@ -1475,7 +1479,7 @@
 
     if (error != ErrorCode_Success)
     {
-      throw OrthancException(error);
+      throw OrthancException(error, errorDetails);
     }
 
     return restart;
@@ -1508,8 +1512,7 @@
   bool orthancExplorerEnabled = false;
   {
     OrthancConfiguration::ReaderLock lock;
-    orthancExplorerEnabled = lock.GetConfiguration().GetBooleanParameter(
-        "OrthancExplorerEnabled", true);
+    orthancExplorerEnabled = lock.GetConfiguration().GetBooleanParameter("OrthancExplorerEnabled");
   }
 
   if (orthancExplorerEnabled)
@@ -1655,26 +1658,26 @@
     // ServerContext, otherwise the possible Lua scripts will not be
     // able to properly issue HTTP/HTTPS queries
 
-    std::string httpsCaCertificatesStr = lock.GetConfiguration().GetStringParameter("HttpsCACertificates", "");
+    std::string httpsCaCertificatesStr = lock.GetConfiguration().GetStringParameter("HttpsCACertificates");
     boost::filesystem::path httpsCaCertificates;
     if (!httpsCaCertificatesStr.empty())
     {
       httpsCaCertificates = lock.GetConfiguration().InterpretStringParameterAsPath(httpsCaCertificatesStr);
     }
 
-    HttpClient::ConfigureSsl(lock.GetConfiguration().GetBooleanParameter("HttpsVerifyPeers", true),
+    HttpClient::ConfigureSsl(lock.GetConfiguration().GetBooleanParameter("HttpsVerifyPeers"),
                              httpsCaCertificates);
-    HttpClient::SetDefaultVerbose(lock.GetConfiguration().GetBooleanParameter("HttpVerbose", false));
+    HttpClient::SetDefaultVerbose(lock.GetConfiguration().GetBooleanParameter("HttpVerbose"));
 
     // The value "0" below makes the class HttpClient use its default
     // value (DEFAULT_HTTP_TIMEOUT = 60 seconds in Orthanc 1.5.7)
-    HttpClient::SetDefaultTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpTimeout", 0));
+    HttpClient::SetDefaultTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpTimeout"));
     
-    HttpClient::SetDefaultProxy(lock.GetConfiguration().GetStringParameter("HttpProxy", ""));
+    HttpClient::SetDefaultProxy(lock.GetConfiguration().GetStringParameter("HttpProxy"));
     
-    DicomAssociationParameters::SetDefaultTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomScuTimeout", 10));
+    DicomAssociationParameters::SetDefaultTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomScuTimeout"));
 
-    maxCompletedJobs = lock.GetConfiguration().GetUnsignedIntegerParameter("JobsHistorySize", 10);
+    maxCompletedJobs = lock.GetConfiguration().GetUnsignedIntegerParameter("JobsHistorySize");
 
     if (maxCompletedJobs == 0)
     {
@@ -1682,27 +1685,32 @@
     }
 
     // New option in Orthanc 1.12.5
-    readOnly = lock.GetConfiguration().GetBooleanParameter(ORTHANC_CONFIG_READ_ONLY, false);
+    readOnly = lock.GetConfiguration().GetBooleanParameter(ORTHANC_CONFIG_READ_ONLY);
     
     // New option in Orthanc 1.12.6
-    maxDcmtkConcurrentTranscoders = lock.GetConfiguration().GetUnsignedIntegerParameter(KEY_MAXIMUM_CONCURRENT_DCMTK_TRANSCODERS, 0);
+    maxDcmtkConcurrentTranscoders = lock.GetConfiguration().GetUnsignedIntegerParameter(KEY_MAXIMUM_CONCURRENT_DCMTK_TRANSCODERS);
     if (maxDcmtkConcurrentTranscoders == 0)
     {
       maxDcmtkConcurrentTranscoders = static_cast<unsigned int>(boost::thread::hardware_concurrency());
     }
 
     // Configuration of DICOM TLS for Orthanc SCU (since Orthanc 1.9.0)
-    DicomAssociationParameters::SetDefaultOwnCertificatePath(
-      lock.GetConfiguration().GetStringParameter(KEY_DICOM_TLS_PRIVATE_KEY, ""),
-      lock.GetConfiguration().GetStringParameter(KEY_DICOM_TLS_CERTIFICATE, ""));
-    DicomAssociationParameters::SetDefaultTrustedCertificatesPath(
-      lock.GetConfiguration().GetStringParameter(KEY_DICOM_TLS_TRUSTED_CERTIFICATES, ""));
-    DicomAssociationParameters::SetDefaultMaximumPduLength(
-      lock.GetConfiguration().GetUnsignedIntegerParameter(KEY_MAXIMUM_PDU_LENGTH, 16384));
+    std::string tlsPrivateKey, tlsCertificate;
+    if (lock.GetConfiguration().LookupStringParameter(tlsPrivateKey, KEY_DICOM_TLS_PRIVATE_KEY)
+        && lock.GetConfiguration().LookupStringParameter(tlsCertificate, KEY_DICOM_TLS_CERTIFICATE))
+    {
+      DicomAssociationParameters::SetDefaultOwnCertificatePath(tlsPrivateKey, tlsCertificate);
+    }
+
+    std::string tlsTrustedCertificates;
+    if (lock.GetConfiguration().LookupStringParameter(tlsTrustedCertificates, KEY_DICOM_TLS_TRUSTED_CERTIFICATES))
+    {
+      DicomAssociationParameters::SetDefaultTrustedCertificatesPath(tlsTrustedCertificates);
+    }
 
     // New option in Orthanc 1.9.3
     DicomAssociationParameters::SetDefaultRemoteCertificateRequired(
-      lock.GetConfiguration().GetBooleanParameter(KEY_DICOM_TLS_REMOTE_CERTIFICATE_REQUIRED, true));
+      lock.GetConfiguration().GetBooleanParameter(KEY_DICOM_TLS_REMOTE_CERTIFICATE_REQUIRED));
   }
 
   std::unique_ptr<ServerTranscoder> transcoder(new ServerTranscoder(maxDcmtkConcurrentTranscoders));
@@ -1745,7 +1753,7 @@
         }
         else if (lock.GetJson()[ORTHANC_CONFIG_OVERWRITE_INSTANCES].isBool())
         {
-          bool overwriteInstancesLegacy = lock.GetConfiguration().GetBooleanParameter(ORTHANC_CONFIG_OVERWRITE_INSTANCES, false);
+          bool overwriteInstancesLegacy = lock.GetJson()[ORTHANC_CONFIG_OVERWRITE_INSTANCES].asBool();
           overwriteInstancesMode = (overwriteInstancesLegacy ? OverwriteInstancesMode_Always : OverwriteInstancesMode_Never);
         }
       }
@@ -2325,7 +2333,7 @@
   }
   catch (const OrthancException& e)
   {
-    LOG(ERROR) << "Uncaught exception, stopping now: [" << e.What() << "] (code " << e.GetErrorCode() << ")";
+    LOG(ERROR) << "Uncaught exception, stopping now: [" << e.What() << "] (code " << e.GetErrorCode() << ") " << e.GetDetails();
 #if defined(_WIN32)
     if (e.GetErrorCode() >= ErrorCode_START_PLUGINS)
     {