# HG changeset patch # User Sebastien Jodogne # Date 1582709995 -3600 # Node ID 89890302283659799bf8aa623babb8e32026593a # Parent 9dac85e807c2a1040fd30a680d6dc7ff17590231# Parent 9107cca846b6e9368c705ea482f30884d28a5d30 integration mainline->storage-commitment diff -r 9dac85e807c2 -r 898903022836 Core/DicomFormat/DicomMap.cpp --- a/Core/DicomFormat/DicomMap.cpp Thu Feb 20 20:36:47 2020 +0100 +++ b/Core/DicomFormat/DicomMap.cpp Wed Feb 26 10:39:55 2020 +0100 @@ -984,6 +984,21 @@ } } + bool DicomMap::ParseFirstFloat(float& result, + const DicomTag& tag) const + { + const DicomValue* value = TestAndGetValue(tag); + + if (value == NULL) + { + return false; + } + else + { + return value->ParseFirstFloat(result); + } + } + bool DicomMap::ParseDouble(double& result, const DicomTag& tag) const { diff -r 9dac85e807c2 -r 898903022836 Core/DicomFormat/DicomMap.h --- a/Core/DicomFormat/DicomMap.h Thu Feb 20 20:36:47 2020 +0100 +++ b/Core/DicomFormat/DicomMap.h Wed Feb 26 10:39:55 2020 +0100 @@ -205,6 +205,9 @@ bool ParseFloat(float& result, const DicomTag& tag) const; + bool ParseFirstFloat(float& result, + const DicomTag& tag) const; + bool ParseDouble(double& result, const DicomTag& tag) const; diff -r 9dac85e807c2 -r 898903022836 Core/DicomFormat/DicomValue.cpp --- a/Core/DicomFormat/DicomValue.cpp Thu Feb 20 20:36:47 2020 +0100 +++ b/Core/DicomFormat/DicomValue.cpp Wed Feb 26 10:39:55 2020 +0100 @@ -231,6 +231,11 @@ 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); diff -r 9dac85e807c2 -r 898903022836 Core/DicomFormat/DicomValue.h --- a/Core/DicomFormat/DicomValue.h Thu Feb 20 20:36:47 2020 +0100 +++ b/Core/DicomFormat/DicomValue.h Wed Feb 26 10:39:55 2020 +0100 @@ -112,6 +112,8 @@ bool ParseDouble(double& result) const; + bool ParseFirstFloat(float& result) const; + bool ParseFirstUnsignedInteger(unsigned int& result) const; void Serialize(Json::Value& target) const; diff -r 9dac85e807c2 -r 898903022836 Core/DicomNetworking/Internals/CommandDispatcher.cpp diff -r 9dac85e807c2 -r 898903022836 Core/DicomParsing/DicomModification.cpp --- a/Core/DicomParsing/DicomModification.cpp Thu Feb 20 20:36:47 2020 +0100 +++ b/Core/DicomParsing/DicomModification.cpp Wed Feb 26 10:39:55 2020 +0100 @@ -347,7 +347,7 @@ dicom.Replace(*tag, mapped, false /* don't try and decode data URI scheme for UIDs */, - DicomReplaceMode_InsertIfAbsent); + DicomReplaceMode_InsertIfAbsent, privateCreator_); } @@ -359,6 +359,7 @@ keepSeriesInstanceUid_(false), updateReferencedRelationships_(true), isAnonymization_(false), + //privateCreator_("PrivateCreator"), identifierGenerator_(NULL) { } @@ -1067,7 +1068,8 @@ for (Replacements::const_iterator it = replacements_.begin(); it != replacements_.end(); ++it) { - toModify.Replace(it->first, *it->second, true /* decode data URI scheme */, DicomReplaceMode_InsertIfAbsent); + toModify.Replace(it->first, *it->second, true /* decode data URI scheme */, + DicomReplaceMode_InsertIfAbsent, privateCreator_); } // (6) Update the DICOM identifiers @@ -1262,6 +1264,12 @@ { ParseListOfTags(*this, request["Keep"], TagOperation_Keep, force); } + + // New in Orthanc 1.6.0 + if (request.isMember("PrivateCreator")) + { + privateCreator_ = SerializationToolbox::ReadString(request, "PrivateCreator"); + } } @@ -1336,6 +1344,7 @@ static const char* MAP_STUDIES = "MapStudies"; static const char* MAP_SERIES = "MapSeries"; static const char* MAP_INSTANCES = "MapInstances"; + static const char* PRIVATE_CREATOR = "PrivateCreator"; // New in Orthanc 1.6.0 void DicomModification::Serialize(Json::Value& value) const { @@ -1353,6 +1362,7 @@ value[KEEP_SERIES_INSTANCE_UID] = keepSeriesInstanceUid_; value[UPDATE_REFERENCED_RELATIONSHIPS] = updateReferencedRelationships_; value[IS_ANONYMIZATION] = isAnonymization_; + value[PRIVATE_CREATOR] = privateCreator_; SerializationToolbox::WriteSetOfTags(value, removals_, REMOVALS); SerializationToolbox::WriteSetOfTags(value, clearings_, CLEARINGS); @@ -1451,6 +1461,11 @@ (serialized, UPDATE_REFERENCED_RELATIONSHIPS); isAnonymization_ = SerializationToolbox::ReadBoolean(serialized, IS_ANONYMIZATION); + if (serialized.isMember(PRIVATE_CREATOR)) + { + privateCreator_ = SerializationToolbox::ReadString(serialized, PRIVATE_CREATOR); + } + SerializationToolbox::ReadSetOfTags(removals_, serialized, REMOVALS); SerializationToolbox::ReadSetOfTags(clearings_, serialized, CLEARINGS); SerializationToolbox::ReadSetOfTags(privateTagsToKeep_, serialized, PRIVATE_TAGS_TO_KEEP); diff -r 9dac85e807c2 -r 898903022836 Core/DicomParsing/DicomModification.h --- a/Core/DicomParsing/DicomModification.h Thu Feb 20 20:36:47 2020 +0100 +++ b/Core/DicomParsing/DicomModification.h Wed Feb 26 10:39:55 2020 +0100 @@ -86,6 +86,7 @@ bool updateReferencedRelationships_; bool isAnonymization_; DicomMap currentSource_; + std::string privateCreator_; IDicomIdentifierGenerator* identifierGenerator_; @@ -185,5 +186,15 @@ } void Serialize(Json::Value& value) const; + + void SetPrivateCreator(std::string& privateCreator) + { + privateCreator_ = privateCreator; + } + + const std::string& GetPrivateCreator() + { + return privateCreator_; + } }; } diff -r 9dac85e807c2 -r 898903022836 Core/DicomParsing/FromDcmtkBridge.cpp --- a/Core/DicomParsing/FromDcmtkBridge.cpp Thu Feb 20 20:36:47 2020 +0100 +++ b/Core/DicomParsing/FromDcmtkBridge.cpp Wed Feb 26 10:39:55 2020 +0100 @@ -115,6 +115,16 @@ namespace Orthanc { + static bool IsBinaryTag(const DcmTag& key) + { + return (key.isUnknownVR() || + key.getEVR() == EVR_OB || + key.getEVR() == EVR_OW || + key.getEVR() == EVR_UN || + key.getEVR() == EVR_ox); + } + + #if DCMTK_USE_EMBEDDED_DICTIONARIES == 1 static void LoadEmbeddedDictionary(DcmDataDictionary& dictionary, EmbeddedResources::FileResourceId resource) @@ -938,18 +948,13 @@ if (!(flags & DicomToJsonFlags_IncludeUnknownTags)) { DictionaryLocker locker; - if (locker->findEntry(element->getTag(), NULL) == NULL) + if (locker->findEntry(element->getTag(), element->getTag().getPrivateCreator()) == NULL) { continue; } } - DcmEVR evr = element->getTag().getEVR(); - if (evr == EVR_OB || - evr == EVR_OF || - evr == EVR_OW || - evr == EVR_UN || - evr == EVR_ox) + if (IsBinaryTag(element->getTag())) { // This is a binary tag if ((tag == DICOM_TAG_PIXEL_DATA && !(flags & DicomToJsonFlags_IncludePixelData)) || @@ -1371,189 +1376,49 @@ } - static bool IsBinaryTag(const DcmTag& key) + DcmElement* FromDcmtkBridge::CreateElementForTag(const DicomTag& tag, + const std::string& privateCreator) { - return (key.isUnknownVR() || -#if DCMTK_VERSION_NUMBER >= 361 - key.getEVR() == EVR_OD || -#endif - + if (tag.IsPrivate() && + privateCreator.empty()) + { + // This solves issue 140 (Modifying private tags with REST API + // changes VR from LO to UN) + // https://bitbucket.org/sjodogne/orthanc/issues/140 + LOG(WARNING) << "Private creator should not be empty while creating a private tag: " << tag.Format(); + } + #if DCMTK_VERSION_NUMBER >= 362 - key.getEVR() == EVR_OL || -#endif - key.getEVR() == EVR_OB || - key.getEVR() == EVR_OF || - key.getEVR() == EVR_OW || - key.getEVR() == EVR_UN || - key.getEVR() == EVR_ox); - } - - - DcmElement* FromDcmtkBridge::CreateElementForTag(const DicomTag& tag) - { DcmTag key(tag.GetGroup(), tag.GetElement()); - - if (tag.IsPrivate() || - IsBinaryTag(key)) + if (tag.IsPrivate()) + { + return DcmItem::newDicomElement(key, privateCreator.c_str()); + } + else { + return DcmItem::newDicomElement(key, NULL); + } + +#else + DcmTag key(tag.GetGroup(), tag.GetElement()); + if (tag.IsPrivate()) + { + // https://forum.dcmtk.org/viewtopic.php?t=4527 + LOG(WARNING) << "You are using DCMTK <= 3.6.0: All the private tags " + "are considered as having a binary value representation"; + key.setPrivateCreator(privateCreator.c_str()); return new DcmOtherByteOtherWord(key); } - - switch (key.getEVR()) + else { - // http://support.dcmtk.org/docs/dcvr_8h-source.html - - /** - * Binary types, handled above - **/ - -#if DCMTK_VERSION_NUMBER >= 361 - case EVR_OD: -#endif - -#if DCMTK_VERSION_NUMBER >= 362 - case EVR_OL: -#endif - - case EVR_OB: // other byte - case EVR_OF: // other float - case EVR_OW: // other word - case EVR_UN: // unknown value representation - case EVR_ox: // OB or OW depending on context - throw OrthancException(ErrorCode_InternalError); - - - /** - * String types. - * http://support.dcmtk.org/docs/classDcmByteString.html - **/ - - case EVR_AS: // age string - return new DcmAgeString(key); - - case EVR_AE: // application entity title - return new DcmApplicationEntity(key); - - case EVR_CS: // code string - return new DcmCodeString(key); - - case EVR_DA: // date string - return new DcmDate(key); - - case EVR_DT: // date time string - return new DcmDateTime(key); - - case EVR_DS: // decimal string - return new DcmDecimalString(key); - - case EVR_IS: // integer string - return new DcmIntegerString(key); - - case EVR_TM: // time string - return new DcmTime(key); - - case EVR_UI: // unique identifier - return new DcmUniqueIdentifier(key); - - case EVR_ST: // short text - return new DcmShortText(key); - - case EVR_LO: // long string - return new DcmLongString(key); - - case EVR_LT: // long text - return new DcmLongText(key); - - case EVR_UT: // unlimited text - return new DcmUnlimitedText(key); - - case EVR_SH: // short string - return new DcmShortString(key); - - case EVR_PN: // person name - return new DcmPersonName(key); - -#if DCMTK_VERSION_NUMBER >= 361 - case EVR_UC: // unlimited characters - return new DcmUnlimitedCharacters(key); -#endif - -#if DCMTK_VERSION_NUMBER >= 361 - case EVR_UR: // URI/URL - return new DcmUniversalResourceIdentifierOrLocator(key); -#endif - - - /** - * Numerical types - **/ - - case EVR_SL: // signed long - return new DcmSignedLong(key); - - case EVR_SS: // signed short - return new DcmSignedShort(key); - - case EVR_UL: // unsigned long - return new DcmUnsignedLong(key); - - case EVR_US: // unsigned short - return new DcmUnsignedShort(key); - - case EVR_FL: // float single-precision - return new DcmFloatingPointSingle(key); - - case EVR_FD: // float double-precision - return new DcmFloatingPointDouble(key); - - - /** - * Sequence types, should never occur at this point. - **/ - - case EVR_SQ: // sequence of items - throw OrthancException(ErrorCode_ParameterOutOfRange); - - - /** - * TODO - **/ - - case EVR_AT: // attribute tag - throw OrthancException(ErrorCode_NotImplemented); - - - /** - * Internal to DCMTK. - **/ - - case EVR_xs: // SS or US 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 - case EVR_item: // used internally for items - case EVR_metainfo: // used internally for meta info datasets - case EVR_dataset: // used internally for datasets - case EVR_fileFormat: // used internally for DICOM files - case EVR_dicomDir: // used internally for DICOMDIR objects - case EVR_dirRecord: // used internally for DICOMDIR records - case EVR_pixelSQ: // used internally for pixel sequences in a compressed image - case EVR_pixelItem: // used internally for pixel items in a compressed image - case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR) - case EVR_PixelData: // used internally for uncompressed pixeld data - case EVR_OverlayData: // used internally for overlay data - case EVR_UNKNOWN2B: // used internally for elements with unknown VR with 2-byte length field in explicit VR - default: - break; + return newDicomElement(key); } - - throw OrthancException(ErrorCode_InternalError); +#endif } void FromDcmtkBridge::FillElementWithString(DcmElement& element, - const DicomTag& tag, const std::string& utf8Value, bool decodeDataUriScheme, Encoding dicomEncoding) @@ -1578,14 +1443,11 @@ decoded = &binary; } - DcmTag key(tag.GetGroup(), tag.GetElement()); - - if (tag.IsPrivate() || - IsBinaryTag(key)) + if (IsBinaryTag(element.getTag())) { bool ok; - switch (key.getEVR()) + switch (element.getTag().getEVR()) { case EVR_OW: if (decoded->size() % sizeof(Uint16) != 0) @@ -1619,7 +1481,7 @@ try { - switch (key.getEVR()) + switch (element.getTag().getEVR()) { // http://support.dcmtk.org/docs/dcvr_8h-source.html @@ -1628,7 +1490,6 @@ **/ case EVR_OB: // other byte - case EVR_OF: // other float case EVR_OW: // other word case EVR_AT: // attribute tag throw OrthancException(ErrorCode_NotImplemented); @@ -1683,6 +1544,9 @@ } case EVR_UL: // unsigned long +#if DCMTK_VERSION_NUMBER >= 362 + case EVR_OL: // other long (requires byte-swapping) +#endif { ok = element.putUint32(boost::lexical_cast(*decoded)).good(); break; @@ -1695,12 +1559,16 @@ } case EVR_FL: // float single-precision + case EVR_OF: // other float (requires byte swapping) { ok = element.putFloat32(boost::lexical_cast(*decoded)).good(); break; } case EVR_FD: // float double-precision +#if DCMTK_VERSION_NUMBER >= 361 + case EVR_OD: // other double (requires byte-swapping) +#endif { ok = element.putFloat64(boost::lexical_cast(*decoded)).good(); break; @@ -1750,6 +1618,7 @@ if (!ok) { + DicomTag tag(element.getTag().getGroup(), element.getTag().getElement()); throw OrthancException(ErrorCode_BadFileFormat, "While creating a DICOM instance, tag (" + tag.Format() + ") has out-of-range value: \"" + (*decoded) + "\""); @@ -1760,20 +1629,21 @@ DcmElement* FromDcmtkBridge::FromJson(const DicomTag& tag, const Json::Value& value, bool decodeDataUriScheme, - Encoding dicomEncoding) + Encoding dicomEncoding, + const std::string& privateCreator) { std::auto_ptr element; switch (value.type()) { case Json::stringValue: - element.reset(CreateElementForTag(tag)); - FillElementWithString(*element, tag, value.asString(), decodeDataUriScheme, dicomEncoding); + element.reset(CreateElementForTag(tag, privateCreator)); + FillElementWithString(*element, value.asString(), decodeDataUriScheme, dicomEncoding); break; case Json::nullValue: - element.reset(CreateElementForTag(tag)); - FillElementWithString(*element, tag, "", decodeDataUriScheme, dicomEncoding); + element.reset(CreateElementForTag(tag, privateCreator)); + FillElementWithString(*element, "", decodeDataUriScheme, dicomEncoding); break; case Json::arrayValue: @@ -1798,7 +1668,7 @@ Json::Value::Members members = value[i].getMemberNames(); for (Json::Value::ArrayIndex j = 0; j < members.size(); j++) { - item->insert(FromJson(ParseTag(members[j]), value[i][members[j]], decodeDataUriScheme, dicomEncoding)); + item->insert(FromJson(ParseTag(members[j]), value[i][members[j]], decodeDataUriScheme, dicomEncoding, privateCreator)); } break; } @@ -1907,7 +1777,8 @@ DcmDataset* FromDcmtkBridge::FromJson(const Json::Value& json, // Encoded using UTF-8 bool generateIdentifiers, bool decodeDataUriScheme, - Encoding defaultEncoding) + Encoding defaultEncoding, + const std::string& privateCreator) { std::auto_ptr result(new DcmDataset); Encoding encoding = ExtractEncoding(json, defaultEncoding); @@ -1945,7 +1816,7 @@ if (tag != DICOM_TAG_SPECIFIC_CHARACTER_SET) { - std::auto_ptr element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding)); + std::auto_ptr element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding, privateCreator)); const DcmTagKey& tag = element->getTag(); result->findAndDeleteElement(tag); @@ -2268,13 +2139,6 @@ **/ if (evr == EVR_OB || // other byte - evr == EVR_OF || // other float -#if DCMTK_VERSION_NUMBER >= 361 - evr == EVR_OD || // other double -#endif -#if DCMTK_VERSION_NUMBER >= 362 - evr == EVR_OL || // other long -#endif evr == EVR_OW || // other word evr == EVR_UN) // unknown value representation { @@ -2464,6 +2328,9 @@ } case EVR_UL: // unsigned long +#if DCMTK_VERSION_NUMBER >= 362 + case EVR_OL: +#endif { DcmUnsignedLong& content = dynamic_cast(element); @@ -2504,6 +2371,7 @@ } case EVR_FL: // float single-precision + case EVR_OF: { DcmFloatingPointSingle& content = dynamic_cast(element); @@ -2524,6 +2392,9 @@ } case EVR_FD: // float double-precision +#if DCMTK_VERSION_NUMBER >= 361 + case EVR_OD: +#endif { DcmFloatingPointDouble& content = dynamic_cast(element); diff -r 9dac85e807c2 -r 898903022836 Core/DicomParsing/FromDcmtkBridge.h --- a/Core/DicomParsing/FromDcmtkBridge.h Thu Feb 20 20:36:47 2020 +0100 +++ b/Core/DicomParsing/FromDcmtkBridge.h Wed Feb 26 10:39:55 2020 +0100 @@ -209,10 +209,10 @@ static ValueRepresentation LookupValueRepresentation(const DicomTag& tag); - static DcmElement* CreateElementForTag(const DicomTag& tag); + static DcmElement* CreateElementForTag(const DicomTag& tag, + const std::string& privateCreator); static void FillElementWithString(DcmElement& element, - const DicomTag& tag, const std::string& utf8alue, // Encoded using UTF-8 bool decodeDataUriScheme, Encoding dicomEncoding); @@ -220,7 +220,8 @@ static DcmElement* FromJson(const DicomTag& tag, const Json::Value& element, // Encoded using UTF-8 bool decodeDataUriScheme, - Encoding dicomEncoding); + Encoding dicomEncoding, + const std::string& privateCreator); static DcmPixelSequence* GetPixelSequence(DcmDataset& dataset); @@ -230,7 +231,8 @@ static DcmDataset* FromJson(const Json::Value& json, // Encoded using UTF-8 bool generateIdentifiers, bool decodeDataUriScheme, - Encoding defaultEncoding); + Encoding defaultEncoding, + const std::string& privateCreator); static DcmFileFormat* LoadFromMemoryBuffer(const void* buffer, size_t size); diff -r 9dac85e807c2 -r 898903022836 Core/DicomParsing/ITagVisitor.h --- a/Core/DicomParsing/ITagVisitor.h Thu Feb 20 20:36:47 2020 +0100 +++ b/Core/DicomParsing/ITagVisitor.h Wed Feb 26 10:39:55 2020 +0100 @@ -71,7 +71,7 @@ ValueRepresentation vr, const std::vector& values) = 0; - // FL, FD + // FL, FD, OD, OF virtual void VisitDoubles(const std::vector& parentTags, const std::vector& parentIndexes, const DicomTag& tag, @@ -84,7 +84,7 @@ const DicomTag& tag, const std::vector& values) = 0; - // OB, OD, OF, OL, OW, UN + // OB, OL, OW, UN virtual void VisitBinary(const std::vector& parentTags, const std::vector& parentIndexes, const DicomTag& tag, diff -r 9dac85e807c2 -r 898903022836 Core/DicomParsing/ParsedDicomFile.cpp --- a/Core/DicomParsing/ParsedDicomFile.cpp Thu Feb 20 20:36:47 2020 +0100 +++ b/Core/DicomParsing/ParsedDicomFile.cpp Wed Feb 26 10:39:55 2020 +0100 @@ -619,7 +619,8 @@ void ParsedDicomFile::Insert(const DicomTag& tag, const Json::Value& value, - bool decodeDataUriScheme) + bool decodeDataUriScheme, + const std::string& privateCreator) { if (tag.GetElement() == 0x0000) { @@ -648,11 +649,38 @@ bool hasCodeExtensions; Encoding encoding = DetectEncoding(hasCodeExtensions); - std::auto_ptr element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding)); + std::auto_ptr element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding, privateCreator)); InsertInternal(*pimpl_->file_->getDataset(), element.release()); } + void ParsedDicomFile::ReplacePlainString(const DicomTag& tag, + const std::string& utf8Value) + { + if (tag.IsPrivate()) + { + throw OrthancException(ErrorCode_InternalError, + "Cannot apply this function to private tags: " + tag.Format()); + } + else + { + Replace(tag, utf8Value, false, DicomReplaceMode_InsertIfAbsent, + "" /* not a private tag, so no private creator */); + } + } + + + void ParsedDicomFile::SetIfAbsent(const DicomTag& tag, + const std::string& utf8Value) + { + std::string currentValue; + if (!GetTagValue(currentValue, tag)) + { + ReplacePlainString(tag, utf8Value); + } + } + + static bool CanReplaceProceed(DcmDataset& dicom, const DcmTagKey& tag, DicomReplaceMode mode) @@ -742,7 +770,8 @@ void ParsedDicomFile::Replace(const DicomTag& tag, const std::string& utf8Value, bool decodeDataUriScheme, - DicomReplaceMode mode) + DicomReplaceMode mode, + const std::string& privateCreator) { if (tag.GetElement() == 0x0000) { @@ -769,13 +798,13 @@ } } - std::auto_ptr element(FromDcmtkBridge::CreateElementForTag(tag)); + std::auto_ptr element(FromDcmtkBridge::CreateElementForTag(tag, privateCreator)); if (!utf8Value.empty()) { bool hasCodeExtensions; Encoding encoding = DetectEncoding(hasCodeExtensions); - FromDcmtkBridge::FillElementWithString(*element, tag, utf8Value, decodeDataUriScheme, encoding); + FromDcmtkBridge::FillElementWithString(*element, utf8Value, decodeDataUriScheme, encoding); } InsertInternal(dicom, element.release()); @@ -787,7 +816,8 @@ void ParsedDicomFile::Replace(const DicomTag& tag, const Json::Value& value, bool decodeDataUriScheme, - DicomReplaceMode mode) + DicomReplaceMode mode, + const std::string& privateCreator) { if (tag.GetElement() == 0x0000) { @@ -817,7 +847,7 @@ bool hasCodeExtensions; Encoding encoding = DetectEncoding(hasCodeExtensions); - InsertInternal(dicom, FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding)); + InsertInternal(dicom, FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding, privateCreator)); if (tag == DICOM_TAG_SOP_CLASS_UID || tag == DICOM_TAG_SOP_INSTANCE_UID) @@ -1483,7 +1513,8 @@ ParsedDicomFile* ParsedDicomFile::CreateFromJson(const Json::Value& json, - DicomFromJsonFlags flags) + DicomFromJsonFlags flags, + const std::string& privateCreator) { const bool generateIdentifiers = (flags & DicomFromJsonFlags_GenerateIdentifiers) ? true : false; const bool decodeDataUriScheme = (flags & DicomFromJsonFlags_DecodeDataUriScheme) ? true : false; @@ -1512,7 +1543,7 @@ } else if (tag != DICOM_TAG_SPECIFIC_CHARACTER_SET) { - result->Replace(tag, value, decodeDataUriScheme, DicomReplaceMode_InsertIfAbsent); + result->Replace(tag, value, decodeDataUriScheme, DicomReplaceMode_InsertIfAbsent, privateCreator); } } diff -r 9dac85e807c2 -r 898903022836 Core/DicomParsing/ParsedDicomFile.h --- a/Core/DicomParsing/ParsedDicomFile.h Thu Feb 20 20:36:47 2020 +0100 +++ b/Core/DicomParsing/ParsedDicomFile.h Wed Feb 26 10:39:55 2020 +0100 @@ -138,32 +138,27 @@ void Replace(const DicomTag& tag, const std::string& utf8Value, bool decodeDataUriScheme, - DicomReplaceMode mode); + DicomReplaceMode mode, + const std::string& privateCreator /* used only for private tags */); void Replace(const DicomTag& tag, const Json::Value& value, // Assumed to be encoded with UTF-8 bool decodeDataUriScheme, - DicomReplaceMode mode); + DicomReplaceMode mode, + const std::string& privateCreator /* used only for private tags */); void Insert(const DicomTag& tag, const Json::Value& value, // Assumed to be encoded with UTF-8 - bool decodeDataUriScheme); + bool decodeDataUriScheme, + const std::string& privateCreator /* used only for private tags */); + // Cannot be applied to private tags void ReplacePlainString(const DicomTag& tag, - const std::string& utf8Value) - { - Replace(tag, utf8Value, false, DicomReplaceMode_InsertIfAbsent); - } + const std::string& utf8Value); + // Cannot be applied to private tags void SetIfAbsent(const DicomTag& tag, - const std::string& utf8Value) - { - std::string currentValue; - if (!GetTagValue(currentValue, tag)) - { - ReplacePlainString(tag, utf8Value); - } - } + const std::string& utf8Value); void RemovePrivateTags() { @@ -234,7 +229,8 @@ unsigned int GetFramesCount() const; static ParsedDicomFile* CreateFromJson(const Json::Value& value, - DicomFromJsonFlags flags); + DicomFromJsonFlags flags, + const std::string& privateCreator); void ChangeEncoding(Encoding target); diff -r 9dac85e807c2 -r 898903022836 Core/Images/ImageProcessing.cpp --- a/Core/Images/ImageProcessing.cpp Thu Feb 20 20:36:47 2020 +0100 +++ b/Core/Images/ImageProcessing.cpp Wed Feb 26 10:39:55 2020 +0100 @@ -41,11 +41,11 @@ #ifdef __EMSCRIPTEN__ /* -Avoid this error: ------------------ -.../boost/math/special_functions/round.hpp:118:12: warning: implicit conversion from 'std::__2::numeric_limits::type' (aka 'long long') to 'float' changes value from 9223372036854775807 to 9223372036854775808 [-Wimplicit-int-float-conversion] -.../mnt/c/osi/dev/orthanc/Core/Images/ImageProcessing.cpp:333:28: note: in instantiation of function template specialization 'boost::math::llround' requested here -.../mnt/c/osi/dev/orthanc/Core/Images/ImageProcessing.cpp:1006:9: note: in instantiation of function template specialization 'Orthanc::MultiplyConstantInternal' requested here + Avoid this error: + ----------------- + .../boost/math/special_functions/round.hpp:118:12: warning: implicit conversion from 'std::__2::numeric_limits::type' (aka 'long long') to 'float' changes value from 9223372036854775807 to 9223372036854775808 [-Wimplicit-int-float-conversion] + .../mnt/c/osi/dev/orthanc/Core/Images/ImageProcessing.cpp:333:28: note: in instantiation of function template specialization 'boost::math::llround' requested here + .../mnt/c/osi/dev/orthanc/Core/Images/ImageProcessing.cpp:1006:9: note: in instantiation of function template specialization 'Orthanc::MultiplyConstantInternal' requested here */ #pragma GCC diagnostic ignored "-Wimplicit-int-float-conversion" #endif @@ -385,28 +385,47 @@ } - template - static void ShiftScaleInternal(ImageAccessor& image, - float offset, - float scaling, - const PixelType LowestValue = std::numeric_limits::min()) + // Computes "a * x + b" at each pixel => Note that this is not the + // same convention as in "ShiftScale()" + template + static void ShiftScaleInternal(ImageAccessor& target, + const ImageAccessor& source, + float a, + float b, + const TargetType LowestValue) + // This function can be applied inplace (source == target) { - const PixelType minPixelValue = LowestValue; - const PixelType maxPixelValue = std::numeric_limits::max(); + if (source.GetWidth() != target.GetWidth() || + source.GetHeight() != target.GetHeight()) + { + throw OrthancException(ErrorCode_IncompatibleImageSize); + } + + if (&source == &target && + source.GetFormat() != target.GetFormat()) + { + throw OrthancException(ErrorCode_IncompatibleImageFormat); + } + + const TargetType minPixelValue = LowestValue; + const TargetType maxPixelValue = std::numeric_limits::max(); const float minFloatValue = static_cast(LowestValue); const float maxFloatValue = static_cast(maxPixelValue); - const unsigned int height = image.GetHeight(); - const unsigned int width = image.GetWidth(); + const unsigned int height = target.GetHeight(); + const unsigned int width = target.GetWidth(); for (unsigned int y = 0; y < height; y++) { - PixelType* p = reinterpret_cast(image.GetRow(y)); + TargetType* p = reinterpret_cast(target.GetRow(y)); + const SourceType* q = reinterpret_cast(source.GetRow(y)); - for (unsigned int x = 0; x < width; x++, p++) + for (unsigned int x = 0; x < width; x++, p++, q++) { - float v = (static_cast(*p) + offset) * scaling; + float v = a * static_cast(*q) + b; if (v >= maxFloatValue) { @@ -419,11 +438,16 @@ else if (UseRound) { // The "round" operation is very costly - *p = static_cast(boost::math::iround(v)); + *p = static_cast(boost::math::iround(v)); } else { - *p = static_cast(v); + *p = static_cast(std::floor(v)); + } + + if (Invert) + { + *p = maxPixelValue - *p; } } } @@ -498,57 +522,39 @@ float rescaleIntercept, bool invert) { + assert(sizeof(SourceType) == source.GetBytesPerPixel() && + sizeof(TargetType) == target.GetBytesPerPixel()); + // WARNING - "::min()" should be replaced by "::lowest()" if // dealing with float or double (which is not the case so far) assert(sizeof(TargetType) <= 2); // Safeguard to remember about "float/double" const TargetType minTargetValue = std::numeric_limits::min(); const TargetType maxTargetValue = std::numeric_limits::max(); - const float minFloatValue = static_cast(minTargetValue); const float maxFloatValue = static_cast(maxTargetValue); - + const float windowIntercept = windowCenter - windowWidth / 2.0f; const float windowSlope = (maxFloatValue + 1.0f) / windowWidth; - const unsigned int width = source.GetWidth(); - const unsigned int height = source.GetHeight(); - - for (unsigned int y = 0; y < height; y++) - { - TargetType* t = reinterpret_cast(target.GetRow(y)); - const SourceType* s = reinterpret_cast(source.GetConstRow(y)); + const float a = rescaleSlope * windowSlope; + const float b = (rescaleIntercept - windowIntercept) * windowSlope; - for (unsigned int x = 0; x < width; x++, t++, s++) - { - float rescaledValue = *s * rescaleSlope + rescaleIntercept; - float v = (rescaledValue - windowIntercept) * windowSlope; - if (v <= minFloatValue) - { - v = minFloatValue; - } - else if (v >= maxFloatValue) - { - v = maxFloatValue; - } - - TargetType vv = static_cast(v); - - if (invert) - { - vv = maxTargetValue - vv; - } - - *t = static_cast(vv); - } + if (invert) + { + ShiftScaleInternal(target, source, a, b, minTargetValue); + } + else + { + ShiftScaleInternal(target, source, a, b, minTargetValue); } } - void ImageProcessing::ApplyWindowing(ImageAccessor& target, - const ImageAccessor& source, - float windowCenter, - float windowWidth, - float rescaleSlope, - float rescaleIntercept, - bool invert) + void ImageProcessing::ApplyWindowing_Deprecated(ImageAccessor& target, + const ImageAccessor& source, + float windowCenter, + float windowWidth, + float rescaleSlope, + float rescaleIntercept, + bool invert) { if (target.GetWidth() != source.GetWidth() || target.GetHeight() != source.GetHeight()) @@ -562,42 +568,42 @@ { switch (target.GetFormat()) { - case Orthanc::PixelFormat_Grayscale8: - ApplyWindowingInternal(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); - break; - case Orthanc::PixelFormat_Grayscale16: - ApplyWindowingInternal(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); - break; - default: - throw OrthancException(ErrorCode_NotImplemented); + case Orthanc::PixelFormat_Grayscale8: + ApplyWindowingInternal(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); + break; + case Orthanc::PixelFormat_Grayscale16: + ApplyWindowingInternal(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); + break; + default: + throw OrthancException(ErrorCode_NotImplemented); } };break; case Orthanc::PixelFormat_Grayscale8: { switch (target.GetFormat()) { - case Orthanc::PixelFormat_Grayscale8: - ApplyWindowingInternal(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); - break; - case Orthanc::PixelFormat_Grayscale16: - ApplyWindowingInternal(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); - break; - default: - throw OrthancException(ErrorCode_NotImplemented); + case Orthanc::PixelFormat_Grayscale8: + ApplyWindowingInternal(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); + break; + case Orthanc::PixelFormat_Grayscale16: + ApplyWindowingInternal(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); + break; + default: + throw OrthancException(ErrorCode_NotImplemented); } };break; case Orthanc::PixelFormat_Grayscale16: { switch (target.GetFormat()) { - case Orthanc::PixelFormat_Grayscale8: - ApplyWindowingInternal(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); - break; - case Orthanc::PixelFormat_Grayscale16: - ApplyWindowingInternal(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); - break; - default: - throw OrthancException(ErrorCode_NotImplemented); + case Orthanc::PixelFormat_Grayscale8: + ApplyWindowingInternal(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); + break; + case Orthanc::PixelFormat_Grayscale16: + ApplyWindowingInternal(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); + break; + default: + throw OrthancException(ErrorCode_NotImplemented); } };break; default: @@ -1181,7 +1187,7 @@ ShiftRightInternal(image, shift); break; } - default: + default: throw OrthancException(ErrorCode_NotImplemented); } } @@ -1210,7 +1216,7 @@ ShiftLeftInternal(image, shift); break; } - default: + default: throw OrthancException(ErrorCode_NotImplemented); } } @@ -1366,49 +1372,55 @@ float scaling, bool useRound) { + // Rewrite "(x + offset) * scaling" as "a * x + b" + + const float a = scaling; + const float b = offset * scaling; + switch (image.GetFormat()) { case PixelFormat_Grayscale8: if (useRound) { - ShiftScaleInternal(image, offset, scaling); + ShiftScaleInternal(image, image, a, b, std::numeric_limits::min()); } else { - ShiftScaleInternal(image, offset, scaling); + ShiftScaleInternal(image, image, a, b, std::numeric_limits::min()); } return; case PixelFormat_Grayscale16: if (useRound) { - ShiftScaleInternal(image, offset, scaling); + ShiftScaleInternal(image, image, a, b, std::numeric_limits::min()); } else { - ShiftScaleInternal(image, offset, scaling); + ShiftScaleInternal(image, image, a, b, std::numeric_limits::min()); } return; case PixelFormat_SignedGrayscale16: if (useRound) { - ShiftScaleInternal(image, offset, scaling); + ShiftScaleInternal(image, image, a, b, std::numeric_limits::min()); } else { - ShiftScaleInternal(image, offset, scaling); + ShiftScaleInternal(image, image, a, b, std::numeric_limits::min()); } return; case PixelFormat_Float32: + // "::min()" must be replaced by "::lowest()" or "-::max()" if dealing with float or double. if (useRound) { - ShiftScaleInternal(image, offset, scaling, -std::numeric_limits::max()); + ShiftScaleInternal(image, image, a, b, -std::numeric_limits::max()); } else { - ShiftScaleInternal(image, offset, scaling, -std::numeric_limits::max()); + ShiftScaleInternal(image, image, a, b, -std::numeric_limits::max()); } return; @@ -1418,6 +1430,46 @@ } + void ImageProcessing::ShiftScale(ImageAccessor& target, + const ImageAccessor& source, + float offset, + float scaling, + bool useRound) + { + // Rewrite "(x + offset) * scaling" as "a * x + b" + + const float a = scaling; + const float b = offset * scaling; + + switch (target.GetFormat()) + { + case PixelFormat_Grayscale8: + + switch (source.GetFormat()) + { + case PixelFormat_Float32: + if (useRound) + { + ShiftScaleInternal( + target, source, a, b, std::numeric_limits::min()); + } + else + { + ShiftScaleInternal( + target, source, a, b, std::numeric_limits::min()); + } + return; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + void ImageProcessing::Invert(ImageAccessor& image, int64_t maxValue) { const unsigned int width = image.GetWidth(); @@ -2216,7 +2268,7 @@ // Deal with the right border for (unsigned int x = static_cast( - horizontalAnchor + width - horizontal.size() + 1); x < width; x++) + horizontalAnchor + width - horizontal.size() + 1); x < width; x++) { for (unsigned int c = 0; c < ChannelsCount; c++, p++) { diff -r 9dac85e807c2 -r 898903022836 Core/Images/ImageProcessing.h --- a/Core/Images/ImageProcessing.h Thu Feb 20 20:36:47 2020 +0100 +++ b/Core/Images/ImageProcessing.h Wed Feb 26 10:39:55 2020 +0100 @@ -82,13 +82,13 @@ void Convert(ImageAccessor& target, const ImageAccessor& source); - void ApplyWindowing(ImageAccessor& target, - const ImageAccessor& source, - float windowCenter, - float windowWidth, - float rescaleSlope, - float rescaleIntercept, - bool invert); + void ApplyWindowing_Deprecated(ImageAccessor& target, + const ImageAccessor& source, + float windowCenter, + float windowWidth, + float rescaleSlope, + float rescaleIntercept, + bool invert); void Set(ImageAccessor& image, int64_t value); @@ -127,12 +127,18 @@ float factor, bool useRound); - // "useRound" is expensive + // Computes "(x + offset) * scaling" inplace. "useRound" is expensive. void ShiftScale(ImageAccessor& image, float offset, float scaling, bool useRound); + void ShiftScale(ImageAccessor& target, + const ImageAccessor& source, + float offset, + float scaling, + bool useRound); + void Invert(ImageAccessor& image); void Invert(ImageAccessor& image, int64_t maxValue); diff -r 9dac85e807c2 -r 898903022836 NEWS --- a/NEWS Thu Feb 20 20:36:47 2020 +0100 +++ b/NEWS Wed Feb 26 10:39:55 2020 +0100 @@ -12,9 +12,13 @@ * API version has been upgraded to 5 * added "/peers/{id}/system" route to test the connectivity with a remote peer (and eventually retrieve its version number) -* /changes: Allow the "limit" argument to be greater than 100 -* /instances/{id}/preview: Now takes the windowing into account -* /tools/log-level: Possibility to access and change the log level without restarting Orthanc +* "/changes": Allow the "limit" argument to be greater than 100 +* "/instances/{id}/preview": Now takes the windowing into account +* "/tools/log-level": Possibility to access and change the log level without restarting Orthanc +* added "/instances/{id}/frames/{frame}/rendered" and "/instances/{id}/rendered" routes + to render frames, taking windowing and resizing into account +* "/instances": Support "Content-Encoding: gzip" to upload gzip-compressed DICOM files +* ".../modify" and "/tools/create-dicom": New option "PrivateCreator" for private tags Plugins ------- @@ -40,6 +44,7 @@ * More strict C-FIND SCP wrt. the DICOM standard: Forbid wildcard matching on some VRs, ignore main tags below the queried level * Fix issue #65 (Logging improvements) +* Fix issue #140 (Modifying private tags with REST API changes VR from LO to UN) * Fix issue #156 (Chunked Dicom-web transfer uses 100% CPU) * Fix issue #165 (Boundary parameter in multipart Content-Type is too long) * Fix issue #166 (CMake find_boost version is now broken with newer boost/cmake) diff -r 9dac85e807c2 -r 898903022836 OrthancServer/OrthancFindRequestHandler.cpp --- a/OrthancServer/OrthancFindRequestHandler.cpp Thu Feb 20 20:36:47 2020 +0100 +++ b/OrthancServer/OrthancFindRequestHandler.cpp Wed Feb 26 10:39:55 2020 +0100 @@ -394,7 +394,8 @@ content.append(item); } - dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent); + dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, + "" /* no private creator */); } } diff -r 9dac85e807c2 -r 898903022836 OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Thu Feb 20 20:36:47 2020 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Wed Feb 26 10:39:55 2020 +0100 @@ -267,7 +267,8 @@ static void InjectTags(ParsedDicomFile& dicom, const Json::Value& tags, - bool decodeBinaryTags) + bool decodeBinaryTags, + const std::string& privateCreator) { if (tags.type() != Json::objectValue) { @@ -305,7 +306,7 @@ } else { - dicom.Replace(tag, tags[name], decodeBinaryTags, DicomReplaceMode_InsertIfAbsent); + dicom.Replace(tag, tags[name], decodeBinaryTags, DicomReplaceMode_InsertIfAbsent, privateCreator); } } } @@ -315,7 +316,8 @@ static void CreateSeries(RestApiPostCall& call, ParsedDicomFile& base /* in */, const Json::Value& content, - bool decodeBinaryTags) + bool decodeBinaryTags, + const std::string& privateCreator) { assert(content.isArray()); assert(content.size() > 0); @@ -348,7 +350,7 @@ if (content[i].isMember("Tags")) { - InjectTags(*dicom, content[i]["Tags"], decodeBinaryTags); + InjectTags(*dicom, content[i]["Tags"], decodeBinaryTags, privateCreator); } } @@ -538,6 +540,20 @@ decodeBinaryTags = v.asBool(); } + + // New argument in Orthanc 1.6.0 + std::string privateCreator; + if (request.isMember("PrivateCreator")) + { + const Json::Value& v = request["PrivateCreator"]; + if (v.type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest); + } + + privateCreator = v.asString(); + } + // Inject time-related information std::string date, time; @@ -565,7 +581,7 @@ } - InjectTags(dicom, request["Tags"], decodeBinaryTags); + InjectTags(dicom, request["Tags"], decodeBinaryTags, privateCreator); // Inject the content (either an image, or a PDF file) @@ -583,7 +599,7 @@ if (content.size() > 0) { // Let's create a series instead of a single instance - CreateSeries(call, dicom, content, decodeBinaryTags); + CreateSeries(call, dicom, content, decodeBinaryTags, privateCreator); return; } } diff -r 9dac85e807c2 -r 898903022836 OrthancServer/OrthancRestApi/OrthancRestApi.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestApi.cpp Thu Feb 20 20:36:47 2020 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestApi.cpp Wed Feb 26 10:39:55 2020 +0100 @@ -34,11 +34,14 @@ #include "../PrecompiledHeadersServer.h" #include "OrthancRestApi.h" +#include "../../Core/Compression/GzipCompressor.h" #include "../../Core/Logging.h" #include "../../Core/MetricsRegistry.h" #include "../../Core/SerializationToolbox.h" #include "../ServerContext.h" +#include + namespace Orthanc { static void SetupResourceAnswer(Json::Value& result, @@ -118,13 +121,22 @@ "Received an empty DICOM file"); } - // TODO Remove unneccessary memcpy - std::string postData; - call.BodyToString(postData); + std::string dicom; + + if (boost::iequals(call.GetHttpHeader("content-encoding", ""), "gzip")) + { + GzipCompressor compressor; + compressor.Uncompress(dicom, call.GetBodyData(), call.GetBodySize()); + } + else + { + // TODO Remove unneccessary memcpy + call.BodyToString(dicom); + } DicomInstanceToStore toStore; toStore.SetOrigin(DicomInstanceOrigin::FromRest(call)); - toStore.SetBuffer(postData); + toStore.SetBuffer(dicom); std::string publicId; StoreStatus status = context.Store(publicId, toStore); diff -r 9dac85e807c2 -r 898903022836 OrthancServer/OrthancRestApi/OrthancRestModalities.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Thu Feb 20 20:36:47 2020 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Wed Feb 26 10:39:55 2020 +0100 @@ -1277,7 +1277,8 @@ MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); std::auto_ptr query - (ParsedDicomFile::CreateFromJson(json, static_cast(0))); + (ParsedDicomFile::CreateFromJson(json, static_cast(0), + "" /* no private creator */)); DicomFindAnswers answers(true); diff -r 9dac85e807c2 -r 898903022836 OrthancServer/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Thu Feb 20 20:36:47 2020 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Wed Feb 26 10:39:55 2020 +0100 @@ -35,10 +35,12 @@ #include "OrthancRestApi.h" #include "../../Core/Compression/GzipCompressor.h" +#include "../../Core/DicomFormat/DicomImageInformation.h" #include "../../Core/DicomParsing/DicomWebJsonVisitor.h" #include "../../Core/DicomParsing/FromDcmtkBridge.h" #include "../../Core/DicomParsing/Internals/DicomImageDecoder.h" #include "../../Core/HttpServer/HttpContentNegociation.h" +#include "../../Core/Images/Image.h" #include "../../Core/Images/ImageProcessing.h" #include "../../Core/Logging.h" #include "../DefaultDicomImageDecoder.h" @@ -50,6 +52,9 @@ #include "../../Plugins/Engine/OrthancPlugins.h" +// This "include" is mandatory for Release builds using Linux Standard Base +#include + namespace Orthanc { @@ -503,152 +508,445 @@ } - void LookupWindowingTags(const ParsedDicomFile& dicom, float& windowCenter, float& windowWidth, float& rescaleSlope, float& rescaleIntercept, bool& invert) + namespace { - DicomMap dicomTags; - dicom.ExtractDicomSummary(dicomTags); + class IDecodedFrameHandler : public boost::noncopyable + { + public: + virtual ~IDecodedFrameHandler() + { + } + + virtual void Handle(RestApiGetCall& call, + std::auto_ptr& decoded, + const DicomMap& dicom) = 0; + + virtual bool RequiresDicomTags() const = 0; + + static void Apply(RestApiGetCall& call, + IDecodedFrameHandler& handler) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string frameId = call.GetUriComponent("frame", "0"); + + unsigned int frame; + try + { + frame = boost::lexical_cast(frameId); + } + catch (boost::bad_lexical_cast&) + { + return; + } + + DicomMap dicom; + std::auto_ptr decoded; + + try + { + std::string publicId = call.GetUriComponent("id", ""); + +#if ORTHANC_ENABLE_PLUGINS == 1 + if (context.GetPlugins().HasCustomImageDecoder()) + { + // TODO create a cache of file + std::string dicomContent; + context.ReadDicom(dicomContent, publicId); + decoded.reset(context.GetPlugins().DecodeUnsafe(dicomContent.c_str(), dicomContent.size(), frame)); + + /** + * Note that we call "DecodeUnsafe()": We do not fallback to + * the builtin decoder if no installed decoder plugin is able + * to decode the image. This allows us to take advantage of + * the cache below. + **/ + + if (handler.RequiresDicomTags() && + decoded.get() != NULL) + { + // TODO Optimize this lookup for photometric interpretation: + // It should be implemented by the plugin to avoid parsing + // twice the DICOM file + ParsedDicomFile parsed(dicomContent); + parsed.ExtractDicomSummary(dicom); + } + } +#endif + + if (decoded.get() == NULL) + { + // Use Orthanc's built-in decoder, using the cache to speed-up + // things on multi-frame images + ServerContext::DicomCacheLocker locker(context, publicId); + decoded.reset(DicomImageDecoder::Decode(locker.GetDicom(), frame)); + + if (handler.RequiresDicomTags()) + { + locker.GetDicom().ExtractDicomSummary(dicom); + } + } + } + catch (OrthancException& e) + { + if (e.GetErrorCode() == ErrorCode_ParameterOutOfRange || + e.GetErrorCode() == ErrorCode_UnknownResource) + { + // The frame number is out of the range for this DICOM + // instance, the resource is not existent + } + else + { + std::string root = ""; + for (size_t i = 1; i < call.GetFullUri().size(); i++) + { + root += "../"; + } + + call.GetOutput().Redirect(root + "app/images/unsupported.png"); + } + return; + } + + handler.Handle(call, decoded, dicom); + } + + + static void DefaultHandler(RestApiGetCall& call, + std::auto_ptr& decoded, + ImageExtractionMode mode, + bool invert) + { + ImageToEncode image(decoded, mode, invert); + + HttpContentNegociation negociation; + EncodePng png(image); + negociation.Register(MIME_PNG, png); + + EncodeJpeg jpeg(image, call); + negociation.Register(MIME_JPEG, jpeg); + + EncodePam pam(image); + negociation.Register(MIME_PAM, pam); + + if (negociation.Apply(call.GetHttpHeaders())) + { + image.Answer(call.GetOutput()); + } + } + }; + + + class GetImageHandler : public IDecodedFrameHandler + { + private: + ImageExtractionMode mode_; + + public: + GetImageHandler(ImageExtractionMode mode) : + mode_(mode) + { + } + + virtual void Handle(RestApiGetCall& call, + std::auto_ptr& decoded, + const DicomMap& dicom) ORTHANC_OVERRIDE + { + bool invert = false; + + if (mode_ == ImageExtractionMode_Preview) + { + DicomImageInformation info(dicom); + invert = (info.GetPhotometricInterpretation() == PhotometricInterpretation_Monochrome1); + } + + DefaultHandler(call, decoded, mode_, invert); + } + + virtual bool RequiresDicomTags() const ORTHANC_OVERRIDE + { + return mode_ == ImageExtractionMode_Preview; + } + }; - unsigned int bitsStored = boost::lexical_cast(dicomTags.GetStringValue(Orthanc::DICOM_TAG_BITS_STORED, "8", false)); - windowWidth = static_cast(2 << (bitsStored - 1)); - windowCenter = windowWidth / 2; - rescaleSlope = 1.0f; - rescaleIntercept = 0.0f; - invert = false; + 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()); + windowCenter = windowWidth / 2.0f; + + 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 */, + unsigned int& argWidth, + unsigned int& argHeight, + bool& smooth, + RestApiGetCall& call) + { + static const char* ARG_WINDOW_CENTER = "window-center"; + static const char* ARG_WINDOW_WIDTH = "window-width"; + static const char* ARG_WIDTH = "width"; + static const char* ARG_HEIGHT = "height"; + static const char* ARG_SMOOTH = "smooth"; + + if (call.HasArgument(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)); + } + } - if (dicomTags.HasTag(Orthanc::DICOM_TAG_WINDOW_CENTER) && dicomTags.HasTag(Orthanc::DICOM_TAG_WINDOW_WIDTH)) - { - dicomTags.ParseFloat(windowCenter, Orthanc::DICOM_TAG_WINDOW_CENTER); - dicomTags.ParseFloat(windowWidth, Orthanc::DICOM_TAG_WINDOW_WIDTH); - } + if (call.HasArgument(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)); + } + } + + argWidth = 0; + argHeight = 0; + + if (call.HasArgument(ARG_WIDTH)) + { + try + { + int tmp = boost::lexical_cast(call.GetArgument(ARG_WIDTH, "")); + if (tmp < 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Argument cannot be negative: " + std::string(ARG_WIDTH)); + } + else + { + argWidth = static_cast(tmp); + } + } + catch (boost::bad_lexical_cast&) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Bad value for argument: " + std::string(ARG_WIDTH)); + } + } + + if (call.HasArgument(ARG_HEIGHT)) + { + try + { + int tmp = boost::lexical_cast(call.GetArgument(ARG_HEIGHT, "")); + if (tmp < 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Argument cannot be negative: " + std::string(ARG_HEIGHT)); + } + else + { + argHeight = static_cast(tmp); + } + } + catch (boost::bad_lexical_cast&) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Bad value for argument: " + std::string(ARG_HEIGHT)); + } + } + + smooth = false; - if (dicomTags.HasTag(Orthanc::DICOM_TAG_RESCALE_SLOPE) && dicomTags.HasTag(Orthanc::DICOM_TAG_RESCALE_INTERCEPT)) - { - dicomTags.ParseFloat(rescaleSlope, Orthanc::DICOM_TAG_RESCALE_SLOPE); - dicomTags.ParseFloat(rescaleIntercept, Orthanc::DICOM_TAG_RESCALE_INTERCEPT); - } + if (call.HasArgument(ARG_SMOOTH)) + { + std::string value = call.GetArgument(ARG_SMOOTH, ""); + if (value == "0" || + value == "false") + { + smooth = false; + } + else if (value == "1" || + value == "true") + { + smooth = true; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Argument must be Boolean: " + std::string(ARG_SMOOTH)); + } + } + } + + + public: + virtual void Handle(RestApiGetCall& call, + std::auto_ptr& decoded, + const DicomMap& dicom) ORTHANC_OVERRIDE + { + bool invert; + float rescaleSlope, rescaleIntercept, windowWidth, windowCenter; + GetDicomParameters(invert, rescaleSlope, rescaleIntercept, windowWidth, windowCenter, dicom); + + unsigned int argWidth, argHeight; + bool smooth; + GetUserArguments(windowWidth, windowCenter, argWidth, argHeight, smooth, call); + + unsigned int targetWidth = decoded->GetWidth(); + unsigned int targetHeight = decoded->GetHeight(); + + if (decoded->GetWidth() != 0 && + decoded->GetHeight() != 0) + { + float ratio = 1; - PhotometricInterpretation photometric; - if (dicom.LookupPhotometricInterpretation(photometric)) - { - invert = (photometric == PhotometricInterpretation_Monochrome1); - } + if (argWidth != 0 && + argHeight != 0) + { + float ratioX = static_cast(argWidth) / static_cast(decoded->GetWidth()); + float ratioY = static_cast(argHeight) / static_cast(decoded->GetHeight()); + ratio = std::min(ratioX, ratioY); + } + else if (argWidth != 0) + { + ratio = static_cast(argWidth) / static_cast(decoded->GetWidth()); + } + else if (argHeight != 0) + { + ratio = static_cast(argHeight) / static_cast(decoded->GetHeight()); + } + + targetWidth = boost::math::iround(ratio * static_cast(decoded->GetWidth())); + targetHeight = boost::math::iround(ratio * static_cast(decoded->GetHeight())); + } + + if (decoded->GetFormat() == PixelFormat_RGB24) + { + if (targetWidth == decoded->GetWidth() && + targetHeight == decoded->GetHeight()) + { + DefaultHandler(call, decoded, ImageExtractionMode_Preview, false); + } + else + { + std::auto_ptr resized( + new Image(decoded->GetFormat(), targetWidth, targetHeight, false)); + + if (smooth && + (targetWidth < decoded->GetWidth() || + targetHeight < decoded->GetHeight())) + { + ImageProcessing::SmoothGaussian5x5(*decoded); + } + + ImageProcessing::Resize(*resized, *decoded); + DefaultHandler(call, resized, ImageExtractionMode_Preview, false); + } + } + else + { + // Grayscale image: (1) convert to Float32, (2) apply + // windowing to get a Grayscale8, (3) possibly resize + + Image converted(PixelFormat_Float32, decoded->GetWidth(), decoded->GetHeight(), false); + ImageProcessing::Convert(converted, *decoded); + + // Avoid divisions by zero + if (windowWidth <= 1.0f) + { + windowWidth = 1; + } + + if (std::abs(rescaleSlope) <= 0.1f) + { + rescaleSlope = 0.1f; + } + + const float scaling = 255.0f * rescaleSlope / windowWidth; + const float offset = (rescaleIntercept - windowCenter + windowWidth / 2.0f) / rescaleSlope; + + std::auto_ptr rescaled(new Image(PixelFormat_Grayscale8, decoded->GetWidth(), decoded->GetHeight(), false)); + ImageProcessing::ShiftScale(*rescaled, converted, offset, scaling, false); + + if (targetWidth == decoded->GetWidth() && + targetHeight == decoded->GetHeight()) + { + DefaultHandler(call, rescaled, ImageExtractionMode_UInt8, invert); + } + else + { + std::auto_ptr resized( + new Image(PixelFormat_Grayscale8, targetWidth, targetHeight, false)); + + if (smooth && + (targetWidth < decoded->GetWidth() || + targetHeight < decoded->GetHeight())) + { + ImageProcessing::SmoothGaussian5x5(*rescaled); + } + + ImageProcessing::Resize(*resized, *rescaled); + DefaultHandler(call, resized, ImageExtractionMode_UInt8, invert); + } + } + } + + virtual bool RequiresDicomTags() const ORTHANC_OVERRIDE + { + return true; + } + }; } + template static void GetImage(RestApiGetCall& call) { - ServerContext& context = OrthancRestApi::GetContext(call); - - std::string frameId = call.GetUriComponent("frame", "0"); - - unsigned int frame; - try - { - frame = boost::lexical_cast(frameId); - } - catch (boost::bad_lexical_cast&) - { - return; - } - - bool invert = false; - float windowCenter = 128.0f; - float windowWidth = 256.0f; - float rescaleSlope = 1.0f; - float rescaleIntercept = 0.0f; - - std::auto_ptr decoded; - - try - { - std::string publicId = call.GetUriComponent("id", ""); + GetImageHandler handler(mode); + IDecodedFrameHandler::Apply(call, handler); + } -#if ORTHANC_ENABLE_PLUGINS == 1 - if (context.GetPlugins().HasCustomImageDecoder()) - { - // TODO create a cache of file - std::string dicomContent; - context.ReadDicom(dicomContent, publicId); - decoded.reset(context.GetPlugins().DecodeUnsafe(dicomContent.c_str(), dicomContent.size(), frame)); - - /** - * Note that we call "DecodeUnsafe()": We do not fallback to - * the builtin decoder if no installed decoder plugin is able - * to decode the image. This allows us to take advantage of - * the cache below. - **/ - - if (mode == ImageExtractionMode_Preview && - decoded.get() != NULL) - { - // TODO Optimize this lookup for photometric interpretation: - // It should be implemented by the plugin to avoid parsing - // twice the DICOM file - ParsedDicomFile parsed(dicomContent); - - LookupWindowingTags(dicomContent, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); - } - } -#endif - if (decoded.get() == NULL) - { - // Use Orthanc's built-in decoder, using the cache to speed-up - // things on multi-frame images - ServerContext::DicomCacheLocker locker(context, publicId); - decoded.reset(DicomImageDecoder::Decode(locker.GetDicom(), frame)); - LookupWindowingTags(locker.GetDicom(), windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); - - if (mode != ImageExtractionMode_Preview) - { - invert = false; - } - } - } - catch (OrthancException& e) - { - if (e.GetErrorCode() == ErrorCode_ParameterOutOfRange || e.GetErrorCode() == ErrorCode_UnknownResource) - { - // The frame number is out of the range for this DICOM - // instance, the resource is not existent - } - else - { - std::string root = ""; - for (size_t i = 1; i < call.GetFullUri().size(); i++) - { - root += "../"; - } - - call.GetOutput().Redirect(root + "app/images/unsupported.png"); - } - return; - } - - if (mode == ImageExtractionMode_Preview - && (decoded->GetFormat() == Orthanc::PixelFormat_Grayscale8 || decoded->GetFormat() == Orthanc::PixelFormat_Grayscale16)) - { - ImageProcessing::ApplyWindowing(*decoded, *decoded, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); - invert = false; // don't invert it later on when encoding it, it has been inverted in the ApplyWindowing function - } - - ImageToEncode image(decoded, mode, invert); - - HttpContentNegociation negociation; - EncodePng png(image); - negociation.Register(MIME_PNG, png); - - EncodeJpeg jpeg(image, call); - negociation.Register(MIME_JPEG, jpeg); - - EncodePam pam(image); - negociation.Register(MIME_PAM, pam); - - if (negociation.Apply(call.GetHttpHeaders())) - { - image.Answer(call.GetOutput()); - } + static void GetRenderedFrame(RestApiGetCall& call) + { + RenderedFrameHandler handler; + IDecodedFrameHandler::Apply(call, handler); } @@ -1802,6 +2100,7 @@ Register("/instances/{id}/frames", ListFrames); Register("/instances/{id}/frames/{frame}/preview", GetImage); + Register("/instances/{id}/frames/{frame}/rendered", GetRenderedFrame); Register("/instances/{id}/frames/{frame}/image-uint8", GetImage); Register("/instances/{id}/frames/{frame}/image-uint16", GetImage); Register("/instances/{id}/frames/{frame}/image-int16", GetImage); @@ -1810,6 +2109,7 @@ Register("/instances/{id}/frames/{frame}/raw.gz", GetRawFrame); Register("/instances/{id}/pdf", ExtractPdf); Register("/instances/{id}/preview", GetImage); + Register("/instances/{id}/rendered", GetRenderedFrame); Register("/instances/{id}/image-uint8", GetImage); Register("/instances/{id}/image-uint16", GetImage); Register("/instances/{id}/image-int16", GetImage); diff -r 9dac85e807c2 -r 898903022836 OrthancServer/Search/HierarchicalMatcher.cpp --- a/OrthancServer/Search/HierarchicalMatcher.cpp Thu Feb 20 20:36:47 2020 +0100 +++ b/OrthancServer/Search/HierarchicalMatcher.cpp Wed Feb 26 10:39:55 2020 +0100 @@ -268,7 +268,13 @@ if (source.findAndGetElement(tag, element).good() && element != NULL) { - std::auto_ptr cloned(FromDcmtkBridge::CreateElementForTag(*it)); + if (it->IsPrivate()) + { + throw OrthancException(ErrorCode_NotImplemented, + "Not applicable to private tags: " + it->Format()); + } + + std::auto_ptr cloned(FromDcmtkBridge::CreateElementForTag(*it, "" /* no private creator */)); cloned->copyFrom(*element); target->insert(cloned.release()); } diff -r 9dac85e807c2 -r 898903022836 Plugins/Engine/OrthancPlugins.cpp --- a/Plugins/Engine/OrthancPlugins.cpp Thu Feb 20 20:36:47 2020 +0100 +++ b/Plugins/Engine/OrthancPlugins.cpp Wed Feb 26 10:39:55 2020 +0100 @@ -931,7 +931,8 @@ Json::Value target; call.ExecuteToJson(target, true); - filtered_.reset(ParsedDicomFile::CreateFromJson(target, DicomFromJsonFlags_None)); + filtered_.reset(ParsedDicomFile::CreateFromJson(target, DicomFromJsonFlags_None, + "" /* no private creator */)); currentQuery_ = filtered_.get(); } } @@ -3049,7 +3050,8 @@ { std::auto_ptr file - (ParsedDicomFile::CreateFromJson(json, static_cast(p.flags))); + (ParsedDicomFile::CreateFromJson(json, static_cast(p.flags), + "" /* TODO - private creator */)); if (p.pixelData) { diff -r 9dac85e807c2 -r 898903022836 Resources/CMake/CivetwebConfiguration.cmake --- a/Resources/CMake/CivetwebConfiguration.cmake Thu Feb 20 20:36:47 2020 +0100 +++ b/Resources/CMake/CivetwebConfiguration.cmake Wed Feb 26 10:39:55 2020 +0100 @@ -30,6 +30,12 @@ ${CIVETWEB_SOURCES_DIR}/src/civetweb.c ) + # New in Orthanc 1.6.0: Enable support of compression in civetweb + set_source_files_properties( + ${CIVETWEB_SOURCES} + PROPERTIES COMPILE_DEFINITIONS + "USE_ZLIB=1") + if (ENABLE_SSL) add_definitions( -DNO_SSL_DL=1 diff -r 9dac85e807c2 -r 898903022836 Resources/Graveyard/FromDcmtkBridge.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/FromDcmtkBridge.cpp Wed Feb 26 10:39:55 2020 +0100 @@ -0,0 +1,168 @@ + DcmElement* FromDcmtkBridge::CreateElementForTag(const DicomTag& tag) + { + DcmTag key(tag.GetGroup(), tag.GetElement()); + + if (tag.IsPrivate()) + { + // This raises BitBucket issue 140 (Modifying private tags with + // REST API changes VR from LO to UN) + // https://bitbucket.org/sjodogne/orthanc/issues/140 + LOG(WARNING) << "You are using DCMTK < 3.6.1: All the private tags " + "are considered as having a binary value representation"; + return new DcmOtherByteOtherWord(key); + } + else if (IsBinaryTag(key)) + { + return new DcmOtherByteOtherWord(key); + } + + switch (key.getEVR()) + { + // http://support.dcmtk.org/docs/dcvr_8h-source.html + + /** + * Binary types, handled above + **/ + +#if DCMTK_VERSION_NUMBER >= 361 + case EVR_OD: +#endif + +#if DCMTK_VERSION_NUMBER >= 362 + case EVR_OL: +#endif + + case EVR_OB: // other byte + case EVR_OF: // other float + case EVR_OW: // other word + case EVR_UN: // unknown value representation + case EVR_ox: // OB or OW depending on context + throw OrthancException(ErrorCode_InternalError); + + + /** + * String types. + * http://support.dcmtk.org/docs/classDcmByteString.html + **/ + + case EVR_AS: // age string + return new DcmAgeString(key); + + case EVR_AE: // application entity title + return new DcmApplicationEntity(key); + + case EVR_CS: // code string + return new DcmCodeString(key); + + case EVR_DA: // date string + return new DcmDate(key); + + case EVR_DT: // date time string + return new DcmDateTime(key); + + case EVR_DS: // decimal string + return new DcmDecimalString(key); + + case EVR_IS: // integer string + return new DcmIntegerString(key); + + case EVR_TM: // time string + return new DcmTime(key); + + case EVR_UI: // unique identifier + return new DcmUniqueIdentifier(key); + + case EVR_ST: // short text + return new DcmShortText(key); + + case EVR_LO: // long string + return new DcmLongString(key); + + case EVR_LT: // long text + return new DcmLongText(key); + + case EVR_UT: // unlimited text + return new DcmUnlimitedText(key); + + case EVR_SH: // short string + return new DcmShortString(key); + + case EVR_PN: // person name + return new DcmPersonName(key); + +#if DCMTK_VERSION_NUMBER >= 361 + case EVR_UC: // unlimited characters + return new DcmUnlimitedCharacters(key); +#endif + +#if DCMTK_VERSION_NUMBER >= 361 + case EVR_UR: // URI/URL + return new DcmUniversalResourceIdentifierOrLocator(key); +#endif + + + /** + * Numerical types + **/ + + case EVR_SL: // signed long + return new DcmSignedLong(key); + + case EVR_SS: // signed short + return new DcmSignedShort(key); + + case EVR_UL: // unsigned long + return new DcmUnsignedLong(key); + + case EVR_US: // unsigned short + return new DcmUnsignedShort(key); + + case EVR_FL: // float single-precision + return new DcmFloatingPointSingle(key); + + case EVR_FD: // float double-precision + return new DcmFloatingPointDouble(key); + + + /** + * Sequence types, should never occur at this point. + **/ + + case EVR_SQ: // sequence of items + throw OrthancException(ErrorCode_ParameterOutOfRange); + + + /** + * TODO + **/ + + case EVR_AT: // attribute tag + throw OrthancException(ErrorCode_NotImplemented); + + + /** + * Internal to DCMTK. + **/ + + case EVR_xs: // SS or US 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 + case EVR_item: // used internally for items + case EVR_metainfo: // used internally for meta info datasets + case EVR_dataset: // used internally for datasets + case EVR_fileFormat: // used internally for DICOM files + case EVR_dicomDir: // used internally for DICOMDIR objects + case EVR_dirRecord: // used internally for DICOMDIR records + case EVR_pixelSQ: // used internally for pixel sequences in a compressed image + case EVR_pixelItem: // used internally for pixel items in a compressed image + case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR) + case EVR_PixelData: // used internally for uncompressed pixeld data + case EVR_OverlayData: // used internally for overlay data + case EVR_UNKNOWN2B: // used internally for elements with unknown VR with 2-byte length field in explicit VR + default: + break; + } + + throw OrthancException(ErrorCode_InternalError); + } diff -r 9dac85e807c2 -r 898903022836 UnitTestsSources/DicomMapTests.cpp --- a/UnitTestsSources/DicomMapTests.cpp Thu Feb 20 20:36:47 2020 +0100 +++ b/UnitTestsSources/DicomMapTests.cpp Wed Feb 26 10:39:55 2020 +0100 @@ -725,9 +725,9 @@ dicom.ReplacePlainString(DicomTag(0x0008, 0x0070), "LO"); dicom.ReplacePlainString(DicomTag(0x0010, 0x4000), "LT"); dicom.ReplacePlainString(DicomTag(0x0028, 0x2000), "OB"); - dicom.ReplacePlainString(DicomTag(0x7fe0, 0x0009), "OD"); - dicom.ReplacePlainString(DicomTag(0x0064, 0x0009), "OF"); - dicom.ReplacePlainString(DicomTag(0x0066, 0x0040), "46"); + dicom.ReplacePlainString(DicomTag(0x7fe0, 0x0009), "3.14159"); // OD (other double) + 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(0x0010, 0x0010), "PN"); @@ -784,16 +784,17 @@ #if DCMTK_VERSION_NUMBER >= 361 ASSERT_EQ("OD", visitor.GetResult() ["7FE00009"]["vr"].asString()); + ASSERT_FLOAT_EQ(3.14159f, boost::lexical_cast(visitor.GetResult() ["7FE00009"]["Value"][0].asString())); #else ASSERT_EQ("UN", visitor.GetResult() ["7FE00009"]["vr"].asString()); + Toolbox::DecodeBase64(s, visitor.GetResult() ["7FE00009"]["InlineBinary"].asString()); + ASSERT_EQ(8u, s.size()); // Because of padding + ASSERT_EQ(0, s[7]); + ASSERT_EQ("3.14159", s.substr(0, 7)); #endif - Toolbox::DecodeBase64(s, visitor.GetResult() ["7FE00009"]["InlineBinary"].asString()); - ASSERT_EQ("OD", s); - ASSERT_EQ("OF", visitor.GetResult() ["00640009"]["vr"].asString()); - Toolbox::DecodeBase64(s, visitor.GetResult() ["00640009"]["InlineBinary"].asString()); - ASSERT_EQ("OF", s); + ASSERT_FLOAT_EQ(2.71828f, boost::lexical_cast(visitor.GetResult() ["00640009"]["Value"][0].asString())); #if DCMTK_VERSION_NUMBER < 361 ASSERT_EQ("UN", visitor.GetResult() ["00660040"]["vr"].asString()); @@ -802,10 +803,9 @@ #elif DCMTK_VERSION_NUMBER == 361 ASSERT_EQ("UL", visitor.GetResult() ["00660040"]["vr"].asString()); ASSERT_EQ(46, visitor.GetResult() ["00660040"]["Value"][0].asInt()); -#elif DCMTK_VERSION_NUMBER > 361 +#else ASSERT_EQ("OL", visitor.GetResult() ["00660040"]["vr"].asString()); - Toolbox::DecodeBase64(s, visitor.GetResult() ["00660040"]["InlineBinary"].asString()); - ASSERT_EQ("46", s); + ASSERT_EQ(46, visitor.GetResult() ["00660040"]["Value"][0].asInt()); #endif ASSERT_EQ("OW", visitor.GetResult() ["00281201"]["vr"].asString()); @@ -893,8 +893,18 @@ ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0070), false)); ASSERT_EQ("LO", s); ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0010, 0x4000), false)); ASSERT_EQ("LT", s); ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0028, 0x2000), true)); ASSERT_EQ("OB", s); - ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x7fe0, 0x0009), true)); ASSERT_EQ("OD", s); - ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0064, 0x0009), true)); ASSERT_EQ("OF", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x7fe0, 0x0009), true)); + +#if DCMTK_VERSION_NUMBER >= 361 + ASSERT_FLOAT_EQ(3.14159f, boost::lexical_cast(s)); +#else + ASSERT_EQ(8u, s.size()); // Because of padding + ASSERT_EQ(0, s[7]); + ASSERT_EQ("3.14159", s.substr(0, 7)); +#endif + + 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(0x0010, 0x0010), false)); ASSERT_EQ("PN", s); ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0050), false)); ASSERT_EQ("SH", s); @@ -902,20 +912,23 @@ ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0018, 0x9219), false)); ASSERT_EQ("-16", s); ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0081), false)); ASSERT_EQ("ST", s); ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0013), false)); ASSERT_EQ("TM", s); - ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0119), false)); ASSERT_EQ("UC", s); ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0016), false)); ASSERT_EQ("UI", s); ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x1161), false)); ASSERT_EQ("128", s); ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x4342, 0x1234), true)); ASSERT_EQ("UN", s); - ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0120), false)); ASSERT_EQ("UR", s); - ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0301), false)); ASSERT_EQ("17", s); ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0040, 0x0031), false)); ASSERT_EQ("UT", s); -#if DCMTK_VERSION_NUMBER == 361 - ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0066, 0x0040), false)); +#if DCMTK_VERSION_NUMBER >= 361 + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0066, 0x0040), false)); ASSERT_EQ("46", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0119), false)); ASSERT_EQ("UC", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0120), false)); ASSERT_EQ("UR", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0301), false)); ASSERT_EQ("17", s); #else - ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0066, 0x0040), true)); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0066, 0x0040), true)); ASSERT_EQ("46", s); // OL + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0119), true)); ASSERT_EQ("UC", s); + 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 - ASSERT_EQ("46", s); + } } diff -r 9dac85e807c2 -r 898903022836 UnitTestsSources/FromDcmtkTests.cpp --- a/UnitTestsSources/FromDcmtkTests.cpp Thu Feb 20 20:36:47 2020 +0100 +++ b/UnitTestsSources/FromDcmtkTests.cpp Wed Feb 26 10:39:55 2020 +0100 @@ -108,7 +108,7 @@ { ASSERT_EQ(DICOM_TAG_PATIENT_NAME, FromDcmtkBridge::ParseTag("PatientName")); - const DicomTag privateTag(0x0045, 0x0010); + const DicomTag privateTag(0x0045, 0x1010); const DicomTag privateTag2(FromDcmtkBridge::ParseTag("0031-1020")); ASSERT_TRUE(privateTag.IsPrivate()); ASSERT_TRUE(privateTag2.IsPrivate()); @@ -119,19 +119,19 @@ ParsedDicomFile o(true); o.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "coucou"); ASSERT_FALSE(o.GetTagValue(s, privateTag)); - o.Insert(privateTag, "private tag", false); + o.Insert(privateTag, "private tag", false, "OrthancCreator"); ASSERT_TRUE(o.GetTagValue(s, privateTag)); ASSERT_STREQ("private tag", s.c_str()); ASSERT_FALSE(o.GetTagValue(s, privateTag2)); - ASSERT_THROW(o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_ThrowIfAbsent), OrthancException); + ASSERT_THROW(o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_ThrowIfAbsent, "OrthancCreator"), OrthancException); ASSERT_FALSE(o.GetTagValue(s, privateTag2)); - o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_IgnoreIfAbsent); + o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_IgnoreIfAbsent, "OrthancCreator"); ASSERT_FALSE(o.GetTagValue(s, privateTag2)); - o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_InsertIfAbsent); + o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_InsertIfAbsent, "OrthancCreator"); ASSERT_TRUE(o.GetTagValue(s, privateTag2)); ASSERT_STREQ("hello", s.c_str()); - o.ReplacePlainString(privateTag2, "hello world"); + o.Replace(privateTag2, std::string("hello world"), false, DicomReplaceMode_InsertIfAbsent, "OrthancCreator"); ASSERT_TRUE(o.GetTagValue(s, privateTag2)); ASSERT_STREQ("hello world", s.c_str()); @@ -290,7 +290,7 @@ f.SetEncoding(testEncodings[i]); std::string s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false); - f.Insert(DICOM_TAG_PATIENT_NAME, s, false); + f.Insert(DICOM_TAG_PATIENT_NAME, s, false, ""); f.SaveToMemoryBuffer(dicom); } @@ -407,7 +407,7 @@ { Json::Value a; a = "Hello"; - element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, false, Encoding_Utf8)); + element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, false, Encoding_Utf8, "")); Json::Value b; std::set ignoreTagLength; @@ -439,20 +439,20 @@ Json::Value a; a = "Hello"; // Cannot assign a string to a sequence - ASSERT_THROW(element.reset(FromDcmtkBridge::FromJson(REFERENCED_STUDY_SEQUENCE, a, false, Encoding_Utf8)), OrthancException); + ASSERT_THROW(element.reset(FromDcmtkBridge::FromJson(REFERENCED_STUDY_SEQUENCE, a, false, Encoding_Utf8, "")), OrthancException); } { Json::Value a = Json::arrayValue; a.append("Hello"); // Cannot assign an array to a string - ASSERT_THROW(element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, false, Encoding_Utf8)), OrthancException); + ASSERT_THROW(element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, false, Encoding_Utf8, "")), OrthancException); } { Json::Value a; a = "data:application/octet-stream;base64,SGVsbG8="; // echo -n "Hello" | base64 - element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, true, Encoding_Utf8)); + element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, true, Encoding_Utf8, "")); Json::Value b; std::set ignoreTagLength; @@ -464,7 +464,7 @@ { Json::Value a = Json::arrayValue; CreateSampleJson(a); - element.reset(FromDcmtkBridge::FromJson(REFERENCED_STUDY_SEQUENCE, a, true, Encoding_Utf8)); + element.reset(FromDcmtkBridge::FromJson(REFERENCED_STUDY_SEQUENCE, a, true, Encoding_Utf8, "")); { Json::Value b; @@ -506,8 +506,8 @@ { ParsedDicomFile f(true); - f.Insert(DICOM_TAG_PATIENT_NAME, "World", false); - ASSERT_THROW(f.Insert(DICOM_TAG_PATIENT_ID, "Hello", false), OrthancException); // Already existing tag + f.Insert(DICOM_TAG_PATIENT_NAME, "World", false, ""); + ASSERT_THROW(f.Insert(DICOM_TAG_PATIENT_ID, "Hello", false, ""), OrthancException); // Already existing tag f.ReplacePlainString(DICOM_TAG_SOP_INSTANCE_UID, "Toto"); // (*) f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "Tata"); // (**) @@ -515,16 +515,16 @@ ASSERT_FALSE(f.LookupTransferSyntax(s)); ASSERT_THROW(f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"), - false, DicomReplaceMode_ThrowIfAbsent), OrthancException); - f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"), false, DicomReplaceMode_IgnoreIfAbsent); + false, DicomReplaceMode_ThrowIfAbsent, ""), OrthancException); + f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"), false, DicomReplaceMode_IgnoreIfAbsent, ""); ASSERT_FALSE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER)); - f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"), false, DicomReplaceMode_InsertIfAbsent); + f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"), false, DicomReplaceMode_InsertIfAbsent, ""); ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER)); ASSERT_EQ(s, "Accession"); - f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession2"), false, DicomReplaceMode_IgnoreIfAbsent); + f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession2"), false, DicomReplaceMode_IgnoreIfAbsent, ""); ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER)); ASSERT_EQ(s, "Accession2"); - f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession3"), false, DicomReplaceMode_ThrowIfAbsent); + f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession3"), false, DicomReplaceMode_ThrowIfAbsent, ""); ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER)); ASSERT_EQ(s, "Accession3"); @@ -552,20 +552,20 @@ ASSERT_FALSE(f.HasTag(REFERENCED_STUDY_SEQUENCE)); f.Remove(REFERENCED_STUDY_SEQUENCE); // No effect - f.Insert(REFERENCED_STUDY_SEQUENCE, a, true); + f.Insert(REFERENCED_STUDY_SEQUENCE, a, true, ""); ASSERT_TRUE(f.HasTag(REFERENCED_STUDY_SEQUENCE)); - ASSERT_THROW(f.Insert(REFERENCED_STUDY_SEQUENCE, a, true), OrthancException); + ASSERT_THROW(f.Insert(REFERENCED_STUDY_SEQUENCE, a, true, ""), OrthancException); f.Remove(REFERENCED_STUDY_SEQUENCE); ASSERT_FALSE(f.HasTag(REFERENCED_STUDY_SEQUENCE)); - f.Insert(REFERENCED_STUDY_SEQUENCE, a, true); + f.Insert(REFERENCED_STUDY_SEQUENCE, a, true, ""); ASSERT_TRUE(f.HasTag(REFERENCED_STUDY_SEQUENCE)); ASSERT_FALSE(f.HasTag(REFERENCED_PATIENT_SEQUENCE)); - ASSERT_THROW(f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_ThrowIfAbsent), OrthancException); + ASSERT_THROW(f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_ThrowIfAbsent, ""), OrthancException); ASSERT_FALSE(f.HasTag(REFERENCED_PATIENT_SEQUENCE)); - f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_IgnoreIfAbsent); + f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_IgnoreIfAbsent, ""); ASSERT_FALSE(f.HasTag(REFERENCED_PATIENT_SEQUENCE)); - f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_InsertIfAbsent); + f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_InsertIfAbsent, ""); ASSERT_TRUE(f.HasTag(REFERENCED_PATIENT_SEQUENCE)); { @@ -580,8 +580,8 @@ } a = "data:application/octet-stream;base64,VGF0YQ=="; // echo -n "Tata" | base64 - f.Replace(DICOM_TAG_SOP_INSTANCE_UID, a, false, DicomReplaceMode_InsertIfAbsent); // (*) - f.Replace(DICOM_TAG_SOP_CLASS_UID, a, true, DicomReplaceMode_InsertIfAbsent); // (**) + f.Replace(DICOM_TAG_SOP_INSTANCE_UID, a, false, DicomReplaceMode_InsertIfAbsent, ""); // (*) + f.Replace(DICOM_TAG_SOP_CLASS_UID, a, true, DicomReplaceMode_InsertIfAbsent, ""); // (**) std::string s; ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_INSTANCE_UID)); @@ -614,7 +614,7 @@ } Json::Value s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false); - f.Replace(DICOM_TAG_PATIENT_NAME, s, false, DicomReplaceMode_InsertIfAbsent); + f.Replace(DICOM_TAG_PATIENT_NAME, s, false, DicomReplaceMode_InsertIfAbsent, ""); Json::Value v; f.DatasetToJson(v, DicomToJsonFormat_Human, DicomToJsonFlags_Default, 0); @@ -626,13 +626,13 @@ TEST(ParsedDicomFile, ToJsonFlags1) { - FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7053, 0x1000), ValueRepresentation_PersonName, "MyPrivateTag", 1, 1, ""); + FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7053, 0x1000), ValueRepresentation_OtherByte, "MyPrivateTag", 1, 1, "OrthancCreator"); FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7050, 0x1000), ValueRepresentation_PersonName, "Declared public tag", 1, 1, ""); ParsedDicomFile f(true); - f.Insert(DicomTag(0x7050, 0x1000), "Some public tag", false); // Even group => public tag - f.Insert(DicomTag(0x7052, 0x1000), "Some unknown tag", false); // Even group => public, unknown tag - f.Insert(DicomTag(0x7053, 0x1000), "Some private tag", false); // Odd group => private tag + f.Insert(DicomTag(0x7050, 0x1000), "Some public tag", false, ""); // Even group => public tag + f.Insert(DicomTag(0x7052, 0x1000), "Some unknown tag", false, ""); // Even group => public, unknown tag + f.Insert(DicomTag(0x7053, 0x1000), "Some private tag", false, "OrthancCreator"); // Odd group => private tag Json::Value v; f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); @@ -644,7 +644,7 @@ ASSERT_EQ(Json::stringValue, v["7050,1000"].type()); ASSERT_EQ("Some public tag", v["7050,1000"].asString()); - f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast(DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_ConvertBinaryToNull), 0); + f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast(DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_IncludeBinary | DicomToJsonFlags_ConvertBinaryToNull), 0); ASSERT_EQ(Json::objectValue, v.type()); ASSERT_EQ(7u, v.getMemberNames().size()); ASSERT_FALSE(v.isMember("7052,1000")); @@ -653,7 +653,14 @@ ASSERT_EQ("Some public tag", v["7050,1000"].asString()); ASSERT_EQ(Json::nullValue, v["7053,1000"].type()); - f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_IncludePrivateTags, 0); + f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast(DicomToJsonFlags_IncludePrivateTags), 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(6u, v.getMemberNames().size()); + ASSERT_FALSE(v.isMember("7052,1000")); + ASSERT_TRUE(v.isMember("7050,1000")); + ASSERT_FALSE(v.isMember("7053,1000")); + + f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast(DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_IncludeBinary), 0); ASSERT_EQ(Json::objectValue, v.type()); ASSERT_EQ(7u, v.getMemberNames().size()); ASSERT_FALSE(v.isMember("7052,1000")); @@ -666,7 +673,7 @@ ASSERT_EQ("application/octet-stream", mime); ASSERT_EQ("Some private tag", content); - f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_ConvertBinaryToNull), 0); + f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludeBinary | DicomToJsonFlags_ConvertBinaryToNull), 0); ASSERT_EQ(Json::objectValue, v.type()); ASSERT_EQ(7u, v.getMemberNames().size()); ASSERT_TRUE(v.isMember("7050,1000")); @@ -675,7 +682,7 @@ ASSERT_EQ("Some public tag", v["7050,1000"].asString()); ASSERT_EQ(Json::nullValue, v["7052,1000"].type()); - f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast(DicomToJsonFlags_IncludeUnknownTags), 0); + f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludeBinary), 0); ASSERT_EQ(Json::objectValue, v.type()); ASSERT_EQ(7u, v.getMemberNames().size()); ASSERT_TRUE(v.isMember("7050,1000")); @@ -687,7 +694,7 @@ ASSERT_EQ("application/octet-stream", mime); ASSERT_EQ("Some unknown tag", content); - f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_ConvertBinaryToNull), 0); + f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_IncludeBinary | DicomToJsonFlags_ConvertBinaryToNull), 0); ASSERT_EQ(Json::objectValue, v.type()); ASSERT_EQ(8u, v.getMemberNames().size()); ASSERT_TRUE(v.isMember("7050,1000")); @@ -702,7 +709,7 @@ TEST(ParsedDicomFile, ToJsonFlags2) { ParsedDicomFile f(true); - f.Insert(DICOM_TAG_PIXEL_DATA, "Pixels", false); + f.Insert(DICOM_TAG_PIXEL_DATA, "Pixels", false, ""); Json::Value v; f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); @@ -811,7 +818,7 @@ { std::auto_ptr dicom - (ParsedDicomFile::CreateFromJson(v, static_cast(DicomFromJsonFlags_GenerateIdentifiers))); + (ParsedDicomFile::CreateFromJson(v, static_cast(DicomFromJsonFlags_GenerateIdentifiers), "")); Json::Value vv; dicom->DatasetToJson(vv, DicomToJsonFormat_Human, toJsonFlags, 0); @@ -827,7 +834,7 @@ { std::auto_ptr dicom - (ParsedDicomFile::CreateFromJson(v, static_cast(DicomFromJsonFlags_GenerateIdentifiers))); + (ParsedDicomFile::CreateFromJson(v, static_cast(DicomFromJsonFlags_GenerateIdentifiers), "")); Json::Value vv; dicom->DatasetToJson(vv, DicomToJsonFormat_Human, static_cast(DicomToJsonFlags_IncludePixelData), 0); @@ -841,7 +848,7 @@ { std::auto_ptr dicom - (ParsedDicomFile::CreateFromJson(v, static_cast(DicomFromJsonFlags_DecodeDataUriScheme))); + (ParsedDicomFile::CreateFromJson(v, static_cast(DicomFromJsonFlags_DecodeDataUriScheme), "")); Json::Value vv; dicom->DatasetToJson(vv, DicomToJsonFormat_Short, toJsonFlags, 0); diff -r 9dac85e807c2 -r 898903022836 UnitTestsSources/ImageProcessingTests.cpp --- a/UnitTestsSources/ImageProcessingTests.cpp Thu Feb 20 20:36:47 2020 +0100 +++ b/UnitTestsSources/ImageProcessingTests.cpp Wed Feb 26 10:39:55 2020 +0100 @@ -285,7 +285,7 @@ static void SetGrayscale16Pixel(ImageAccessor& image, unsigned int x, unsigned int y, - uint8_t value) + uint16_t value) { ImageTraits::SetPixel(image, value, x, y); } @@ -301,6 +301,25 @@ return p == value; } +static void SetSignedGrayscale16Pixel(ImageAccessor& image, + unsigned int x, + unsigned int y, + int16_t value) +{ + ImageTraits::SetPixel(image, value, x, y); +} + +static bool TestSignedGrayscale16Pixel(const ImageAccessor& image, + unsigned int x, + unsigned int y, + int16_t value) +{ + PixelTraits::PixelType p; + ImageTraits::GetPixel(p, image, x, y); + if (p != value) printf("%d %d\n", p, value); + return p == value; +} + static void SetRGB24Pixel(ImageAccessor& image, unsigned int x, unsigned int y, @@ -807,7 +826,7 @@ { Image target(PixelFormat_Grayscale8, 6, 1, false); - ImageProcessing::ApplyWindowing(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false); + ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false); ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 0)); ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 0)); @@ -819,7 +838,7 @@ { Image target(PixelFormat_Grayscale8, 6, 1, false); - ImageProcessing::ApplyWindowing(target, image, 5.0f, 10.0f, 1.0f, 0.0f, true); + ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, true); ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 255)); ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 255)); @@ -831,7 +850,7 @@ { Image target(PixelFormat_Grayscale8, 6, 1, false); - ImageProcessing::ApplyWindowing(target, image, 5000.0f, 10000.0f, 1000.0f, 0.0f, false); + ImageProcessing::ApplyWindowing_Deprecated(target, image, 5000.0f, 10000.01f, 1000.0f, 0.0f, false); ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 0)); ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 0)); @@ -843,7 +862,7 @@ { Image target(PixelFormat_Grayscale8, 6, 1, false); - ImageProcessing::ApplyWindowing(target, image, 5000.0f, 10000.0f, 1000.0f, 0.0f, true); + ImageProcessing::ApplyWindowing_Deprecated(target, image, 5000.0f, 10000.01f, 1000.0f, 0.0f, true); ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 255)); ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 255)); @@ -855,7 +874,7 @@ { Image target(PixelFormat_Grayscale8, 6, 1, false); - ImageProcessing::ApplyWindowing(target, image, 50.0f, 100.0f, 10.0f, 30.0f, false); + ImageProcessing::ApplyWindowing_Deprecated(target, image, 50.0f, 100.1f, 10.0f, 30.0f, false); ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 0)); // (-5 * 10) + 30 => pixel value = -20 => 0 ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 256*30/100)); // ((0 * 10) + 30 => pixel value = 30 => 30% @@ -881,7 +900,7 @@ { Image target(PixelFormat_Grayscale16, 6, 1, false); - ImageProcessing::ApplyWindowing(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false); + ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false); ASSERT_TRUE(TestGrayscale16Pixel(target, 0, 0, 0)); ASSERT_TRUE(TestGrayscale16Pixel(target, 1, 0, 0)); @@ -905,7 +924,7 @@ { Image target(PixelFormat_Grayscale16, 5, 1, false); - ImageProcessing::ApplyWindowing(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false); + ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false); ASSERT_TRUE(TestGrayscale16Pixel(target, 0, 0, 0)); ASSERT_TRUE(TestGrayscale16Pixel(target, 1, 0, 65536*2/10)); @@ -928,7 +947,7 @@ { Image target(PixelFormat_Grayscale16, 5, 1, false); - ImageProcessing::ApplyWindowing(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false); + ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false); ASSERT_TRUE(TestGrayscale16Pixel(target, 0, 0, 0)); ASSERT_TRUE(TestGrayscale16Pixel(target, 1, 0, 65536*2/10)); @@ -938,3 +957,57 @@ } } } + + +TEST(ImageProcessing, ShiftScaleGrayscale8) +{ + Image image(PixelFormat_Grayscale8, 5, 1, false); + SetGrayscale8Pixel(image, 0, 0, 0); + SetGrayscale8Pixel(image, 1, 0, 2); + SetGrayscale8Pixel(image, 2, 0, 5); + SetGrayscale8Pixel(image, 3, 0, 10); + SetGrayscale8Pixel(image, 4, 0, 255); + + ImageProcessing::ShiftScale(image, -1.1, 1.5, true); + ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 0, 1)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 0, 6)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 3, 0, 13)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 4, 0, 255)); +} + + +TEST(ImageProcessing, ShiftScaleGrayscale16) +{ + Image image(PixelFormat_Grayscale16, 5, 1, false); + SetGrayscale16Pixel(image, 0, 0, 0); + SetGrayscale16Pixel(image, 1, 0, 2); + SetGrayscale16Pixel(image, 2, 0, 5); + SetGrayscale16Pixel(image, 3, 0, 10); + SetGrayscale16Pixel(image, 4, 0, 255); + + ImageProcessing::ShiftScale(image, -1.1, 1.5, true); + ASSERT_TRUE(TestGrayscale16Pixel(image, 0, 0, 0)); + ASSERT_TRUE(TestGrayscale16Pixel(image, 1, 0, 1)); + ASSERT_TRUE(TestGrayscale16Pixel(image, 2, 0, 6)); + ASSERT_TRUE(TestGrayscale16Pixel(image, 3, 0, 13)); + ASSERT_TRUE(TestGrayscale16Pixel(image, 4, 0, 381)); +} + + +TEST(ImageProcessing, ShiftScaleSignedGrayscale16) +{ + 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, -17.1, 11.5, true); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 0, 0, -197)); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 1, 0, -174)); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 2, 0, -139)); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 3, 0, -82)); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 4, 0, 2736)); +} diff -r 9dac85e807c2 -r 898903022836 UnitTestsSources/MultiThreadingTests.cpp --- a/UnitTestsSources/MultiThreadingTests.cpp Thu Feb 20 20:36:47 2020 +0100 +++ b/UnitTestsSources/MultiThreadingTests.cpp Wed Feb 26 10:39:55 2020 +0100 @@ -1143,9 +1143,9 @@ Json::Value s; ParsedDicomFile source(true); - source.Insert(DICOM_TAG_STUDY_DESCRIPTION, "Test 1", false); - source.Insert(DICOM_TAG_SERIES_DESCRIPTION, "Test 2", false); - source.Insert(DICOM_TAG_PATIENT_NAME, "Test 3", false); + source.Insert(DICOM_TAG_STUDY_DESCRIPTION, "Test 1", false, ""); + source.Insert(DICOM_TAG_SERIES_DESCRIPTION, "Test 2", false, ""); + source.Insert(DICOM_TAG_PATIENT_NAME, "Test 3", false, ""); std::auto_ptr modified(source.Clone(true)); @@ -1310,7 +1310,7 @@ // Create a sample DICOM file ParsedDicomFile dicom(true); dicom.Replace(DICOM_TAG_PATIENT_NAME, std::string("JODOGNE"), - false, DicomReplaceMode_InsertIfAbsent); + false, DicomReplaceMode_InsertIfAbsent, ""); DicomInstanceToStore toStore; toStore.SetParsedDicomFile(dicom); diff -r 9dac85e807c2 -r 898903022836 UnitTestsSources/VersionsTests.cpp --- a/UnitTestsSources/VersionsTests.cpp Thu Feb 20 20:36:47 2020 +0100 +++ b/UnitTestsSources/VersionsTests.cpp Wed Feb 26 10:39:55 2020 +0100 @@ -113,6 +113,14 @@ } +#if ORTHANC_ENABLE_CIVETWEB == 1 +TEST(Version, CivetwebCompression) +{ + ASSERT_TRUE(mg_check_feature(MG_FEATURES_COMPRESSION)); +} +#endif + + #if ORTHANC_STATIC == 1 TEST(Versions, ZlibStatic)