Mercurial > hg > orthanc
view OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.cpp @ 5818:86cd21ef81fb
news
author | Alain Mazy <am@orthanc.team> |
---|---|
date | Fri, 27 Sep 2024 07:51:38 +0200 |
parents | 3f10350b26da |
children |
line wrap: on
line source
/** * Orthanc - A Lightweight, RESTful DICOM Store * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium * Copyright (C) 2017-2023 Osimis S.A., Belgium * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/>. **/ #include "../PrecompiledHeaders.h" #include "DicomWebJsonVisitor.h" #include "../Logging.h" #include "../OrthancException.h" #include "../Toolbox.h" #include "../SerializationToolbox.h" #include "FromDcmtkBridge.h" #include <boost/math/special_functions/round.hpp> #include <boost/lexical_cast.hpp> static const char* const KEY_ALPHABETIC = "Alphabetic"; static const char* const KEY_IDEOGRAPHIC = "Ideographic"; static const char* const KEY_PHONETIC = "Phonetic"; static const char* const KEY_BULK_DATA = "BulkData"; static const char* const KEY_BULK_DATA_URI = "BulkDataURI"; static const char* const KEY_INLINE_BINARY = "InlineBinary"; static const char* const KEY_SQ = "SQ"; static const char* const KEY_TAG = "tag"; static const char* const KEY_VALUE = "Value"; static const char* const KEY_VR = "vr"; static const char* const KEY_NUMBER = "number"; namespace Orthanc { #if ORTHANC_ENABLE_PUGIXML == 1 static void DecomposeXmlPersonName(pugi::xml_node& target, const std::string& source) { std::vector<std::string> tokens; Toolbox::TokenizeString(tokens, source, '^'); if (tokens.size() >= 1) { target.append_child("FamilyName").text() = tokens[0].c_str(); } if (tokens.size() >= 2) { target.append_child("GivenName").text() = tokens[1].c_str(); } if (tokens.size() >= 3) { target.append_child("MiddleName").text() = tokens[2].c_str(); } if (tokens.size() >= 4) { target.append_child("NamePrefix").text() = tokens[3].c_str(); } if (tokens.size() >= 5) { target.append_child("NameSuffix").text() = tokens[4].c_str(); } } static void ExploreXmlDataset(pugi::xml_node& target, const Json::Value& source) { // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.3.html#table_F.3.1-1 assert(source.type() == Json::objectValue); Json::Value::Members members = source.getMemberNames(); for (size_t i = 0; i < members.size(); i++) { const DicomTag tag = FromDcmtkBridge::ParseTag(members[i]); const Json::Value& content = source[members[i]]; assert(content.type() == Json::objectValue && content.isMember(KEY_VR) && content[KEY_VR].type() == Json::stringValue); const std::string vr = content[KEY_VR].asString(); const std::string keyword = FromDcmtkBridge::GetTagName(tag, ""); pugi::xml_node node = target.append_child("DicomAttribute"); node.append_attribute(KEY_TAG).set_value(members[i].c_str()); node.append_attribute(KEY_VR).set_value(vr.c_str()); if (keyword != std::string(DcmTag_ERROR_TagName)) { node.append_attribute("keyword").set_value(keyword.c_str()); } if (content.isMember(KEY_VALUE)) { assert(content[KEY_VALUE].type() == Json::arrayValue); for (Json::Value::ArrayIndex j = 0; j < content[KEY_VALUE].size(); j++) { std::string number = boost::lexical_cast<std::string>(j + 1); if (vr == "SQ") { if (content[KEY_VALUE][j].type() == Json::objectValue) { pugi::xml_node child = node.append_child("Item"); child.append_attribute(KEY_NUMBER).set_value(number.c_str()); ExploreXmlDataset(child, content[KEY_VALUE][j]); } } if (vr == "PN") { bool hasAlphabetic = (content[KEY_VALUE][j].isMember(KEY_ALPHABETIC) && content[KEY_VALUE][j][KEY_ALPHABETIC].type() == Json::stringValue); bool hasIdeographic = (content[KEY_VALUE][j].isMember(KEY_IDEOGRAPHIC) && content[KEY_VALUE][j][KEY_IDEOGRAPHIC].type() == Json::stringValue); bool hasPhonetic = (content[KEY_VALUE][j].isMember(KEY_PHONETIC) && content[KEY_VALUE][j][KEY_PHONETIC].type() == Json::stringValue); if (hasAlphabetic || hasIdeographic || hasPhonetic) { pugi::xml_node child = node.append_child("PersonName"); child.append_attribute(KEY_NUMBER).set_value(number.c_str()); if (hasAlphabetic) { pugi::xml_node name = child.append_child(KEY_ALPHABETIC); DecomposeXmlPersonName(name, content[KEY_VALUE][j][KEY_ALPHABETIC].asString()); } if (hasIdeographic) { pugi::xml_node name = child.append_child(KEY_IDEOGRAPHIC); DecomposeXmlPersonName(name, content[KEY_VALUE][j][KEY_IDEOGRAPHIC].asString()); } if (hasPhonetic) { pugi::xml_node name = child.append_child(KEY_PHONETIC); DecomposeXmlPersonName(name, content[KEY_VALUE][j][KEY_PHONETIC].asString()); } } } else { pugi::xml_node child = node.append_child(KEY_VALUE); child.append_attribute(KEY_NUMBER).set_value(number.c_str()); switch (content[KEY_VALUE][j].type()) { case Json::stringValue: child.text() = content[KEY_VALUE][j].asCString(); break; case Json::realValue: child.text() = content[KEY_VALUE][j].asFloat(); break; case Json::intValue: child.text() = content[KEY_VALUE][j].asInt(); break; case Json::uintValue: child.text() = content[KEY_VALUE][j].asUInt(); break; default: break; } } } } else if (content.isMember(KEY_BULK_DATA_URI) && content[KEY_BULK_DATA_URI].type() == Json::stringValue) { pugi::xml_node child = node.append_child(KEY_BULK_DATA); child.append_attribute("URI").set_value(content[KEY_BULK_DATA_URI].asCString()); } else if (content.isMember(KEY_INLINE_BINARY) && content[KEY_INLINE_BINARY].type() == Json::stringValue) { pugi::xml_node child = node.append_child(KEY_INLINE_BINARY); child.text() = content[KEY_INLINE_BINARY].asCString(); } } } #endif #if ORTHANC_ENABLE_PUGIXML == 1 static void DicomWebJsonToXml(pugi::xml_document& target, const Json::Value& source) { 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"); ExploreXmlDataset(root, source); 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"); } #endif std::string DicomWebJsonVisitor::FormatTag(const DicomTag& tag) { char buf[16]; sprintf(buf, "%04X%04X", tag.GetGroup(), tag.GetElement()); return std::string(buf); } Json::Value& DicomWebJsonVisitor::CreateNode(const std::vector<DicomTag>& parentTags, const std::vector<size_t>& parentIndexes, const DicomTag& tag) { assert(parentTags.size() == parentIndexes.size()); Json::Value* node = &result_; for (size_t i = 0; i < parentTags.size(); i++) { std::string t = FormatTag(parentTags[i]); if (!node->isMember(t)) { Json::Value item = Json::objectValue; item[KEY_VR] = KEY_SQ; item[KEY_VALUE] = Json::arrayValue; item[KEY_VALUE].append(Json::objectValue); (*node) [t] = item; node = &(*node)[t][KEY_VALUE][0]; } else if ((*node) [t].type() != Json::objectValue || !(*node) [t].isMember(KEY_VR) || (*node) [t][KEY_VR].type() != Json::stringValue || (*node) [t][KEY_VR].asString() != KEY_SQ || !(*node) [t].isMember(KEY_VALUE) || (*node) [t][KEY_VALUE].type() != Json::arrayValue) { throw OrthancException(ErrorCode_InternalError); } else { size_t currentSize = (*node) [t][KEY_VALUE].size(); if (parentIndexes[i] < currentSize) { // The node already exists } else if (parentIndexes[i] == currentSize) { (*node) [t][KEY_VALUE].append(Json::objectValue); } else { throw OrthancException(ErrorCode_InternalError); } node = &(*node) [t][KEY_VALUE][Json::ArrayIndex(parentIndexes[i])]; } } assert(node->type() == Json::objectValue); std::string t = FormatTag(tag); if (node->isMember(t)) { throw OrthancException(ErrorCode_InternalError); } else { (*node) [t] = Json::objectValue; return (*node) [t]; } } Json::Value DicomWebJsonVisitor::FormatInteger(int64_t value) { if (value < 0) { return Json::Value(static_cast<int32_t>(value)); } else { return Json::Value(static_cast<uint32_t>(value)); } } Json::Value DicomWebJsonVisitor::FormatDouble(double value) { try { long long a = boost::math::llround<double>(value); double d = fabs(value - static_cast<double>(a)); if (d <= std::numeric_limits<double>::epsilon() * 100.0) { return FormatInteger(a); } else { return Json::Value(value); } } catch (boost::math::rounding_error&) { // Can occur if "long long" is too small to receive this value // (e.g. infinity) return Json::Value(value); } } Json::Value DicomWebJsonVisitor::FormatDecimalString(double value, const std::string& originalString) { try { long long a = boost::math::llround<double>(value); double d = fabs(value - static_cast<double>(a)); if (d <= std::numeric_limits<double>::epsilon() * 100.0) { return FormatInteger(a); // if the decimal number is an integer, you can represent it as an integer } else { return Json::Value(originalString); // keep the original string to avoid rounding errors e.g, transforming "0.143" into 0.14299999999999 } } catch (boost::math::rounding_error&) { // Can occur if "long long" is too small to receive this value // (e.g. infinity) return Json::Value(originalString); } } DicomWebJsonVisitor::DicomWebJsonVisitor() : formatter_(NULL) { Clear(); } void DicomWebJsonVisitor::SetFormatter(DicomWebJsonVisitor::IBinaryFormatter &formatter) { formatter_ = &formatter; } void DicomWebJsonVisitor::Clear() { result_ = Json::objectValue; } const Json::Value &DicomWebJsonVisitor::GetResult() const { return result_; } #if ORTHANC_ENABLE_PUGIXML == 1 void DicomWebJsonVisitor::FormatXml(std::string& target) const { pugi::xml_document doc; DicomWebJsonToXml(doc, result_); Toolbox::XmlToString(target, doc); } #endif ITagVisitor::Action DicomWebJsonVisitor::VisitNotSupported(const std::vector<DicomTag> &parentTags, const std::vector<size_t> &parentIndexes, const DicomTag &tag, ValueRepresentation vr) { return Action_None; } ITagVisitor::Action DicomWebJsonVisitor::VisitSequence(const std::vector<DicomTag>& parentTags, const std::vector<size_t>& parentIndexes, const DicomTag& tag, size_t countItems) { if (countItems == 0 && tag.GetElement() != 0x0000) { Json::Value& node = CreateNode(parentTags, parentIndexes, tag); node[KEY_VR] = EnumerationToString(ValueRepresentation_Sequence); } return Action_None; } ITagVisitor::Action DicomWebJsonVisitor::VisitBinary(const std::vector<DicomTag>& parentTags, const std::vector<size_t>& parentIndexes, const DicomTag& tag, ValueRepresentation vr, const void* data, size_t size) { assert(vr == ValueRepresentation_OtherByte || vr == ValueRepresentation_OtherDouble || vr == ValueRepresentation_OtherFloat || vr == ValueRepresentation_OtherLong || vr == ValueRepresentation_OtherWord || vr == ValueRepresentation_Unknown); if (tag.GetElement() != 0x0000) { BinaryMode mode; std::string bulkDataUri; if (formatter_ == NULL) { mode = BinaryMode_InlineBinary; } else { mode = formatter_->Format(bulkDataUri, parentTags, parentIndexes, tag, vr); } if (mode != BinaryMode_Ignore) { Json::Value& node = CreateNode(parentTags, parentIndexes, tag); node[KEY_VR] = EnumerationToString(vr); /** * The test on "size > 0" is new in Orthanc 1.9.3, and fixes * issue #195 (No need for BulkDataURI when Data Element is * empty): https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=195 **/ if (size > 0 || tag == DICOM_TAG_PIXEL_DATA || vr == ValueRepresentation_Sequence /* new in Orthanc 1.9.4 */) { switch (mode) { case BinaryMode_BulkDataUri: node[KEY_BULK_DATA_URI] = bulkDataUri; break; case BinaryMode_InlineBinary: { std::string tmp(static_cast<const char*>(data), size); std::string base64; Toolbox::EncodeBase64(base64, tmp); node[KEY_INLINE_BINARY] = base64; break; } default: throw OrthancException(ErrorCode_ParameterOutOfRange); } } } } return Action_None; } ITagVisitor::Action DicomWebJsonVisitor::VisitIntegers(const std::vector<DicomTag>& parentTags, const std::vector<size_t>& parentIndexes, const DicomTag& tag, ValueRepresentation vr, const std::vector<int64_t>& values) { if (tag.GetElement() != 0x0000 && vr != ValueRepresentation_NotSupported) { Json::Value& node = CreateNode(parentTags, parentIndexes, tag); node[KEY_VR] = EnumerationToString(vr); if (!values.empty()) { Json::Value content = Json::arrayValue; for (size_t i = 0; i < values.size(); i++) { content.append(FormatInteger(values[i])); } node[KEY_VALUE] = content; } } return Action_None; } ITagVisitor::Action DicomWebJsonVisitor::VisitDoubles(const std::vector<DicomTag>& parentTags, const std::vector<size_t>& parentIndexes, const DicomTag& tag, ValueRepresentation vr, const std::vector<double>& values) { if (tag.GetElement() != 0x0000 && vr != ValueRepresentation_NotSupported) { Json::Value& node = CreateNode(parentTags, parentIndexes, tag); node[KEY_VR] = EnumerationToString(vr); if (!values.empty()) { Json::Value content = Json::arrayValue; for (size_t i = 0; i < values.size(); i++) { content.append(FormatDouble(values[i])); } node[KEY_VALUE] = content; } } return Action_None; } ITagVisitor::Action DicomWebJsonVisitor::VisitAttributes(const std::vector<DicomTag>& parentTags, const std::vector<size_t>& parentIndexes, const DicomTag& tag, const std::vector<DicomTag>& values) { if (tag.GetElement() != 0x0000) { Json::Value& node = CreateNode(parentTags, parentIndexes, tag); node[KEY_VR] = EnumerationToString(ValueRepresentation_AttributeTag); if (!values.empty()) { Json::Value content = Json::arrayValue; for (size_t i = 0; i < values.size(); i++) { content.append(FormatTag(values[i])); } node[KEY_VALUE] = content; } } return Action_None; } ITagVisitor::Action DicomWebJsonVisitor::VisitString(std::string& newValue, const std::vector<DicomTag>& parentTags, const std::vector<size_t>& parentIndexes, const DicomTag& tag, ValueRepresentation vr, const std::string& value) { if (tag.GetElement() == 0x0000 || vr == ValueRepresentation_NotSupported) { return Action_None; } else { Json::Value& node = CreateNode(parentTags, parentIndexes, tag); node[KEY_VR] = EnumerationToString(vr); #if 0 /** * TODO - The JSON file has an UTF-8 encoding, thus DCMTK * replaces the specific character set with "ISO_IR 192" * (UNICODE UTF-8). On Google Cloud Healthcare, however, the * source encoding is reported, which seems more logical. We * thus choose the Google convention. Enabling this block will * mimic the DCMTK behavior. **/ if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET) { node[KEY_VALUE].append("ISO_IR 192"); } else #endif { std::string truncated; if (!value.empty() && value[value.size() - 1] == '\0') { truncated = value.substr(0, value.size() - 1); } else { truncated = value; } if (!truncated.empty()) { std::vector<std::string> tokens; Toolbox::TokenizeString(tokens, truncated, '\\'); if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET && tokens.size() > 1 && tokens[0].empty()) { // Specific character set with code extension: Remove the // first element from the vector of encodings tokens.erase(tokens.begin()); } node[KEY_VALUE] = Json::arrayValue; for (size_t i = 0; i < tokens.size(); i++) { try { switch (vr) { case ValueRepresentation_PersonName: { Json::Value tmp = Json::objectValue; if (!tokens[i].empty()) { std::vector<std::string> components; Toolbox::TokenizeString(components, tokens[i], '='); if (components.size() >= 1) { tmp[KEY_ALPHABETIC] = components[0]; } if (components.size() >= 2) { tmp[KEY_IDEOGRAPHIC] = components[1]; } if (components.size() >= 3) { tmp[KEY_PHONETIC] = components[2]; } } node[KEY_VALUE].append(tmp); break; } case ValueRepresentation_IntegerString: { /** * The calls to "StripSpaces()" below fix the * issue reported by Rana Asim Wajid on 2019-06-05 * ("Error Exception while invoking plugin service * 32: Bad file format"): * https://groups.google.com/d/msg/orthanc-users/T32FovWPcCE/-hKFbfRJBgAJ **/ std::string t = Toolbox::StripSpaces(tokens[i]); if (t.empty()) { node[KEY_VALUE].append(Json::nullValue); } else { int64_t tmp = boost::lexical_cast<int64_t>(t); node[KEY_VALUE].append(FormatInteger(tmp)); } break; } case ValueRepresentation_DecimalString: { std::string t = Toolbox::StripSpaces(tokens[i]); boost::replace_all(t, ",", "."); // some invalid files uses "," instead of "." // remove invalid/useless trailing decimal separator if (t.size() > 0 && t[t.size()-1] == '.') { t.resize(t.size() -1); } if (t.empty()) { node[KEY_VALUE].append(Json::nullValue); } else { // https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.3.html // DS values can be represented as String or Number in Json. // For IS, DS, SV and UV, a JSON String representation can be used to preserve the original format during transformation of the representation, or if needed to avoid losing precision of a decimal string. // Since 1.12.5, always use the string repesentation. Before, decimal numbers were represented as double which led to loss of precision (e.g: 0.143 represented as 0.1429999999) double tmp; if (SerializationToolbox::ParseDouble(tmp, t)) // make sure that the string contains a valid decimal number { node[KEY_VALUE].append(t); } else { throw boost::bad_lexical_cast(); } } break; } default: if (tokens[i].empty()) { node[KEY_VALUE].append(Json::nullValue); } else { node[KEY_VALUE].append(tokens[i]); } break; } } catch (boost::bad_lexical_cast&) { std::string tmp; if (value.size() < 64 && Toolbox::IsAsciiString(value)) { tmp = ": " + value; } LOG(WARNING) << "Ignoring DICOM tag (" << tag.Format() << ") with invalid content for VR " << EnumerationToString(vr) << tmp; } } } } } return Action_None; } }