comparison OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.cpp @ 4044:d25f4c0fa160 framework

splitting code into OrthancFramework and OrthancServer
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 10 Jun 2020 20:30:34 +0200
parents Core/DicomParsing/DicomWebJsonVisitor.cpp@94f4a18a79cc
children bf7b9edf6b81
comparison
equal deleted inserted replaced
4043:6c6239aec462 4044:d25f4c0fa160
1 /**
2 * Orthanc - A Lightweight, RESTful DICOM Store
3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
4 * Department, University Hospital of Liege, Belgium
5 * Copyright (C) 2017-2020 Osimis S.A., Belgium
6 *
7 * This program is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License as
9 * published by the Free Software Foundation, either version 3 of the
10 * License, or (at your option) any later version.
11 *
12 * In addition, as a special exception, the copyright holders of this
13 * program give permission to link the code of its release with the
14 * OpenSSL project's "OpenSSL" library (or with modified versions of it
15 * that use the same license as the "OpenSSL" library), and distribute
16 * the linked executables. You must obey the GNU General Public License
17 * in all respects for all of the code used other than "OpenSSL". If you
18 * modify file(s) with this exception, you may extend this exception to
19 * your version of the file(s), but you are not obligated to do so. If
20 * you do not wish to do so, delete this exception statement from your
21 * version. If you delete this exception statement from all source files
22 * in the program, then also delete it here.
23 *
24 * This program is distributed in the hope that it will be useful, but
25 * WITHOUT ANY WARRANTY; without even the implied warranty of
26 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
27 * General Public License for more details.
28 *
29 * You should have received a copy of the GNU General Public License
30 * along with this program. If not, see <http://www.gnu.org/licenses/>.
31 **/
32
33
34 #include "../PrecompiledHeaders.h"
35 #include "DicomWebJsonVisitor.h"
36
37 #include "../OrthancException.h"
38 #include "../Toolbox.h"
39 #include "FromDcmtkBridge.h"
40
41 #include <boost/math/special_functions/round.hpp>
42 #include <boost/lexical_cast.hpp>
43
44
45 static const char* const KEY_ALPHABETIC = "Alphabetic";
46 static const char* const KEY_IDEOGRAPHIC = "Ideographic";
47 static const char* const KEY_PHONETIC = "Phonetic";
48 static const char* const KEY_BULK_DATA_URI = "BulkDataURI";
49 static const char* const KEY_INLINE_BINARY = "InlineBinary";
50 static const char* const KEY_SQ = "SQ";
51 static const char* const KEY_TAG = "tag";
52 static const char* const KEY_VALUE = "Value";
53 static const char* const KEY_VR = "vr";
54
55
56 namespace Orthanc
57 {
58 #if ORTHANC_ENABLE_PUGIXML == 1
59 static void DecomposeXmlPersonName(pugi::xml_node& target,
60 const std::string& source)
61 {
62 std::vector<std::string> tokens;
63 Toolbox::TokenizeString(tokens, source, '^');
64
65 if (tokens.size() >= 1)
66 {
67 target.append_child("FamilyName").text() = tokens[0].c_str();
68 }
69
70 if (tokens.size() >= 2)
71 {
72 target.append_child("GivenName").text() = tokens[1].c_str();
73 }
74
75 if (tokens.size() >= 3)
76 {
77 target.append_child("MiddleName").text() = tokens[2].c_str();
78 }
79
80 if (tokens.size() >= 4)
81 {
82 target.append_child("NamePrefix").text() = tokens[3].c_str();
83 }
84
85 if (tokens.size() >= 5)
86 {
87 target.append_child("NameSuffix").text() = tokens[4].c_str();
88 }
89 }
90
91 static void ExploreXmlDataset(pugi::xml_node& target,
92 const Json::Value& source)
93 {
94 // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.3.html#table_F.3.1-1
95 assert(source.type() == Json::objectValue);
96
97 Json::Value::Members members = source.getMemberNames();
98 for (size_t i = 0; i < members.size(); i++)
99 {
100 const DicomTag tag = FromDcmtkBridge::ParseTag(members[i]);
101 const Json::Value& content = source[members[i]];
102
103 assert(content.type() == Json::objectValue &&
104 content.isMember(KEY_VR) &&
105 content[KEY_VR].type() == Json::stringValue);
106 const std::string vr = content[KEY_VR].asString();
107
108 const std::string keyword = FromDcmtkBridge::GetTagName(tag, "");
109
110 pugi::xml_node node = target.append_child("DicomAttribute");
111 node.append_attribute(KEY_TAG).set_value(members[i].c_str());
112 node.append_attribute(KEY_VR).set_value(vr.c_str());
113
114 if (keyword != std::string(DcmTag_ERROR_TagName))
115 {
116 node.append_attribute("keyword").set_value(keyword.c_str());
117 }
118
119 if (content.isMember(KEY_VALUE))
120 {
121 assert(content[KEY_VALUE].type() == Json::arrayValue);
122
123 for (Json::Value::ArrayIndex j = 0; j < content[KEY_VALUE].size(); j++)
124 {
125 std::string number = boost::lexical_cast<std::string>(j + 1);
126
127 if (vr == "SQ")
128 {
129 if (content[KEY_VALUE][j].type() == Json::objectValue)
130 {
131 pugi::xml_node child = node.append_child("Item");
132 child.append_attribute("number").set_value(number.c_str());
133 ExploreXmlDataset(child, content[KEY_VALUE][j]);
134 }
135 }
136 if (vr == "PN")
137 {
138 bool hasAlphabetic = (content[KEY_VALUE][j].isMember(KEY_ALPHABETIC) &&
139 content[KEY_VALUE][j][KEY_ALPHABETIC].type() == Json::stringValue);
140
141 bool hasIdeographic = (content[KEY_VALUE][j].isMember(KEY_IDEOGRAPHIC) &&
142 content[KEY_VALUE][j][KEY_IDEOGRAPHIC].type() == Json::stringValue);
143
144 bool hasPhonetic = (content[KEY_VALUE][j].isMember(KEY_PHONETIC) &&
145 content[KEY_VALUE][j][KEY_PHONETIC].type() == Json::stringValue);
146
147 if (hasAlphabetic ||
148 hasIdeographic ||
149 hasPhonetic)
150 {
151 pugi::xml_node child = node.append_child("PersonName");
152 child.append_attribute("number").set_value(number.c_str());
153
154 if (hasAlphabetic)
155 {
156 pugi::xml_node name = child.append_child(KEY_ALPHABETIC);
157 DecomposeXmlPersonName(name, content[KEY_VALUE][j][KEY_ALPHABETIC].asString());
158 }
159
160 if (hasIdeographic)
161 {
162 pugi::xml_node name = child.append_child(KEY_IDEOGRAPHIC);
163 DecomposeXmlPersonName(name, content[KEY_VALUE][j][KEY_IDEOGRAPHIC].asString());
164 }
165
166 if (hasPhonetic)
167 {
168 pugi::xml_node name = child.append_child(KEY_PHONETIC);
169 DecomposeXmlPersonName(name, content[KEY_VALUE][j][KEY_PHONETIC].asString());
170 }
171 }
172 }
173 else
174 {
175 pugi::xml_node child = node.append_child("Value");
176 child.append_attribute("number").set_value(number.c_str());
177
178 switch (content[KEY_VALUE][j].type())
179 {
180 case Json::stringValue:
181 child.text() = content[KEY_VALUE][j].asCString();
182 break;
183
184 case Json::realValue:
185 child.text() = content[KEY_VALUE][j].asFloat();
186 break;
187
188 case Json::intValue:
189 child.text() = content[KEY_VALUE][j].asInt();
190 break;
191
192 case Json::uintValue:
193 child.text() = content[KEY_VALUE][j].asUInt();
194 break;
195
196 default:
197 break;
198 }
199 }
200 }
201 }
202 else if (content.isMember(KEY_BULK_DATA_URI) &&
203 content[KEY_BULK_DATA_URI].type() == Json::stringValue)
204 {
205 pugi::xml_node child = node.append_child("BulkData");
206 child.append_attribute("URI").set_value(content[KEY_BULK_DATA_URI].asCString());
207 }
208 else if (content.isMember(KEY_INLINE_BINARY) &&
209 content[KEY_INLINE_BINARY].type() == Json::stringValue)
210 {
211 pugi::xml_node child = node.append_child("InlineBinary");
212 child.text() = content[KEY_INLINE_BINARY].asCString();
213 }
214 }
215 }
216 #endif
217
218
219 #if ORTHANC_ENABLE_PUGIXML == 1
220 static void DicomWebJsonToXml(pugi::xml_document& target,
221 const Json::Value& source)
222 {
223 pugi::xml_node root = target.append_child("NativeDicomModel");
224 root.append_attribute("xmlns").set_value("http://dicom.nema.org/PS3.19/models/NativeDICOM");
225 root.append_attribute("xsi:schemaLocation").set_value("http://dicom.nema.org/PS3.19/models/NativeDICOM");
226 root.append_attribute("xmlns:xsi").set_value("http://www.w3.org/2001/XMLSchema-instance");
227
228 ExploreXmlDataset(root, source);
229
230 pugi::xml_node decl = target.prepend_child(pugi::node_declaration);
231 decl.append_attribute("version").set_value("1.0");
232 decl.append_attribute("encoding").set_value("utf-8");
233 }
234 #endif
235
236
237 std::string DicomWebJsonVisitor::FormatTag(const DicomTag& tag)
238 {
239 char buf[16];
240 sprintf(buf, "%04X%04X", tag.GetGroup(), tag.GetElement());
241 return std::string(buf);
242 }
243
244
245 Json::Value& DicomWebJsonVisitor::CreateNode(const std::vector<DicomTag>& parentTags,
246 const std::vector<size_t>& parentIndexes,
247 const DicomTag& tag)
248 {
249 assert(parentTags.size() == parentIndexes.size());
250
251 Json::Value* node = &result_;
252
253 for (size_t i = 0; i < parentTags.size(); i++)
254 {
255 std::string t = FormatTag(parentTags[i]);
256
257 if (!node->isMember(t))
258 {
259 Json::Value item = Json::objectValue;
260 item[KEY_VR] = KEY_SQ;
261 item[KEY_VALUE] = Json::arrayValue;
262 item[KEY_VALUE].append(Json::objectValue);
263 (*node) [t] = item;
264
265 node = &(*node)[t][KEY_VALUE][0];
266 }
267 else if ((*node) [t].type() != Json::objectValue ||
268 !(*node) [t].isMember(KEY_VR) ||
269 (*node) [t][KEY_VR].type() != Json::stringValue ||
270 (*node) [t][KEY_VR].asString() != KEY_SQ ||
271 !(*node) [t].isMember(KEY_VALUE) ||
272 (*node) [t][KEY_VALUE].type() != Json::arrayValue)
273 {
274 throw OrthancException(ErrorCode_InternalError);
275 }
276 else
277 {
278 size_t currentSize = (*node) [t][KEY_VALUE].size();
279
280 if (parentIndexes[i] < currentSize)
281 {
282 // The node already exists
283 }
284 else if (parentIndexes[i] == currentSize)
285 {
286 (*node) [t][KEY_VALUE].append(Json::objectValue);
287 }
288 else
289 {
290 throw OrthancException(ErrorCode_InternalError);
291 }
292
293 node = &(*node) [t][KEY_VALUE][Json::ArrayIndex(parentIndexes[i])];
294 }
295 }
296
297 assert(node->type() == Json::objectValue);
298
299 std::string t = FormatTag(tag);
300 if (node->isMember(t))
301 {
302 throw OrthancException(ErrorCode_InternalError);
303 }
304 else
305 {
306 (*node) [t] = Json::objectValue;
307 return (*node) [t];
308 }
309 }
310
311
312 Json::Value DicomWebJsonVisitor::FormatInteger(int64_t value)
313 {
314 if (value < 0)
315 {
316 return Json::Value(static_cast<int32_t>(value));
317 }
318 else
319 {
320 return Json::Value(static_cast<uint32_t>(value));
321 }
322 }
323
324
325 Json::Value DicomWebJsonVisitor::FormatDouble(double value)
326 {
327 try
328 {
329 long long a = boost::math::llround<double>(value);
330
331 double d = fabs(value - static_cast<double>(a));
332
333 if (d <= std::numeric_limits<double>::epsilon() * 100.0)
334 {
335 return FormatInteger(a);
336 }
337 else
338 {
339 return Json::Value(value);
340 }
341 }
342 catch (boost::math::rounding_error&)
343 {
344 // Can occur if "long long" is too small to receive this value
345 // (e.g. infinity)
346 return Json::Value(value);
347 }
348 }
349
350
351 #if ORTHANC_ENABLE_PUGIXML == 1
352 void DicomWebJsonVisitor::FormatXml(std::string& target) const
353 {
354 pugi::xml_document doc;
355 DicomWebJsonToXml(doc, result_);
356 Toolbox::XmlToString(target, doc);
357 }
358 #endif
359
360
361 void DicomWebJsonVisitor::VisitEmptySequence(const std::vector<DicomTag>& parentTags,
362 const std::vector<size_t>& parentIndexes,
363 const DicomTag& tag)
364 {
365 if (tag.GetElement() != 0x0000)
366 {
367 Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
368 node[KEY_VR] = EnumerationToString(ValueRepresentation_Sequence);
369 }
370 }
371
372
373 void DicomWebJsonVisitor::VisitBinary(const std::vector<DicomTag>& parentTags,
374 const std::vector<size_t>& parentIndexes,
375 const DicomTag& tag,
376 ValueRepresentation vr,
377 const void* data,
378 size_t size)
379 {
380 assert(vr == ValueRepresentation_OtherByte ||
381 vr == ValueRepresentation_OtherDouble ||
382 vr == ValueRepresentation_OtherFloat ||
383 vr == ValueRepresentation_OtherLong ||
384 vr == ValueRepresentation_OtherWord ||
385 vr == ValueRepresentation_Unknown);
386
387 if (tag.GetElement() != 0x0000)
388 {
389 BinaryMode mode;
390 std::string bulkDataUri;
391
392 if (formatter_ == NULL)
393 {
394 mode = BinaryMode_InlineBinary;
395 }
396 else
397 {
398 mode = formatter_->Format(bulkDataUri, parentTags, parentIndexes, tag, vr);
399 }
400
401 if (mode != BinaryMode_Ignore)
402 {
403 Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
404 node[KEY_VR] = EnumerationToString(vr);
405
406 switch (mode)
407 {
408 case BinaryMode_BulkDataUri:
409 node[KEY_BULK_DATA_URI] = bulkDataUri;
410 break;
411
412 case BinaryMode_InlineBinary:
413 {
414 std::string tmp(static_cast<const char*>(data), size);
415
416 std::string base64;
417 Toolbox::EncodeBase64(base64, tmp);
418
419 node[KEY_INLINE_BINARY] = base64;
420 break;
421 }
422
423 default:
424 throw OrthancException(ErrorCode_ParameterOutOfRange);
425 }
426 }
427 }
428 }
429
430
431 void DicomWebJsonVisitor::VisitIntegers(const std::vector<DicomTag>& parentTags,
432 const std::vector<size_t>& parentIndexes,
433 const DicomTag& tag,
434 ValueRepresentation vr,
435 const std::vector<int64_t>& values)
436 {
437 if (tag.GetElement() != 0x0000 &&
438 vr != ValueRepresentation_NotSupported)
439 {
440 Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
441 node[KEY_VR] = EnumerationToString(vr);
442
443 if (!values.empty())
444 {
445 Json::Value content = Json::arrayValue;
446 for (size_t i = 0; i < values.size(); i++)
447 {
448 content.append(FormatInteger(values[i]));
449 }
450
451 node[KEY_VALUE] = content;
452 }
453 }
454 }
455
456 void DicomWebJsonVisitor::VisitDoubles(const std::vector<DicomTag>& parentTags,
457 const std::vector<size_t>& parentIndexes,
458 const DicomTag& tag,
459 ValueRepresentation vr,
460 const std::vector<double>& values)
461 {
462 if (tag.GetElement() != 0x0000 &&
463 vr != ValueRepresentation_NotSupported)
464 {
465 Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
466 node[KEY_VR] = EnumerationToString(vr);
467
468 if (!values.empty())
469 {
470 Json::Value content = Json::arrayValue;
471 for (size_t i = 0; i < values.size(); i++)
472 {
473 content.append(FormatDouble(values[i]));
474 }
475
476 node[KEY_VALUE] = content;
477 }
478 }
479 }
480
481
482 void DicomWebJsonVisitor::VisitAttributes(const std::vector<DicomTag>& parentTags,
483 const std::vector<size_t>& parentIndexes,
484 const DicomTag& tag,
485 const std::vector<DicomTag>& values)
486 {
487 if (tag.GetElement() != 0x0000)
488 {
489 Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
490 node[KEY_VR] = EnumerationToString(ValueRepresentation_AttributeTag);
491
492 if (!values.empty())
493 {
494 Json::Value content = Json::arrayValue;
495 for (size_t i = 0; i < values.size(); i++)
496 {
497 content.append(FormatTag(values[i]));
498 }
499
500 node[KEY_VALUE] = content;
501 }
502 }
503 }
504
505
506 ITagVisitor::Action
507 DicomWebJsonVisitor::VisitString(std::string& newValue,
508 const std::vector<DicomTag>& parentTags,
509 const std::vector<size_t>& parentIndexes,
510 const DicomTag& tag,
511 ValueRepresentation vr,
512 const std::string& value)
513 {
514 if (tag.GetElement() == 0x0000 ||
515 vr == ValueRepresentation_NotSupported)
516 {
517 return Action_None;
518 }
519 else
520 {
521 Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
522 node[KEY_VR] = EnumerationToString(vr);
523
524 #if 0
525 /**
526 * TODO - The JSON file has an UTF-8 encoding, thus DCMTK
527 * replaces the specific character set with "ISO_IR 192"
528 * (UNICODE UTF-8). On Google Cloud Healthcare, however, the
529 * source encoding is reported, which seems more logical. We
530 * thus choose the Google convention. Enabling this block will
531 * mimic the DCMTK behavior.
532 **/
533 if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET)
534 {
535 node[KEY_VALUE].append("ISO_IR 192");
536 }
537 else
538 #endif
539 {
540 std::string truncated;
541
542 if (!value.empty() &&
543 value[value.size() - 1] == '\0')
544 {
545 truncated = value.substr(0, value.size() - 1);
546 }
547 else
548 {
549 truncated = value;
550 }
551
552 if (!truncated.empty())
553 {
554 std::vector<std::string> tokens;
555 Toolbox::TokenizeString(tokens, truncated, '\\');
556
557 if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET &&
558 tokens.size() > 1 &&
559 tokens[0].empty())
560 {
561 // Specific character set with code extension: Remove the
562 // first element from the vector of encodings
563 tokens.erase(tokens.begin());
564 }
565
566 node[KEY_VALUE] = Json::arrayValue;
567 for (size_t i = 0; i < tokens.size(); i++)
568 {
569 try
570 {
571 switch (vr)
572 {
573 case ValueRepresentation_PersonName:
574 {
575 Json::Value value = Json::objectValue;
576 if (!tokens[i].empty())
577 {
578 std::vector<std::string> components;
579 Toolbox::TokenizeString(components, tokens[i], '=');
580
581 if (components.size() >= 1)
582 {
583 value[KEY_ALPHABETIC] = components[0];
584 }
585
586 if (components.size() >= 2)
587 {
588 value[KEY_IDEOGRAPHIC] = components[1];
589 }
590
591 if (components.size() >= 3)
592 {
593 value[KEY_PHONETIC] = components[2];
594 }
595 }
596
597 node[KEY_VALUE].append(value);
598 break;
599 }
600
601 case ValueRepresentation_IntegerString:
602 {
603 /**
604 * The calls to "StripSpaces()" below fix the
605 * issue reported by Rana Asim Wajid on 2019-06-05
606 * ("Error Exception while invoking plugin service
607 * 32: Bad file format"):
608 * https://groups.google.com/d/msg/orthanc-users/T32FovWPcCE/-hKFbfRJBgAJ
609 **/
610
611 std::string t = Orthanc::Toolbox::StripSpaces(tokens[i]);
612 if (t.empty())
613 {
614 node[KEY_VALUE].append(Json::nullValue);
615 }
616 else
617 {
618 int64_t value = boost::lexical_cast<int64_t>(t);
619 node[KEY_VALUE].append(FormatInteger(value));
620 }
621
622 break;
623 }
624
625 case ValueRepresentation_DecimalString:
626 {
627 std::string t = Orthanc::Toolbox::StripSpaces(tokens[i]);
628 if (t.empty())
629 {
630 node[KEY_VALUE].append(Json::nullValue);
631 }
632 else
633 {
634 double value = boost::lexical_cast<double>(t);
635 node[KEY_VALUE].append(FormatDouble(value));
636 }
637
638 break;
639 }
640
641 default:
642 if (tokens[i].empty())
643 {
644 node[KEY_VALUE].append(Json::nullValue);
645 }
646 else
647 {
648 node[KEY_VALUE].append(tokens[i]);
649 }
650
651 break;
652 }
653 }
654 catch (boost::bad_lexical_cast&)
655 {
656 std::string tmp;
657 if (value.size() < 64 &&
658 Toolbox::IsAsciiString(value))
659 {
660 tmp = ": " + value;
661 }
662
663 LOG(WARNING) << "Ignoring DICOM tag (" << tag.Format()
664 << ") with invalid content for VR " << EnumerationToString(vr) << tmp;
665 }
666 }
667 }
668 }
669 }
670
671 return Action_None;
672 }
673 }