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