Mercurial > hg > orthanc
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); } } }
