Mercurial > hg > orthanc-dicomweb
changeset 113:04fbfd59a60e dev
integration mainline->dev
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Thu, 28 Apr 2016 17:22:02 +0200 |
parents | bcc9e98bb725 (current diff) 528d18573c09 (diff) |
children | bee10bb1331b |
files | NEWS Plugin/Configuration.cpp Plugin/Configuration.h |
diffstat | 9 files changed, 792 insertions(+), 144 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Thu Apr 28 09:14:06 2016 +0200 +++ b/NEWS Thu Apr 28 17:22:02 2016 +0200 @@ -7,6 +7,10 @@ * Better robustness in the STOW-RS server +* Fix issue #13 (QIDO-RS study-level query is slow) +* Fix issue #14 (Aggregate fields empty for QIDO-RS study/series-level queries) + + Version 0.2 (2015/12/10) ========================
--- a/Plugin/Configuration.cpp Thu Apr 28 09:14:06 2016 +0200 +++ b/Plugin/Configuration.cpp Thu Apr 28 17:22:02 2016 +0200 @@ -430,5 +430,27 @@ return (ssl ? "https://" : "http://") + host + GetRoot(configuration); } + + + + std::string GetWadoUrl(const std::string& wadoBase, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const std::string& sopInstanceUid) + { + if (studyInstanceUid.empty() || + seriesInstanceUid.empty() || + sopInstanceUid.empty()) + { + return ""; + } + else + { + return (wadoBase + + "studies/" + studyInstanceUid + + "/series/" + seriesInstanceUid + + "/instances/" + sopInstanceUid + "/"); + } + } } }
--- a/Plugin/Configuration.h Thu Apr 28 09:14:06 2016 +0200 +++ b/Plugin/Configuration.h Thu Apr 28 17:22:02 2016 +0200 @@ -93,5 +93,10 @@ std::string GetBaseUrl(const Json::Value& configuration, const OrthancPluginHttpRequest* request); + + std::string GetWadoUrl(const std::string& wadoBase, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const std::string& sopInstanceUid); } }
--- a/Plugin/Dicom.cpp Thu Apr 28 09:14:06 2016 +0200 +++ b/Plugin/Dicom.cpp Thu Apr 28 17:22:02 2016 +0200 @@ -33,30 +33,6 @@ namespace OrthancPlugins { - namespace - { - class ChunkedBufferWriter : public pugi::xml_writer - { - private: - ChunkedBuffer buffer_; - - public: - virtual void write(const void *data, size_t size) - { - if (size > 0) - { - buffer_.AddChunk(reinterpret_cast<const char*>(data), size); - } - } - - void Flatten(std::string& s) - { - buffer_.Flatten(s); - } - }; - } - - static std::string MyStripSpaces(const std::string& source) { size_t first = 0; @@ -89,12 +65,12 @@ static const char* GetVRName(bool& isSequence, const gdcm::Dict& dictionary, - const gdcm::DataElement& element) + const gdcm::Tag& tag, + gdcm::VR vr) { - gdcm::VR vr = element.GetVR(); if (vr == gdcm::VR::INVALID) { - const gdcm::DictEntry &entry = dictionary.GetDictEntry(element.GetTag()); + const gdcm::DictEntry &entry = dictionary.GetDictEntry(tag); vr = entry.GetVR(); if (vr == gdcm::VR::OB_OW) @@ -125,6 +101,21 @@ } + const char* GetVRName(bool& isSequence, + const gdcm::Dict& dictionary, + const gdcm::Tag& tag) + { + return GetVRName(isSequence, dictionary, tag, gdcm::VR::INVALID); + } + + + static const char* GetVRName(bool& isSequence, + const gdcm::Dict& dictionary, + const gdcm::DataElement& element) + { + return GetVRName(isSequence, dictionary, element.GetTag(), element.GetVR()); + } + static bool ConvertDicomStringToUtf8(std::string& result, const gdcm::Dict& dictionary, @@ -295,7 +286,7 @@ - static std::string FormatTag(const gdcm::Tag& tag) + std::string FormatTag(const gdcm::Tag& tag) { char tmp[16]; sprintf(tmp, "%04X%04X", tag.GetGroup(), tag.GetElement()); @@ -303,8 +294,8 @@ } - static const char* GetKeyword(const gdcm::Dict& dictionary, - const gdcm::Tag& tag) + const char* GetKeyword(const gdcm::Dict& dictionary, + const gdcm::Tag& tag) { const gdcm::DictEntry &entry = dictionary.GetDictEntry(tag); const char* keyword = entry.GetKeyword(); @@ -363,7 +354,7 @@ } else { - return wadoBase + "studies/" + study + "/series/" + series + "/instances/" + instance + "/"; + return Configuration::GetWadoUrl(wadoBase, study, series, instance); } } @@ -424,12 +415,6 @@ pugi::xml_node node = target.append_child("DicomAttribute"); node.append_attribute("tag").set_value(FormatTag(it->GetTag()).c_str()); - const char* keyword = GetKeyword(dictionary, it->GetTag()); - if (keyword != NULL) - { - node.append_attribute("keyword").set_value(keyword); - } - bool isSequence = false; std::string vr; if (it->GetTag() == DICOM_TAG_RETRIEVE_URL) @@ -444,6 +429,12 @@ node.append_attribute("vr").set_value(vr.c_str()); + const char* keyword = GetKeyword(dictionary, it->GetTag()); + if (keyword != NULL) + { + node.append_attribute("keyword").set_value(keyword); + } + if (isSequence) { gdcm::SmartPointer<gdcm::SequenceOfItems> seq = it->GetValueAsSQ(); @@ -486,6 +477,10 @@ { value.append_child(pugi::node_pcdata).set_value(tmp.c_str()); } + else + { + value.append_child(pugi::node_pcdata).set_value(""); + } } } } @@ -542,6 +537,7 @@ node["vr"] = vr.c_str(); + bool ok = true; if (isSequence) { // Deal with sequences @@ -565,6 +561,8 @@ node["Value"].append(child); } } + + ok = true; } else if (IsBulkData(vr)) { @@ -572,6 +570,7 @@ if (!bulkUri.empty()) { node["BulkDataURI"] = bulkUri + std::string(path); + ok = true; } } else @@ -584,9 +583,18 @@ { node["Value"].append(value.c_str()); } + else + { + node["Value"].append(""); + } + + ok = true; } - target[FormatTag(it->GetTag())] = node; + if (ok) + { + target[FormatTag(it->GetTag())] = node; + } } } @@ -657,4 +665,88 @@ const std::string base = OrthancPlugins::Configuration::GetBaseUrl(configuration_, request); return OrthancPlugins::GetWadoUrl(base, GetDataSet()); } + + + static inline uint16_t GetCharValue(char c) + { + if (c >= '0' && c <= '9') + return c - '0'; + else if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + else if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + else + return 0; + } + + static inline uint16_t GetTagValue(const char* c) + { + return ((GetCharValue(c[0]) << 12) + + (GetCharValue(c[1]) << 8) + + (GetCharValue(c[2]) << 4) + + GetCharValue(c[3])); + } + + + gdcm::Tag ParseTag(const gdcm::Dict& dictionary, + const std::string& key) + { + if (key.find('.') != std::string::npos) + { + std::string s = "This DICOMweb plugin does not support hierarchical queries: " + key; + OrthancPluginLogError(context_, s.c_str()); + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + if (key.size() == 8 && // This is the DICOMweb convention + isxdigit(key[0]) && + isxdigit(key[1]) && + isxdigit(key[2]) && + isxdigit(key[3]) && + isxdigit(key[4]) && + isxdigit(key[5]) && + isxdigit(key[6]) && + isxdigit(key[7])) + { + return gdcm::Tag(GetTagValue(key.c_str()), + GetTagValue(key.c_str() + 4)); + } + else if (key.size() == 9 && // This is the Orthanc convention + isxdigit(key[0]) && + isxdigit(key[1]) && + isxdigit(key[2]) && + isxdigit(key[3]) && + key[4] == ',' && + isxdigit(key[5]) && + isxdigit(key[6]) && + isxdigit(key[7]) && + isxdigit(key[8])) + { + return gdcm::Tag(GetTagValue(key.c_str()), + GetTagValue(key.c_str() + 5)); + } + else + { + gdcm::Tag tag; + dictionary.GetDictEntryByKeyword(key.c_str(), tag); + + if (tag.IsIllegal() || tag.IsPrivate()) + { + if (key.find('.') != std::string::npos) + { + std::string s = "This QIDO-RS implementation does not support search over sequences: " + key; + OrthancPluginLogError(context_, s.c_str()); + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + else + { + std::string s = "Illegal tag name in QIDO-RS: " + key; + OrthancPluginLogError(context_, s.c_str()); + throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownDicomTag); + } + } + + return tag; + } + } }
--- a/Plugin/Dicom.h Thu Apr 28 09:14:06 2016 +0200 +++ b/Plugin/Dicom.h Thu Apr 28 17:22:02 2016 +0200 @@ -22,6 +22,7 @@ #include "Configuration.h" +#include "../Orthanc/Core/ChunkedBuffer.h" #include "../Orthanc/Core/Enumerations.h" #include <gdcmReader.h> @@ -99,6 +100,10 @@ }; + const char* GetVRName(bool& isSequence /* out */, + const gdcm::Dict& dictionary, + const gdcm::Tag& tag); + void GenerateSingleDicomAnswer(std::string& result, const std::string& wadoBase, const gdcm::Dict& dictionary, @@ -114,4 +119,32 @@ const gdcm::DataSet& dicom, bool isXml, bool isBulkAccessible); + + gdcm::Tag ParseTag(const gdcm::Dict& dictionary, + const std::string& key); + + std::string FormatTag(const gdcm::Tag& tag); + + const char* GetKeyword(const gdcm::Dict& dictionary, + const gdcm::Tag& tag); + + class ChunkedBufferWriter : public pugi::xml_writer + { + private: + Orthanc::ChunkedBuffer buffer_; + + public: + virtual void write(const void *data, size_t size) + { + if (size > 0) + { + buffer_.AddChunk(reinterpret_cast<const char*>(data), size); + } + } + + void Flatten(std::string& s) + { + buffer_.Flatten(s); + } + }; }
--- a/Plugin/DicomResults.cpp Thu Apr 28 09:14:06 2016 +0200 +++ b/Plugin/DicomResults.cpp Thu Apr 28 17:22:02 2016 +0200 @@ -22,6 +22,10 @@ #include "Dicom.h" #include "../Orthanc/Core/OrthancException.h" +#include "../Orthanc/Core/Toolbox.h" + +#include <boost/lexical_cast.hpp> +#include <boost/noncopyable.hpp> namespace OrthancPlugins { @@ -50,15 +54,11 @@ } - void DicomResults::AddInternal(const gdcm::File* file, - const gdcm::DataSet& dicom) + void DicomResults::AddInternal(const std::string& item) { if (isXml_) { - std::string answer; - GenerateSingleDicomAnswer(answer, wadoBase_, dictionary_, file, dicom, true, isBulkAccessible_); - - if (OrthancPluginSendMultipartItem(context_, output_, answer.c_str(), answer.size()) != 0) + if (OrthancPluginSendMultipartItem(context_, output_, item.c_str(), item.size()) != 0) { OrthancPluginLogError(context_, "Unable to create a multipart stream of DICOM+XML answers"); throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); @@ -71,14 +71,342 @@ jsonWriter_.AddChunk(",\n"); } - std::string item; - GenerateSingleDicomAnswer(item, wadoBase_, dictionary_, file, dicom, false, isBulkAccessible_); jsonWriter_.AddChunk(item); } isFirst_ = false; } + + void DicomResults::AddInternal(const gdcm::File* file, + const gdcm::DataSet& dicom) + { + std::string item; + + if (isXml_) + { + GenerateSingleDicomAnswer(item, wadoBase_, dictionary_, file, dicom, true, isBulkAccessible_); + } + else + { + GenerateSingleDicomAnswer(item, wadoBase_, dictionary_, file, dicom, false, isBulkAccessible_); + } + + AddInternal(item); + + isFirst_ = false; + } + + + + namespace + { + class ITagVisitor : public boost::noncopyable + { + public: + virtual ~ITagVisitor() + { + } + + virtual void Visit(const gdcm::Tag& tag, + bool isSequence, + const std::string& vr, + const std::string& type, + const Json::Value& value) = 0; + + static void Apply(ITagVisitor& visitor, + const Json::Value& source, + const gdcm::Dict& dictionary) + { + if (source.type() != Json::objectValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + Json::Value::Members members = source.getMemberNames(); + for (size_t i = 0; i < members.size(); i++) + { + if (members[i].size() != 9 || + members[i][4] != ',' || + source[members[i]].type() != Json::objectValue || + !source[members[i]].isMember("Value") || + !source[members[i]].isMember("Type") || + source[members[i]]["Type"].type() != Json::stringValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + const Json::Value& value = source[members[i]]["Value"]; + const std::string type = source[members[i]]["Type"].asString(); + + gdcm::Tag tag(OrthancPlugins::ParseTag(dictionary, members[i])); + + bool isSequence = false; + std::string vr = GetVRName(isSequence, dictionary, tag); + + if (tag == DICOM_TAG_RETRIEVE_URL) + { + // The VR of this attribute has changed from UT to UR. + vr = "UR"; + } + else + { + vr = GetVRName(isSequence, dictionary, tag); + } + + visitor.Visit(tag, isSequence, vr, type, value); + } + } + }; + + + class TagVisitorBase : public ITagVisitor + { + protected: + const Json::Value& source_; + const gdcm::Dict& dictionary_; + const std::string& bulkUri_; + + public: + TagVisitorBase(const Json::Value& source, + const gdcm::Dict& dictionary, + const std::string& bulkUri) : + source_(source), + dictionary_(dictionary), + bulkUri_(bulkUri) + { + } + }; + + + class JsonVisitor : public TagVisitorBase + { + private: + Json::Value& target_; + + public: + JsonVisitor(Json::Value& target, + const Json::Value& source, + const gdcm::Dict& dictionary, + const std::string& bulkUri) : + TagVisitorBase(source, dictionary, bulkUri), + target_(target) + { + target_ = Json::objectValue; + } + + virtual void Visit(const gdcm::Tag& tag, + bool isSequence, + const std::string& vr, + const std::string& type, + const Json::Value& value) + { + const std::string formattedTag = OrthancPlugins::FormatTag(tag); + + Json::Value node = Json::objectValue; + node["vr"] = vr; + + bool ok = false; + if (isSequence) + { + // Deal with sequences + if (type != "Sequence" || + value.type() != Json::arrayValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + node["Value"] = Json::arrayValue; + + for (Json::Value::ArrayIndex i = 0; i < value.size(); i++) + { + if (value[i].type() != Json::objectValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + Json::Value child; + + std::string childUri; + if (!bulkUri_.empty()) + { + std::string number = boost::lexical_cast<std::string>(i); + childUri = bulkUri_ + formattedTag + "/" + number + "/"; + } + + JsonVisitor visitor(child, value[i], dictionary_, childUri); + JsonVisitor::Apply(visitor, value[i], dictionary_); + + node["Value"].append(child); + } + + ok = true; + } + else if (type == "String" && + value.type() == Json::stringValue) + { + // Deal with string representations + node["Value"] = Json::arrayValue; + node["Value"].append(value.asString()); + ok = true; + } + else + { + // Bulk data + if (!bulkUri_.empty()) + { + node["BulkDataURI"] = bulkUri_ + formattedTag; + ok = true; + } + } + + if (ok) + { + target_[formattedTag] = node; + } + } + }; + + + class XmlVisitor : public TagVisitorBase + { + private: + pugi::xml_node& target_; + + public: + XmlVisitor(pugi::xml_node& target, + const Json::Value& source, + const gdcm::Dict& dictionary, + const std::string& bulkUri) : + TagVisitorBase(source, dictionary, bulkUri), + target_(target) + { + } + + virtual void Visit(const gdcm::Tag& tag, + bool isSequence, + const std::string& vr, + const std::string& type, + const Json::Value& value) + { + const std::string formattedTag = OrthancPlugins::FormatTag(tag); + + pugi::xml_node node = target_.append_child("DicomAttribute"); + node.append_attribute("tag").set_value(formattedTag.c_str()); + node.append_attribute("vr").set_value(vr.c_str()); + + const char* keyword = GetKeyword(dictionary_, tag); + if (keyword != NULL) + { + node.append_attribute("keyword").set_value(keyword); + } + + if (isSequence) + { + // Deal with sequences + if (type != "Sequence" || + value.type() != Json::arrayValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + for (Json::Value::ArrayIndex i = 0; i < value.size(); i++) + { + if (value[i].type() != Json::objectValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + pugi::xml_node child = node.append_child("Item"); + std::string number = boost::lexical_cast<std::string>(i + 1); + child.append_attribute("number").set_value(number.c_str()); + + std::string childUri; + if (!bulkUri_.empty()) + { + childUri = bulkUri_ + formattedTag + "/" + number + "/"; + } + + XmlVisitor visitor(child, value[i], dictionary_, childUri); + XmlVisitor::Apply(visitor, value[i], dictionary_); + } + } + else if (type == "String" && + value.type() == Json::stringValue) + { + // Deal with string representations + pugi::xml_node item = node.append_child("Value"); + item.append_attribute("number").set_value("1"); + item.append_child(pugi::node_pcdata).set_value(value.asCString()); + } + else + { + // Bulk data + if (!bulkUri_.empty()) + { + pugi::xml_node value = node.append_child("BulkData"); + std::string uri = bulkUri_ + formattedTag; + value.append_attribute("uri").set_value(uri.c_str()); + } + } + } + }; + } + + + static void OrthancToDicomWebXml(pugi::xml_document& target, + const Json::Value& source, + const gdcm::Dict& dictionary, + const std::string& bulkUriRoot) + { + pugi::xml_node root = target.append_child("NativeDicomModel"); + root.append_attribute("xmlns").set_value("http://dicom.nema.org/PS3.19/models/NativeDICOM"); + root.append_attribute("xsi:schemaLocation").set_value("http://dicom.nema.org/PS3.19/models/NativeDICOM"); + root.append_attribute("xmlns:xsi").set_value("http://www.w3.org/2001/XMLSchema-instance"); + + XmlVisitor visitor(root, source, dictionary, bulkUriRoot); + ITagVisitor::Apply(visitor, source, dictionary); + + pugi::xml_node decl = target.prepend_child(pugi::node_declaration); + decl.append_attribute("version").set_value("1.0"); + decl.append_attribute("encoding").set_value("utf-8"); + } + + + void DicomResults::AddFromOrthanc(const Json::Value& dicom, + const std::string& wadoUrl) + { + std::string bulkUriRoot; + if (isBulkAccessible_) + { + bulkUriRoot = wadoUrl + "bulk/"; + } + + if (isXml_) + { + pugi::xml_document doc; + OrthancToDicomWebXml(doc, dicom, dictionary_, bulkUriRoot); + + ChunkedBufferWriter writer; + doc.save(writer, " ", pugi::format_default, pugi::encoding_utf8); + + std::string item; + writer.Flatten(item); + + AddInternal(item); + } + else + { + Json::Value v; + JsonVisitor visitor(v, dicom, dictionary_, bulkUriRoot); + ITagVisitor::Apply(visitor, dicom, dictionary_); + + Json::FastWriter writer; + AddInternal(writer.write(v)); + } + } + + void DicomResults::Answer() { if (isXml_)
--- a/Plugin/DicomResults.h Thu Apr 28 09:14:06 2016 +0200 +++ b/Plugin/DicomResults.h Thu Apr 28 17:22:02 2016 +0200 @@ -26,6 +26,7 @@ #include <gdcmDataSet.h> #include <gdcmDict.h> #include <gdcmFile.h> +#include <json/value.h> namespace OrthancPlugins { @@ -41,6 +42,8 @@ bool isXml_; bool isBulkAccessible_; + void AddInternal(const std::string& item); + void AddInternal(const gdcm::File* file, const gdcm::DataSet& dicom); @@ -63,6 +66,9 @@ AddInternal(&file, subset); } + void AddFromOrthanc(const Json::Value& dicom, + const std::string& wadoUrl); + void Answer(); }; }
--- a/Plugin/QidoRs.cpp Thu Apr 28 09:14:06 2016 +0200 +++ b/Plugin/QidoRs.cpp Thu Apr 28 17:22:02 2016 +0200 @@ -42,6 +42,35 @@ namespace { + static std::string FormatOrthancTag(const gdcm::Tag& tag) + { + char b[16]; + sprintf(b, "%04x,%04x", tag.GetGroup(), tag.GetElement()); + return std::string(b); + } + + + static std::string GetOrthancTag(const Json::Value& source, + const gdcm::Tag& tag, + const std::string& defaultValue) + { + std::string s = FormatOrthancTag(tag); + + if (source.isMember(s) && + source[s].type() == Json::objectValue && + source[s].isMember("Value") && + source[s].isMember("Type") && + source[s]["Type"] == "String" && + source[s]["Value"].type() == Json::stringValue) + { + return source[s]["Value"].asString(); + } + else + { + return defaultValue; + } + } + enum QueryLevel { @@ -53,10 +82,10 @@ class ModuleMatcher { - private: + public: typedef std::map<gdcm::Tag, std::string> Filters; - const gdcm::Dict& dictionary_; + private: bool fuzzy_; unsigned int offset_; unsigned int limit_; @@ -65,84 +94,6 @@ Filters filters_; - - static inline uint16_t GetCharValue(char c) - { - if (c >= '0' && c <= '9') - return c - '0'; - else if (c >= 'a' && c <= 'f') - return c - 'a' + 10; - else if (c >= 'A' && c <= 'F') - return c - 'A' + 10; - else - return 0; - } - - static inline uint16_t GetTagValue(const char* c) - { - return ((GetCharValue(c[0]) << 12) + - (GetCharValue(c[1]) << 8) + - (GetCharValue(c[2]) << 4) + - GetCharValue(c[3])); - } - - - static std::string Format(const gdcm::Tag& tag) - { - char b[16]; - sprintf(b, "%04x,%04x", tag.GetGroup(), tag.GetElement()); - return std::string(b); - } - - - gdcm::Tag ParseTag(const std::string& key) const - { - if (key.find('.') != std::string::npos) - { - std::string s = "This DICOMweb plugin does not support hierarchical queries: " + key; - OrthancPluginLogError(context_, s.c_str()); - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - if (key.size() == 8 && - isxdigit(key[0]) && - isxdigit(key[1]) && - isxdigit(key[2]) && - isxdigit(key[3]) && - isxdigit(key[4]) && - isxdigit(key[5]) && - isxdigit(key[6]) && - isxdigit(key[7])) - { - return gdcm::Tag(GetTagValue(key.c_str()), - GetTagValue(key.c_str() + 4)); - } - else - { - gdcm::Tag tag; - dictionary_.GetDictEntryByKeyword(key.c_str(), tag); - - if (tag.IsIllegal() || tag.IsPrivate()) - { - if (key.find('.') != std::string::npos) - { - std::string s = "This QIDO-RS implementation does not support search over sequences: " + key; - OrthancPluginLogError(context_, s.c_str()); - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - else - { - std::string s = "Illegal tag name in QIDO-RS: " + key; - OrthancPluginLogError(context_, s.c_str()); - throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownDicomTag); - } - } - - return tag; - } - } - - static void AddResultAttributesForLevel(std::list<gdcm::Tag>& result, QueryLevel level) { @@ -155,7 +106,7 @@ result.push_back(gdcm::Tag(0x0008, 0x0030)); // Study Time result.push_back(gdcm::Tag(0x0008, 0x0050)); // Accession Number result.push_back(gdcm::Tag(0x0008, 0x0056)); // Instance Availability - result.push_back(gdcm::Tag(0x0008, 0x0061)); // Modalities in Study + //result.push_back(gdcm::Tag(0x0008, 0x0061)); // Modalities in Study => SPECIAL CASE result.push_back(gdcm::Tag(0x0008, 0x0090)); // Referring Physician's Name result.push_back(gdcm::Tag(0x0008, 0x0201)); // Timezone Offset From UTC //result.push_back(gdcm::Tag(0x0008, 0x1190)); // Retrieve URL => SPECIAL CASE @@ -165,20 +116,20 @@ result.push_back(gdcm::Tag(0x0010, 0x0040)); // Patient's Sex result.push_back(gdcm::Tag(0x0020, 0x000D)); // Study Instance UID result.push_back(gdcm::Tag(0x0020, 0x0010)); // Study ID - result.push_back(gdcm::Tag(0x0020, 0x1206)); // Number of Study Related Series - result.push_back(gdcm::Tag(0x0020, 0x1208)); // Number of Study Related Instances + //result.push_back(gdcm::Tag(0x0020, 0x1206)); // Number of Study Related Series => SPECIAL CASE + //result.push_back(gdcm::Tag(0x0020, 0x1208)); // Number of Study Related Instances => SPECIAL CASE break; case QueryLevel_Series: // http://medical.nema.org/medical/dicom/current/output/html/part18.html#table_6.7.1-2a result.push_back(gdcm::Tag(0x0008, 0x0005)); // Specific Character Set - result.push_back(gdcm::Tag(0x0008, 0x0056)); // Modality + result.push_back(gdcm::Tag(0x0008, 0x0060)); // Modality result.push_back(gdcm::Tag(0x0008, 0x0201)); // Timezone Offset From UTC result.push_back(gdcm::Tag(0x0008, 0x103E)); // Series Description //result.push_back(gdcm::Tag(0x0008, 0x1190)); // Retrieve URL => SPECIAL CASE result.push_back(gdcm::Tag(0x0020, 0x000E)); // Series Instance UID result.push_back(gdcm::Tag(0x0020, 0x0011)); // Series Number - result.push_back(gdcm::Tag(0x0020, 0x1209)); // Number of Series Related Instances + //result.push_back(gdcm::Tag(0x0020, 0x1209)); // Number of Series Related Instances => SPECIAL CASE result.push_back(gdcm::Tag(0x0040, 0x0244)); // Performed Procedure Step Start Date result.push_back(gdcm::Tag(0x0040, 0x0245)); // Performed Procedure Step Start Time result.push_back(gdcm::Tag(0x0040, 0x0275)); // Request Attribute Sequence @@ -205,10 +156,8 @@ } - public: ModuleMatcher(const OrthancPluginHttpRequest* request) : - dictionary_(gdcm::Global::GetInstance().GetDicts().GetPublicDict()), fuzzy_(false), offset_(0), limit_(0), @@ -257,13 +206,13 @@ Orthanc::Toolbox::TokenizeString(tags, value, ','); for (size_t i = 0; i < tags.size(); i++) { - includeFields_.push_back(ParseTag(tags[i])); + includeFields_.push_back(OrthancPlugins::ParseTag(*dictionary_, tags[i])); } } } else { - filters_[ParseTag(key)] = value; + filters_[OrthancPlugins::ParseTag(*dictionary_, key)] = value; } } } @@ -323,11 +272,88 @@ for (Filters::const_iterator it = filters_.begin(); it != filters_.end(); ++it) { - result["Query"][Format(it->first)] = it->second; + result["Query"][FormatOrthancTag(it->first)] = it->second; } } + void ComputeDerivedTags(Filters& target, + QueryLevel level, + const std::string& resource) const + { + target.clear(); + + switch (level) + { + case QueryLevel_Study: + { + Json::Value series, instances; + if (OrthancPlugins::RestApiGetJson(series, context_, "/studies/" + resource + "/series?expand") && + OrthancPlugins::RestApiGetJson(instances, context_, "/studies/" + resource + "/instances")) + { + // Number of Study Related Series + target[gdcm::Tag(0x0020, 0x1206)] = boost::lexical_cast<std::string>(series.size()); + + // Number of Study Related Instances + target[gdcm::Tag(0x0020, 0x1208)] = boost::lexical_cast<std::string>(instances.size()); + + // Collect the Modality of all the child series + std::set<std::string> modalities; + for (Json::Value::ArrayIndex i = 0; i < series.size(); i++) + { + if (series[i].isMember("MainDicomTags") && + series[i]["MainDicomTags"].isMember("Modality")) + { + modalities.insert(series[i]["MainDicomTags"]["Modality"].asString()); + } + } + + std::string s; + for (std::set<std::string>::const_iterator + it = modalities.begin(); it != modalities.end(); ++it) + { + if (!s.empty()) + { + s += "\\"; + } + + s += *it; + } + + target[gdcm::Tag(0x0008, 0x0061)] = s; // Modalities in Study + } + else + { + target[gdcm::Tag(0x0008, 0x0061)] = ""; // Modalities in Study + target[gdcm::Tag(0x0020, 0x1206)] = "0"; // Number of Study Related Series + target[gdcm::Tag(0x0020, 0x1208)] = "0"; // Number of Study Related Instances + } + + break; + } + + case QueryLevel_Series: + { + Json::Value instances; + if (OrthancPlugins::RestApiGetJson(instances, context_, "/series/" + resource + "/instances")) + { + // Number of Series Related Instances + target[gdcm::Tag(0x0020, 0x1209)] = boost::lexical_cast<std::string>(instances.size()); + } + else + { + target[gdcm::Tag(0x0020, 0x1209)] = "0"; // Number of Series Related Instances + } + + break; + } + + default: + break; + } + } + + void ExtractFields(gdcm::DataSet& result, const OrthancPlugins::ParsedDicomFile& dicom, const std::string& wadoBase, @@ -365,7 +391,7 @@ // Copy all the required fields to the target for (std::list<gdcm::Tag>::const_iterator - it = fields.begin(); it != fields.end(); it++) + it = fields.begin(); it != fields.end(); ++it) { if (dicom.GetDataSet().FindDataElement(*it)) { @@ -392,6 +418,76 @@ element.SetByteValue(url.c_str(), url.size()); result.Replace(element); } + + + void ExtractFields(Json::Value& result, + const Json::Value& source, + const std::string& wadoBase, + QueryLevel level) const + { + result = Json::objectValue; + std::list<gdcm::Tag> fields = includeFields_; + + // The list of attributes for this query level + AddResultAttributesForLevel(fields, level); + + // All other attributes passed as query keys + for (Filters::const_iterator it = filters_.begin(); + it != filters_.end(); ++it) + { + fields.push_back(it->first); + } + + // For instances and series, add all Study-level attributes if + // {StudyInstanceUID} is not specified. + if ((level == QueryLevel_Instance || level == QueryLevel_Series) + && filters_.find(OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID) == filters_.end() + ) + { + AddResultAttributesForLevel(fields, QueryLevel_Study); + } + + // For instances, add all Series-level attributes if + // {SeriesInstanceUID} is not specified. + if (level == QueryLevel_Instance + && filters_.find(OrthancPlugins::DICOM_TAG_SERIES_INSTANCE_UID) == filters_.end() + ) + { + AddResultAttributesForLevel(fields, QueryLevel_Series); + } + + // Copy all the required fields to the target + for (std::list<gdcm::Tag>::const_iterator + it = fields.begin(); it != fields.end(); ++it) + { + std::string tag = FormatOrthancTag(*it); + if (source.isMember(tag)) + { + result[tag] = source[tag]; + } + } + + // Set the retrieve URL for WADO-RS + std::string url = (wadoBase + "studies/" + + GetOrthancTag(source, OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID, "")); + + if (level == QueryLevel_Series || level == QueryLevel_Instance) + { + url += "/series/" + GetOrthancTag(source, OrthancPlugins::DICOM_TAG_SERIES_INSTANCE_UID, ""); + } + + if (level == QueryLevel_Instance) + { + url += "/instances/" + GetOrthancTag(source, OrthancPlugins::DICOM_TAG_SOP_INSTANCE_UID, ""); + } + + Json::Value tmp = Json::objectValue; + tmp["Name"] = "RetrieveURL"; + tmp["Type"] = "String"; + tmp["Value"] = url; + + result[FormatOrthancTag(OrthancPlugins::DICOM_TAG_RETRIEVE_URL)] = tmp; + } }; } @@ -415,26 +511,30 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); } - std::list<std::string> instances; + typedef std::list< std::pair<std::string, std::string> > ResourcesAndInstances; + + ResourcesAndInstances resourcesAndInstances; std::string root = (level == QueryLevel_Study ? "/studies/" : "/series/"); for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++) { + const std::string resource = resources[i].asString(); + if (level == QueryLevel_Study || level == QueryLevel_Series) { // Find one child instance of this resource Json::Value tmp; - if (OrthancPlugins::RestApiGetJson(tmp, context_, root + resources[i].asString() + "/instances") && + if (OrthancPlugins::RestApiGetJson(tmp, context_, root + resource + "/instances") && tmp.type() == Json::arrayValue && tmp.size() > 0) { - instances.push_back(tmp[0]["ID"].asString()); + resourcesAndInstances.push_back(std::make_pair(resource, tmp[0]["ID"].asString())); } } else { - instances.push_back(resources[i].asString()); + resourcesAndInstances.push_back(std::make_pair(resource, resource)); } } @@ -442,20 +542,77 @@ OrthancPlugins::DicomResults results(context_, output, wadoBase, *dictionary_, IsXmlExpected(request), true); - for (std::list<std::string>::const_iterator - it = instances.begin(); it != instances.end(); it++) +#if 0 + // Implementation up to version 0.2 of the plugin. Each instance is + // downloaded and decoded using GDCM, which slows down things + // wrt. the new implementation below that directly uses the Orthanc + // pre-computed JSON summary. + for (ResourcesAndInstances::const_iterator + it = resourcesAndInstances.begin(); it != resourcesAndInstances.end(); ++it) { + ModuleMatcher::Filters derivedTags; + matcher.ComputeDerivedTags(derivedTags, level, it->first); + std::string file; - if (OrthancPlugins::RestApiGetString(file, context_, "/instances/" + *it + "/file")) + if (OrthancPlugins::RestApiGetString(file, context_, "/instances/" + it->second + "/file")) { OrthancPlugins::ParsedDicomFile dicom(file); std::auto_ptr<gdcm::DataSet> result(new gdcm::DataSet); matcher.ExtractFields(*result, dicom, wadoBase, level); + + // Inject the derived tags + ModuleMatcher::Filters derivedTags; + matcher.ComputeDerivedTags(derivedTags, level, it->first); + + for (ModuleMatcher::Filters::const_iterator + tag = derivedTags.begin(); tag != derivedTags.end(); ++tag) + { + gdcm::DataElement element(tag->first); + element.SetByteValue(tag->second.c_str(), tag->second.size()); + result->Replace(element); + } + results.Add(dicom.GetFile(), *result); } } +#else + // Fix of issue #13 + for (ResourcesAndInstances::const_iterator + it = resourcesAndInstances.begin(); it != resourcesAndInstances.end(); ++it) + { + Json::Value tags; + if (OrthancPlugins::RestApiGetJson(tags, context_, "/instances/" + it->second + "/tags")) + { + std::string wadoUrl = OrthancPlugins::Configuration::GetWadoUrl( + wadoBase, + GetOrthancTag(tags, OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID, ""), + GetOrthancTag(tags, OrthancPlugins::DICOM_TAG_SERIES_INSTANCE_UID, ""), + GetOrthancTag(tags, OrthancPlugins::DICOM_TAG_SOP_INSTANCE_UID, "")); + + Json::Value result; + matcher.ExtractFields(result, tags, wadoBase, level); + + // Inject the derived tags + ModuleMatcher::Filters derivedTags; + matcher.ComputeDerivedTags(derivedTags, level, it->first); + + for (ModuleMatcher::Filters::const_iterator + tag = derivedTags.begin(); tag != derivedTags.end(); ++tag) + { + Json::Value tmp = Json::objectValue; + tmp["Name"] = OrthancPlugins::GetKeyword(*dictionary_, tag->first); + tmp["Type"] = "String"; + tmp["Value"] = tag->second; + result[FormatOrthancTag(tag->first)] = tmp; + } + + results.AddFromOrthanc(result, wadoUrl); + } + } +#endif + results.Answer(); }
--- a/UnitTestsSources/UnitTestsMain.cpp Thu Apr 28 09:14:06 2016 +0200 +++ b/UnitTestsSources/UnitTestsMain.cpp Thu Apr 28 17:22:02 2016 +0200 @@ -27,6 +27,7 @@ using namespace OrthancPlugins; Json::Value configuration_ = Json::objectValue; +OrthancPluginContext* context_ = NULL; TEST(ContentType, Parse)