changeset 7001:e9cebd8402c5 streaming

merge
author Alain Mazy <am@orthanc.team>
date Mon, 06 Jul 2026 15:07:11 +0200
parents 053ce2231c18 (current diff) 8c303fedfe87 (diff)
children a998c3b34bf4
files OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/main.cpp
diffstat 19 files changed, 1155 insertions(+), 249 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Mon Jul 06 15:06:54 2026 +0200
+++ b/NEWS	Mon Jul 06 15:07:11 2026 +0200
@@ -39,6 +39,13 @@
   - Some line of log now contain additional contextual informations like the job id they relate to.
     A new "--logs-no-context" command line option can be used to get back to the previous behavior to
     keep backward compatibility and reduce the lines length.
+* C-Find SCP:
+  - When Orthanc was requested a sequence in a C-Find query, Orthanc was actually not returning
+    any sequences in the C-Find answer if the matched resource did contain the requested sequence and
+    was returning an empty sequence if the matched resource did not contain the requested sequence.
+    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.
 
 REST API
 --------
@@ -56,6 +63,12 @@
 * Updated the naming structure of the zip downloaded from "/../../archive" routes.  Notably added
   SeriesNumber at the series level and now using " - " instead of " " to separate DICOM tags used
   in folder names.
+* In "/tools/metrics-prometheus":
+  - Removed "orthanc_jobs_completed", "orthanc_jobs_success" and "orthanc_jobs_failed" that were
+    only considering the jobs currently stored in the JobsHistory and that were not suitable for
+    monitoring.  These metrics have been replaced by "orthanc_jobs_total_completed",
+    "orthanc_jobs_total_success" and "orthanc_jobs_total_failed" that are now considering all the
+    jobs that have run since Orthanc started.
 
 Plugin SDK
 ----------
@@ -68,6 +81,7 @@
 -----------
 
 * Added internal support for OV, SV, and UV value representations
+* Added support for multiple values in AT value representation
 * 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/Sources/DicomFormat/DicomTag.cpp	Mon Jul 06 15:06:54 2026 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomTag.cpp	Mon Jul 06 15:07:11 2026 +0200
@@ -77,6 +77,10 @@
     return group_ % 2 == 1;
   }
 
+  bool DicomTag::IsPrivateCreator() const
+  {
+    return IsPrivate() && (element_ >= 0x0010 && element_ <= 0x00FF);
+  }
 
   bool DicomTag::operator< (const DicomTag& other) const
   {
--- a/OrthancFramework/Sources/DicomFormat/DicomTag.h	Mon Jul 06 15:06:54 2026 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomTag.h	Mon Jul 06 15:07:11 2026 +0200
@@ -51,6 +51,8 @@
 
     bool IsPrivate() const;
 
+    bool IsPrivateCreator() const;
+
     bool operator< (const DicomTag& other) const;
 
     bool operator<= (const DicomTag& other) const;
--- a/OrthancFramework/Sources/DicomFormat/DicomValue.cpp	Mon Jul 06 15:06:54 2026 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomValue.cpp	Mon Jul 06 15:07:11 2026 +0200
@@ -330,4 +330,14 @@
       throw OrthancException(ErrorCode_BadFileFormat);
     }
   }
+
+
+  DicomValue* DicomValue::CreateFromSwap(std::string& content,
+                                         bool isBinary)
+  {
+    std::unique_ptr<DicomValue> value(new DicomValue);
+    value->type_ = isBinary ? Type_Binary : Type_String;
+    value->content_.swap(content);
+    return value.release();
+  }
 }
--- a/OrthancFramework/Sources/DicomFormat/DicomValue.h	Mon Jul 06 15:06:54 2026 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomValue.h	Mon Jul 06 15:07:11 2026 +0200
@@ -109,5 +109,9 @@
     void Serialize(Json::Value& target) const;
 
     void Unserialize(const Json::Value& source);
+
+    // This constructor is most efficient
+    static DicomValue* CreateFromSwap(std::string& content,
+                                      bool isBinary);
   };
 }
--- a/OrthancFramework/Sources/DicomNetworking/DicomServer.cpp	Mon Jul 06 15:06:54 2026 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomServer.cpp	Mon Jul 06 15:07:11 2026 +0200
@@ -63,7 +63,7 @@
                                  unsigned int maximumPduLength,
                                  bool useDicomTls)
   {
-    Logging::ScopedCurrentThreadNameSetter setter("DICOM-SERVER");
+    Logging::ScopedCurrentThreadNameSetter setter(server->name_);
     CLOG(INFO, DICOM) << "DICOM server started";
 
     while (server->continue_)
@@ -90,12 +90,15 @@
   }
 
 
-  DicomServer::DicomServer() :
+  DicomServer::DicomServer(const std::string& name,
+                           const std::string& dicomThreadNamesPrefix) :
     pimpl_(new PImpl),
     checkCalledAet_(true),
     aet_("ANY-SCP"),
     port_(104),
     continue_(false),
+    name_(name),
+    dicomThreadNamesPrefix_(dicomThreadNamesPrefix),
     associationTimeout_(30),
     threadsCount_(4),
     modalities_(NULL),
@@ -436,11 +439,11 @@
 
     if (metricsRegistry_ == NULL)
     {
-      pimpl_->workers_.reset(new RunnableWorkersPool(threadsCount_, "DICOM-"));
+      pimpl_->workers_.reset(new RunnableWorkersPool(threadsCount_, dicomThreadNamesPrefix_ + "-"));
     }
     else
     {
-      pimpl_->workers_.reset(new RunnableWorkersPool(threadsCount_, "DICOM-", *metricsRegistry_, "orthanc_available_dicom_threads"));
+      pimpl_->workers_.reset(new RunnableWorkersPool(threadsCount_, dicomThreadNamesPrefix_ + "-", *metricsRegistry_, "orthanc_available_dicom_threads"));
     }
 
     pimpl_->thread_ = boost::thread(ServerThread, this, maximumPduLength_, useDicomTls_);
--- a/OrthancFramework/Sources/DicomNetworking/DicomServer.h	Mon Jul 06 15:06:54 2026 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomServer.h	Mon Jul 06 15:07:11 2026 +0200
@@ -75,6 +75,8 @@
     std::string aet_;
     uint16_t port_;
     bool continue_;
+    std::string name_;
+    std::string dicomThreadNamesPrefix_;
     uint32_t associationTimeout_;
     unsigned int threadsCount_;
     IRemoteModalities* modalities_;
@@ -102,7 +104,8 @@
                              bool useDicomTls);
 
   public:
-    DicomServer();
+    DicomServer(const std::string& name,
+                const std::string& dicomThreadNamesPrefix);
 
     ~DicomServer();
 
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Mon Jul 06 15:06:54 2026 +0200
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Mon Jul 06 15:07:11 2026 +0200
@@ -94,6 +94,12 @@
 #  include <dcmtk/dcmdata/dcvrur.h>
 #endif
 
+#if DCMTK_VERSION_NUMBER >= 365
+#  include <dcmtk/dcmdata/dcvrov.h>
+#  include <dcmtk/dcmdata/dcvrsv.h>
+#  include <dcmtk/dcmdata/dcvruv.h>
+#endif
+
 #if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
 #  if !defined(ORTHANC_FRAMEWORK_INCLUDE_RESOURCES) || (ORTHANC_FRAMEWORK_INCLUDE_RESOURCES == 1)
 #    include <OrthancFrameworkResources.h>
@@ -324,10 +330,20 @@
     };
     // NOLINTEND(bugprone-macro-parentheses)
 
+    DCMTK_TO_CTYPE_CONVERTER(DcmtkToSint16Converter, Sint16, DcmSignedShort, getSint16, boost::lexical_cast<std::string>)
     DCMTK_TO_CTYPE_CONVERTER(DcmtkToSint32Converter, Sint32, DcmSignedLong, getSint32, boost::lexical_cast<std::string>)
-    DCMTK_TO_CTYPE_CONVERTER(DcmtkToSint16Converter, Sint16, DcmSignedShort, getSint16, boost::lexical_cast<std::string>)
+
+#if DCMTK_VERSION_NUMBER >= 365
+    DCMTK_TO_CTYPE_CONVERTER(DcmtkToSint64Converter, Sint64, DcmSigned64bitVeryLong, getSint64, boost::lexical_cast<std::string>)
+#endif
+
+    DCMTK_TO_CTYPE_CONVERTER(DcmtkToUint16Converter, Uint16, DcmUnsignedShort, getUint16, boost::lexical_cast<std::string>)
     DCMTK_TO_CTYPE_CONVERTER(DcmtkToUint32Converter, Uint32, DcmUnsignedLong, getUint32, boost::lexical_cast<std::string>)
