Mercurial > hg > orthanc
changeset 3016:777762336381 db-changes
integration mainline->db-changes
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Fri, 14 Dec 2018 14:21:27 +0100 |
parents | c0a766e68d6c (current diff) abe49ca61cd5 (diff) |
children | 517fc4767ae0 |
files | |
diffstat | 20 files changed, 913 insertions(+), 233 deletions(-) [+] |
line wrap: on
line diff
--- a/Core/DicomFormat/DicomImageInformation.cpp Tue Dec 11 13:45:47 2018 +0100 +++ b/Core/DicomFormat/DicomImageInformation.cpp Fri Dec 14 14:21:27 2018 +0100 @@ -20,7 +20,7 @@ * you do not wish to do so, delete this exception statement from your * version. If you delete this exception statement from all source files * in the program, then also delete it here. - * + * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU @@ -195,17 +195,23 @@ numberOfFrames_ = 1; } - if ((bitsAllocated_ != 8 && bitsAllocated_ != 16 && - bitsAllocated_ != 24 && bitsAllocated_ != 32) || - numberOfFrames_ == 0 || - (planarConfiguration != 0 && planarConfiguration != 1)) + if (bitsAllocated_ != 8 && bitsAllocated_ != 16 && + bitsAllocated_ != 24 && bitsAllocated_ != 32) { - throw OrthancException(ErrorCode_NotImplemented); + throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: " + boost::lexical_cast<std::string>(bitsAllocated_) + " bits allocated"); + } + else if (numberOfFrames_ == 0) + { + throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported (no frames)"); + } + else if (planarConfiguration != 0 && planarConfiguration != 1) + { + throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: planar configuration is " + boost::lexical_cast<std::string>(planarConfiguration)); } if (samplesPerPixel_ == 0) { - throw OrthancException(ErrorCode_NotImplemented); + throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: samples per pixel is 0"); } bytesPerValue_ = bitsAllocated_ / 8; @@ -279,8 +285,8 @@ } } - if (GetBitsStored() == 8 && - GetChannelCount() == 3 && + if (GetBitsStored() == 8 && + GetChannelCount() == 3 && !IsSigned() && (ignorePhotometricInterpretation || photometric_ == PhotometricInterpretation_RGB)) { @@ -294,9 +300,9 @@ size_t DicomImageInformation::GetFrameSize() const { - return (GetHeight() * - GetWidth() * - GetBytesPerValue() * + return (GetHeight() * + GetWidth() * + GetBytesPerValue() * GetChannelCount()); } }
--- a/Core/DicomFormat/DicomMap.cpp Tue Dec 11 13:45:47 2018 +0100 +++ b/Core/DicomFormat/DicomMap.cpp Fri Dec 14 14:21:27 2018 +0100 @@ -982,6 +982,112 @@ } + void DicomMap::FromDicomAsJson(const Json::Value& dicomAsJson) + { + Clear(); + + Json::Value::Members tags = dicomAsJson.getMemberNames(); + for (Json::Value::Members::const_iterator + it = tags.begin(); it != tags.end(); ++it) + { + DicomTag tag(0, 0); + if (!DicomTag::ParseHexadecimal(tag, it->c_str())) + { + throw OrthancException(ErrorCode_CorruptedFile); + } + + const Json::Value& value = dicomAsJson[*it]; + + if (value.type() != Json::objectValue || + !value.isMember("Type") || + !value.isMember("Value") || + value["Type"].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_CorruptedFile); + } + + if (value["Type"] == "String") + { + if (value["Value"].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_CorruptedFile); + } + else + { + SetValue(tag, value["Value"].asString(), false /* not binary */); + } + } + } + } + + + void DicomMap::Merge(const DicomMap& other) + { + for (Map::const_iterator it = other.map_.begin(); + it != other.map_.end(); ++it) + { + assert(it->second != NULL); + + if (map_.find(it->first) == map_.end()) + { + map_[it->first] = it->second->Clone(); + } + } + } + + + void DicomMap::ExtractMainDicomTagsInternal(const DicomMap& other, + ResourceType level) + { + const DicomTag* tags = NULL; + size_t size = 0; + + LoadMainDicomTags(tags, size, level); + assert(tags != NULL && size > 0); + + for (size_t i = 0; i < size; i++) + { + Map::const_iterator found = other.map_.find(tags[i]); + + if (found != other.map_.end() && + map_.find(tags[i]) == map_.end()) + { + assert(found->second != NULL); + map_[tags[i]] = found->second->Clone(); + } + } + } + + + void DicomMap::ExtractMainDicomTags(const DicomMap& other) + { + Clear(); + ExtractMainDicomTagsInternal(other, ResourceType_Patient); + ExtractMainDicomTagsInternal(other, ResourceType_Study); + ExtractMainDicomTagsInternal(other, ResourceType_Series); + ExtractMainDicomTagsInternal(other, ResourceType_Instance); + } + + + bool DicomMap::HasOnlyMainDicomTags() const + { + // TODO - Speed up possible by making this std::set a global variable + + std::set<DicomTag> mainDicomTags; + GetMainDicomTags(mainDicomTags); + + for (Map::const_iterator it = map_.begin(); it != map_.end(); ++it) + { + if (mainDicomTags.find(it->first) == mainDicomTags.end()) + { + return false; + } + } + + return true; + } + + void DicomMap::Serialize(Json::Value& target) const { target = Json::objectValue;
--- a/Core/DicomFormat/DicomMap.h Tue Dec 11 13:45:47 2018 +0100 +++ b/Core/DicomFormat/DicomMap.h Fri Dec 14 14:21:27 2018 +0100 @@ -59,7 +59,7 @@ uint16_t element, DicomValue* value); - void SetValue(DicomTag tag, + void SetValue(DicomTag tag, DicomValue* value); void ExtractTags(DicomMap& source, @@ -68,6 +68,9 @@ static void GetMainDicomTagsInternal(std::set<DicomTag>& result, ResourceType level); + void ExtractMainDicomTagsInternal(const DicomMap& other, + ResourceType level); + public: DicomMap() { @@ -217,6 +220,14 @@ bool ParseDouble(double& result, const DicomTag& tag) const; + void FromDicomAsJson(const Json::Value& dicomAsJson); + + void Merge(const DicomMap& other); + + void ExtractMainDicomTags(const DicomMap& other); + + bool HasOnlyMainDicomTags() const; + void Serialize(Json::Value& target) const; void Unserialize(const Json::Value& source);
--- a/LinuxCompilation.txt Tue Dec 11 13:45:47 2018 +0100 +++ b/LinuxCompilation.txt Fri Dec 14 14:21:27 2018 +0100 @@ -115,8 +115,8 @@ -SUPPORTED - Ubuntu 14.04 LTS ----------------------------- +SUPPORTED - Ubuntu 14.04 LTS and 16.04 LTS +------------------------------------------ # sudo apt-get install build-essential unzip cmake mercurial \ uuid-dev libcurl4-openssl-dev liblua5.1-0-dev \
--- a/NEWS Tue Dec 11 13:45:47 2018 +0100 +++ b/NEWS Fri Dec 14 14:21:27 2018 +0100 @@ -2,6 +2,20 @@ =============================== +General +------- + +* Optimization: On C-FIND, avoid accessing the storage area whenever possible +* New configuration option: + - "StorageAccessOnFind" to rule the access to the storage area during C-FIND + +Maintenance +----------- + +* Removal of the "AllowFindSopClassesInStudy" old configuration option +* "/tools/create-dicom" is more tolerant wrt. invalid specific character set + + Version 1.5.0 (2018-12-10) ==========================
--- a/OrthancServer/OrthancFindRequestHandler.cpp Tue Dec 11 13:45:47 2018 +0100 +++ b/OrthancServer/OrthancFindRequestHandler.cpp Fri Dec 14 14:21:27 2018 +0100 @@ -89,82 +89,6 @@ } - static void ExtractTagFromMainDicomTags(std::set<std::string>& target, - ServerIndex& index, - const DicomTag& tag, - const std::list<std::string>& resources, - ResourceType level) - { - for (std::list<std::string>::const_iterator - it = resources.begin(); it != resources.end(); ++it) - { - DicomMap tags; - if (index.GetMainDicomTags(tags, *it, level, level) && - tags.HasTag(tag)) - { - target.insert(tags.GetValue(tag).GetContent()); - } - } - } - - - static bool ExtractMetadata(std::set<std::string>& target, - ServerIndex& index, - MetadataType metadata, - const std::list<std::string>& resources) - { - for (std::list<std::string>::const_iterator - it = resources.begin(); it != resources.end(); ++it) - { - std::string value; - if (index.LookupMetadata(value, *it, metadata)) - { - target.insert(value); - } - else - { - // This metadata is unavailable for some resource, give up - return false; - } - } - - return true; - } - - - static void ExtractTagFromInstancesOnDisk(std::set<std::string>& target, - ServerContext& context, - const DicomTag& tag, - const std::list<std::string>& instances) - { - // WARNING: This function is slow, as it reads the JSON file - // summarizing each instance of interest from the hard drive. - - std::string formatted = tag.Format(); - - for (std::list<std::string>::const_iterator - it = instances.begin(); it != instances.end(); ++it) - { - Json::Value dicom; - context.ReadDicomAsJson(dicom, *it); - - if (dicom.isMember(formatted)) - { - const Json::Value& source = dicom[formatted]; - - if (source.type() == Json::objectValue && - source.isMember("Type") && - source.isMember("Value") && - source["Type"].asString() == "String" && - source["Value"].type() == Json::stringValue) - { - target.insert(source["Value"].asString()); - } - } - } - } - - static void ComputePatientCounters(DicomMap& result, ServerIndex& index, const std::string& patient, @@ -230,7 +154,24 @@ if (query.HasTag(DICOM_TAG_MODALITIES_IN_STUDY)) { std::set<std::string> values; - ExtractTagFromMainDicomTags(values, index, DICOM_TAG_MODALITY, series, ResourceType_Series); + + for (std::list<std::string>::const_iterator + it = series.begin(); it != series.end(); ++it) + { + DicomMap tags; + if (index.GetMainDicomTags(tags, *it, ResourceType_Series, ResourceType_Series)) + { + const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY); + + if (value != NULL && + !value->IsNull() && + !value->IsBinary()) + { + values.insert(value->GetContent()); + } + } + } + StoreSetOfStrings(result, DICOM_TAG_MODALITIES_IN_STUDY, values); } @@ -253,27 +194,17 @@ { std::set<std::string> values; - if (ExtractMetadata(values, index, MetadataType_Instance_SopClassUid, instances)) - { - // The metadata "SopClassUid" is available for each of these instances - StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values); - } - else + for (std::list<std::string>::const_iterator + it = instances.begin(); it != instances.end(); ++it) { - OrthancConfiguration::ReaderLock lock; - - if (lock.GetConfiguration().GetBooleanParameter("AllowFindSopClassesInStudy", false)) + std::string value; + if (context.LookupOrReconstructMetadata(value, *it, MetadataType_Instance_SopClassUid)) { - ExtractTagFromInstancesOnDisk(values, context, DICOM_TAG_SOP_CLASS_UID, instances); - StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values); - } - else - { - result.SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, "", false); - LOG(WARNING) << "The handling of \"SOP Classes in Study\" (0008,0062) " - << "in C-FIND requests is disabled"; + values.insert(value); } } + + StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values); } } @@ -365,11 +296,23 @@ static void AddAnswer(DicomFindAnswers& answers, - const Json::Value& resource, + const DicomMap& mainDicomTags, + const Json::Value* dicomAsJson, const DicomArray& query, const std::list<DicomTag>& sequencesToReturn, const DicomMap* counters) { + DicomMap match; + + if (dicomAsJson != NULL) + { + match.FromDicomAsJson(*dicomAsJson); + } + else + { + match.Assign(mainDicomTags); + } + DicomMap result; for (size_t i = 0; i < query.GetSize(); i++) @@ -385,16 +328,18 @@ } else { - std::string tag = query.GetElement(i).GetTag().Format(); - std::string value; - if (resource.isMember(tag)) + const DicomTag& tag = query.GetElement(i).GetTag(); + const DicomValue* value = match.TestAndGetValue(tag); + + if (value != NULL && + !value->IsNull() && + !value->IsBinary()) { - value = resource.get(tag, Json::arrayValue).get("Value", "").asString(); - result.SetValue(query.GetElement(i).GetTag(), value, false); + result.SetValue(tag, value->GetContent(), false); } else { - result.SetValue(query.GetElement(i).GetTag(), "", false); + result.SetValue(tag, "", false); } } } @@ -417,6 +362,11 @@ { answers.Add(result); } + else if (dicomAsJson == NULL) + { + LOG(WARNING) << "C-FIND query requesting a sequence, but reading JSON from disk is disabled"; + answers.Add(result); + } else { ParsedDicomFile dicom(result); @@ -424,7 +374,8 @@ for (std::list<DicomTag>::const_iterator tag = sequencesToReturn.begin(); tag != sequencesToReturn.end(); ++tag) { - const Json::Value& source = resource[tag->Format()]; + assert(dicomAsJson != NULL); + const Json::Value& source = (*dicomAsJson) [tag->Format()]; if (source.type() == Json::objectValue && source.isMember("Type") && @@ -521,6 +472,76 @@ } + class OrthancFindRequestHandler::LookupVisitor : public LookupResource::IVisitor + { + private: + DicomFindAnswers& answers_; + ServerContext& context_; + ResourceType level_; + const DicomMap& query_; + DicomArray queryAsArray_; + const std::list<DicomTag>& sequencesToReturn_; + + public: + LookupVisitor(DicomFindAnswers& answers, + ServerContext& context, + ResourceType level, + const DicomMap& query, + const std::list<DicomTag>& sequencesToReturn) : + answers_(answers), + context_(context), + level_(level), + query_(query), + queryAsArray_(query), + sequencesToReturn_(sequencesToReturn) + { + answers_.SetComplete(false); + } + + virtual bool IsDicomAsJsonNeeded() const + { + // Ask the "DICOM-as-JSON" attachment only if sequences are to + // be returned OR if "query_" contains non-main DICOM tags! + + DicomMap withoutSpecialTags; + withoutSpecialTags.Assign(query_); + + // Check out "ComputeCounters()" + withoutSpecialTags.Remove(DICOM_TAG_MODALITIES_IN_STUDY); + withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES); + withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES); + withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES); + withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES); + withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES); + withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES); + withoutSpecialTags.Remove(DICOM_TAG_SOP_CLASSES_IN_STUDY); + + // Check out "AddAnswer()" + withoutSpecialTags.Remove(DICOM_TAG_SPECIFIC_CHARACTER_SET); + withoutSpecialTags.Remove(DICOM_TAG_QUERY_RETRIEVE_LEVEL); + + return (!sequencesToReturn_.empty() || + !withoutSpecialTags.HasOnlyMainDicomTags()); + } + + virtual void MarkAsComplete() + { + answers_.SetComplete(true); + } + + virtual void Visit(const std::string& publicId, + const std::string& instanceId, + const DicomMap& mainDicomTags, + const Json::Value* dicomAsJson) + { + std::auto_ptr<DicomMap> counters(ComputeCounters(context_, instanceId, level_, query_)); + + AddAnswer(answers_, mainDicomTags, dicomAsJson, + queryAsArray_, sequencesToReturn_, counters.get()); + } + }; + + void OrthancFindRequestHandler::Handle(DicomFindAnswers& answers, const DicomMap& input, const std::list<DicomTag>& sequencesToReturn, @@ -623,8 +644,6 @@ if (FilterQueryTag(value, level, tag, manufacturer)) { - // TODO - Move this to "ResourceLookup::AddDicomConstraint()" - ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag); // DICOM specifies that searches must be case sensitive, except @@ -651,42 +670,9 @@ size_t limit = (level == ResourceType_Instance) ? maxInstances_ : maxResults_; - // TODO - Use ServerContext::Apply() at this point, in order to - // share the code with the "/tools/find" REST URI - std::vector<std::string> resources, instances; - context_.GetIndex().FindCandidates(resources, instances, lookup); - LOG(INFO) << "Number of candidate resources after fast DB filtering: " << resources.size(); - - assert(resources.size() == instances.size()); - bool complete = true; - - for (size_t i = 0; i < instances.size(); i++) - { - // TODO - Don't read the full JSON from the disk if only "main - // DICOM tags" are to be returned - Json::Value dicom; - context_.ReadDicomAsJson(dicom, instances[i]); - - if (lookup.IsMatch(dicom)) - { - if (limit != 0 && - answers.GetSize() >= limit) - { - complete = false; - break; - } - else - { - std::auto_ptr<DicomMap> counters(ComputeCounters(context_, instances[i], level, *filteredInput)); - AddAnswer(answers, dicom, query, sequencesToReturn, counters.get()); - } - } - } - - LOG(INFO) << "Number of matching resources: " << answers.GetSize(); - - answers.SetComplete(complete); + LookupVisitor visitor(answers, context_, level, *filteredInput, sequencesToReturn); + context_.Apply(visitor, lookup, 0 /* "since" is not relevant to C-FIND */, limit); }
--- a/OrthancServer/OrthancFindRequestHandler.h Tue Dec 11 13:45:47 2018 +0100 +++ b/OrthancServer/OrthancFindRequestHandler.h Fri Dec 14 14:21:27 2018 +0100 @@ -41,6 +41,8 @@ class OrthancFindRequestHandler : public IFindRequestHandler { private: + class LookupVisitor; + ServerContext& context_; unsigned int maxResults_; unsigned int maxInstances_;
--- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Tue Dec 11 13:45:47 2018 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Fri Dec 14 14:21:27 2018 +0100 @@ -440,6 +440,7 @@ // Select one existing child instance of the parent resource, to // retrieve all its tags Json::Value siblingTags; + std::string siblingInstanceId; { // Retrieve all the instances of the parent resource @@ -452,7 +453,8 @@ throw OrthancException(ErrorCode_InternalError); } - context.ReadDicomAsJson(siblingTags, siblingInstances.front()); + siblingInstanceId = siblingInstances.front(); + context.ReadDicomAsJson(siblingTags, siblingInstanceId); } @@ -463,11 +465,14 @@ if (siblingTags.isMember(SPECIFIC_CHARACTER_SET)) { Encoding encoding; + if (!siblingTags[SPECIFIC_CHARACTER_SET].isMember("Value") || siblingTags[SPECIFIC_CHARACTER_SET]["Value"].type() != Json::stringValue || !GetDicomEncoding(encoding, siblingTags[SPECIFIC_CHARACTER_SET]["Value"].asCString())) { - throw OrthancException(ErrorCode_CreateDicomParentEncoding); + LOG(WARNING) << "Instance with an incorrect Specific Character Set, " + << "using the default Orthanc encoding: " << siblingInstanceId; + encoding = GetDefaultDicomEncoding(); } dicom.SetEncoding(encoding);
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Tue Dec 11 13:45:47 2018 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Fri Dec 14 14:21:27 2018 +0100 @@ -1269,6 +1269,49 @@ } + namespace + { + class FindVisitor : public LookupResource::IVisitor + { + private: + bool isComplete_; + std::list<std::string> resources_; + + public: + FindVisitor() : + isComplete_(false) + { + } + + virtual bool IsDicomAsJsonNeeded() const + { + return false; // (*) + } + + virtual void MarkAsComplete() + { + isComplete_ = true; // Unused information as of Orthanc 1.5.0 + } + + virtual void Visit(const std::string& publicId, + const std::string& instanceId /* unused */, + const DicomMap& mainDicomTags /* unused */, + const Json::Value* dicomAsJson /* unused (*) */) + { + resources_.push_back(publicId); + } + + void Answer(RestApiOutput& output, + ServerIndex& index, + ResourceType level, + bool expand) const + { + AnswerListOfResources(output, index, resources_, level, expand); + } + }; + } + + static void Find(RestApiPostCall& call) { static const char* const KEY_CASE_SENSITIVE = "CaseSensitive"; @@ -1281,15 +1324,43 @@ ServerContext& context = OrthancRestApi::GetContext(call); Json::Value request; - if (call.ParseJsonRequest(request) && - request.type() == Json::objectValue && - request.isMember(KEY_LEVEL) && - request.isMember(KEY_QUERY) && - request[KEY_LEVEL].type() == Json::stringValue && - request[KEY_QUERY].type() == Json::objectValue && - (!request.isMember(KEY_CASE_SENSITIVE) || request[KEY_CASE_SENSITIVE].type() == Json::booleanValue) && - (!request.isMember(KEY_LIMIT) || request[KEY_LIMIT].type() == Json::intValue) && - (!request.isMember(KEY_SINCE) || request[KEY_SINCE].type() == Json::intValue)) + if (!call.ParseJsonRequest(request) || + request.type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadRequest, + "The body must contain a JSON object"); + } + else if (!request.isMember(KEY_LEVEL) || + request[KEY_LEVEL].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_LEVEL) + "\" is missing, or should be a string"); + } + else if (!request.isMember(KEY_QUERY) && + request[KEY_QUERY].type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_QUERY) + "\" is missing, or should be a JSON object"); + } + else if (request.isMember(KEY_CASE_SENSITIVE) && + request[KEY_CASE_SENSITIVE].type() != Json::booleanValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_CASE_SENSITIVE) + "\" should be a Boolean"); + } + else if (request.isMember(KEY_LIMIT) && + request[KEY_LIMIT].type() != Json::intValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_LIMIT) + "\" should be an integer"); + } + else if (request.isMember(KEY_SINCE) && + request[KEY_SINCE].type() != Json::intValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_SINCE) + "\" should be an integer"); + } + else { bool expand = false; if (request.isMember(KEY_EXPAND)) @@ -1309,7 +1380,8 @@ int tmp = request[KEY_LIMIT].asInt(); if (tmp < 0) { - throw OrthancException(ErrorCode_ParameterOutOfRange); + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Field \"" + std::string(KEY_LIMIT) + "\" should be a positive integer"); } limit = static_cast<size_t>(tmp); @@ -1321,7 +1393,8 @@ int tmp = request[KEY_SINCE].asInt(); if (tmp < 0) { - throw OrthancException(ErrorCode_ParameterOutOfRange); + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Field \"" + std::string(KEY_SINCE) + "\" should be a positive integer"); } since = static_cast<size_t>(tmp); @@ -1336,7 +1409,8 @@ { if (request[KEY_QUERY][members[i]].type() != Json::stringValue) { - throw OrthancException(ErrorCode_BadRequest); + throw OrthancException(ErrorCode_BadRequest, + "Tag \"" + members[i] + "\" should be associated with a string"); } query.AddDicomConstraint(FromDcmtkBridge::ParseTag(members[i]), @@ -1344,15 +1418,9 @@ caseSensitive); } - bool isComplete; - std::list<std::string> resources; - context.Apply(isComplete, resources, query, since, limit); - AnswerListOfResources(call.GetOutput(), context.GetIndex(), - resources, query.GetLevel(), expand); - } - else - { - throw OrthancException(ErrorCode_BadRequest); + FindVisitor visitor; + context.Apply(visitor, query, since, limit); + visitor.Answer(call.GetOutput(), context.GetIndex(), query.GetLevel(), expand); } }
--- a/OrthancServer/Search/LookupIdentifierQuery.cpp Tue Dec 11 13:45:47 2018 +0100 +++ b/OrthancServer/Search/LookupIdentifierQuery.cpp Fri Dec 14 14:21:27 2018 +0100 @@ -34,9 +34,10 @@ #include "../PrecompiledHeadersServer.h" #include "LookupIdentifierQuery.h" +#include "../../Core/DicomParsing/FromDcmtkBridge.h" #include "../../Core/OrthancException.h" +#include "../ServerToolbox.h" #include "SetOfResources.h" -#include "../../Core/DicomParsing/FromDcmtkBridge.h" #include <cassert> @@ -44,6 +45,28 @@ namespace Orthanc { + LookupIdentifierQuery::SingleConstraint:: + SingleConstraint(const DicomTag& tag, + IdentifierConstraintType type, + const std::string& value) : + tag_(tag), + type_(type), + value_(ServerToolbox::NormalizeIdentifier(value)) + { + } + + + LookupIdentifierQuery::RangeConstraint:: + RangeConstraint(const DicomTag& tag, + const std::string& start, + const std::string& end) : + tag_(tag), + start_(ServerToolbox::NormalizeIdentifier(start)), + end_(ServerToolbox::NormalizeIdentifier(end)) + { + } + + LookupIdentifierQuery::Disjunction::~Disjunction() { for (size_t i = 0; i < singleConstraints_.size(); i++) @@ -84,6 +107,12 @@ } + bool LookupIdentifierQuery::IsIdentifier(const DicomTag& tag) + { + return ServerToolbox::IsIdentifier(tag, level_); + } + + void LookupIdentifierQuery::AddConstraint(DicomTag tag, IdentifierConstraintType type, const std::string& value)
--- a/OrthancServer/Search/LookupIdentifierQuery.h Tue Dec 11 13:45:47 2018 +0100 +++ b/OrthancServer/Search/LookupIdentifierQuery.h Fri Dec 14 14:21:27 2018 +0100 @@ -33,7 +33,6 @@ #pragma once -#include "../ServerToolbox.h" #include "../IDatabaseWrapper.h" #include "SetOfResources.h" @@ -79,12 +78,7 @@ public: SingleConstraint(const DicomTag& tag, IdentifierConstraintType type, - const std::string& value) : - tag_(tag), - type_(type), - value_(ServerToolbox::NormalizeIdentifier(value)) - { - } + const std::string& value); const DicomTag& GetTag() const { @@ -113,12 +107,7 @@ public: RangeConstraint(const DicomTag& tag, const std::string& start, - const std::string& end) : - tag_(tag), - start_(ServerToolbox::NormalizeIdentifier(start)), - end_(ServerToolbox::NormalizeIdentifier(end)) - { - } + const std::string& end); const DicomTag& GetTag() const { @@ -189,10 +178,7 @@ ~LookupIdentifierQuery(); - bool IsIdentifier(const DicomTag& tag) - { - return ServerToolbox::IsIdentifier(tag, level_); - } + bool IsIdentifier(const DicomTag& tag); void AddConstraint(DicomTag tag, IdentifierConstraintType type,
--- a/OrthancServer/Search/LookupResource.cpp Tue Dec 11 13:45:47 2018 +0100 +++ b/OrthancServer/Search/LookupResource.cpp Fri Dec 14 14:21:27 2018 +0100 @@ -42,6 +42,19 @@ namespace Orthanc { + static bool DoesDicomMapMatch(const DicomMap& dicom, + const DicomTag& tag, + const IFindConstraint& constraint) + { + const DicomValue* value = dicom.TestAndGetValue(tag); + + return (value != NULL && + !value->IsNull() && + !value->IsBinary() && + constraint.Match(value->GetContent())); + } + + LookupResource::Level::Level(ResourceType level) : level_(level) { const DicomTag* tags = NULL; @@ -113,11 +126,40 @@ } else { + // This is not a main DICOM tag return false; } } + bool LookupResource::Level::IsMatch(const DicomMap& dicom) const + { + for (Constraints::const_iterator it = identifiersConstraints_.begin(); + it != identifiersConstraints_.end(); ++it) + { + assert(it->second != NULL); + + if (!DoesDicomMapMatch(dicom, it->first, *it->second)) + { + return false; + } + } + + for (Constraints::const_iterator it = mainTagsConstraints_.begin(); + it != mainTagsConstraints_.end(); ++it) + { + assert(it->second != NULL); + + if (!DoesDicomMapMatch(dicom, it->first, *it->second)) + { + return false; + } + } + + return true; + } + + LookupResource::LookupResource(ResourceType level) : level_(level) { switch (level) @@ -282,22 +324,22 @@ - bool LookupResource::IsMatch(const Json::Value& dicomAsJson) const + bool LookupResource::IsMatch(const DicomMap& dicom) const { + for (Levels::const_iterator it = levels_.begin(); it != levels_.end(); ++it) + { + if (!it->second->IsMatch(dicom)) + { + return false; + } + } + for (Constraints::const_iterator it = unoptimizedConstraints_.begin(); it != unoptimizedConstraints_.end(); ++it) { - std::string tag = it->first.Format(); - if (dicomAsJson.isMember(tag) && - dicomAsJson[tag]["Type"] == "String") - { - std::string value = dicomAsJson[tag]["Value"].asString(); - if (!it->second->Match(value)) - { - return false; - } - } - else + assert(it->second != NULL); + + if (!DoesDicomMapMatch(dicom, it->first, *it->second)) { return false; }
--- a/OrthancServer/Search/LookupResource.h Tue Dec 11 13:45:47 2018 +0100 +++ b/OrthancServer/Search/LookupResource.h Fri Dec 14 14:21:27 2018 +0100 @@ -64,13 +64,15 @@ void Apply(SetOfResources& candidates, IDatabaseWrapper& database) const; + + bool IsMatch(const DicomMap& dicom) const; }; typedef std::map<ResourceType, Level*> Levels; ResourceType level_; Levels levels_; - Constraints unoptimizedConstraints_; + Constraints unoptimizedConstraints_; // Constraints on non-main DICOM tags std::auto_ptr<ListConstraint> modalitiesInStudy_; bool AddInternal(ResourceType level, @@ -82,6 +84,23 @@ IDatabaseWrapper& database) const; public: + class IVisitor : public boost::noncopyable + { + public: + virtual ~IVisitor() + { + } + + virtual bool IsDicomAsJsonNeeded() const = 0; + + virtual void MarkAsComplete() = 0; + + virtual void Visit(const std::string& publicId, + const std::string& instanceId, + const DicomMap& mainDicomTags, + const Json::Value* dicomAsJson) = 0; + }; + LookupResource(ResourceType level); ~LookupResource(); @@ -103,6 +122,11 @@ void FindCandidates(std::list<int64_t>& result, IDatabaseWrapper& database) const; - bool IsMatch(const Json::Value& dicomAsJson) const; + bool HasOnlyMainDicomTags() const + { + return unoptimizedConstraints_.empty(); + } + + bool IsMatch(const DicomMap& dicom) const; }; }
--- a/OrthancServer/ServerContext.cpp Tue Dec 11 13:45:47 2018 +0100 +++ b/OrthancServer/ServerContext.cpp Fri Dec 14 14:21:27 2018 +0100 @@ -773,27 +773,91 @@ } - void ServerContext::Apply(bool& isComplete, - std::list<std::string>& result, + void ServerContext::Apply(LookupResource::IVisitor& visitor, const ::Orthanc::LookupResource& lookup, size_t since, size_t limit) { - result.clear(); - isComplete = true; + LookupMode mode; + + { + // New configuration option in 1.5.1 + OrthancConfiguration::ReaderLock lock; + + std::string value = lock.GetConfiguration().GetStringParameter("StorageAccessOnFind", "Always"); + + if (value == "Always") + { + mode = LookupMode_DiskOnLookupAndAnswer; + } + else if (value == "Never") + { + mode = LookupMode_DatabaseOnly; + } + else if (value == "Answers") + { + mode = LookupMode_DiskOnAnswer; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Configuration option \"StorageAccessOnFind\" " + "should be \"Always\", \"Never\" or \"Answers\": " + value); + } + } + std::vector<std::string> resources, instances; GetIndex().FindCandidates(resources, instances, lookup); + LOG(INFO) << "Number of candidate resources after fast DB filtering on main DICOM tags: " << resources.size(); + assert(resources.size() == instances.size()); + size_t countResults = 0; size_t skipped = 0; + bool complete = true; + + const bool isDicomAsJsonNeeded = visitor.IsDicomAsJsonNeeded(); + for (size_t i = 0; i < instances.size(); i++) { - // TODO - Don't read the full JSON from the disk if only "main - // DICOM tags" are to be returned - Json::Value dicom; - ReadDicomAsJson(dicom, instances[i]); + // Optimization in Orthanc 1.5.1 - Don't read the full JSON from + // the disk if only "main DICOM tags" are to be returned + + std::auto_ptr<Json::Value> dicomAsJson; + + bool hasOnlyMainDicomTags; + DicomMap dicom; + + if (mode == LookupMode_DatabaseOnly || + mode == LookupMode_DiskOnAnswer || + lookup.HasOnlyMainDicomTags()) + { + // Case (1): The main DICOM tags, as stored in the database, + // are sufficient to look for match + + if (!GetIndex().GetAllMainDicomTags(dicom, instances[i])) + { + // The instance has been removed during the execution of the + // lookup, ignore it + continue; + } + + hasOnlyMainDicomTags = true; + } + else + { + // Case (2): Need to read the "DICOM-as-JSON" attachment from + // the storage area + dicomAsJson.reset(new Json::Value); + ReadDicomAsJson(*dicomAsJson, instances[i]); + + dicom.FromDicomAsJson(*dicomAsJson); + + // This map contains the entire JSON, i.e. more than the main DICOM tags + hasOnlyMainDicomTags = false; + } if (lookup.IsMatch(dicom)) { @@ -802,17 +866,120 @@ skipped++; } else if (limit != 0 && - result.size() >= limit) + countResults >= limit) { - isComplete = false; - return; // too many results + // Too many results, don't mark as complete + complete = false; + break; } else { - result.push_back(resources[i]); + if ((mode == LookupMode_DiskOnLookupAndAnswer || + mode == LookupMode_DiskOnAnswer) && + dicomAsJson.get() == NULL && + isDicomAsJsonNeeded) + { + dicomAsJson.reset(new Json::Value); + ReadDicomAsJson(*dicomAsJson, instances[i]); + } + + if (hasOnlyMainDicomTags) + { + // This is Case (1): The variable "dicom" only contains the main DICOM tags + visitor.Visit(resources[i], instances[i], dicom, dicomAsJson.get()); + } + else + { + // Remove the non-main DICOM tags from "dicom" if Case (2) + // was used, for consistency with Case (1) + + DicomMap mainDicomTags; + mainDicomTags.ExtractMainDicomTags(dicom); + visitor.Visit(resources[i], instances[i], mainDicomTags, dicomAsJson.get()); + } + + countResults ++; } } } + + if (complete) + { + visitor.MarkAsComplete(); + } + + LOG(INFO) << "Number of matching resources: " << countResults; + } + + + bool ServerContext::LookupOrReconstructMetadata(std::string& target, + const std::string& publicId, + MetadataType metadata) + { + // This is a backwards-compatibility function, that can + // reconstruct metadata that were not generated by an older + // release of Orthanc + + if (metadata == MetadataType_Instance_SopClassUid || + metadata == MetadataType_Instance_TransferSyntax) + { + if (index_.LookupMetadata(target, publicId, metadata)) + { + return true; + } + else + { + // These metadata are mandatory in DICOM instances, and were + // introduced in Orthanc 1.2.0. The fact that + // "LookupMetadata()" has failed indicates that this database + // comes from an older release of Orthanc. + + DicomTag tag(0, 0); + + switch (metadata) + { + case MetadataType_Instance_SopClassUid: + tag = DICOM_TAG_SOP_CLASS_UID; + break; + + case MetadataType_Instance_TransferSyntax: + tag = DICOM_TAG_TRANSFER_SYNTAX_UID; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + Json::Value dicomAsJson; + ReadDicomAsJson(dicomAsJson, publicId); + + DicomMap tags; + tags.FromDicomAsJson(dicomAsJson); + + const DicomValue* value = tags.TestAndGetValue(tag); + + if (value != NULL && + !value->IsNull() && + !value->IsBinary()) + { + target = value->GetContent(); + + // Store for reuse + index_.SetMetadata(publicId, metadata, target); + return true; + } + else + { + // Should never happen + return false; + } + } + } + else + { + // No backward + return index_.LookupMetadata(target, publicId, metadata); + } }
--- a/OrthancServer/ServerContext.h Tue Dec 11 13:45:47 2018 +0100 +++ b/OrthancServer/ServerContext.h Fri Dec 14 14:21:27 2018 +0100 @@ -38,6 +38,7 @@ #include "LuaScripting.h" #include "OrthancHttpHandler.h" #include "ServerIndex.h" +#include "Search/LookupResource.h" #include "../Core/Cache/MemoryCache.h" #include "../Core/Cache/SharedArchive.h" @@ -63,6 +64,14 @@ class ServerContext : private JobsRegistry::IObserver { private: + enum LookupMode + { + LookupMode_DatabaseOnly, + LookupMode_DiskOnAnswer, + LookupMode_DiskOnLookupAndAnswer + }; + + class LuaServerListener : public IServerListener { private: @@ -334,12 +343,15 @@ void Stop(); - void Apply(bool& isComplete, - std::list<std::string>& result, + void Apply(LookupResource::IVisitor& visitor, const ::Orthanc::LookupResource& lookup, size_t since, size_t limit); + bool LookupOrReconstructMetadata(std::string& target, + const std::string& publicId, + MetadataType type); + /** * Management of the plugins
--- a/OrthancServer/ServerIndex.cpp Tue Dec 11 13:45:47 2018 +0100 +++ b/OrthancServer/ServerIndex.cpp Fri Dec 14 14:21:27 2018 +0100 @@ -2248,6 +2248,78 @@ } + bool ServerIndex::GetAllMainDicomTags(DicomMap& result, + const std::string& instancePublicId) + { + result.Clear(); + + boost::mutex::scoped_lock lock(mutex_); + + // Lookup for the requested resource + int64_t instance; + ResourceType type; + if (!db_.LookupResource(instance, type, instancePublicId) || + type != ResourceType_Instance) + { + return false; + } + else + { + DicomMap tmp; + + db_.GetMainDicomTags(tmp, instance); + result.Merge(tmp); + + int64_t series; + if (!db_.LookupParent(series, instance)) + { + throw OrthancException(ErrorCode_InternalError); + } + + tmp.Clear(); + db_.GetMainDicomTags(tmp, series); + result.Merge(tmp); + + int64_t study; + if (!db_.LookupParent(study, series)) + { + throw OrthancException(ErrorCode_InternalError); + } + + tmp.Clear(); + db_.GetMainDicomTags(tmp, study); + result.Merge(tmp); + +#ifndef NDEBUG + { + // Sanity test to check that all the main DICOM tags from the + // patient level are copied at the study level + + int64_t patient; + if (!db_.LookupParent(patient, study)) + { + throw OrthancException(ErrorCode_InternalError); + } + + tmp.Clear(); + db_.GetMainDicomTags(tmp, study); + + std::set<DicomTag> patientTags; + tmp.GetTags(patientTags); + + for (std::set<DicomTag>::const_iterator + it = patientTags.begin(); it != patientTags.end(); ++it) + { + assert(result.HasTag(*it)); + } + } +#endif + + return true; + } + } + + bool ServerIndex::LookupResourceType(ResourceType& type, const std::string& publicId) {
--- a/OrthancServer/ServerIndex.h Tue Dec 11 13:45:47 2018 +0100 +++ b/OrthancServer/ServerIndex.h Fri Dec 14 14:21:27 2018 +0100 @@ -275,6 +275,10 @@ ResourceType expectedType, ResourceType levelOfInterest); + // Only applicable at the instance level + bool GetAllMainDicomTags(DicomMap& result, + const std::string& instancePublicId); + bool LookupResourceType(ResourceType& type, const std::string& publicId);
--- a/Resources/Configuration.json Tue Dec 11 13:45:47 2018 +0100 +++ b/Resources/Configuration.json Fri Dec 14 14:21:27 2018 +0100 @@ -384,14 +384,6 @@ } **/ - // If set to "true", Orthanc will still handle "SOP Classes in - // Study" (0008,0062) in C-FIND requests, even if the "SOP Class - // UID" metadata is not available in the database (which is the case - // if the DB was previously used by Orthanc <= 1.1.0). This option - // is turned off by default, as it requires intensive accesses to - // the hard drive. - "AllowFindSopClassesInStudy" : false, - // If set to "false", Orthanc will not load its default dictionary // of private tags. This might be necessary if you cannot import a // DICOM file encoded using the Implicit VR Endian transfer syntax, @@ -447,5 +439,17 @@ // The least recently used archives get deleted as new archives are // generated. This option was introduced in Orthanc 1.5.0, and has // no effect on the synchronous generation of archives. - "MediaArchiveSize" : 1 + "MediaArchiveSize" : 1, + + // Performance setting to specify how Orthanc accesses the storage + // area during C-FIND. Three modes are available: (1) "Always" + // allows Orthanc to read the storage area as soon as it needs an + // information that is not present in its database (slowest mode), + // (2) "Never" prevents Orthanc from accessing the storage area, and + // makes it uses exclusively its database (fastest mode), and (3) + // "Answers" allows Orthanc to read the storage area to generate its + // answers, but not to filter the DICOM resources (balance between + // the two modes). By default, the mode is "Always", which + // corresponds to the behavior of Orthanc <= 1.5.0. + "StorageAccessOnFind" : "Always" }
--- a/UnitTestsSources/DicomMapTests.cpp Tue Dec 11 13:45:47 2018 +0100 +++ b/UnitTestsSources/DicomMapTests.cpp Fri Dec 14 14:21:27 2018 +0100 @@ -37,8 +37,12 @@ #include "../Core/OrthancException.h" #include "../Core/DicomFormat/DicomMap.h" #include "../Core/DicomParsing/FromDcmtkBridge.h" +#include "../Core/DicomParsing/ParsedDicomFile.h" + +#include "../OrthancServer/DicomInstanceToStore.h" #include <memory> +#include <dcmtk/dcmdata/dcdeftag.h> using namespace Orthanc; @@ -409,3 +413,140 @@ ASSERT_THROW(v->GetContent(), OrthancException); } } + + + +TEST(DicomMap, DicomAsJson) +{ + // This is a Latin-1 test string: "crane" with a circumflex accent + const unsigned char raw[] = { 0x63, 0x72, 0xe2, 0x6e, 0x65 }; + std::string latin1((char*) &raw[0], sizeof(raw) / sizeof(char)); + + std::string utf8 = Toolbox::ConvertToUtf8(latin1, Encoding_Latin1); + + ParsedDicomFile dicom(false); + dicom.SetEncoding(Encoding_Latin1); + dicom.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "Hello"); + dicom.ReplacePlainString(DICOM_TAG_STUDY_DESCRIPTION, utf8); + dicom.ReplacePlainString(DICOM_TAG_SERIES_DESCRIPTION, std::string(ORTHANC_MAXIMUM_TAG_LENGTH, 'a')); + dicom.ReplacePlainString(DICOM_TAG_MANUFACTURER, std::string(ORTHANC_MAXIMUM_TAG_LENGTH + 1, 'a')); + dicom.ReplacePlainString(DICOM_TAG_PIXEL_DATA, "binary"); + dicom.ReplacePlainString(DICOM_TAG_ROWS, "512"); + + DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset(); + dataset.insertEmptyElement(DCM_StudyID, OFFalse); + + { + std::auto_ptr<DcmSequenceOfItems> sequence(new DcmSequenceOfItems(DCM_ReferencedSeriesSequence)); + + { + std::auto_ptr<DcmItem> item(new DcmItem); + item->putAndInsertString(DCM_ReferencedSOPInstanceUID, "nope", OFFalse); + ASSERT_TRUE(sequence->insert(item.release(), false, false).good()); + } + + ASSERT_TRUE(dataset.insert(sequence.release(), false, false).good()); + } + + + // Check re-encoding + DcmElement* element = NULL; + ASSERT_TRUE(dataset.findAndGetElement(DCM_StudyDescription, element).good() && + element != NULL); + + char* c = NULL; + ASSERT_TRUE(element != NULL && + element->isLeaf() && + element->isaString() && + element->getString(c).good()); + ASSERT_EQ(0, memcmp(c, raw, latin1.length())); + + ASSERT_TRUE(dataset.findAndGetElement(DCM_Rows, element).good() && + element != NULL && + element->getTag().getEVR() == EVR_US); + + DicomInstanceToStore toStore; + toStore.SetParsedDicomFile(dicom); + + DicomMap m; + m.FromDicomAsJson(toStore.GetJson()); + + ASSERT_EQ("ISO_IR 100", m.GetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET).GetContent()); + + ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).IsBinary()); + ASSERT_EQ("Hello", m.GetValue(DICOM_TAG_PATIENT_NAME).GetContent()); + + ASSERT_FALSE(m.GetValue(DICOM_TAG_STUDY_DESCRIPTION).IsBinary()); + ASSERT_EQ(utf8, m.GetValue(DICOM_TAG_STUDY_DESCRIPTION).GetContent()); + + ASSERT_FALSE(m.HasTag(DICOM_TAG_MANUFACTURER)); // Too long + ASSERT_FALSE(m.HasTag(DICOM_TAG_PIXEL_DATA)); // Pixel data + ASSERT_FALSE(m.HasTag(DICOM_TAG_REFERENCED_SERIES_SEQUENCE)); // Sequence + ASSERT_EQ(DICOM_TAG_REFERENCED_SERIES_SEQUENCE.GetGroup(), DCM_ReferencedSeriesSequence.getGroup()); + ASSERT_EQ(DICOM_TAG_REFERENCED_SERIES_SEQUENCE.GetElement(), DCM_ReferencedSeriesSequence.getElement()); + + ASSERT_TRUE(m.HasTag(DICOM_TAG_SERIES_DESCRIPTION)); // Maximum length + ASSERT_FALSE(m.GetValue(DICOM_TAG_SERIES_DESCRIPTION).IsBinary()); + ASSERT_EQ(ORTHANC_MAXIMUM_TAG_LENGTH, m.GetValue(DICOM_TAG_SERIES_DESCRIPTION).GetContent().length()); + + ASSERT_FALSE(m.GetValue(DICOM_TAG_ROWS).IsBinary()); + ASSERT_EQ("512", m.GetValue(DICOM_TAG_ROWS).GetContent()); + + ASSERT_FALSE(m.GetValue(DICOM_TAG_STUDY_ID).IsNull()); + ASSERT_FALSE(m.GetValue(DICOM_TAG_STUDY_ID).IsBinary()); + ASSERT_EQ("", m.GetValue(DICOM_TAG_STUDY_ID).GetContent()); + + DicomArray a(m); + ASSERT_EQ(6u, a.GetSize()); + + + //dicom.SaveToFile("/tmp/test.dcm"); + //std::cout << toStore.GetJson() << std::endl; + //a.Print(stdout); +} + + + +TEST(DicomMap, ExtractMainDicomTags) +{ + DicomMap b; + b.SetValue(DICOM_TAG_PATIENT_NAME, "E", false); + ASSERT_TRUE(b.HasOnlyMainDicomTags()); + + { + DicomMap a; + a.SetValue(DICOM_TAG_PATIENT_NAME, "A", false); + a.SetValue(DICOM_TAG_STUDY_DESCRIPTION, "B", false); + a.SetValue(DICOM_TAG_SERIES_DESCRIPTION, "C", false); + a.SetValue(DICOM_TAG_NUMBER_OF_FRAMES, "D", false); + a.SetValue(DICOM_TAG_SLICE_THICKNESS, "F", false); + ASSERT_FALSE(a.HasOnlyMainDicomTags()); + b.ExtractMainDicomTags(a); + } + + ASSERT_EQ(4u, b.GetSize()); + ASSERT_EQ("A", b.GetValue(DICOM_TAG_PATIENT_NAME).GetContent()); + ASSERT_EQ("B", b.GetValue(DICOM_TAG_STUDY_DESCRIPTION).GetContent()); + ASSERT_EQ("C", b.GetValue(DICOM_TAG_SERIES_DESCRIPTION).GetContent()); + ASSERT_EQ("D", b.GetValue(DICOM_TAG_NUMBER_OF_FRAMES).GetContent()); + ASSERT_FALSE(b.HasTag(DICOM_TAG_SLICE_THICKNESS)); + ASSERT_TRUE(b.HasOnlyMainDicomTags()); + + b.SetValue(DICOM_TAG_PATIENT_NAME, "G", false); + + { + DicomMap a; + a.SetValue(DICOM_TAG_PATIENT_NAME, "A", false); + a.SetValue(DICOM_TAG_SLICE_THICKNESS, "F", false); + ASSERT_FALSE(a.HasOnlyMainDicomTags()); + b.Merge(a); + } + + ASSERT_EQ(5u, b.GetSize()); + ASSERT_EQ("G", b.GetValue(DICOM_TAG_PATIENT_NAME).GetContent()); + ASSERT_EQ("B", b.GetValue(DICOM_TAG_STUDY_DESCRIPTION).GetContent()); + ASSERT_EQ("C", b.GetValue(DICOM_TAG_SERIES_DESCRIPTION).GetContent()); + ASSERT_EQ("D", b.GetValue(DICOM_TAG_NUMBER_OF_FRAMES).GetContent()); + ASSERT_EQ("F", b.GetValue(DICOM_TAG_SLICE_THICKNESS).GetContent()); + ASSERT_FALSE(b.HasOnlyMainDicomTags()); +}
--- a/UnitTestsSources/ServerIndexTests.cpp Tue Dec 11 13:45:47 2018 +0100 +++ b/UnitTestsSources/ServerIndexTests.cpp Fri Dec 14 14:21:27 2018 +0100 @@ -38,9 +38,10 @@ #include "../Core/FileStorage/MemoryStorageArea.h" #include "../Core/Logging.h" #include "../OrthancServer/DatabaseWrapper.h" +#include "../OrthancServer/Search/LookupIdentifierQuery.h" #include "../OrthancServer/ServerContext.h" #include "../OrthancServer/ServerIndex.h" -#include "../OrthancServer/Search/LookupIdentifierQuery.h" +#include "../OrthancServer/ServerToolbox.h" #include <ctype.h> #include <algorithm>