# HG changeset patch # User Sebastien Jodogne # Date 1630354884 -7200 # Node ID 61da49321754b976d610e2e59db58f8e0f427f40 # Parent b2417ac5055aa40db6b5f83a431fd7db33e66523# Parent 113afe7b594d47b376608ae5f3e28d3b6dcd0806 integration mainline->openssl-3.x diff -r b2417ac5055a -r 61da49321754 CITATION.cff --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/CITATION.cff Mon Aug 30 22:21:24 2021 +0200 @@ -0,0 +1,14 @@ +cff-version: "1.1.0" +message: "If you use this software, please cite it using these metadata." +title: Orthanc +abstract: "Orthanc is a lightweight open-source DICOM server for medical imaging supporting representational state transfer (REST)." +authors: + - + affiliation: UCLouvain + family-names: Jodogne + given-names: "Sébastien" +doi: "10.1007/s10278-018-0082-y" +license: "GPL-3.0-or-later" +repository-code: "https://hg.orthanc-server.com/orthanc/" +version: 1.9.7 +date-released: 2021-08-31 diff -r b2417ac5055a -r 61da49321754 NEWS --- a/NEWS Wed Jul 21 10:48:14 2021 +0200 +++ b/NEWS Mon Aug 30 22:21:24 2021 +0200 @@ -16,6 +16,32 @@ - openssl 3.0.0-beta1 +Version 1.9.7 (2021-08-31) +========================== + +General +------- + +* New configuration option "DicomAlwaysAllowMove" to disable verification of + the remote modality in C-MOVE SCP + +REST API +-------- + +* API version upgraded to 15 +* Added "Level" option to POST /tools/bulk-modify +* Added missing OpenAPI documentation of "KeepSource" in ".../modify" and ".../anonymize" + +Maintenance +----------- + +* Added file CITATION.cff +* Linux Standard Base (LSB) builds of Orthanc can load non-LSB builds of plugins +* Fix upload of ZIP archives containing a DICOMDIR file +* Fix computation of the estimated time of arrival in jobs +* Support detection of windowing and rescale in Philips multiframe images + + Version 1.9.6 (2021-07-21) ========================== diff -r b2417ac5055a -r 61da49321754 OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake --- a/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake Wed Jul 21 10:48:14 2021 +0200 +++ b/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake Mon Aug 30 22:21:24 2021 +0200 @@ -136,6 +136,8 @@ set(ORTHANC_FRAMEWORK_MD5 "10fc64de1254a095e5d3ed3931f0cfbb") elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.6") set(ORTHANC_FRAMEWORK_MD5 "4b5d05683d747c29b2860ad79d11e62e") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.7") + set(ORTHANC_FRAMEWORK_MD5 "c912bbb860d640d3ae3003b5c9698205") # Below this point are development snapshots that were used to # release some plugin, before an official release of the Orthanc diff -r b2417ac5055a -r 61da49321754 OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake --- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake Wed Jul 21 10:48:14 2021 +0200 +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake Mon Aug 30 22:21:24 2021 +0200 @@ -37,7 +37,7 @@ # Version of the Orthanc API, can be retrieved from "/system" URI in # order to check whether new URI endpoints are available even if using # the mainline version of Orthanc -set(ORTHANC_API_VERSION "14") +set(ORTHANC_API_VERSION "15") ##################################################################### diff -r b2417ac5055a -r 61da49321754 OrthancFramework/Sources/DicomFormat/DicomTag.h --- a/OrthancFramework/Sources/DicomFormat/DicomTag.h Wed Jul 21 10:48:14 2021 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomTag.h Mon Aug 30 22:21:24 2021 +0200 @@ -172,6 +172,9 @@ static const DicomTag DICOM_TAG_PATIENT_SPECIES_DESCRIPTION(0x0010, 0x2201); static const DicomTag DICOM_TAG_STUDY_COMMENTS(0x0032, 0x4000); static const DicomTag DICOM_TAG_OTHER_PATIENT_IDS(0x0010, 0x1000); + static const DicomTag DICOM_TAG_PER_FRAME_FUNCTIONAL_GROUP_SEQUENCE(0x5200, 0x9230); + static const DicomTag DICOM_TAG_PIXEL_VALUE_TRANSFORMATION_SEQUENCE(0x0028, 0x9145); + static const DicomTag DICOM_TAG_FRAME_VOI_LUT_SEQUENCE(0x0028, 0x9132); // Tags used within the Stone of Orthanc static const DicomTag DICOM_TAG_FRAME_INCREMENT_POINTER(0x0028, 0x0009); diff -r b2417ac5055a -r 61da49321754 OrthancFramework/Sources/DicomFormat/DicomValue.cpp --- a/OrthancFramework/Sources/DicomFormat/DicomValue.cpp Wed Jul 21 10:48:14 2021 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomValue.cpp Mon Aug 30 22:21:24 2021 +0200 @@ -104,102 +104,110 @@ } #endif - // same as ParseValue but in case the value actually contains a sequence, - // it will return the first value - // this has been introduced to support invalid "width/height" DICOM tags in some US - // images where the width is stored as "800\0" ! - template - static bool ParseFirstValue(T& result, - const DicomValue& source) + bool DicomValue::ParseInteger32(int32_t& result) const + { + if (IsBinary() || + IsNull()) + { + return false; + } + else + { + return SerializationToolbox::ParseInteger32(result, GetContent()); + } + } + + bool DicomValue::ParseInteger64(int64_t& result) const { - if (source.IsBinary() || - source.IsNull()) + if (IsBinary() || + IsNull()) + { + return false; + } + else + { + return SerializationToolbox::ParseInteger64(result, GetContent()); + } + } + + bool DicomValue::ParseUnsignedInteger32(uint32_t& result) const + { + if (IsBinary() || + IsNull()) + { + return false; + } + else + { + return SerializationToolbox::ParseUnsignedInteger32(result, GetContent()); + } + } + + bool DicomValue::ParseUnsignedInteger64(uint64_t& result) const + { + if (IsBinary() || + IsNull()) { return false; } - - try + else { - std::string value = Toolbox::StripSpaces(source.GetContent()); - if (value.empty()) - { - return false; - } - - if (!allowSigned && - value[0] == '-') - { - return false; - } + return SerializationToolbox::ParseUnsignedInteger64(result, GetContent()); + } + } - if (value.find("\\") == std::string::npos) - { - result = boost::lexical_cast(value); - return true; - } - else - { - std::vector tokens; - Toolbox::TokenizeString(tokens, value, '\\'); - - if (tokens.size() >= 1) - { - result = boost::lexical_cast(tokens[0]); - return true; - } - - return false; - } - } - catch (boost::bad_lexical_cast&) + bool DicomValue::ParseFloat(float& result) const + { + if (IsBinary() || + IsNull()) { return false; } + else + { + return SerializationToolbox::ParseFloat(result, GetContent()); + } } + bool DicomValue::ParseDouble(double& result) const + { + if (IsBinary() || + IsNull()) + { + return false; + } + else + { + return SerializationToolbox::ParseDouble(result, GetContent()); + } + } - template - static bool ParseValue(T& result, - const DicomValue& source) + bool DicomValue::ParseFirstFloat(float& result) const { - if (source.IsBinary() || - source.IsNull()) + if (IsBinary() || + IsNull()) { return false; } - - try + else { - std::string value = Toolbox::StripSpaces(source.GetContent()); - if (value.empty()) - { - return false; - } + return SerializationToolbox::ParseFirstFloat(result, GetContent()); + } + } - if (!allowSigned && - value[0] == '-') - { - return false; - } - - result = boost::lexical_cast(value); - return true; - } - catch (boost::bad_lexical_cast&) + bool DicomValue::ParseFirstUnsignedInteger(unsigned int& result) const + { + uint64_t value; + + if (IsBinary() || + IsNull()) { return false; } - } - - bool DicomValue::ParseInteger32(int32_t& result) const - { - int64_t tmp; - if (ParseValue(tmp, *this)) + else if (SerializationToolbox::ParseFirstUnsignedInteger64(value, GetContent())) { - result = static_cast(tmp); - return (tmp == static_cast(result)); // Check no overflow occurs + result = static_cast(value); + return (static_cast(result) == value); // Check no overflow } else { @@ -207,50 +215,6 @@ } } - bool DicomValue::ParseInteger64(int64_t& result) const - { - return ParseValue(result, *this); - } - - bool DicomValue::ParseUnsignedInteger32(uint32_t& result) const - { - uint64_t tmp; - if (ParseValue(tmp, *this)) - { - result = static_cast(tmp); - return (tmp == static_cast(result)); // Check no overflow occurs - } - else - { - return false; - } - } - - bool DicomValue::ParseUnsignedInteger64(uint64_t& result) const - { - return ParseValue(result, *this); - } - - bool DicomValue::ParseFloat(float& result) const - { - return ParseValue(result, *this); - } - - bool DicomValue::ParseDouble(double& result) const - { - return ParseValue(result, *this); - } - - bool DicomValue::ParseFirstFloat(float& result) const - { - return ParseFirstValue(result, *this); - } - - bool DicomValue::ParseFirstUnsignedInteger(unsigned int& result) const - { - return ParseFirstValue(result, *this); - } - bool DicomValue::CopyToString(std::string& result, bool allowBinary) const { diff -r b2417ac5055a -r 61da49321754 OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp --- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Wed Jul 21 10:48:14 2021 +0200 +++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Mon Aug 30 22:21:24 2021 +0200 @@ -2978,10 +2978,10 @@ } - static void ApplyInternal(FromDcmtkBridge::IDicomPathVisitor& visitor, - DcmItem& item, - const DicomPath& pattern, - const DicomPath& actualPath) + void FromDcmtkBridge::IDicomPathVisitor::ApplyInternal(FromDcmtkBridge::IDicomPathVisitor& visitor, + DcmItem& item, + const DicomPath& pattern, + const DicomPath& actualPath) { const size_t level = actualPath.GetPrefixLength(); @@ -3020,9 +3020,9 @@ } - void FromDcmtkBridge::Apply(IDicomPathVisitor& visitor, - DcmDataset& dataset, - const DicomPath& path) + void FromDcmtkBridge::IDicomPathVisitor::Apply(IDicomPathVisitor& visitor, + DcmDataset& dataset, + const DicomPath& path) { DicomPath actualPath(path.GetFinalTag()); ApplyInternal(visitor, dataset, path, actualPath); @@ -3044,7 +3044,7 @@ }; Visitor visitor; - Apply(visitor, dataset, path); + IDicomPathVisitor::Apply(visitor, dataset, path); } @@ -3084,7 +3084,7 @@ }; Visitor visitor(onlyIfExists); - Apply(visitor, dataset, path); + IDicomPathVisitor::Apply(visitor, dataset, path); } @@ -3159,9 +3159,59 @@ else { Visitor visitor(element, mode); - Apply(visitor, dataset, path); + IDicomPathVisitor::Apply(visitor, dataset, path); } } + + + bool FromDcmtkBridge::LookupSequenceItem(DicomMap& target, + DcmDataset& dataset, + const DicomPath& path, + size_t sequenceIndex) + { + class Visitor : public FromDcmtkBridge::IDicomPathVisitor + { + private: + bool found_; + DicomMap& target_; + size_t sequenceIndex_; + + public: + Visitor(DicomMap& target, + size_t sequenceIndex) : + found_(false), + target_(target), + sequenceIndex_(sequenceIndex) + { + } + + virtual void Visit(DcmItem& item, + const DicomPath& path) ORTHANC_OVERRIDE + { + DcmTagKey tag(path.GetFinalTag().GetGroup(), path.GetFinalTag().GetElement()); + + DcmSequenceOfItems *sequence = NULL; + + if (item.findAndGetSequence(tag, sequence).good() && + sequence != NULL && + sequenceIndex_ < sequence->card()) + { + std::set ignoreTagLength; + ExtractDicomSummary(target_, *sequence->getItem(sequenceIndex_), 0, ignoreTagLength); + found_ = true; + } + } + + bool HasFound() const + { + return found_; + } + }; + + Visitor visitor(target, sequenceIndex); + IDicomPathVisitor::Apply(visitor, dataset, path); + return visitor.HasFound(); + } } diff -r b2417ac5055a -r 61da49321754 OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h --- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h Wed Jul 21 10:48:14 2021 +0200 +++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h Mon Aug 30 22:21:24 2021 +0200 @@ -64,6 +64,12 @@ // New in Orthanc 1.9.4 class ORTHANC_PUBLIC IDicomPathVisitor : public boost::noncopyable { + private: + static void ApplyInternal(FromDcmtkBridge::IDicomPathVisitor& visitor, + DcmItem& item, + const DicomPath& pattern, + const DicomPath& actualPath); + public: virtual ~IDicomPathVisitor() { @@ -71,6 +77,10 @@ virtual void Visit(DcmItem& item, const DicomPath& path) = 0; + + static void Apply(IDicomPathVisitor& visitor, + DcmDataset& dataset, + const DicomPath& path); }; @@ -256,10 +266,6 @@ static void LogMissingTagsForStore(DcmDataset& dicom); - static void Apply(IDicomPathVisitor& visitor, - DcmDataset& dataset, - const DicomPath& path); - static void RemovePath(DcmDataset& dataset, const DicomPath& path); @@ -271,5 +277,10 @@ const DicomPath& path, const DcmElement& element, DicomReplaceMode mode); + + static bool LookupSequenceItem(DicomMap& target, + DcmDataset& dataset, + const DicomPath& path, + size_t sequenceIndex); }; } diff -r b2417ac5055a -r 61da49321754 OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp --- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp Wed Jul 21 10:48:14 2021 +0200 +++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp Mon Aug 30 22:21:24 2021 +0200 @@ -78,6 +78,7 @@ #include "../Images/PamReader.h" #include "../Logging.h" #include "../OrthancException.h" +#include "../SerializationToolbox.h" #include "../Toolbox.h" #if ORTHANC_SANDBOXED == 0 @@ -1777,6 +1778,117 @@ } + bool ParsedDicomFile::LookupSequenceItem(DicomMap& target, + const DicomPath& path, + size_t sequenceIndex) const + { + DcmDataset& dataset = *const_cast(*this).GetDcmtkObject().getDataset(); + return FromDcmtkBridge::LookupSequenceItem(target, dataset, path, sequenceIndex); + } + + + void ParsedDicomFile::GetDefaultWindowing(double& windowCenter, + double& windowWidth, + unsigned int frame) const + { + DcmDataset& dataset = *const_cast(*this).GetDcmtkObject().getDataset(); + + const char* wc = NULL; + const char* ww = NULL; + DcmItem *item1 = NULL; + DcmItem *item2 = NULL; + + if (dataset.findAndGetString(DCM_WindowCenter, wc).good() && + dataset.findAndGetString(DCM_WindowWidth, ww).good() && + wc != NULL && + ww != NULL && + SerializationToolbox::ParseFirstDouble(windowCenter, wc) && + SerializationToolbox::ParseFirstDouble(windowWidth, ww)) + { + return; // OK + } + else if (dataset.findAndGetSequenceItem(DCM_PerFrameFunctionalGroupsSequence, item1, frame).good() && + item1 != NULL && + item1->findAndGetSequenceItem(DCM_FrameVOILUTSequence, item2, 0).good() && + item2 != NULL && + item2->findAndGetString(DCM_WindowCenter, wc).good() && + item2->findAndGetString(DCM_WindowWidth, ww).good() && + wc != NULL && + ww != NULL && + SerializationToolbox::ParseFirstDouble(windowCenter, wc) && + SerializationToolbox::ParseFirstDouble(windowWidth, ww)) + { + // New in Orthanc 1.9.7, to deal with Philips multiframe images + // (cf. private mail from Tomas Kenda on 2021-08-17) + return; // OK + } + else + { + Uint16 bitsStored = 0; + if (!dataset.findAndGetUint16(DCM_BitsStored, bitsStored).good() || + bitsStored == 0) + { + bitsStored = 8; // Rough assumption + } + + windowWidth = static_cast(1 << bitsStored); + windowCenter = windowWidth / 2.0f; + } + } + + + void ParsedDicomFile::GetRescale(double& rescaleIntercept, + double& rescaleSlope, + unsigned int frame) const + { + DcmDataset& dataset = *const_cast(*this).GetDcmtkObject().getDataset(); + + const char* sopClassUid = NULL; + const char* intercept = NULL; + const char* slope = NULL; + DcmItem *item1 = NULL; + DcmItem *item2 = NULL; + + if (dataset.findAndGetString(DCM_SOPClassUID, sopClassUid).good() && + sopClassUid != NULL && + std::string(sopClassUid) == std::string(UID_RTDoseStorage)) + { + // We must not take the rescale value into account in the case of doses + rescaleIntercept = 0; + rescaleSlope = 1; + } + else if (dataset.findAndGetString(DCM_RescaleIntercept, intercept).good() && + dataset.findAndGetString(DCM_RescaleSlope, slope).good() && + intercept != NULL && + slope != NULL && + SerializationToolbox::ParseDouble(rescaleIntercept, intercept) && + SerializationToolbox::ParseDouble(rescaleSlope, slope)) + { + return; // OK + } + else if (dataset.findAndGetSequenceItem(DCM_PerFrameFunctionalGroupsSequence, item1, frame).good() && + item1 != NULL && + item1->findAndGetSequenceItem(DCM_PixelValueTransformationSequence, item2, 0).good() && + item2 != NULL && + item2->findAndGetString(DCM_RescaleIntercept, intercept).good() && + item2->findAndGetString(DCM_RescaleSlope, slope).good() && + intercept != NULL && + slope != NULL && + SerializationToolbox::ParseDouble(rescaleIntercept, intercept) && + SerializationToolbox::ParseDouble(rescaleSlope, slope)) + { + // New in Orthanc 1.9.7, to deal with Philips multiframe images + // (cf. private mail from Tomas Kenda on 2021-08-17) + return; // OK + } + else + { + rescaleIntercept = 0; + rescaleSlope = 1; + } + } + + #if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1 // Alias for binary compatibility with Orthanc Framework 1.7.2 => don't use it anymore void ParsedDicomFile::DatasetToJson(Json::Value& target, diff -r b2417ac5055a -r 61da49321754 OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h --- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h Wed Jul 21 10:48:14 2021 +0200 +++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h Mon Aug 30 22:21:24 2021 +0200 @@ -285,5 +285,17 @@ void ClearPath(const DicomPath& path, bool onlyIfExists); + + bool LookupSequenceItem(DicomMap& target, + const DicomPath& path, + size_t sequenceIndex) const; + + void GetDefaultWindowing(double& windowCenter, + double& windowWidth, + unsigned int frame) const; + + void GetRescale(double& rescaleIntercept, + double& rescaleSlope, + unsigned int frame) const; }; } diff -r b2417ac5055a -r 61da49321754 OrthancFramework/Sources/Images/ImageProcessing.cpp --- a/OrthancFramework/Sources/Images/ImageProcessing.cpp Wed Jul 21 10:48:14 2021 +0200 +++ b/OrthancFramework/Sources/Images/ImageProcessing.cpp Mon Aug 30 22:21:24 2021 +0200 @@ -1405,6 +1405,14 @@ } + static bool IsIdentityRescaling(float offset, + float scaling) + { + return (std::abs(offset) <= 10.0f * std::numeric_limits::epsilon() && + std::abs(scaling - 1.0f) <= 10.0f * std::numeric_limits::epsilon()); + } + + void ImageProcessing::ShiftScale2(ImageAccessor& image, float offset, float scaling, @@ -1413,6 +1421,11 @@ // We compute "a * x + b" const float a = scaling; const float b = offset; + + if (IsIdentityRescaling(offset, scaling)) + { + return; + } switch (image.GetFormat()) { @@ -1477,6 +1490,13 @@ const float a = scaling; const float b = offset; + if (target.GetFormat() == source.GetFormat() && + IsIdentityRescaling(offset, scaling)) + { + Copy(target, source); + return; + } + switch (target.GetFormat()) { case PixelFormat_Grayscale8: diff -r b2417ac5055a -r 61da49321754 OrthancFramework/Sources/JobsEngine/JobInfo.cpp --- a/OrthancFramework/Sources/JobsEngine/JobInfo.cpp Wed Jul 21 10:48:14 2021 +0200 +++ b/OrthancFramework/Sources/JobsEngine/JobInfo.cpp Mon Aug 30 22:21:24 2021 +0200 @@ -70,8 +70,8 @@ if (status_.GetProgress() > 0.01f && ms > 0.01f) { - float ratio = static_cast(1.0 - status_.GetProgress()); - long long remaining = boost::math::llround(ratio * ms); + float progress = status_.GetProgress(); + long long remaining = boost::math::llround(ms / progress * (1.0f - progress)); eta_ = timestamp_ + boost::posix_time::milliseconds(remaining); hasEta_ = true; } diff -r b2417ac5055a -r 61da49321754 OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp --- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp Wed Jul 21 10:48:14 2021 +0200 +++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp Mon Aug 30 22:21:24 2021 +0200 @@ -226,6 +226,11 @@ return runtime_; } + void ResetRuntime() + { + runtime_ = boost::posix_time::milliseconds(0); + } + const JobStatus& GetLastStatus() const { return lastStatus_; @@ -1071,6 +1076,7 @@ (void) ok; // Remove warning about unused variable in release builds assert(ok); + found->second->ResetRuntime(); found->second->SetState(JobState_Pending); pendingJobs_.push(found->second); pendingJobAvailable_.notify_one(); diff -r b2417ac5055a -r 61da49321754 OrthancFramework/Sources/SerializationToolbox.cpp --- a/OrthancFramework/Sources/SerializationToolbox.cpp Wed Jul 21 10:48:14 2021 +0200 +++ b/OrthancFramework/Sources/SerializationToolbox.cpp Mon Aug 30 22:21:24 2021 +0200 @@ -24,11 +24,15 @@ #include "SerializationToolbox.h" #include "OrthancException.h" +#include "Toolbox.h" #if ORTHANC_ENABLE_DCMTK == 1 # include "DicomParsing/FromDcmtkBridge.h" #endif +#include + + namespace Orthanc { static bool ParseTagInternal(DicomTag& tag, @@ -445,4 +449,202 @@ value[it->first.Format()] = it->second; } } + + + template + static bool ParseValue(T& target, + const std::string& source) + { + try + { + std::string value = Toolbox::StripSpaces(source); + if (value.empty()) + { + return false; + } + else if (!allowSigned && + value[0] == '-') + { + return false; + } + else + { + target = boost::lexical_cast(value); + return true; + } + } + catch (boost::bad_lexical_cast&) + { + return false; + } + } + + + bool SerializationToolbox::ParseInteger32(int32_t& target, + const std::string& source) + { + int64_t tmp; + if (ParseValue(tmp, source)) + { + target = static_cast(tmp); + return (tmp == static_cast(target)); // Check no overflow occurs + } + else + { + return false; + } + } + + + bool SerializationToolbox::ParseInteger64(int64_t& target, + const std::string& source) + { + return ParseValue(target, source); + } + + + bool SerializationToolbox::ParseUnsignedInteger32(uint32_t& target, + const std::string& source) + { + uint64_t tmp; + if (ParseValue(tmp, source)) + { + target = static_cast(tmp); + return (tmp == static_cast(target)); // Check no overflow occurs + } + else + { + return false; + } + } + + + bool SerializationToolbox::ParseUnsignedInteger64(uint64_t& target, + const std::string& source) + { + return ParseValue(target, source); + } + + + bool SerializationToolbox::ParseFloat(float& target, + const std::string& source) + { + return ParseValue(target, source); + } + + + bool SerializationToolbox::ParseDouble(double& target, + const std::string& source) + { + return ParseValue(target, source); + } + + + static bool GetFirstItem(std::string& target, + const std::string& source) + { + std::vector tokens; + Toolbox::TokenizeString(tokens, source, '\\'); + + if (tokens.empty()) + { + return false; + } + else + { + target = tokens[0]; + return true; + } + } + + + bool SerializationToolbox::ParseFirstInteger32(int32_t& target, + const std::string& source) + { + std::string first; + if (GetFirstItem(first, source)) + { + return ParseInteger32(target, first); + } + else + { + return false; + } + } + + + bool SerializationToolbox::ParseFirstInteger64(int64_t& target, + const std::string& source) + { + std::string first; + if (GetFirstItem(first, source)) + { + return ParseInteger64(target, first); + } + else + { + return false; + } + } + + + bool SerializationToolbox::ParseFirstUnsignedInteger32(uint32_t& target, + const std::string& source) + { + std::string first; + if (GetFirstItem(first, source)) + { + return ParseUnsignedInteger32(target, first); + } + else + { + return false; + } + } + + + bool SerializationToolbox::ParseFirstUnsignedInteger64(uint64_t& target, + const std::string& source) + { + std::string first; + if (GetFirstItem(first, source)) + { + return ParseUnsignedInteger64(target, first); + } + else + { + return false; + } + } + + + bool SerializationToolbox::ParseFirstFloat(float& target, + const std::string& source) + { + std::string first; + if (GetFirstItem(first, source)) + { + return ParseFloat(target, first); + } + else + { + return false; + } + } + + + bool SerializationToolbox::ParseFirstDouble(double& target, + const std::string& source) + { + std::string first; + if (GetFirstItem(first, source)) + { + return ParseDouble(target, first); + } + else + { + return false; + } + } } diff -r b2417ac5055a -r 61da49321754 OrthancFramework/Sources/SerializationToolbox.h --- a/OrthancFramework/Sources/SerializationToolbox.h Wed Jul 21 10:48:14 2021 +0200 +++ b/OrthancFramework/Sources/SerializationToolbox.h Mon Aug 30 22:21:24 2021 +0200 @@ -101,5 +101,41 @@ static void WriteMapOfTags(Json::Value& target, const std::map& values, const std::string& field); + + static bool ParseInteger32(int32_t& result, + const std::string& value); + + static bool ParseInteger64(int64_t& result, + const std::string& value); + + static bool ParseUnsignedInteger32(uint32_t& result, + const std::string& value); + + static bool ParseUnsignedInteger64(uint64_t& result, + const std::string& value); + + static bool ParseFloat(float& result, + const std::string& value); + + static bool ParseDouble(double& result, + const std::string& value); + + static bool ParseFirstInteger32(int32_t& result, + const std::string& value); + + static bool ParseFirstInteger64(int64_t& result, + const std::string& value); + + static bool ParseFirstUnsignedInteger32(uint32_t& result, + const std::string& value); + + static bool ParseFirstUnsignedInteger64(uint64_t& result, + const std::string& value); + + static bool ParseFirstFloat(float& result, + const std::string& value); + + static bool ParseFirstDouble(double& result, + const std::string& value); }; } diff -r b2417ac5055a -r 61da49321754 OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp --- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Wed Jul 21 10:48:14 2021 +0200 +++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Mon Aug 30 22:21:24 2021 +0200 @@ -2739,6 +2739,30 @@ ASSERT_EQ("1.2.840.113619.2.176.2025.1499492.7040.1171286241.726", vv[REF_IM_SEQ][1][REF_SOP_INSTANCE].asString()); // kept ASSERT_EQ("1.2.840.113704.1.111.7016.1342451220.40", vv[REL_SERIES_SEQ][0][STUDY_INSTANCE_UID].asString()); // kept } + + { + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + + DicomMap m; + ASSERT_TRUE(dicom->LookupSequenceItem(m, DicomPath(DICOM_TAG_REFERENCED_IMAGE_SEQUENCE), 0)); + ASSERT_EQ(2u, m.GetSize()); + ASSERT_EQ("1.2.840.113619.2.176.2025.1499492.7040.1171286241.719", + m.GetStringValue(DICOM_TAG_REFERENCED_SOP_INSTANCE_UID, "", false)); + + ASSERT_TRUE(dicom->LookupSequenceItem(m, DicomPath(DICOM_TAG_REFERENCED_IMAGE_SEQUENCE), 1)); + ASSERT_EQ(2u, m.GetSize()); + ASSERT_EQ("1.2.840.113619.2.176.2025.1499492.7040.1171286241.726", + m.GetStringValue(DICOM_TAG_REFERENCED_SOP_INSTANCE_UID, "", false)); + + ASSERT_FALSE(dicom->LookupSequenceItem(m, DicomPath(DICOM_TAG_REFERENCED_IMAGE_SEQUENCE), 2)); + + ASSERT_TRUE(dicom->LookupSequenceItem(m, DicomPath(DicomTag(0x0008, 0x1250), 0, DicomTag(0x0040, 0xa170)), 0)); + ASSERT_EQ(2u, m.GetSize()); + ASSERT_EQ("122403", m.GetStringValue(DicomTag(0x0008, 0x0100), "", false)); + ASSERT_EQ("WORLD", m.GetStringValue(DICOM_TAG_SERIES_DESCRIPTION, "", false)); + + ASSERT_FALSE(dicom->LookupSequenceItem(m, DicomPath(DicomTag(0x0008, 0x1250), 0, DicomTag(0x0040, 0xa170)), 1)); + } } @@ -2995,6 +3019,121 @@ +TEST(ParsedDicomFile, ImageInformation) +{ + double wc, ww; + double ri, rs; + PhotometricInterpretation p; + + { + ParsedDicomFile dicom(false); + dicom.GetDefaultWindowing(wc, ww, 5); + dicom.GetRescale(ri, rs, 5); + ASSERT_DOUBLE_EQ(128.0, wc); + ASSERT_DOUBLE_EQ(256.0, ww); + ASSERT_FALSE(dicom.LookupPhotometricInterpretation(p)); + ASSERT_DOUBLE_EQ(0.0, ri); + ASSERT_DOUBLE_EQ(1.0, rs); + } + + { + ParsedDicomFile dicom(false); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_BitsStored, "4").good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_PhotometricInterpretation, "RGB").good()); + dicom.GetDefaultWindowing(wc, ww, 5); + ASSERT_DOUBLE_EQ(8.0, wc); + ASSERT_DOUBLE_EQ(16.0, ww); + ASSERT_TRUE(dicom.LookupPhotometricInterpretation(p)); + ASSERT_EQ(PhotometricInterpretation_RGB, p); + } + + { + ParsedDicomFile dicom(false); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_WindowCenter, "12").good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_WindowWidth, "-22").good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_RescaleIntercept, "-22").good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_RescaleSlope, "-23").good()); + dicom.GetDefaultWindowing(wc, ww, 5); + dicom.GetRescale(ri, rs, 5); + ASSERT_DOUBLE_EQ(12.0, wc); + ASSERT_DOUBLE_EQ(-22.0, ww); + ASSERT_DOUBLE_EQ(-22.0, ri); + ASSERT_DOUBLE_EQ(-23.0, rs); + } + + { + ParsedDicomFile dicom(false); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_WindowCenter, "12\\13\\14").good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_WindowWidth, "-22\\-23\\-24").good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_RescaleIntercept, "32\\33\\34").good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_RescaleSlope, "-42\\-43\\-44").good()); + dicom.GetDefaultWindowing(wc, ww, 5); + dicom.GetRescale(ri, rs, 5); + ASSERT_DOUBLE_EQ(12.0, wc); + ASSERT_DOUBLE_EQ(-22.0, ww); + ASSERT_DOUBLE_EQ(0.0, ri); + ASSERT_DOUBLE_EQ(1.0, rs); + } + + { + // Philips multiframe + Json::Value v = Json::objectValue; + v["PerFrameFunctionalGroupsSequence"][0]["FrameVOILUTSequence"][0]["WindowCenter"] = "614"; + v["PerFrameFunctionalGroupsSequence"][0]["FrameVOILUTSequence"][0]["WindowWidth"] = "1067"; + v["PerFrameFunctionalGroupsSequence"][0]["PixelValueTransformationSequence"][0]["RescaleIntercept"] = "12"; + v["PerFrameFunctionalGroupsSequence"][0]["PixelValueTransformationSequence"][0]["RescaleSlope"] = "2.551648"; + v["PerFrameFunctionalGroupsSequence"][1]["FrameVOILUTSequence"][0]["WindowCenter"] = "-61"; + v["PerFrameFunctionalGroupsSequence"][1]["FrameVOILUTSequence"][0]["WindowWidth"] = "-63"; + v["PerFrameFunctionalGroupsSequence"][1]["PixelValueTransformationSequence"][0]["RescaleIntercept"] = "13"; + v["PerFrameFunctionalGroupsSequence"][1]["PixelValueTransformationSequence"][0]["RescaleSlope"] = "-14"; + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + + dicom->GetDefaultWindowing(wc, ww, 0); + dicom->GetRescale(ri, rs, 0); + ASSERT_DOUBLE_EQ(614.0, wc); + ASSERT_DOUBLE_EQ(1067.0, ww); + ASSERT_DOUBLE_EQ(12.0, ri); + ASSERT_DOUBLE_EQ(2.551648, rs); + + dicom->GetDefaultWindowing(wc, ww, 1); + dicom->GetRescale(ri, rs, 1); + ASSERT_DOUBLE_EQ(-61.0, wc); + ASSERT_DOUBLE_EQ(-63.0, ww); + ASSERT_DOUBLE_EQ(13.0, ri); + ASSERT_DOUBLE_EQ(-14.0, rs); + + dicom->GetDefaultWindowing(wc, ww, 2); + dicom->GetRescale(ri, rs, 2); + ASSERT_DOUBLE_EQ(128.0, wc); + ASSERT_DOUBLE_EQ(256.0, ww); + ASSERT_DOUBLE_EQ(0.0, ri); + ASSERT_DOUBLE_EQ(1.0, rs); + } + + { + // RT-DOSE + Json::Value v = Json::objectValue; + v["RescaleIntercept"] = "10"; + v["RescaleSlope"] = "20"; + v["PerFrameFunctionalGroupsSequence"][0]["PixelValueTransformationSequence"][0]["RescaleIntercept"] = "30"; + v["PerFrameFunctionalGroupsSequence"][0]["PixelValueTransformationSequence"][0]["RescaleSlope"] = "40"; + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + + dicom->GetRescale(ri, rs, 0); + ASSERT_DOUBLE_EQ(10.0, ri); + ASSERT_DOUBLE_EQ(20.0, rs); + + v["SOPClassUID"] = "1.2.840.10008.5.1.4.1.1.481.2"; + dicom.reset(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + dicom->GetRescale(ri, rs, 0); + ASSERT_DOUBLE_EQ(0.0, ri); + ASSERT_DOUBLE_EQ(1.0, rs); + } +} + + + + #if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 #include "../Sources/DicomNetworking/DicomStoreUserConnection.h" diff -r b2417ac5055a -r 61da49321754 OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp --- a/OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp Wed Jul 21 10:48:14 2021 +0200 +++ b/OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp Mon Aug 30 22:21:24 2021 +0200 @@ -975,6 +975,25 @@ } +TEST(ImageProcessing, Grayscale8_Identity) +{ + Image image(PixelFormat_Float32, 5, 1, false); + ImageTraits::SetPixel(image, 0, 0, 0); + ImageTraits::SetPixel(image, 2.5, 1, 0); + ImageTraits::SetPixel(image, 5.5, 2, 0); + ImageTraits::SetPixel(image, 10.5, 3, 0); + ImageTraits::SetPixel(image, 255.5, 4, 0); + + Image image2(PixelFormat_Grayscale8, 5, 1, false); + ImageProcessing::ShiftScale(image2, image, 0, 1, false); + ASSERT_TRUE(TestGrayscale8Pixel(image2, 0, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(image2, 1, 0, 2)); + ASSERT_TRUE(TestGrayscale8Pixel(image2, 2, 0, 5)); + ASSERT_TRUE(TestGrayscale8Pixel(image2, 3, 0, 10)); + ASSERT_TRUE(TestGrayscale8Pixel(image2, 4, 0, 255)); +} + + TEST(ImageProcessing, ShiftScaleGrayscale16) { Image image(PixelFormat_Grayscale16, 5, 1, false); @@ -1011,6 +1030,24 @@ } +TEST(ImageProcessing, ShiftScaleSignedGrayscale16_Identity) +{ + Image image(PixelFormat_SignedGrayscale16, 5, 1, false); + SetSignedGrayscale16Pixel(image, 0, 0, 0); + SetSignedGrayscale16Pixel(image, 1, 0, 2); + SetSignedGrayscale16Pixel(image, 2, 0, 5); + SetSignedGrayscale16Pixel(image, 3, 0, 10); + SetSignedGrayscale16Pixel(image, 4, 0, 255); + + ImageProcessing::ShiftScale(image, 0, 1, true); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 0, 0, 0)); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 1, 0, 2)); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 2, 0, 5)); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 3, 0, 10)); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 4, 0, 255)); +} + + TEST(ImageProcessing, ShiftScale2) { std::vector va; diff -r b2417ac5055a -r 61da49321754 OrthancFramework/UnitTestsSources/JobsTests.cpp --- a/OrthancFramework/UnitTestsSources/JobsTests.cpp Wed Jul 21 10:48:14 2021 +0200 +++ b/OrthancFramework/UnitTestsSources/JobsTests.cpp Mon Aug 30 22:21:24 2021 +0200 @@ -1575,3 +1575,86 @@ ASSERT_FALSE(b.IsRemoteCertificateRequired()); } } + + +TEST(SerializationToolbox, Numbers) +{ + { + int32_t i; + ASSERT_FALSE(SerializationToolbox::ParseInteger32(i, "")); + ASSERT_FALSE(SerializationToolbox::ParseInteger32(i, "ee")); + ASSERT_TRUE(SerializationToolbox::ParseInteger32(i, "42")); ASSERT_EQ(42, i); + ASSERT_TRUE(SerializationToolbox::ParseInteger32(i, "-42")); ASSERT_EQ(-42, i); + ASSERT_TRUE(SerializationToolbox::ParseInteger32(i, "-2147483648")); ASSERT_EQ(-2147483648l, i); + ASSERT_TRUE(SerializationToolbox::ParseInteger32(i, "2147483647")); ASSERT_EQ(2147483647l, i); + ASSERT_FALSE(SerializationToolbox::ParseInteger32(i, "-2147483649")); + ASSERT_FALSE(SerializationToolbox::ParseInteger32(i, "2147483648")); + ASSERT_FALSE(SerializationToolbox::ParseInteger32(i, "-2\\-3\\-4")); + ASSERT_TRUE(SerializationToolbox::ParseFirstInteger32(i, "-2\\-3\\-4")); ASSERT_EQ(-2, i); + } + + { + uint32_t i; + ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger32(i, "")); + ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger32(i, "ee")); + ASSERT_TRUE(SerializationToolbox::ParseUnsignedInteger32(i, "42")); ASSERT_EQ(42u, i); + ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger32(i, "-42")); + ASSERT_TRUE(SerializationToolbox::ParseUnsignedInteger32(i, "4294967295")); ASSERT_EQ(4294967295u, i); + ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger32(i, "4294967296")); + ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger32(i, "2\\3\\4")); + ASSERT_TRUE(SerializationToolbox::ParseFirstUnsignedInteger32(i, "2\\3\\4")); ASSERT_EQ(2u, i); + } + + { + int64_t i; + ASSERT_FALSE(SerializationToolbox::ParseInteger64(i, "")); + ASSERT_FALSE(SerializationToolbox::ParseInteger64(i, "ee")); + ASSERT_TRUE(SerializationToolbox::ParseInteger64(i, "42")); ASSERT_EQ(42, i); + ASSERT_TRUE(SerializationToolbox::ParseInteger64(i, "-42")); ASSERT_EQ(-42, i); + ASSERT_TRUE(SerializationToolbox::ParseInteger64(i, "-2147483649")); ASSERT_EQ(-2147483649ll, i); + ASSERT_TRUE(SerializationToolbox::ParseInteger64(i, "2147483648")); ASSERT_EQ(2147483648ll, i); + ASSERT_FALSE(SerializationToolbox::ParseInteger64(i, "-2\\-3\\-4")); + ASSERT_TRUE(SerializationToolbox::ParseFirstInteger64(i, "-2\\-3\\-4")); ASSERT_EQ(-2, i); + } + + { + uint64_t i; + ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger64(i, "")); + ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger64(i, "ee")); + ASSERT_TRUE(SerializationToolbox::ParseUnsignedInteger64(i, "42")); ASSERT_EQ(42u, i); + ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger64(i, "-42")); + ASSERT_TRUE(SerializationToolbox::ParseUnsignedInteger64(i, "4294967296")); ASSERT_EQ(4294967296lu, i); + ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger64(i, "2\\3\\4")); + ASSERT_TRUE(SerializationToolbox::ParseFirstUnsignedInteger64(i, "2\\3\\4")); ASSERT_EQ(2u, i); + } + + { + float i; + ASSERT_FALSE(SerializationToolbox::ParseFloat(i, "")); + ASSERT_FALSE(SerializationToolbox::ParseFloat(i, "ee")); + ASSERT_TRUE(SerializationToolbox::ParseFloat(i, "42")); ASSERT_FLOAT_EQ(42.0f, i); + ASSERT_TRUE(SerializationToolbox::ParseFloat(i, "-42")); ASSERT_FLOAT_EQ(-42.0f, i); + ASSERT_FALSE(SerializationToolbox::ParseFloat(i, "2\\3\\4")); + ASSERT_TRUE(SerializationToolbox::ParseFirstFloat(i, "1.367\\2.367\\3.367")); ASSERT_FLOAT_EQ(1.367f, i); + + ASSERT_TRUE(SerializationToolbox::ParseFloat(i, "1.2")); ASSERT_FLOAT_EQ(1.2f, i); + ASSERT_TRUE(SerializationToolbox::ParseFloat(i, "-1.2e+2")); ASSERT_FLOAT_EQ(-120.0f, i); + ASSERT_TRUE(SerializationToolbox::ParseFloat(i, "-1e-2")); ASSERT_FLOAT_EQ(-0.01f, i); + ASSERT_TRUE(SerializationToolbox::ParseFloat(i, "1.3671875")); ASSERT_FLOAT_EQ(1.3671875f, i); + } + + { + double i; + ASSERT_FALSE(SerializationToolbox::ParseDouble(i, "")); + ASSERT_FALSE(SerializationToolbox::ParseDouble(i, "ee")); + ASSERT_TRUE(SerializationToolbox::ParseDouble(i, "42")); ASSERT_DOUBLE_EQ(42.0, i); + ASSERT_TRUE(SerializationToolbox::ParseDouble(i, "-42")); ASSERT_DOUBLE_EQ(-42.0, i); + ASSERT_FALSE(SerializationToolbox::ParseDouble(i, "2\\3\\4")); + ASSERT_TRUE(SerializationToolbox::ParseFirstDouble(i, "1.367\\2.367\\3.367")); ASSERT_DOUBLE_EQ(1.367, i); + + ASSERT_TRUE(SerializationToolbox::ParseDouble(i, "1.2")); ASSERT_DOUBLE_EQ(1.2, i); + ASSERT_TRUE(SerializationToolbox::ParseDouble(i, "-1.2e+2")); ASSERT_DOUBLE_EQ(-120.0, i); + ASSERT_TRUE(SerializationToolbox::ParseDouble(i, "-1e-2")); ASSERT_DOUBLE_EQ(-0.01, i); + ASSERT_TRUE(SerializationToolbox::ParseDouble(i, "1.3671875")); ASSERT_DOUBLE_EQ(1.3671875, i); + } +} diff -r b2417ac5055a -r 61da49321754 OrthancServer/CMakeLists.txt --- a/OrthancServer/CMakeLists.txt Wed Jul 21 10:48:14 2021 +0200 +++ b/OrthancServer/CMakeLists.txt Mon Aug 30 22:21:24 2021 +0200 @@ -385,6 +385,16 @@ target_link_libraries(Orthanc ServerLibrary CoreLibrary ${DCMTK_LIBRARIES}) +if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + # The link flag below hides all the global functions so that a Linux + # Standard Base (LSB) build of Orthanc can load plugins that are not + # built using LSB (new in Orthanc 1.9.7) + set_property( + TARGET Orthanc + PROPERTY LINK_FLAGS "-Wl,--version-script=${CMAKE_SOURCE_DIR}/Resources/VersionScriptOrthanc.map" + ) +endif() + install( TARGETS Orthanc RUNTIME DESTINATION sbin diff -r b2417ac5055a -r 61da49321754 OrthancServer/Resources/Configuration.json --- a/OrthancServer/Resources/Configuration.json Wed Jul 21 10:48:14 2021 +0200 +++ b/OrthancServer/Resources/Configuration.json Mon Aug 30 22:21:24 2021 +0200 @@ -291,6 +291,12 @@ // option to "true" implies security risks. (new in Orthanc 1.9.0) "DicomAlwaysAllowGet" : false, + // Whether the Orthanc SCP allows incoming C-MOVE requests, even + // from SCU modalities it does not know about (i.e. that are not + // listed in the "DicomModalities" option above). Setting this + // option to "true" implies security risks. (new in Orthanc 1.9.7) + "DicomAlwaysAllowMove" : false, + // Whether Orthanc checks the IP/hostname address of the remote // modality initiating a DICOM connection (as listed in the // "DicomModalities" option above). If this option is set to diff -r b2417ac5055a -r 61da49321754 OrthancServer/Resources/VersionScriptOrthanc.map --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Resources/VersionScriptOrthanc.map Mon Aug 30 22:21:24 2021 +0200 @@ -0,0 +1,13 @@ +# This is a version-script for the main Orthanc binary, that hides all +# the global functions of the executable so that a Linux Standard Base +# (LSB) build of Orthanc can load plugins that are not built using +# LSB. Otherwise, the dynamic loader of the plugins will try to use +# the global functions published by the Orthanc server, which results +# in a segmentation fault if the data structures don't have the same +# memory layout (e.g. debug vs. release, or another version of some +# C/C++ library used both by Orthanc, typically jsoncpp). + +{ +local: + *; +}; diff -r b2417ac5055a -r 61da49321754 OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp Wed Jul 21 10:48:14 2021 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp Mon Aug 30 22:21:24 2021 +0200 @@ -46,6 +46,7 @@ static const char* const KEEP = "Keep"; static const char* const KEEP_PRIVATE_TAGS = "KeepPrivateTags"; static const char* const KEEP_SOURCE = "KeepSource"; +static const char* const LEVEL = "Level"; static const char* const PARENT = "Parent"; static const char* const PRIVATE_CREATOR = "PrivateCreator"; static const char* const REMOVE = "Remove"; @@ -68,6 +69,15 @@ } + static void DocumentKeepSource(RestApiPostCall& call) + { + call.GetDocumentation() + .SetRequestField(KEEP_SOURCE, RestApiCallDocumentation::Type_Boolean, + "If set to `false`, instructs Orthanc to the remove original resources. " + "By default, the original resources are kept in Orthanc.", false); + } + + static void DocumentModifyOptions(RestApiPostCall& call) { // Check out "DicomModification::ParseModifyRequest()" @@ -90,6 +100,9 @@ "as this breaks the DICOM model of the real world.", false) .SetRequestField(PRIVATE_CREATOR, RestApiCallDocumentation::Type_String, "The private creator to be used for private tags in `Replace`", false); + + // This was existing, but undocumented in Orthanc <= 1.9.6 + DocumentKeepSource(call); } @@ -113,6 +126,9 @@ "List of DICOM tags whose value must not be destroyed by the anonymization. " INFO_SUBSEQUENCES, false) .SetRequestField(PRIVATE_CREATOR, RestApiCallDocumentation::Type_String, "The private creator to be used for private tags in `Replace`", false); + + // This was existing, but undocumented in Orthanc <= 1.9.6 + DocumentKeepSource(call); } @@ -431,6 +447,11 @@ .SetSummary("Modify a set of resources") .SetRequestField(RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings, "List of the Orthanc identifiers of the patients/studies/series/instances of interest.", true) + .SetRequestField(LEVEL, RestApiCallDocumentation::Type_String, + "Level of the modification (`Patient`, `Study`, `Series` or `Instance`). If absent, " + "the level defaults to `Instance`, but is set to `Patient` if `PatientID` is modified, " + "to `Study` if `StudyInstanceUID` is modified, or to `Series` if `SeriesInstancesUID` " + "is modified. (new in Orthanc 1.9.7)", false) .SetDescription("Start a job that will modify all the DICOM patients, studies, series or instances " "whose identifiers are provided in the `Resources` field.") .AddAnswerType(MimeType_Json, "The list of all the resources that have been altered by this modification"); @@ -442,7 +463,15 @@ Json::Value body; ParseModifyRequest(body, *modification, call); - modification->SetLevel(DetectModifyLevel(*modification)); + if (body.isMember(LEVEL)) + { + // This case was introduced in Orthanc 1.9.7 + modification->SetLevel(StringToResourceType(body[LEVEL].asCString())); + } + else + { + modification->SetLevel(DetectModifyLevel(*modification)); + } SubmitBulkJob(modification, false /* not an anonymization */, call, body); } diff -r b2417ac5055a -r 61da49321754 OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp Wed Jul 21 10:48:14 2021 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp Mon Aug 30 22:21:24 2021 +0200 @@ -199,6 +199,15 @@ { LOG(ERROR) << "Cannot import non-DICOM file from ZIP archive: " << filename; } + else if (e.GetErrorCode() == ErrorCode_InexistentTag) + { + /** + * Allow upload of ZIP archives containing a DICOMDIR + * file (new in Orthanc 1.9.7): + * https://groups.google.com/g/orthanc-users/c/sgBU89o4nhU/m/kbRAYiQUAAAJ + **/ + LOG(ERROR) << "Ignoring what is probably a DICOMDIR file within a ZIP archive: \"" << filename << "\""; + } else { throw; diff -r b2417ac5055a -r 61da49321754 OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Wed Jul 21 10:48:14 2021 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Mon Aug 30 22:21:24 2021 +0200 @@ -651,9 +651,11 @@ { } + // "dicom" is non-NULL iff. "RequiresDicomTags() == true" virtual void Handle(RestApiGetCall& call, std::unique_ptr& decoded, - const DicomMap& dicom) = 0; + const ParsedDicomFile* dicom, + unsigned int frame) = 0; virtual bool RequiresDicomTags() const = 0; @@ -764,7 +766,6 @@ return; } - DicomMap dicom; std::unique_ptr decoded; try @@ -787,7 +788,11 @@ * interpretation, and with windowing parameters. **/ ServerContext::DicomCacheLocker locker(context, publicId); - OrthancConfiguration::DefaultExtractDicomSummary(dicom, locker.GetDicom()); + handler.Handle(call, decoded, &locker.GetDicom(), frame); + } + else + { + handler.Handle(call, decoded, NULL, frame); } } catch (OrthancException& e) @@ -811,7 +816,6 @@ return; } - handler.Handle(call, decoded, dicom); } @@ -853,13 +857,22 @@ virtual void Handle(RestApiGetCall& call, std::unique_ptr& decoded, - const DicomMap& dicom) ORTHANC_OVERRIDE + const ParsedDicomFile* dicom, + unsigned int frame) ORTHANC_OVERRIDE { bool invert = false; if (mode_ == ImageExtractionMode_Preview) { - DicomImageInformation info(dicom); + if (dicom == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + DicomMap tags; + OrthancConfiguration::DefaultExtractDicomSummary(tags, *dicom); + + DicomImageInformation info(tags); invert = (info.GetPhotometricInterpretation() == PhotometricInterpretation_Monochrome1); } @@ -876,40 +889,8 @@ class RenderedFrameHandler : public IDecodedFrameHandler { private: - static void GetDicomParameters(bool& invert, - float& rescaleSlope, - float& rescaleIntercept, - float& windowWidth, - float& windowCenter, - const DicomMap& dicom) - { - DicomImageInformation info(dicom); - - invert = (info.GetPhotometricInterpretation() == PhotometricInterpretation_Monochrome1); - - rescaleSlope = 1.0f; - rescaleIntercept = 0.0f; - - if (dicom.HasTag(Orthanc::DICOM_TAG_RESCALE_SLOPE) && - dicom.HasTag(Orthanc::DICOM_TAG_RESCALE_INTERCEPT)) - { - dicom.ParseFloat(rescaleSlope, Orthanc::DICOM_TAG_RESCALE_SLOPE); - dicom.ParseFloat(rescaleIntercept, Orthanc::DICOM_TAG_RESCALE_INTERCEPT); - } - - windowWidth = static_cast(1 << info.GetBitsStored()) * rescaleSlope; - windowCenter = windowWidth / 2.0f + rescaleIntercept; - - if (dicom.HasTag(Orthanc::DICOM_TAG_WINDOW_CENTER) && - dicom.HasTag(Orthanc::DICOM_TAG_WINDOW_WIDTH)) - { - dicom.ParseFirstFloat(windowCenter, Orthanc::DICOM_TAG_WINDOW_CENTER); - dicom.ParseFirstFloat(windowWidth, Orthanc::DICOM_TAG_WINDOW_WIDTH); - } - } - - static void GetUserArguments(float& windowWidth /* inout */, - float& windowCenter /* inout */, + static void GetUserArguments(double& windowWidth /* inout */, + double& windowCenter /* inout */, unsigned int& argWidth, unsigned int& argHeight, bool& smooth, @@ -921,30 +902,18 @@ static const char* ARG_HEIGHT = "height"; static const char* ARG_SMOOTH = "smooth"; - if (call.HasArgument(ARG_WINDOW_WIDTH)) + if (call.HasArgument(ARG_WINDOW_WIDTH) && + !SerializationToolbox::ParseDouble(windowWidth, call.GetArgument(ARG_WINDOW_WIDTH, ""))) { - try - { - windowWidth = boost::lexical_cast(call.GetArgument(ARG_WINDOW_WIDTH, "")); - } - catch (boost::bad_lexical_cast&) - { - throw OrthancException(ErrorCode_ParameterOutOfRange, - "Bad value for argument: " + std::string(ARG_WINDOW_WIDTH)); - } + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Bad value for argument: " + std::string(ARG_WINDOW_WIDTH)); } - if (call.HasArgument(ARG_WINDOW_CENTER)) + if (call.HasArgument(ARG_WINDOW_CENTER) && + !SerializationToolbox::ParseDouble(windowCenter, call.GetArgument(ARG_WINDOW_CENTER, ""))) { - try - { - windowCenter = boost::lexical_cast(call.GetArgument(ARG_WINDOW_CENTER, "")); - } - catch (boost::bad_lexical_cast&) - { - throw OrthancException(ErrorCode_ParameterOutOfRange, - "Bad value for argument: " + std::string(ARG_WINDOW_CENTER)); - } + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Bad value for argument: " + std::string(ARG_WINDOW_CENTER)); } argWidth = 0; @@ -1006,12 +975,22 @@ public: virtual void Handle(RestApiGetCall& call, std::unique_ptr& decoded, - const DicomMap& dicom) ORTHANC_OVERRIDE + const ParsedDicomFile* dicom, + unsigned int frame) ORTHANC_OVERRIDE { - bool invert; - float rescaleSlope, rescaleIntercept, windowWidth, windowCenter; - GetDicomParameters(invert, rescaleSlope, rescaleIntercept, windowWidth, windowCenter, dicom); - + if (dicom == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + PhotometricInterpretation photometric; + const bool invert = (dicom->LookupPhotometricInterpretation(photometric) && + photometric == PhotometricInterpretation_Monochrome1); + + double rescaleIntercept, rescaleSlope, windowCenter, windowWidth; + dicom->GetRescale(rescaleIntercept, rescaleSlope, frame); + dicom->GetDefaultWindowing(windowCenter, windowWidth, frame); + unsigned int argWidth, argHeight; bool smooth; GetUserArguments(windowWidth, windowCenter, argWidth, argHeight, smooth, call); @@ -1081,16 +1060,16 @@ windowWidth = 1; } - if (std::abs(rescaleSlope) <= 0.1f) + if (std::abs(rescaleSlope) <= 0.1) { - rescaleSlope = 0.1f; + rescaleSlope = 0.1; } - const float scaling = 255.0f * rescaleSlope / windowWidth; - const float offset = (rescaleIntercept - windowCenter + windowWidth / 2.0f) / rescaleSlope; + const double scaling = 255.0 * rescaleSlope / windowWidth; + const double offset = (rescaleIntercept - windowCenter + windowWidth / 2.0) / rescaleSlope; std::unique_ptr rescaled(new Image(PixelFormat_Grayscale8, decoded->GetWidth(), decoded->GetHeight(), false)); - ImageProcessing::ShiftScale(*rescaled, converted, offset, scaling, false); + ImageProcessing::ShiftScale(*rescaled, converted, static_cast(offset), static_cast(scaling), false); if (targetWidth == decoded->GetWidth() && targetHeight == decoded->GetHeight()) diff -r b2417ac5055a -r 61da49321754 OrthancServer/Sources/main.cpp --- a/OrthancServer/Sources/main.cpp Wed Jul 21 10:48:14 2021 +0200 +++ b/OrthancServer/Sources/main.cpp Mon Aug 30 22:21:24 2021 +0200 @@ -267,6 +267,7 @@ bool alwaysAllowEcho_; bool alwaysAllowFind_; // New in Orthanc 1.9.0 bool alwaysAllowGet_; // New in Orthanc 1.9.0 + bool alwaysAllowMove_; // New in Orthanc 1.9.7 bool alwaysAllowStore_; public: @@ -278,6 +279,7 @@ alwaysAllowEcho_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowEcho", true); alwaysAllowFind_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowFind", false); alwaysAllowGet_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowGet", false); + alwaysAllowMove_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowMove", false); alwaysAllowStore_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowStore", true); } @@ -290,6 +292,11 @@ { LOG(WARNING) << "Security risk in DICOM SCP: C-GET requests are always allowed, even from unknown modalities"; } + + if (alwaysAllowMove_) + { + LOG(WARNING) << "Security risk in DICOM SCP: C-MOOVE requests are always allowed, even from unknown modalities"; + } } virtual bool IsAllowedConnection(const std::string& remoteIp, @@ -302,6 +309,7 @@ if (alwaysAllowEcho_ || alwaysAllowFind_ || alwaysAllowGet_ || + alwaysAllowMove_ || alwaysAllowStore_) { return true; @@ -356,6 +364,12 @@ // Incoming C-Get requests are always accepted, even from unknown AET return true; } + else if (type == DicomRequestType_Move && + alwaysAllowMove_) + { + // Incoming C-Move requests are always accepted, even from unknown AET + return true; + } else { bool checkIp;