-    DCMTK_TO_CTYPE_CONVERTER(DcmtkToUint16Converter, Uint16, DcmUnsignedShort, getUint16, boost::lexical_cast<std::string>)
+
+#if DCMTK_VERSION_NUMBER >= 365
+    DCMTK_TO_CTYPE_CONVERTER(DcmtkToUint64Converter, Uint64, DcmUnsigned64bitVeryLong, getUint64, boost::lexical_cast<std::string>)
+#endif
+
     DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat32Converter, Float32, DcmFloatingPointSingle, getFloat32, FloatToString)
     DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDouble, getFloat64, DoubleToString)
 
@@ -357,6 +373,588 @@
         return new DicomValue;
       }
     }
+
+
+    class IElementFiller : public boost::noncopyable
+    {
+    public:
+      virtual ~IElementFiller()
+      {
+      }
+
+      virtual bool FillSingleValue(DcmElement& element,
+                                   const std::string& value) const = 0;
+
+      virtual bool FillMultipleValues(DcmElement& element,
+                                      const std::vector<std::string>& values) const = 0;
+
+      static bool Apply(DcmElement& element,
+                        const IElementFiller& filler,
+                        const std::string& value)
+      {
+        if (value.find('\\') != std::string::npos)
+        {
+          std::vector<std::string> tokens;
+          Toolbox::TokenizeString(tokens, value, '\\');
+          return filler.FillMultipleValues(element, tokens);
+        }
+        else
+        {
+          return filler.FillSingleValue(element, value);
+        }
+      }
+    };
+
+
+    class ValueRepresentationFiller_AT : public IElementFiller
+    {
+    public:
+      bool FillSingleValue(DcmElement& element,
+                           const std::string& value) const ORTHANC_OVERRIDE
+      {
+        DicomTag tag = FromDcmtkBridge::ParseTag(value);
+        return element.putTagVal(DcmTagKey(tag.GetGroup(), tag.GetElement())).good();
+      }
+
+      bool FillMultipleValues(DcmElement& element,
+                              const std::vector<std::string>& values) const ORTHANC_OVERRIDE
+      {
+        std::vector<Uint16> tags;
+        tags.resize(2 * values.size());
+
+        for (size_t i = 0; i < values.size(); i++)
+        {
+          DicomTag tag = FromDcmtkBridge::ParseTag(values[i]);
+          tags[2 * i] = tag.GetGroup();
+          tags[2 * i + 1] = tag.GetElement();
+        }
+
+        return element.putUint16Array(tags.empty() ? NULL : &tags[0], values.size()).good();
+      }
+    };
+
+
+    template <typename DcmtkType>
+    class ValueRepresentationFillerGeneric : public IElementFiller
+    {
+    protected:
+      virtual bool PutSingleValue(DcmElement& element,
+                                  const DcmtkType& value) const = 0;
+
+      virtual bool PutMultipleValues(DcmElement& element,
+                                     const std::vector<DcmtkType>& values) const = 0;
+
+    public:
+      bool FillSingleValue(DcmElement& element,
+                           const std::string& value) const ORTHANC_OVERRIDE
+      {
+        return PutSingleValue(element, boost::lexical_cast<DcmtkType>(value));
+      }
+
+      bool FillMultipleValues(DcmElement& element,
+                              const std::vector<std::string>& values) const ORTHANC_OVERRIDE
+      {
+        std::vector<DcmtkType> numeric;
+        numeric.resize(values.size());
+
+        for (size_t i = 0; i < values.size(); i++)
+        {
+          numeric[i] = boost::lexical_cast<DcmtkType>(values[i]);
+        }
+
+        return PutMultipleValues(element, numeric);
+      }
+    };
+
+
+    class ValueRepresentationFiller_SL : public ValueRepresentationFillerGeneric<Sint32>
+    {
+    protected:
+      virtual bool PutSingleValue(DcmElement& element,
+                                  const Sint32& value) const ORTHANC_OVERRIDE
+      {
+        return element.putSint32(value).good();
+      }
+
+      virtual bool PutMultipleValues(DcmElement& element,
+                                     const std::vector<Sint32>& values) const ORTHANC_OVERRIDE
+      {
+        return element.putSint32Array(values.empty() ? NULL : &values[0], values.size()).good();
+      }
+    };
+
+
+    class ValueRepresentationFiller_SS : public ValueRepresentationFillerGeneric<Sint16>
+    {
+    protected:
+      virtual bool PutSingleValue(DcmElement& element,
+                                  const Sint16& value) const ORTHANC_OVERRIDE
+      {
+        return element.putSint16(value).good();
+      }
+
+      virtual bool PutMultipleValues(DcmElement& element,
+                                     const std::vector<Sint16>& values) const ORTHANC_OVERRIDE
+      {
+        return element.putSint16Array(values.empty() ? NULL : &values[0], values.size()).good();
+      }
+    };
+
+
+#if DCMTK_VERSION_NUMBER >= 365
+    class ValueRepresentationFiller_SV : public ValueRepresentationFillerGeneric<Sint64>
+    {
+    protected:
+      virtual bool PutSingleValue(DcmElement& element,
+                                  const Sint64& value) const ORTHANC_OVERRIDE
+      {
+        // The method "DcmElement::putSint64()" was only added in DCMTK 3.7.0,
+        // so we have to cast to support earlier versions of DCMTK
+        DcmSigned64bitVeryLong& e = dynamic_cast<DcmSigned64bitVeryLong&>(element);
+        return e.putSint64(value).good();
+      }
+
+      virtual bool PutMultipleValues(DcmElement& element,
+                                     const std::vector<Sint64>& values) const ORTHANC_OVERRIDE
+      {
+        DcmSigned64bitVeryLong& e = dynamic_cast<DcmSigned64bitVeryLong&>(element);
+        return e.putSint64Array(values.empty() ? NULL : &values[0], values.size()).good();
+      }
+    };
+#endif
+
+
+    class ValueRepresentationFiller_UL : public ValueRepresentationFillerGeneric<Uint32>
+    {
+    protected:
+      virtual bool PutSingleValue(DcmElement& element,
+                                  const Uint32& value) const ORTHANC_OVERRIDE
+      {
+        return element.putUint32(value).good();
+      }
+
+      virtual bool PutMultipleValues(DcmElement& element,
+                                     const std::vector<Uint32>& values) const ORTHANC_OVERRIDE
+      {
+        return element.putUint32Array(values.empty() ? NULL : &values[0], values.size()).good();
+      }
+    };
+
+
+    class ValueRepresentationFiller_US : public ValueRepresentationFillerGeneric<Uint16>
+    {
+    protected:
+      virtual bool PutSingleValue(DcmElement& element,
+                                  const Uint16& value) const ORTHANC_OVERRIDE
+      {
+        return element.putUint16(value).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();
+      }
+    };
+
+
+#if DCMTK_VERSION_NUMBER >= 365
+    class ValueRepresentationFiller_UV : public ValueRepresentationFillerGeneric<Uint64>
+    {
+    protected:
+      virtual bool PutSingleValue(DcmElement& element,
+                                  const Uint64& value) const ORTHANC_OVERRIDE
+      {
+        // The method "DcmElement::putUint64()" was only added in DCMTK 3.7.0,
+        // so we have to cast to support earlier versions of DCMTK
+        DcmUnsigned64bitVeryLong& e = dynamic_cast<DcmUnsigned64bitVeryLong&>(element);
+        return e.putUint64(value).good();
+      }
+
+      virtual bool PutMultipleValues(DcmElement& element,
+                                     const std::vector<Uint64>& values) const ORTHANC_OVERRIDE
+      {
+        DcmUnsigned64bitVeryLong& e = dynamic_cast<DcmUnsigned64bitVeryLong&>(element);
+        return e.putUint64Array(values.empty() ? NULL : &values[0], values.size()).good();
+      }
+    };
+#endif
+
+
+    class ValueRepresentationFiller_FL : public ValueRepresentationFillerGeneric<Float32>
+    {
+    protected:
+      virtual bool PutSingleValue(DcmElement& element,
+                                  const Float32& value) const ORTHANC_OVERRIDE
+      {
+        return element.putFloat32(value).good();
+      }
+
+      virtual bool PutMultipleValues(DcmElement& element,
+                                     const std::vector<Float32>& values) const ORTHANC_OVERRIDE
+      {
+        return element.putFloat32Array(values.empty() ? NULL : &values[0], values.size()).good();
+      }
+    };
+
+
+    class ValueRepresentationFiller_FD : public ValueRepresentationFillerGeneric<Float64>
+    {
+    protected:
+      virtual bool PutSingleValue(DcmElement& element,
+                                  const Float64& value) const ORTHANC_OVERRIDE
+      {
+        return element.putFloat64(value).good();
+      }
+
+      virtual bool PutMultipleValues(DcmElement& element,
+                                     const std::vector<Float64>& values) const ORTHANC_OVERRIDE
+      {
+        return element.putFloat64Array(values.empty() ? NULL : &values[0], values.size()).good();
+      }
+    };
+
+
+    class ILeafElementArrayReader : public boost::noncopyable
+    {
+    public:
+      virtual ~ILeafElementArrayReader()
+      {
+      }
+
+      virtual bool IsValid() const = 0;
+
+      virtual uint64_t GetSize() const = 0;
+
+      virtual void ReadValue(std::string& value,
+                             uint64_t index) const = 0;
+
+      static DicomValue* Apply(const ILeafElementArrayReader& reader,
+                               const DcmTagKey& tag,
+                               size_t maxStringLength,
+                               uint64_t maxBinaryArrayLength)
+      {
+        if (reader.IsValid())
+        {
+          uint64_t size = reader.GetSize();
+
+          if (maxBinaryArrayLength != 0 &&
+              size > maxBinaryArrayLength)
+          {
+            /**
+             * We have seen files with 100M floats (value representation OF):
+             * https://discourse.orthanc-server.org/t/orthanc-1-12-11-performance-issues-with-deformable-registrations/6448
+             **/
+
+            DicomTag t(tag.getGroup(), tag.getElement());
+            LOG(WARNING) << "Truncating the DICOM tag " << t.Format() << " containing "
+                         << size << " values to " << maxBinaryArrayLength << " values";
+            size = maxBinaryArrayLength;
+          }
+
+          ChunkedBuffer buffer;
+          for (uint64_t i = 0; i < size; i++)
+          {
+            if (maxStringLength != 0 &&
+                buffer.GetNumBytes() > maxStringLength)
+            {
+              return new DicomValue;
+            }
+
+            if (i > 0)
+            {
+              buffer.AddChunk("\\", 1);
+            }
+
+            std::string s;
+            reader.ReadValue(s, i);
+            buffer.AddChunk(s);
+          }
+
+          if (maxStringLength != 0 &&
+              buffer.GetNumBytes() > maxStringLength)
+          {
+            return new DicomValue;
+          }
+          else
+          {
+            std::string s;
+            buffer.Flatten(s);
+            return DicomValue::CreateFromSwap(s, false);
+          }
+        }
+        else
+        {
+          return new DicomValue;
+        }
+      }
+    };
+
+
+    class ValueRepresentationReader_AT : public ILeafElementArrayReader
+    {
+    private:
+      bool     valid_;
+      uint64_t size_;
+      Uint16*  content_;
+
+    public:
+      ValueRepresentationReader_AT(DcmElement& element) :
+        valid_(false)
+      {
+        DcmAttributeTag& e = dynamic_cast<DcmAttributeTag&>(element);
+
+        if (e.getUint16Array(content_).good() &&
+            content_ != NULL)
+        {
+          if (element.getLength() % sizeof(Uint16) != 0)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat);
+          }
+
+          size_ = static_cast<uint64_t>(element.getLength()) / sizeof(Uint16);
+          if (size_ % 2 != 0)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat);
+          }
+          else
+          {
+            // Each DICOM tag is stored as two successive Uint16
+            size_ /= 2;
+          }
+
+          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_);
+        DicomTag tag(content_[2 * index], content_[2 * index + 1]);
+        value = tag.Format();
+      }
+    };
+
+
+    class ValueRepresentationReader_OF : public ILeafElementArrayReader
+    {
+    private:
+      bool      valid_;
+      uint64_t  size_;
+      Float32*  content_;
+
+    public:
+      ValueRepresentationReader_OF(DcmElement& element) :
+        valid_(false)
+      {
+        /**
+         * 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 build a string of all values.
+         * The resulting string is formatted as in ApplyDcmtkToCTypeConverter().
+         **/
+        DcmFloatingPointSingle& e = dynamic_cast<DcmFloatingPointSingle&>(element);
+
+        if (e.getFloat32Array(content_).good() &&
+            content_ != NULL)
+        {
+          if (element.getLength() % sizeof(Float32) != 0)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat);
+          }
+
+          size_ = static_cast<uint64_t>(element.getLength()) / sizeof(Float32);
+          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_);
+        value = boost::lexical_cast<std::string>(content_[index]);
+      }
+    };
+
+
+#if DCMTK_VERSION_NUMBER >= 361
+    class ValueRepresentationReader_OD : public ILeafElementArrayReader
+    {
+    private:
+      bool      valid_;
+      uint64_t  size_;
+      Float64*  content_;
+
+    public:
+      ValueRepresentationReader_OD(DcmElement& element) :
+        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().
+         **/
+        DcmFloatingPointDouble& e = dynamic_cast<DcmFloatingPointDouble&>(element);
+
+        if (e.getFloat64Array(content_).good() &&
+            content_ != NULL)
+        {
+          if (element.getLength() % sizeof(Float64) != 0)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat);
+          }
+
+          size_ = static_cast<uint64_t>(element.getLength()) / sizeof(Float64);
+          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_);
+        value = boost::lexical_cast<std::string>(content_[index]);
+      }
+    };
+#endif
+
+
+#if DCMTK_VERSION_NUMBER >= 362
+    class ValueRepresentationReader_OL : public ILeafElementArrayReader
+    {
+    private:
+      bool      valid_;
+      uint64_t  size_;
+      Uint32*   content_;
+
+    public:
+      ValueRepresentationReader_OL(DcmElement& element) :
+        valid_(false)
+      {
+        /**
+         * 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.
+         * The resulting string is formatted as in ApplyDcmtkToCTypeConverter().
+         **/
+        DcmUnsignedLong& e = dynamic_cast<DcmUnsignedLong&>(element);
+
+        if (e.getUint32Array(content_).good() &&
+            content_ != NULL)
+        {
+          if (element.getLength() % sizeof(Uint32) != 0)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat);
+          }
+
+          size_ = static_cast<uint64_t>(element.getLength()) / sizeof(Uint32);
+          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_);
+        value = boost::lexical_cast<std::string>(content_[index]);
+      }
+    };
+#endif
+
+
+#if DCMTK_VERSION_NUMBER >= 365
+    class ValueRepresentationReader_OV : public ILeafElementArrayReader
+    {
+    private:
+      bool      valid_;
+      uint64_t  size_;
+      Uint64*   content_;
+
+    public:
+      ValueRepresentationReader_OV(DcmElement& element) :
+        valid_(false)
+      {
+        DcmUnsigned64bitVeryLong& e = dynamic_cast<DcmOther64bitVeryLong&>(element);
+
+        if (e.getUint64Array(content_).good() &&
+            content_ != NULL)
+        {
+          if (element.getLength() % sizeof(Uint64) != 0)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat);
+          }
+
+          size_ = static_cast<uint64_t>(element.getLength()) / sizeof(Uint64);
+          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_);
+        value = boost::lexical_cast<std::string>(content_[index]);
+      }
+    };
+#endif
   }
 
 
@@ -879,142 +1477,47 @@
           return ApplyDcmtkToCTypeConverter<DcmtkToFloat64Converter>(element);
         }
 
+#if DCMTK_VERSION_NUMBER >= 365
+        case EVR_SV:  // signed very long (64-bit, new in Orthanc 1.12.12)
+        {
+          return ApplyDcmtkToCTypeConverter<DcmtkToSint64Converter>(element);
+        }
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 365
+        case EVR_UV:  // unsigned very long (64-bit, new in Orthanc 1.12.12)
+        {
+          return ApplyDcmtkToCTypeConverter<DcmtkToUint64Converter>(element);
+        }
+#endif
+
         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 build a string of all values.
-           * The resulting string is formatted as in ApplyDcmtkToCTypeConverter().
-           **/
-          DcmFloatingPointSingle& content = dynamic_cast<DcmFloatingPointSingle&>(element);
-          Float32* floatArray = NULL;
-          if (content.getFloat32Array(floatArray).good() && floatArray != NULL)
-          {
-            if (element.getLength() % sizeof(Float32) != 0)
-            {
-              throw OrthancException(ErrorCode_BadFileFormat);
-            }
-
-            uint64_t numFloats = static_cast<uint64_t>(element.getLength()) / sizeof(Float32);
-            numFloats = std::min(numFloats, static_cast<uint64_t>(maxBinaryArrayLength));
-
-            uint64_t preAllocateStringSize = static_cast<uint64_t>(maxStringLength); // We have seen files with 100M floats: https://discourse.orthanc-server.org/t/orthanc-1-12-11-performance-issues-with-deformable-registrations/6448
-            if (maxStringLength == 0) // no limit, use an heuristic to reserve the string: 12 chars per float
-            {
-              preAllocateStringSize = 12ull * numFloats;
-              if (preAllocateStringSize > static_cast<uint64_t>(std::numeric_limits<size_t>::max()))
-              {
-                throw OrthancException(ErrorCode_BadFileFormat, "Too many values in binary array");
-              }
-            }
-
-            std::string result;
-            result.reserve(static_cast<size_t>(preAllocateStringSize));
-            for (unsigned long i = 0; i < numFloats; i++)
-            {
-              if (i > 0)
-              {
-                result += "\\";
-              }
-              result += boost::lexical_cast<std::string>(floatArray[i]);
-            }
-            return new DicomValue(result, false);
-          }
-          return new DicomValue;
+          ValueRepresentationReader_OF reader(element);
+          return ILeafElementArrayReader::Apply(reader, element.getTag(), maxStringLength, maxBinaryArrayLength);
         }
 
 #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. 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& content = dynamic_cast<DcmFloatingPointDouble&>(element);
-          Float64* doubleArray = NULL;
-          if (content.getFloat64Array(doubleArray).good() && doubleArray != NULL)
-          {
-            if (element.getLength() % sizeof(Float64) != 0)
-            {
-              throw OrthancException(ErrorCode_BadFileFormat);
-            }
-
-            uint64_t numDoubles = static_cast<uint64_t>(element.getLength()) / sizeof(Float64);
-            numDoubles = std::min(numDoubles, static_cast<uint64_t>(maxBinaryArrayLength));
-
-            uint64_t preAllocateStringSize = static_cast<uint64_t>(maxStringLength); // We have seen files with 100M floats: https://discourse.orthanc-server.org/t/orthanc-1-12-11-performance-issues-with-deformable-registrations/6448
-            if (maxStringLength == 0) // no limit, use an heuristic to reserve the string: 20 chars per float
-            {
-              preAllocateStringSize = 20ull * numDoubles;
-              if (preAllocateStringSize > static_cast<uint64_t>(std::numeric_limits<size_t>::max()))
-              {
-                throw OrthancException(ErrorCode_BadFileFormat, "Too many values in binary array");
-              }
-            }
-
-            std::string result;
-            result.reserve(static_cast<size_t>(preAllocateStringSize));
-            for (unsigned long i = 0; i < numDoubles; i++)
-            {
-              if (i > 0)
-              {
-                result += "\\";
-              }
-              result += boost::lexical_cast<std::string>(doubleArray[i]);
-            }
-            return new DicomValue(result, false);
-          }
-          return new DicomValue;
+          ValueRepresentationReader_OD reader(element);
+          return ILeafElementArrayReader::Apply(reader, element.getTag(), maxStringLength, maxBinaryArrayLength);
         }
 #endif
 
 #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.
-           * The resulting string is formatted as in ApplyDcmtkToCTypeConverter().
-           **/
-          DcmUnsignedLong& content = dynamic_cast<DcmUnsignedLong&>(element);
-          Uint32* uint32Array = NULL;
-          if (content.getUint32Array(uint32Array).good() && uint32Array != NULL)
-          {
-            if (element.getLength() % sizeof(Uint32) != 0)
-            {
-              throw OrthancException(ErrorCode_BadFileFormat);
-            }
-
-            uint64_t numValues = static_cast<uint64_t>(element.getLength()) / sizeof(Uint32);
-            numValues = std::min(numValues, static_cast<uint64_t>(maxBinaryArrayLength));
-
-            uint64_t preAllocateStringSize = static_cast<uint64_t>(maxStringLength); // We have seen files with 100M floats: https://discourse.orthanc-server.org/t/orthanc-1-12-11-performance-issues-with-deformable-registrations/6448
-            if (maxStringLength == 0) // no limit, use an heuristic to reserve the string: 10 chars per float
-            {
-              preAllocateStringSize = 10ull * numValues;
-              if (preAllocateStringSize > static_cast<uint64_t>(std::numeric_limits<size_t>::max()))
-              {
-                throw OrthancException(ErrorCode_BadFileFormat, "Too many values in binary array");
-              }
-            }
-
-            std::string result;
-            for (unsigned long i = 0; i < numValues; i++)
-            {
-              if (i > 0)
-              {
-                result += "\\";
-              }
-              result += boost::lexical_cast<std::string>(uint32Array[i]);
-            }
-            return new DicomValue(result, false);
-          }
-          return new DicomValue;
+          ValueRepresentationReader_OL reader(element);
+          return ILeafElementArrayReader::Apply(reader, element.getTag(), maxStringLength, maxBinaryArrayLength);
+        }
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 365
+        case EVR_OV:  // other very long - binary array of 32-bit unsigned integers (new in Orthanc 1.12.11)
+        {
+          ValueRepresentationReader_OV reader(element);
+          return ILeafElementArrayReader::Apply(reader, element.getTag(), maxStringLength, maxBinaryArrayLength);
         }
 #endif
 
@@ -1025,16 +1528,9 @@
 
         case EVR_AT:
         {
-          DcmTagKey tag;
-          if (dynamic_cast<DcmAttributeTag&>(element).getTagVal(tag, 0).good())
-          {
-            DicomTag t(tag.getGroup(), tag.getElement());
-            return new DicomValue(t.Format(), false);
-          }
-          else
-          {
-            return new DicomValue;
-          }
+          // Support for multiple values was added in Orthanc 1.12.12
+          ValueRepresentationReader_AT reader(element);
+          return ILeafElementArrayReader::Apply(reader, element.getTag(), maxStringLength, maxBinaryArrayLength);
         }
 
 
@@ -2262,46 +2758,25 @@
          **/ 
       
         case EVR_SL:  // signed long
-        {
-          if (decoded->find('\\') != std::string::npos)
-          {
-            ok = element.putString(decoded->c_str()).good();
-          }
-          else
-          {
-            ok = element.putSint32(boost::lexical_cast<Sint32>(*decoded)).good();
-          }
+          ok = IElementFiller::Apply(element, ValueRepresentationFiller_SL(), *decoded);
           break;
-        }
 
         case EVR_SS:  // signed short
-        {
-          if (decoded->find('\\') != std::string::npos)
-          {
-            ok = element.putString(decoded->c_str()).good();
-          }
-          else
-          {
-            ok = element.putSint16(boost::lexical_cast<Sint16>(*decoded)).good();
-          }
+          ok = IElementFiller::Apply(element, ValueRepresentationFiller_SS(), *decoded);
           break;
-        }
+
+#if DCMTK_VERSION_NUMBER >= 365
+        case EVR_SV:  // signed very long (64-bit, new in Orthanc 1.12.12)
+          ok = IElementFiller::Apply(element, ValueRepresentationFiller_SV(), *decoded);
+          break;
+#endif
 
         case EVR_UL:  // unsigned long
 #if DCMTK_VERSION_NUMBER >= 362
         case EVR_OL:  // other long (requires byte-swapping)
 #endif
-        {
-          if (decoded->find('\\') != std::string::npos)
-          {
-            ok = element.putString(decoded->c_str()).good();
-          }
-          else
-          {
-            ok = element.putUint32(boost::lexical_cast<Uint32>(*decoded)).good();
-          }
+          ok = IElementFiller::Apply(element, ValueRepresentationFiller_UL(), *decoded);
           break;
-        }
 
         case EVR_xs: // unsigned short, signed short or multiple values
         {
@@ -2321,47 +2796,27 @@
         }
 
         case EVR_US:  // unsigned short
-        {
-          if (decoded->find('\\') != std::string::npos)
-          {
-            ok = element.putString(decoded->c_str()).good();
-          }
-          else
-          {
-            ok = element.putUint16(boost::lexical_cast<Uint16>(*decoded)).good();
-          }
+          ok = IElementFiller::Apply(element, ValueRepresentationFiller_US(), *decoded);
           break;
-        }
+
+#if DCMTK_VERSION_NUMBER >= 365
+        case EVR_UV:  // unsigned very long (64-bit, new in Orthanc 1.12.12)
+        case EVR_OV:  // other very long (64-bit, new in Orthanc 1.12.12)
+          ok = IElementFiller::Apply(element, ValueRepresentationFiller_UV(), *decoded);
+          break;
+#endif
 
         case EVR_FL:  // float single-precision
         case EVR_OF:  // other float (requires byte swapping)
-        {
-          if (decoded->find('\\') != std::string::npos)
-          {
-            ok = element.putString(decoded->c_str()).good();
-          }
-          else
-          {
-            ok = element.putFloat32(boost::lexical_cast<float>(*decoded)).good();
-          }
+          ok = IElementFiller::Apply(element, ValueRepresentationFiller_FL(), *decoded);
           break;
-        }
 
         case EVR_FD:  // float double-precision
 #if DCMTK_VERSION_NUMBER >= 361
         case EVR_OD:  // other double (requires byte-swapping)
 #endif
-        {
-          if (decoded->find('\\') != std::string::npos)
-          {
-            ok = element.putString(decoded->c_str()).good();
-          }
-          else
-          {
-            ok = element.putFloat64(boost::lexical_cast<double>(*decoded)).good();
-          }
+          ok = IElementFiller::Apply(element, ValueRepresentationFiller_FD(), *decoded);
           break;
-        }
 
 
         /**
@@ -2369,11 +2824,9 @@
          **/
         
         case EVR_AT:  // attribute tag, new in Orthanc 1.9.4
-        {
-          DicomTag value = ParseTag(utf8Value);
-          ok = element.putTagVal(DcmTagKey(value.GetGroup(), value.GetElement())).good();
+          // Multiple values are supported since Orthanc 1.12.12
+          ok = IElementFiller::Apply(element, ValueRepresentationFiller_AT(), *decoded);
           break;
-        }
 
           
         /**
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Mon Jul 06 15:06:54 2026 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Mon Jul 06 15:07:11 2026 +0200
@@ -1641,17 +1641,13 @@
 
 
   void JobsRegistry::GetStatistics(unsigned int& pending,
-                                   unsigned int& running,
-                                   unsigned int& success,
-                                   unsigned int& failed)
+                                   unsigned int& running)
   {
     boost::mutex::scoped_lock lock(mutex_);
     CheckInvariants();
 
     pending = 0;
     running = 0;
-    success = 0;
-    failed = 0;
 
     for (JobsIndex::const_iterator it = jobsIndex_.begin();
          it != jobsIndex_.end(); ++it)
@@ -1671,11 +1667,8 @@
           break;
 
         case JobState_Success:
-          success ++;
-          break;
-
         case JobState_Failure:
-          failed ++;
+          // not counted since 1.12.11+
           break;
 
         default:
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.h	Mon Jul 06 15:06:54 2026 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.h	Mon Jul 06 15:07:11 2026 +0200
@@ -196,9 +196,7 @@
     void ResetObserver();
 
     void GetStatistics(unsigned int& pending,
-                       unsigned int& running,
-                       unsigned int& success,
-                       unsigned int& errors);
+                       unsigned int& running);
 
     void GetLastModificationTime(boost::posix_time::ptime& modificationTime) const;
     
--- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Mon Jul 06 15:06:54 2026 +0200
+++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Mon Jul 06 15:07:11 2026 +0200
@@ -64,12 +64,24 @@
 #  include "../Sources/SystemToolbox.h"
 #endif
 
+#include <dcmtk/dcmdata/dcbytstr.h>
 #include <dcmtk/dcmdata/dcdeftag.h>
 #include <dcmtk/dcmdata/dcelem.h>
+#include <dcmtk/dcmdata/dcpxitem.h>
 #include <dcmtk/dcmdata/dcvrat.h>
-#include <dcmtk/dcmdata/dcpxitem.h>
+#include <dcmtk/dcmdata/dcvrat.h>
+#include <dcmtk/dcmdata/dcvrfd.h>
+#include <dcmtk/dcmdata/dcvrfl.h>
+#include <dcmtk/dcmdata/dcvrobow.h>
+#include <dcmtk/dcmdata/dcvrsl.h>
 #include <dcmtk/dcmdata/dcvrss.h>
-#include <dcmtk/dcmdata/dcvrfl.h>
+#include <dcmtk/dcmdata/dcvrul.h>
+#include <dcmtk/dcmdata/dcvrus.h>
+
+#if DCMTK_VERSION_NUMBER >= 365
+#  include <dcmtk/dcmdata/dcvrsv.h>
+#  include <dcmtk/dcmdata/dcvruv.h>
+#endif
 
 #include <boost/algorithm/string/predicate.hpp>
 #include <boost/lexical_cast.hpp>
@@ -3559,6 +3571,405 @@
 }
 
 
+TEST(ParsedDicomFile, FillElementWithString)
+{
+  // This test follows the inheritance diagram of DcmElement:
+  // https://support.dcmtk.org/docs/classDcmElement.html
+
+#if DCMTK_VERSION_NUMBER >= 365
+  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7054, 0x1000), ValueRepresentation_SignedVeryLong, "Tag1", 1, 1, "");
+  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7054, 0x1001), ValueRepresentation_SignedVeryLong, "Tag2", 3, 3, "");
+  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7054, 0x1002), ValueRepresentation_UnsignedVeryLong, "Tag3", 1, 1, "");
+  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7054, 0x1003), ValueRepresentation_UnsignedVeryLong, "Tag4", 3, 3, "");
+#endif
+
+  ParsedDicomFile f(true);
+
+  std::unique_ptr<DcmElement> e;
+  std::unique_ptr<DicomValue> v;
+  const std::set<DicomTag> ignoreLength;
+
+  {
+    e.reset(new DcmAttributeTag(DcmTag(0x0072, 0x0026)));  // SelectorAttribute tag, which has AT as value representation
+    FromDcmtkBridge::FillElementWithString(*e, "HangingProtocolCreator" /* some random tag */, false, Encoding_Utf8);
+    ASSERT_EQ(1u, e->getVM());
+    ASSERT_EQ(EVR_AT, e->getTag().getEVR());
+
+    v.reset(FromDcmtkBridge::ConvertLeafElement(*e, DicomToJsonFlags_None, 0, 0, Encoding_Utf8,
+                                                false, ignoreLength, ValueRepresentation_AttributeTag));
+    ASSERT_TRUE(v->IsString());
+    ASSERT_EQ("0072,0008", v->GetContent());  // This is the "HangingProtocolCreator" tag
+
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->insert(e.release()).good());
+  }
+
+  {
+    e.reset(new DcmAttributeTag(DcmTag(0x0072, 0x0052)));  // SelectorSequencePointer tag, which has AT as value representation (multiple)
+    FromDcmtkBridge::FillElementWithString(*e, "0072,0008\\PatientName\\HangingProtocolCreator" /* some random tags */, false, Encoding_Utf8);
+    ASSERT_EQ(3u, e->getVM());
+    ASSERT_EQ(EVR_AT, e->getTag().getEVR());
+
+    v.reset(FromDcmtkBridge::ConvertLeafElement(*e, DicomToJsonFlags_None, 0, 0, Encoding_Utf8,
+                                                false, ignoreLength, ValueRepresentation_AttributeTag));
+    ASSERT_TRUE(v->IsString());
+    ASSERT_EQ("0072,0008\\0010,0010\\0072,0008", v->GetContent());
+
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->insert(e.release()).good());
+  }
+
+  {
+    e.reset(new DcmByteString(DcmTag(0x0010, 0x0010)));
+    FromDcmtkBridge::FillElementWithString(*e, "HELLO", false, Encoding_Utf8);
+    ASSERT_EQ(1u, e->getVM());
+    ASSERT_EQ(EVR_PN, e->getTag().getEVR());
+
+    v.reset(FromDcmtkBridge::ConvertLeafElement(*e, DicomToJsonFlags_None, 0, 0, Encoding_Utf8,
+                                                false, ignoreLength, ValueRepresentation_PersonName));
+    ASSERT_TRUE(v->IsString());
+    ASSERT_EQ("HELLO", v->GetContent());
+
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->insert(e.release()).good());
+  }
+
+  {
+    e.reset(new DcmByteString(DcmTag(0x0010, 0x1001)));  // Other patient names
+    FromDcmtkBridge::FillElementWithString(*e, "HELLO\\WORLD\\NOPE", false, Encoding_Utf8);
+    ASSERT_EQ(3u, e->getVM());
+    ASSERT_EQ(EVR_PN, e->getTag().getEVR());
+
+    v.reset(FromDcmtkBridge::ConvertLeafElement(*e, DicomToJsonFlags_None, 0, 0, Encoding_Utf8,
+                                                false, ignoreLength, ValueRepresentation_PersonName));
+    ASSERT_TRUE(v->IsString());
+    ASSERT_EQ("HELLO\\WORLD\\NOPE", v->GetContent());
+
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->insert(e.release()).good());
+  }
+
+  {
+    e.reset(new DcmFloatingPointDouble(DcmTag(0x0008, 0x2134)));  // EventTimeOffset
+    FromDcmtkBridge::FillElementWithString(*e, "10.75", false, Encoding_Utf8);
+    ASSERT_EQ(1u, e->getVM());
+    ASSERT_EQ(EVR_FD, e->getTag().getEVR());
+
+    v.reset(FromDcmtkBridge::ConvertLeafElement(*e, DicomToJsonFlags_None, 0, 0, Encoding_Utf8,
+                                                false, ignoreLength, ValueRepresentation_FloatingPointDouble));
+    ASSERT_TRUE(v->IsString());
+    ASSERT_EQ("10.75", v->GetContent());
+
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->insert(e.release()).good());
+  }
+
+  {
+    e.reset(new DcmFloatingPointDouble(DcmTag(0x0008, 0x1163)));  // TimeRange
+    FromDcmtkBridge::FillElementWithString(*e, "5.5\\20.25", false, Encoding_Utf8);
+    ASSERT_EQ(2u, e->getVM());
+    ASSERT_EQ(EVR_FD, e->getTag().getEVR());
+
+    v.reset(FromDcmtkBridge::ConvertLeafElement(*e, DicomToJsonFlags_None, 0, 0, Encoding_Utf8,
+                                                false, ignoreLength, ValueRepresentation_FloatingPointDouble));
+    ASSERT_TRUE(v->IsString());
+    ASSERT_EQ("5.5\\20.25", v->GetContent());
+
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->insert(e.release()).good());
+  }
+
+  {
+    e.reset(new DcmFloatingPointSingle(DcmTag(0x0018, 0x9402)));  // DistanceSourceToIsocenter
+    FromDcmtkBridge::FillElementWithString(*e, "9.75", false, Encoding_Utf8);
+    ASSERT_EQ(1u, e->getVM());
+    ASSERT_EQ(EVR_FL, e->getTag().getEVR());
+
+    v.reset(FromDcmtkBridge::ConvertLeafElement(*e, DicomToJsonFlags_None, 0, 0, Encoding_Utf8,
+                                                false, ignoreLength, ValueRepresentation_FloatingPointSingle));
+    ASSERT_TRUE(v->IsString());
+    ASSERT_EQ("9.75", v->GetContent());
+
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->insert(e.release()).good());
+  }
+
+  {
+    e.reset(new DcmFloatingPointSingle(DcmTag(0x0018, 0x9404)));  // ObjectPixelSpacingInCenterOfBeam
+    FromDcmtkBridge::FillElementWithString(*e, "2.5\\3.25", false, Encoding_Utf8);
+    ASSERT_EQ(2u, e->getVM());
+    ASSERT_EQ(EVR_FL, e->getTag().getEVR());
+
+    v.reset(FromDcmtkBridge::ConvertLeafElement(*e, DicomToJsonFlags_None, 0, 0, Encoding_Utf8,
+                                                false, ignoreLength, ValueRepresentation_FloatingPointSingle));
+    ASSERT_TRUE(v->IsString());
+    ASSERT_EQ("2.5\\3.25", v->GetContent());
+
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->insert(e.release()).good());
+  }
+
+  {
+    e.reset(new DcmOtherByteOtherWord(DcmTag(0x0002, 0x0102)));  // PrivateInformation
+    FromDcmtkBridge::FillElementWithString(*e, "HELLO", false, Encoding_Utf8);
+    ASSERT_EQ(1u, e->getVM());
+    ASSERT_EQ(EVR_OB, e->getTag().getEVR());
+
+    v.reset(FromDcmtkBridge::ConvertLeafElement(*e, DicomToJsonFlags_None, 0, 0, Encoding_Utf8,
+                                                false, ignoreLength, ValueRepresentation_OtherByte));
+    ASSERT_TRUE(v->IsBinary());
+    ASSERT_EQ(6u, v->GetContent().size());  // The value is padded to 2 bytes
+    ASSERT_EQ('H', v->GetContent() [0]);
+    ASSERT_EQ('E', v->GetContent() [1]);
+    ASSERT_EQ('L', v->GetContent() [2]);
+    ASSERT_EQ('L', v->GetContent() [3]);
+    ASSERT_EQ('O', v->GetContent() [4]);
+    ASSERT_EQ('\0', v->GetContent() [5]);
+
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->insert(e.release()).good());
+  }
+
+  {
+    e.reset(new DcmOtherByteOtherWord(DcmTag(0x0002, 0x0102)));  // PrivateInformation
+    FromDcmtkBridge::FillElementWithString(*e, "A\\B\\C", false, Encoding_Utf8);  // No multiplicity exists for OB
+    ASSERT_EQ(1u, e->getVM());
+    ASSERT_EQ(EVR_OB, e->getTag().getEVR());
+
+    v.reset(FromDcmtkBridge::ConvertLeafElement(*e, DicomToJsonFlags_None, 0, 0, Encoding_Utf8,
+                                                false, ignoreLength, ValueRepresentation_OtherByte));
+    ASSERT_TRUE(v->IsBinary());
+    ASSERT_EQ(6u, v->GetContent().size());  // The value is padded to 2 bytes
+  }
+
+#if DCMTK_VERSION_NUMBER >= 365
+  {
+    e.reset(new DcmSigned64bitVeryLong(DcmTag(0x7054, 0x1000)));
+    FromDcmtkBridge::FillElementWithString(*e, "-43", false, Encoding_Utf8);
+    ASSERT_EQ(1u, e->getVM());
+    ASSERT_EQ(EVR_SV, e->getTag().getEVR());
+
+    v.reset(FromDcmtkBridge::ConvertLeafElement(*e, DicomToJsonFlags_None, 0, 0, Encoding_Utf8,
+                                                false, ignoreLength, ValueRepresentation_SignedVeryLong));
+    ASSERT_TRUE(v->IsString());
+    ASSERT_EQ("-43", v->GetContent());
+
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->insert(e.release()).good());
+  }
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 365
+  {
+    e.reset(new DcmSigned64bitVeryLong(DcmTag(0x7054, 0x1001)));
+    FromDcmtkBridge::FillElementWithString(*e, "10\\-41\\-34", false, Encoding_Utf8);
+    ASSERT_EQ(3u, e->getVM());
+    ASSERT_EQ(EVR_SV, e->getTag().getEVR());
+
+    v.reset(FromDcmtkBridge::ConvertLeafElement(*e, DicomToJsonFlags_None, 0, 0, Encoding_Utf8,
+                                                false, ignoreLength, ValueRepresentation_SignedVeryLong));
+    ASSERT_TRUE(v->IsString());
+    ASSERT_EQ("10\\-41\\-34", v->GetContent());
+
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->insert(e.release()).good());
+  }
+#endif
+
+  {
+    e.reset(new DcmSignedLong(DcmTag(0x0018, 0x6020)));  // ReferencePixelX0
+    FromDcmtkBridge::FillElementWithString(*e, "-3", false, Encoding_Utf8);
+    ASSERT_EQ(1u, e->getVM());
+    ASSERT_EQ(EVR_SL, e->getTag().getEVR());
+
+    v.reset(FromDcmtkBridge::ConvertLeafElement(*e, DicomToJsonFlags_None, 0, 0, Encoding_Utf8,
+                                                false, ignoreLength, ValueRepresentation_SignedLong));
+    ASSERT_TRUE(v->IsString());
+    ASSERT_EQ("-3", v->GetContent());
+
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->insert(e.release()).good());
+  }
+
+  {
+    e.reset(new DcmSignedLong(DcmTag(0x0070, 0x0052)));  // DisplayedAreaTopLeftHandCorner
+    FromDcmtkBridge::FillElementWithString(*e, "14\\-13", false, Encoding_Utf8);
+    ASSERT_EQ(2u, e->getVM());
+    ASSERT_EQ(EVR_SL, e->getTag().getEVR());
+
+    v.reset(FromDcmtkBridge::ConvertLeafElement(*e, DicomToJsonFlags_None, 0, 0, Encoding_Utf8,
+                                                false, ignoreLength, ValueRepresentation_SignedLong));
+    ASSERT_TRUE(v->IsString());
+    ASSERT_EQ("14\\-13", v->GetContent());
+
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->insert(e.release()).good());
+  }
+
+  {
+    e.reset(new DcmSignedShort(DcmTag(0x0028, 0x6120)));  // TIDOffset
+    FromDcmtkBridge::FillElementWithString(*e, "-9", false, Encoding_Utf8);
+    ASSERT_EQ(1u, e->getVM());
+    ASSERT_EQ(EVR_SS, e->getTag().getEVR());
+
+    v.reset(FromDcmtkBridge::ConvertLeafElement(*e, DicomToJsonFlags_None, 0, 0, Encoding_Utf8,
+                                                false, ignoreLength, ValueRepresentation_SignedShort));
+    ASSERT_TRUE(v->IsString());
+    ASSERT_EQ("-9", v->GetContent());
+
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->insert(e.release()).good());
+  }
+
+  {
+    e.reset(new DcmSignedShort(DcmTag(0x0028, 0x9503)));  // VerticesOfTheRegion
+    FromDcmtkBridge::FillElementWithString(*e, "1\\-8\\2", false, Encoding_Utf8);
+    ASSERT_EQ(3u, e->getVM());
+    ASSERT_EQ(EVR_SS, e->getTag().getEVR());
+
+    v.reset(FromDcmtkBridge::ConvertLeafElement(*e, DicomToJsonFlags_None, 0, 0, Encoding_Utf8,
+                                                false, ignoreLength, ValueRepresentation_SignedShort));
+    ASSERT_TRUE(v->IsString());
+    ASSERT_EQ("1\\-8\\2", v->GetContent());
+
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->insert(e.release()).good());
+  }
+
+#if DCMTK_VERSION_NUMBER >= 365
+  {
+    e.reset(new DcmUnsigned64bitVeryLong(DcmTag(0x7054, 0x1002)));
+    FromDcmtkBridge::FillElementWithString(*e, "43", false, Encoding_Utf8);
+    ASSERT_EQ(1u, e->getVM());
+    ASSERT_EQ(EVR_UV, e->getTag().getEVR());
+
+    v.reset(FromDcmtkBridge::ConvertLeafElement(*e, DicomToJsonFlags_None, 0, 0, Encoding_Utf8,
+                                                false, ignoreLength, ValueRepresentation_UnsignedVeryLong));
+    ASSERT_TRUE(v->IsString());
+    ASSERT_EQ("43", v->GetContent());
+
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->insert(e.release()).good());
+  }
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 365
+  {
+    e.reset(new DcmUnsigned64bitVeryLong(DcmTag(0x7054, 0x1003)));
+    FromDcmtkBridge::FillElementWithString(*e, "10\\41\\34", false, Encoding_Utf8);
+    ASSERT_EQ(3u, e->getVM());
+    ASSERT_EQ(EVR_UV, e->getTag().getEVR());
+
+    v.reset(FromDcmtkBridge::ConvertLeafElement(*e, DicomToJsonFlags_None, 0, 0, Encoding_Utf8,
+                                                false, ignoreLength, ValueRepresentation_UnsignedVeryLong));
+    ASSERT_TRUE(v->IsString());
+    ASSERT_EQ("10\\41\\34", v->GetContent());
+
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->insert(e.release()).good());
+  }
+#endif
+
+  {
+    e.reset(new DcmUnsignedLong(DcmTag(0x0018, 0x6050)));  // NumberOfTableBreakPoints
+    FromDcmtkBridge::FillElementWithString(*e, "3", false, Encoding_Utf8);
+    ASSERT_EQ(1u, e->getVM());
+    ASSERT_EQ(EVR_UL, e->getTag().getEVR());
+
+    v.reset(FromDcmtkBridge::ConvertLeafElement(*e, DicomToJsonFlags_None, 0, 0, Encoding_Utf8,
+                                                false, ignoreLength, ValueRepresentation_UnsignedLong));
+    ASSERT_TRUE(v->IsString());
+    ASSERT_EQ("3", v->GetContent());
+
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->insert(e.release()).good());
+  }
+
+  {
+    e.reset(new DcmUnsignedLong(DcmTag(0x0018, 0x6052)));  // TableOfXBreakPoints
+    FromDcmtkBridge::FillElementWithString(*e, "14\\13", false, Encoding_Utf8);
+    ASSERT_EQ(2u, e->getVM());
+    ASSERT_EQ(EVR_UL, e->getTag().getEVR());
+
+    v.reset(FromDcmtkBridge::ConvertLeafElement(*e, DicomToJsonFlags_None, 0, 0, Encoding_Utf8,
+                                                false, ignoreLength, ValueRepresentation_UnsignedLong));
+    ASSERT_TRUE(v->IsString());
+    ASSERT_EQ("14\\13", v->GetContent());
+
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->insert(e.release()).good());
+  }
+
+  {
+    e.reset(new DcmUnsignedShort(DcmTag(0x0028, 0x6010)));  // RepresentativeFrameNumber
+    FromDcmtkBridge::FillElementWithString(*e, "9", false, Encoding_Utf8);
+    ASSERT_EQ(1u, e->getVM());
+    ASSERT_EQ(EVR_US, e->getTag().getEVR());
+
+    v.reset(FromDcmtkBridge::ConvertLeafElement(*e, DicomToJsonFlags_None, 0, 0, Encoding_Utf8,
+                                                false, ignoreLength, ValueRepresentation_UnsignedShort));
+    ASSERT_TRUE(v->IsString());
+    ASSERT_EQ("9", v->GetContent());
+
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->insert(e.release()).good());
+  }
+
+  {
+    e.reset(new DcmUnsignedShort(DcmTag(0x0028, 0x6020)));  // FrameNumbersOfInterest
+    FromDcmtkBridge::FillElementWithString(*e, "1\\8\\2", false, Encoding_Utf8);
+    ASSERT_EQ(3u, e->getVM());
+    ASSERT_EQ(EVR_US, e->getTag().getEVR());
+
+    v.reset(FromDcmtkBridge::ConvertLeafElement(*e, DicomToJsonFlags_None, 0, 0, Encoding_Utf8,
+                                                false, ignoreLength, ValueRepresentation_UnsignedShort));
+    ASSERT_TRUE(v->IsString());
+    ASSERT_EQ("1\\8\\2", v->GetContent());
+
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->insert(e.release()).good());
+  }
+
+  std::string s;
+  ASSERT_TRUE(f.GetTagValue(s, DicomTag(0x0072, 0x0026)));
+  ASSERT_EQ("0072,0008", s);
+  ASSERT_TRUE(f.GetTagValue(s, DicomTag(0x0072, 0x0052)));
+  ASSERT_EQ("0072,0008\\0010,0010\\0072,0008", s);
+  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_PATIENT_NAME));
+  ASSERT_EQ("HELLO", s);
+  ASSERT_TRUE(f.GetTagValue(s, DicomTag(0x0010, 0x1001)));
+  ASSERT_EQ("HELLO\\WORLD\\NOPE", s);
+  ASSERT_TRUE(f.GetTagValue(s, DicomTag(0x0008, 0x2134)));
+  ASSERT_EQ("10.75", s);
+  ASSERT_TRUE(f.GetTagValue(s, DicomTag(0x0008, 0x1163)));
+  ASSERT_EQ("5.5\\20.25", s);
+  ASSERT_TRUE(f.GetTagValue(s, DicomTag(0x0018, 0x9402)));
+  ASSERT_EQ("9.75", s);
+  ASSERT_TRUE(f.GetTagValue(s, DicomTag(0x0018, 0x9404)));
+  ASSERT_EQ("2.5\\3.25", s);
+
+  const Uint8 *a = NULL;
+  ASSERT_TRUE(f.GetDcmtkObject().getDataset()->findAndGetUint8Array(DcmTagKey(0x0002, 0x0102), a).good());
+  ASSERT_EQ('H', a[0]);
+  ASSERT_EQ('E', a[1]);
+  ASSERT_EQ('L', a[2]);
+  ASSERT_EQ('L', a[3]);
+  ASSERT_EQ('O', a[4]);
+  ASSERT_EQ('\0', a[5]);
+
+#if DCMTK_VERSION_NUMBER >= 365
+  ASSERT_TRUE(f.GetTagValue(s, DicomTag(0x7054, 0x1000)));
+  ASSERT_EQ("-43", s);
+  ASSERT_TRUE(f.GetTagValue(s, DicomTag(0x7054, 0x1001)));
+  ASSERT_EQ("10\\-41\\-34", s);
+#endif
+
+  ASSERT_TRUE(f.GetTagValue(s, DicomTag(0x0018, 0x6020)));
+  ASSERT_EQ("-3", s);
+  ASSERT_TRUE(f.GetTagValue(s, DicomTag(0x0070, 0x0052)));
+  ASSERT_EQ("14\\-13", s);
+  ASSERT_TRUE(f.GetTagValue(s, DicomTag(0x0028, 0x6120)));
+  ASSERT_EQ("-9", s);
+  ASSERT_TRUE(f.GetTagValue(s, DicomTag(0x0028, 0x9503)));
+  ASSERT_EQ("1\\-8\\2", s);
+
+#if DCMTK_VERSION_NUMBER >= 365
+  ASSERT_TRUE(f.GetTagValue(s, DicomTag(0x7054, 0x1002)));
+  ASSERT_EQ("43", s);
+  ASSERT_TRUE(f.GetTagValue(s, DicomTag(0x7054, 0x1003)));
+  ASSERT_EQ("10\\41\\34", s);
+#endif
+
+  ASSERT_TRUE(f.GetTagValue(s, DicomTag(0x0018, 0x6050)));
+  ASSERT_EQ("3", s);
+  ASSERT_TRUE(f.GetTagValue(s, DicomTag(0x0018, 0x6052)));
+  ASSERT_EQ("14\\13", s);
+  ASSERT_TRUE(f.GetTagValue(s, DicomTag(0x0028, 0x6010)));
+  ASSERT_EQ("9", s);
+  ASSERT_TRUE(f.GetTagValue(s, DicomTag(0x0028, 0x6020)));
+  ASSERT_EQ("1\\8\\2", s);
+}
+
+
 #if ORTHANC_SANDBOXED != 1
 TEST(ParsedDicomFile, DISABLED_InjectEmptyPixelData2)
 {
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/FindRequestHandler.cpp	Mon Jul 06 15:06:54 2026 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/FindRequestHandler.cpp	Mon Jul 06 15:07:11 2026 +0200
@@ -27,6 +27,7 @@
 
 #include "../../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
 #include "../../../../OrthancFramework/Sources/OrthancException.h"
+#include "../../../../OrthancFramework/Sources/Logging.h"
 
 #include "../Common/OrthancPluginCppWrapper.h"
 
@@ -50,6 +51,11 @@
   {
     std::string s;
 
+    if (*it == Orthanc::DICOM_TAG_SPECIFIC_CHARACTER_SET)
+    {
+      continue;  // skip it since we don't want it to be a filter criteria
+    }
+
     if (input.LookupStringValue(s, *it, false) &&
         !s.empty())
     {
@@ -72,6 +78,8 @@
   request["Level"] = EnumerationToString(PluginToolbox::ParseQueryRetrieveLevel(level));
   request["Query"] = query;
 
+  // LOG(INFO) << request.toStyledString();
+
   Json::Value response;
   if (!OrthancPlugins::RestApiPost(response, "/tools/find", request, false) ||
       response.type() != Json::arrayValue)
@@ -79,6 +87,8 @@
     throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, "Invalid DICOM C-FIND request");
   }
 
+  // LOG(INFO) << response.toStyledString();
+
   for (Json::Value::ArrayIndex i = 0; i < response.size(); i++)
   {
     if (response[i].type() != Json::objectValue ||
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.cpp	Mon Jul 06 15:06:54 2026 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.cpp	Mon Jul 06 15:07:11 2026 +0200
@@ -93,7 +93,7 @@
     labelsStoreLevels_.insert(Orthanc::ResourceType_Instance);
   }
   
-  server_.reset(new Orthanc::DicomServer);
+  server_.reset(new Orthanc::DicomServer("MULTI-DCM-SERVER", "MULTI-DCM"));
 
   {
     OrthancPlugins::OrthancConfiguration globalConfig;
--- a/OrthancServer/Sources/OrthancFindRequestHandler.cpp	Mon Jul 06 15:06:54 2026 +0200
+++ b/OrthancServer/Sources/OrthancFindRequestHandler.cpp	Mon Jul 06 15:07:11 2026 +0200
@@ -41,25 +41,22 @@
 
 namespace Orthanc
 {
+
   static void CopySequence(ParsedDicomFile& dicom,
                            const DicomTag& tag,
-                           const Json::Value& source,
+                           const Json::Value& sequenceContent,
                            const std::string& defaultPrivateCreator,
                            const std::map<uint16_t, std::string>& privateCreators)
   {
-    if (source.type() == Json::objectValue &&
-        source.isMember("Type") &&
-        source.isMember("Value") &&
-        source["Type"].asString() == "Sequence" &&
-        source["Value"].type() == Json::arrayValue)
+    if (sequenceContent.isArray())
     {
-      Json::Value content = Json::arrayValue;
+      Json::Value simplifiedContent = Json::arrayValue;
 
-      for (Json::Value::ArrayIndex i = 0; i < source["Value"].size(); i++)
+      for (Json::Value::ArrayIndex i = 0; i < sequenceContent.size(); i++)
       {
         Json::Value item;
-        Toolbox::SimplifyDicomAsJson(item, source["Value"][i], DicomToJsonFormat_Short);
-        content.append(item);
+        Toolbox::SimplifyDicomAsJson(item, sequenceContent[i], DicomToJsonFormat_Short);
+        simplifiedContent.append(item);
       }
 
       if (tag.IsPrivate())
@@ -68,16 +65,16 @@
 
         if (found != privateCreators.end())
         {
-          dicom.Replace(tag, content, false, DicomReplaceMode_InsertIfAbsent, found->second.c_str());
+          dicom.Replace(tag, simplifiedContent, false, DicomReplaceMode_InsertIfAbsent, found->second.c_str());
         }
         else
         {
-          dicom.Replace(tag, content, false, DicomReplaceMode_InsertIfAbsent, defaultPrivateCreator);
+          dicom.Replace(tag, simplifiedContent, false, DicomReplaceMode_InsertIfAbsent, defaultPrivateCreator);
         }
       }
       else
       {
-        dicom.Replace(tag, content, false, DicomReplaceMode_InsertIfAbsent, "" /* no private creator */);
+        dicom.Replace(tag, simplifiedContent, false, DicomReplaceMode_InsertIfAbsent, "" /* no private creator */);
       }
     }
   }
@@ -411,6 +408,11 @@
         continue;
       }
 
+      if (tag.IsPrivateCreator())  // new in 1.12.11+: don't try to match PrivateCreators themselves (they are not mandatory); match only the private tags
+      {
+        continue;
+      }
+
       if (FilterQueryTag(level, tag, connection.GetModalityManufacturer()))
       {
         ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag);
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Mon Jul 06 15:06:54 2026 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Mon Jul 06 15:07:11 2026 +0200
@@ -1005,8 +1005,8 @@
     context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
                                            countStudies, countSeries, countInstances);
 
-    unsigned int jobsPending, jobsRunning, jobsSuccess, jobsFailed;
-    context.GetJobsEngine().GetRegistry().GetStatistics(jobsPending, jobsRunning, jobsSuccess, jobsFailed);
+    unsigned int jobsPending, jobsRunning;
+    context.GetJobsEngine().GetRegistry().GetStatistics(jobsPending, jobsRunning);
 
     int64_t serverUpTime = context.GetServerUpTime();
     Json::Value lastChange;
@@ -1021,9 +1021,6 @@
     registry.SetIntegerValue("orthanc_count_instances", static_cast<int64_t>(countInstances));
     registry.SetIntegerValue("orthanc_jobs_pending", jobsPending);
     registry.SetIntegerValue("orthanc_jobs_running", jobsRunning);
-    registry.SetIntegerValue("orthanc_jobs_completed", jobsSuccess + jobsFailed);
-    registry.SetIntegerValue("orthanc_jobs_success", jobsSuccess);
-    registry.SetIntegerValue("orthanc_jobs_failed", jobsFailed);
     registry.SetIntegerValue("orthanc_up_time_s", serverUpTime);
     registry.SetIntegerValue("orthanc_last_change", lastChange["Last"].asInt64());
 
--- a/OrthancServer/Sources/ResourceFinder.cpp	Mon Jul 06 15:06:54 2026 +0200
+++ b/OrthancServer/Sources/ResourceFinder.cpp	Mon Jul 06 15:07:11 2026 +0200
@@ -980,8 +980,6 @@
     }
     else
     {
-      // TODO-FIND: This fallback shouldn't be necessary
-
       FindRequest requestDicomAttachment(request.GetLevel());
       requestDicomAttachment.SetOrthancId(request.GetLevel(), resource.GetIdentifier());
 
--- a/OrthancServer/Sources/ServerContext.cpp	Mon Jul 06 15:06:54 2026 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Mon Jul 06 15:07:11 2026 +0200
@@ -403,6 +403,8 @@
 
   void ServerContext::SignalJobSuccess(const std::string& jobId)
   {
+    metricsRegistry_->IncrementIntegerValue("orthanc_jobs_total_completed", 1);
+    metricsRegistry_->IncrementIntegerValue("orthanc_jobs_total_success", 1);
     haveJobsChanged_ = true;
     pendingJobEvents_.Enqueue(new JobEvent(JobEventType_Success, jobId));
   }
@@ -410,6 +412,8 @@
   
   void ServerContext::SignalJobFailure(const std::string& jobId)
   {
+    metricsRegistry_->IncrementIntegerValue("orthanc_jobs_total_completed", 1);
+    metricsRegistry_->IncrementIntegerValue("orthanc_jobs_total_failed", 1);
     haveJobsChanged_ = true;
     pendingJobEvents_.Enqueue(new JobEvent(JobEventType_Failure, jobId));
   }
--- a/OrthancServer/Sources/main.cpp	Mon Jul 06 15:06:54 2026 +0200
+++ b/OrthancServer/Sources/main.cpp	Mon Jul 06 15:07:11 2026 +0200
@@ -1382,7 +1382,7 @@
     ModalitiesFromConfiguration modalities;
   
     // Setup the DICOM server  
-    DicomServer dicomServer;
+    DicomServer dicomServer("DICOM-SERVER", "DICOM");
     dicomServer.SetMetricsRegistry(context.GetMetricsRegistry());
     dicomServer.SetRemoteModalities(modalities);
     dicomServer.SetStoreRequestHandlerFactory(serverFactory);
--- a/OrthancServer/UnitTestsSources/UnitTestsMain.cpp	Mon Jul 06 15:06:54 2026 +0200
+++ b/OrthancServer/UnitTestsSources/UnitTestsMain.cpp	Mon Jul 06 15:07:11 2026 +0200
@@ -164,21 +164,19 @@
     }
     else
     {
-      bool skip = false;
-
 #if DCMTK_VERSION_NUMBER < 361
       if (i == ValueRepresentation_OtherDouble ||
           i == ValueRepresentation_UnlimitedCharacters ||
           i == ValueRepresentation_UniversalResource)
       {
-        skip = true;
+        continue;  // Not supported in DCMTK < 3.6.1
       }
 #endif
 
 #if DCMTK_VERSION_NUMBER < 362
       if (i == ValueRepresentation_OtherLong)
       {
-        skip = true;
+        continue;  // Not supported in DCMTK < 3.6.2
       }
 #endif
 
@@ -187,15 +185,17 @@
           i == ValueRepresentation_SignedVeryLong ||
           i == ValueRepresentation_UnsignedVeryLong)
       {
-        skip = true;
+        continue;  // Not supported in DCMTK < 3.6.5
       }
 #endif
 
-      if (!skip)
+      ASSERT_EQ(vr, FromDcmtkBridge::Convert(ToDcmtkBridge::Convert(vr)));
+      OrthancPluginValueRepresentation plugins = Plugins::Convert(vr);
+      ASSERT_EQ(vr, Plugins::Convert(plugins));
+
+      if (vr != ValueRepresentation_Unknown)
       {
-        ASSERT_EQ(vr, FromDcmtkBridge::Convert(ToDcmtkBridge::Convert(vr)));
-        OrthancPluginValueRepresentation plugins = Plugins::Convert(vr);
-        ASSERT_EQ(vr, Plugins::Convert(plugins));
+        ASSERT_NE(OrthancPluginValueRepresentation_UN, plugins);
       }
     }
   }