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-2021 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 Affero General Public License
9 * as published by the Free Software Foundation, either version 3 of
10 * the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Affero General Public License for more details.
16 *
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 **/
20
21
22 #include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"
23 #include "Configuration.h"
24 #include "DicomWebFormatter.h"
25
26 #include <Compatibility.h>
27 #include <ChunkedBuffer.h>
28 #include <Logging.h>
29 #include <Toolbox.h>
30
31 #include <memory>
32
33
34 static const char* const MAIN_DICOM_TAGS = "MainDicomTags";
35 static const char* const INSTANCES = "Instances";
36 static const char* const PATIENT_MAIN_DICOM_TAGS = "PatientMainDicomTags";
37
38
GetResourceUri(Orthanc::ResourceType level,const std::string & publicId)39 static std::string GetResourceUri(Orthanc::ResourceType level,
40 const std::string& publicId)
41 {
42 switch (level)
43 {
44 case Orthanc::ResourceType_Study:
45 return "/studies/" + publicId;
46
47 case Orthanc::ResourceType_Series:
48 return "/series/" + publicId;
49
50 case Orthanc::ResourceType_Instance:
51 return "/instances/" + publicId;
52
53 default:
54 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
55 }
56 }
57
58
59
AcceptMultipartDicom(bool & transcode,Orthanc::DicomTransferSyntax & targetSyntax,const OrthancPluginHttpRequest * request)60 static void AcceptMultipartDicom(bool& transcode,
61 Orthanc::DicomTransferSyntax& targetSyntax /* only if transcoding */,
62 const OrthancPluginHttpRequest* request)
63 {
64 /**
65 * Up to release 1.4 of the DICOMweb plugin, WADO-RS
66 * RetrieveInstance, RetrieveSeries and RetrieveStudy did *NOT*
67 * transcode if no transer syntax was explicitly provided. This was
68 * because the DICOM standard didn't specify a behavior in this case
69 * up to DICOM 2016b:
70 * http://dicom.nema.org/medical/dicom/2016b/output/chtml/part18/sect_6.5.3.html
71 *
72 * However, starting with DICOM 2016c, it is explicitly stated that
73 * "If transfer-syntax is not specified in the dcm-parameters the
74 * origin server shall use the Explicit VR Little Endian Transfer
75 * Syntax "1.2.840.10008.1.2.1" for each Instance":
76 * http://dicom.nema.org/medical/dicom/2016c/output/chtml/part18/sect_6.5.3.html
77 *
78 * As a consequence, starting with release 1.5 of the DICOMweb
79 * plugin, transcoding to "Little Endian Explicit" takes place by
80 * default. If this transcoding is not desirable, the "Accept" HTTP
81 * header can be set to
82 * "multipart/related;type=application/dicom;transfer-syntax=*" (not
83 * the asterisk "*") in order to prevent transcoding. The same
84 * convention is used by the Google Cloud Platform:
85 * https://cloud.google.com/healthcare/docs/dicom
86 **/
87 transcode = true;
88 targetSyntax = Orthanc::DicomTransferSyntax_LittleEndianExplicit;
89
90 std::string accept;
91
92 if (!OrthancPlugins::LookupHttpHeader(accept, request, "accept"))
93 {
94 return; // By default, return "multipart/related; type=application/dicom;"
95 }
96
97 std::string application;
98 std::map<std::string, std::string> attributes;
99 OrthancPlugins::ParseContentType(application, attributes, accept);
100
101 if (application != "multipart/related" &&
102 application != "*/*")
103 {
104 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest,
105 "This WADO-RS plugin cannot generate the following content type: " + accept);
106 }
107
108 if (attributes.find("type") != attributes.end())
109 {
110 std::string s = attributes["type"];
111 Orthanc::Toolbox::ToLowerCase(s);
112 if (s != "application/dicom")
113 {
114 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest,
115 "This WADO-RS plugin only supports application/dicom "
116 "return type for DICOM retrieval (" + accept + ")");
117 }
118 }
119
120 static const char* const TRANSFER_SYNTAX = "transfer-syntax";
121
122 std::map<std::string, std::string>::const_iterator found = attributes.find(TRANSFER_SYNTAX);
123 if (found != attributes.end())
124 {
125 /**
126 * The "*" case below is related to Google Healthcare API:
127 * https://groups.google.com/d/msg/orthanc-users/w1Ekrsc6-U8/T2a_DoQ5CwAJ
128 **/
129 if (found->second == "*")
130 {
131 transcode = false;
132 }
133 else
134 {
135 transcode = true;
136
137 if (!Orthanc::LookupTransferSyntax(targetSyntax, found->second))
138 {
139 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest,
140 "Unsupported transfer syntax in WADO-RS: " + found->second);
141 }
142 }
143 }
144 }
145
146
147
AcceptMetadata(const OrthancPluginHttpRequest * request,bool & isXml)148 static bool AcceptMetadata(const OrthancPluginHttpRequest* request,
149 bool& isXml)
150 {
151 isXml = false; // By default, return application/dicom+json
152
153 std::string accept;
154 if (!OrthancPlugins::LookupHttpHeader(accept, request, "accept"))
155 {
156 return true;
157 }
158
159 std::string application;
160 std::map<std::string, std::string> attributes;
161 OrthancPlugins::ParseContentType(application, attributes, accept);
162
163 std::vector<std::string> applicationTokens;
164 Orthanc::Toolbox::TokenizeString(applicationTokens, application, ',');
165
166 for (size_t i = 0; i < applicationTokens.size(); i++)
167 {
168 std::string token = Orthanc::Toolbox::StripSpaces(applicationTokens[i]);
169
170 if (token == "application/json" ||
171 token == "application/dicom+json" ||
172 token == "*/*")
173 {
174 return true;
175 }
176 }
177
178 if (application != "multipart/related")
179 {
180 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest,
181 "This WADO-RS plugin cannot generate the following content type: " + accept);
182 }
183
184 if (attributes.find("type") != attributes.end())
185 {
186 std::string s = attributes["type"];
187 Orthanc::Toolbox::ToLowerCase(s);
188 if (s == "application/dicom+xml")
189 {
190 isXml = true;
191 }
192 else
193 {
194 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest,
195 "This WADO-RS plugin only supports application/dicom+xml "
196 "type for multipart/related accept (" + accept + ")");
197 }
198 }
199 else
200 {
201 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest,
202 "Missing \"type\" in multipart/related accept type (" + accept + ")");
203 }
204
205 if (attributes.find("transfer-syntax") != attributes.end())
206 {
207 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest,
208 "This WADO-RS plugin cannot change the transfer syntax to " +
209 attributes["transfer-syntax"]);
210 }
211
212 return true;
213 }
214
215
216
AcceptBulkData(const OrthancPluginHttpRequest * request)217 static bool AcceptBulkData(const OrthancPluginHttpRequest* request)
218 {
219 std::string accept;
220
221 if (!OrthancPlugins::LookupHttpHeader(accept, request, "accept"))
222 {
223 return true; // By default, return "multipart/related; type=application/octet-stream;"
224 }
225
226 std::string application;
227 std::map<std::string, std::string> attributes;
228 OrthancPlugins::ParseContentType(application, attributes, accept);
229
230 if (application != "multipart/related" &&
231 application != "*/*")
232 {
233 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest,
234 "This WADO-RS plugin cannot generate the following "
235 "bulk data type: " + accept);
236 }
237
238 if (attributes.find("type") != attributes.end())
239 {
240 std::string s = attributes["type"];
241 Orthanc::Toolbox::ToLowerCase(s);
242 if (s != "application/octet-stream")
243 {
244 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest,
245 "This WADO-RS plugin only supports application/octet-stream "
246 "return type for bulk data retrieval (" + accept + ")");
247 }
248 }
249
250 if (attributes.find("ra,ge") != attributes.end())
251 {
252 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest,
253 "This WADO-RS plugin does not support Range retrieval, "
254 "it can only return entire bulk data object");
255 }
256
257 return true;
258 }
259
260
AnswerListOfDicomInstances(OrthancPluginRestOutput * output,Orthanc::ResourceType level,const std::string & publicId,bool transcode,Orthanc::DicomTransferSyntax targetSyntax)261 static void AnswerListOfDicomInstances(OrthancPluginRestOutput* output,
262 Orthanc::ResourceType level,
263 const std::string& publicId,
264 bool transcode,
265 Orthanc::DicomTransferSyntax targetSyntax /* only if transcoding */)
266 {
267 if (level != Orthanc::ResourceType_Study &&
268 level != Orthanc::ResourceType_Series &&
269 level != Orthanc::ResourceType_Instance)
270 {
271 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
272 }
273
274 OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
275
276 Json::Value instances;
277
278 if (level == Orthanc::ResourceType_Instance)
279 {
280 Json::Value tmp = Json::objectValue;
281 tmp["ID"] = publicId;
282
283 instances = Json::arrayValue;
284 instances.append(tmp);
285 }
286 else
287 {
288 if (!OrthancPlugins::RestApiGet(instances, GetResourceUri(level, publicId) + "/instances", false))
289 {
290 // Internal error
291 OrthancPluginSendHttpStatusCode(context, output, 400);
292 return;
293 }
294 }
295
296 if (OrthancPluginStartMultipartAnswer(context, output, "related", "application/dicom"))
297 {
298 throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
299 }
300
301 for (Json::Value::ArrayIndex i = 0; i < instances.size(); i++)
302 {
303 const std::string uri = "/instances/" + instances[i]["ID"].asString();
304
305 bool transcodeThisInstance;
306
307 std::string sourceTransferSyntax;
308 if (!transcode)
309 {
310 transcodeThisInstance = false;
311 }
312 else if (OrthancPlugins::RestApiGetString(sourceTransferSyntax, uri + "/metadata/TransferSyntax", false))
313 {
314 // Avoid transcoding if the source file already uses the expected transfer syntax
315 Orthanc::DicomTransferSyntax syntax;
316 if (Orthanc::LookupTransferSyntax(syntax, sourceTransferSyntax))
317 {
318 transcodeThisInstance = (syntax != targetSyntax);
319 }
320 else
321 {
322 transcodeThisInstance = true;
323 }
324 }
325 else
326 {
327 // The transfer syntax of the source file is unknown, transcode it to be sure
328 transcodeThisInstance = true;
329 }
330
331 OrthancPlugins::MemoryBuffer dicom;
332 if (dicom.RestApiGet(uri + "/file", false))
333 {
334 if (transcodeThisInstance)
335 {
336 std::unique_ptr<OrthancPlugins::DicomInstance> transcoded(
337 OrthancPlugins::DicomInstance::Transcode(
338 dicom.GetData(), dicom.GetSize(), Orthanc::GetTransferSyntaxUid(targetSyntax)));
339
340 if (OrthancPluginSendMultipartItem(
341 context, output, reinterpret_cast<const char*>(transcoded->GetBuffer()),
342 transcoded->GetSize()) != 0)
343 {
344 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
345 }
346 }
347 else
348 {
349 if (OrthancPluginSendMultipartItem(context, output, dicom.GetData(), dicom.GetSize()) != 0)
350 {
351 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
352 }
353 }
354 }
355 }
356 }
357
358
359
360 namespace
361 {
362 class SetOfDicomInstances : public boost::noncopyable
363 {
364 private:
365 std::vector<Orthanc::DicomMap*> instances_;
366
367 public:
~SetOfDicomInstances()368 ~SetOfDicomInstances()
369 {
370 for (size_t i = 0; i < instances_.size(); i++)
371 {
372 assert(instances_[i] != NULL);
373 delete instances_[i];
374 }
375 }
376
GetSize() const377 size_t GetSize() const
378 {
379 return instances_.size();
380 }
381
ReadInstance(const std::string & publicId)382 bool ReadInstance(const std::string& publicId)
383 {
384 Json::Value dicomAsJson;
385
386 if (OrthancPlugins::RestApiGet(dicomAsJson, "/instances/" + publicId + "/tags", false))
387 {
388 std::unique_ptr<Orthanc::DicomMap> instance(new Orthanc::DicomMap);
389 instance->FromDicomAsJson(dicomAsJson);
390 instances_.push_back(instance.release());
391
392 return true;
393 }
394 else
395 {
396 return false;
397 }
398 }
399
400
MinorityReport(Orthanc::DicomMap & target,const Orthanc::DicomTag & tag) const401 void MinorityReport(Orthanc::DicomMap& target,
402 const Orthanc::DicomTag& tag) const
403 {
404 typedef std::map<std::string, unsigned int> Counters;
405
406 Counters counters;
407
408 for (size_t i = 0; i < instances_.size(); i++)
409 {
410 assert(instances_[i] != NULL);
411
412 std::string value;
413 if (instances_[i]->LookupStringValue(value, tag, false))
414 {
415 Counters::iterator found = counters.find(value);
416 if (found == counters.end())
417 {
418 counters[value] = 1;
419 }
420 else
421 {
422 found->second ++;
423 }
424 }
425 }
426
427 if (!counters.empty())
428 {
429 Counters::const_iterator current = counters.begin();
430
431 std::string maxValue = current->first;
432 size_t maxCount = current->second;
433
434 ++current;
435
436 while (current != counters.end())
437 {
438 if (maxCount < current->second)
439 {
440 maxValue = current->first;
441 maxCount = current->second;
442 }
443
444 ++current;
445 }
446
447 target.SetValue(tag, maxValue, false);
448
449 // Take the ceiling of the number of available instances
450 const size_t threshold = instances_.size() / 2 + 1;
451 if (maxCount < threshold)
452 {
453 LOG(WARNING) << "No consensus on the value of a tag during WADO-RS Retrieve "
454 << "Metadata in Extrapolate mode: " << tag.Format();
455 }
456 }
457 }
458 };
459
460
461 class MainDicomTagsCache : public boost::noncopyable
462 {
463 private:
464 struct Info : public boost::noncopyable
465 {
466 Orthanc::DicomMap dicom_;
467 std::string parent_;
468 };
469
470 typedef std::pair<std::string, Orthanc::ResourceType> Index;
471 typedef std::map<Index, Info*> Content;
472
473 Content content_;
474
ReadResource(Orthanc::DicomMap & dicom,std::string & parent,OrthancPlugins::MetadataMode mode,const std::string & orthancId,Orthanc::ResourceType level)475 static bool ReadResource(Orthanc::DicomMap& dicom,
476 std::string& parent,
477 OrthancPlugins::MetadataMode mode,
478 const std::string& orthancId,
479 Orthanc::ResourceType level)
480 {
481 std::string uri;
482 std::string parentField;
483
484 switch (level)
485 {
486 case Orthanc::ResourceType_Study:
487 uri = "/studies/" + orthancId;
488 break;
489
490 case Orthanc::ResourceType_Series:
491 uri = "/series/" + orthancId;
492 parentField = "ParentStudy";
493 break;
494
495 case Orthanc::ResourceType_Instance:
496 uri = "/instances/" + orthancId;
497 parentField = "ParentSeries";
498 break;
499
500 default:
501 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
502 }
503
504 Json::Value value;
505 if (!OrthancPlugins::RestApiGet(value, uri, false))
506 {
507 return false;
508 }
509
510
511 if (value.type() != Json::objectValue ||
512 !value.isMember(MAIN_DICOM_TAGS))
513 {
514 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
515 }
516
517 dicom.ParseMainDicomTags(value[MAIN_DICOM_TAGS], level);
518
519 if (level == Orthanc::ResourceType_Study)
520 {
521 if (value.isMember(PATIENT_MAIN_DICOM_TAGS))
522 {
523 dicom.ParseMainDicomTags(value[PATIENT_MAIN_DICOM_TAGS], Orthanc::ResourceType_Patient);
524 }
525 else
526 {
527 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
528 }
529 }
530
531 if (!parentField.empty())
532 {
533 if (value.isMember(parentField) &&
534 value[parentField].type() == Json::stringValue)
535 {
536 parent = value[parentField].asString();
537 }
538 else
539 {
540 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
541 }
542 }
543
544
545 if (mode == OrthancPlugins::MetadataMode_Extrapolate &&
546 (level == Orthanc::ResourceType_Series ||
547 level == Orthanc::ResourceType_Study))
548 {
549 std::set<Orthanc::DicomTag> tags;
550 OrthancPlugins::Configuration::GetExtrapolatedMetadataTags(tags, level);
551
552 if (!tags.empty())
553 {
554 /**
555 * Complete the series/study-level tags, with instance-level
556 * tags that are not considered as "main DICOM tags" in
557 * Orthanc, but that are necessary for Web viewers, and that
558 * are expected to be constant throughout all the instances of
559 * the study/series. To this end, we read up to "N" DICOM
560 * instances of this study/series from disk, and for the tags
561 * of interest, we look at whether there is a consensus in the
562 * value among these instances. Obviously, this is an
563 * approximation to improve performance.
564 **/
565
566 std::set<std::string> allInstances;
567
568 switch (level)
569 {
570 case Orthanc::ResourceType_Series:
571 if (!value.isMember(INSTANCES) ||
572 value[INSTANCES].type() != Json::arrayValue)
573 {
574 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
575 }
576 else
577 {
578 for (Json::Value::ArrayIndex i = 0; i < value[INSTANCES].size(); i++)
579 {
580 if (value[INSTANCES][i].type() != Json::stringValue)
581 {
582 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
583 }
584 else
585 {
586 allInstances.insert(value[INSTANCES][i].asString());
587 }
588 }
589 }
590
591 break;
592
593 case Orthanc::ResourceType_Study:
594 {
595 Json::Value tmp;
596 if (OrthancPlugins::RestApiGet(tmp, "/studies/" + orthancId + "/instances", false))
597 {
598 if (tmp.type() != Json::arrayValue)
599 {
600 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
601 }
602
603 for (Json::Value::ArrayIndex i = 0; i < tmp.size(); i++)
604 {
605 if (tmp[i].type() != Json::objectValue ||
606 !tmp[i].isMember("ID") ||
607 tmp[i]["ID"].type() != Json::stringValue)
608 {
609 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
610 }
611 else
612 {
613 allInstances.insert(tmp[i]["ID"].asString());
614 }
615 }
616 }
617
618 break;
619 }
620
621 default:
622 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
623 }
624
625
626 // Select up to N random instances. The instances are
627 // implicitly selected randomly, as the public ID of an
628 // instance is a SHA-1 hash (whose domain is uniformly distributed)
629
630 static const size_t N = 3;
631 SetOfDicomInstances selectedInstances;
632
633 for (std::set<std::string>::const_iterator it = allInstances.begin();
634 selectedInstances.GetSize() < N && it != allInstances.end(); ++it)
635 {
636 selectedInstances.ReadInstance(*it);
637 }
638
639 for (std::set<Orthanc::DicomTag>::const_iterator
640 it = tags.begin(); it != tags.end(); ++it)
641 {
642 selectedInstances.MinorityReport(dicom, *it);
643 }
644 }
645 }
646
647 return true;
648 }
649
650
Lookup(Orthanc::DicomMap & dicom,std::string & parent,OrthancPlugins::MetadataMode mode,const std::string & orthancId,Orthanc::ResourceType level)651 bool Lookup(Orthanc::DicomMap& dicom,
652 std::string& parent,
653 OrthancPlugins::MetadataMode mode,
654 const std::string& orthancId,
655 Orthanc::ResourceType level)
656 {
657 Content::iterator found = content_.find(std::make_pair(orthancId, level));
658
659 if (found == content_.end())
660 {
661 std::unique_ptr<Info> info(new Info);
662 if (!ReadResource(info->dicom_, info->parent_, mode, orthancId, level))
663 {
664 return false;
665 }
666
667 found = content_.insert(std::make_pair(std::make_pair(orthancId, level), info.release())).first;
668 }
669
670 assert(found != content_.end() &&
671 found->second != NULL);
672 dicom.Merge(found->second->dicom_);
673 parent = found->second->parent_;
674
675 return true;
676 }
677
678
679 public:
~MainDicomTagsCache()680 ~MainDicomTagsCache()
681 {
682 for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
683 {
684 assert(it->second != NULL);
685 delete it->second;
686 }
687 }
688
689
GetInstance(Orthanc::DicomMap & dicom,OrthancPlugins::MetadataMode mode,const std::string & orthancId)690 bool GetInstance(Orthanc::DicomMap& dicom,
691 OrthancPlugins::MetadataMode mode,
692 const std::string& orthancId)
693 {
694 std::string seriesId, studyId, nope;
695
696 return (ReadResource(dicom, seriesId, mode, orthancId, Orthanc::ResourceType_Instance) &&
697 Lookup(dicom, studyId, mode, seriesId, Orthanc::ResourceType_Series) &&
698 Lookup(dicom, nope /* patient id is unused */, mode, studyId, Orthanc::ResourceType_Study));
699 }
700 };
701 }
702
703
704
WriteInstanceMetadata(OrthancPlugins::DicomWebFormatter::HttpWriter & writer,OrthancPlugins::MetadataMode mode,MainDicomTagsCache & cache,const std::string & orthancId,const std::string & studyInstanceUid,const std::string & seriesInstanceUid,const std::string & sopInstanceUid,const std::string & wadoBase)705 static void WriteInstanceMetadata(OrthancPlugins::DicomWebFormatter::HttpWriter& writer,
706 OrthancPlugins::MetadataMode mode,
707 MainDicomTagsCache& cache,
708 const std::string& orthancId,
709 const std::string& studyInstanceUid,
710 const std::string& seriesInstanceUid,
711 const std::string& sopInstanceUid,
712 const std::string& wadoBase)
713 {
714 assert(!orthancId.empty() &&
715 !studyInstanceUid.empty() &&
716 !seriesInstanceUid.empty() &&
717 !sopInstanceUid.empty() &&
718 !wadoBase.empty());
719
720 const std::string bulkRoot = (wadoBase +
721 "studies/" + studyInstanceUid +
722 "/series/" + seriesInstanceUid +
723 "/instances/" + sopInstanceUid + "/bulk");
724
725 switch (mode)
726 {
727 case OrthancPlugins::MetadataMode_MainDicomTags:
728 case OrthancPlugins::MetadataMode_Extrapolate:
729 {
730 Orthanc::DicomMap dicom;
731 if (cache.GetInstance(dicom, mode, orthancId))
732 {
733 writer.AddOrthancMap(dicom);
734 }
735
736 break;
737 }
738
739 case OrthancPlugins::MetadataMode_Full:
740 {
741 // On a SSD drive, this version is twice slower than if using
742 // cache (see below)
743
744 OrthancPlugins::MemoryBuffer dicom;
745 if (dicom.RestApiGet("/instances/" + orthancId + "/file", false))
746 {
747 writer.AddDicom(dicom.GetData(), dicom.GetSize(), bulkRoot);
748 }
749
750 break;
751 }
752
753 default:
754 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
755 }
756
757
758 #if 0
759 /**
760 **/
761
762 // TODO - Have a global setting to enable/disable caching of DICOMweb
763
764 // TODO - Have a way to clear the "4444" attachments if Orthanc
765 // version changes => Store Orthanc core version in a prefix or in
766 // another attachment?
767
768 OrthancPlugins::MemoryBuffer buffer;
769
770 if (writer.IsXml())
771 {
772 // DICOMweb XML is not cached
773 if (buffer.RestApiGet("/instances/" + orthancId + "/file", false))
774 {
775 writer.AddDicom(buffer.GetData(), buffer.GetSize(), bulkRoot);
776 }
777 }
778 else
779 {
780 if (buffer.RestApiGet("/instances/" + orthancId + "/attachments/4444/data", false))
781 {
782 writer.AddDicomWebSerializedJson(buffer.GetData(), buffer.GetSize());
783 }
784 else if (buffer.RestApiGet("/instances/" + orthancId + "/file", false))
785 {
786 // "Ignore binary mode" in DICOMweb conversion if caching is
787 // enabled, as the bulk root can change across executions
788
789 std::string dicomweb;
790 {
791 // TODO - Avoid a global mutex => Need to change Orthanc SDK
792 OrthancPlugins::DicomWebFormatter::Apply(
793 dicomweb, OrthancPlugins::GetGlobalContext(), buffer.GetData(), buffer.GetSize(),
794 false /* JSON */, OrthancPluginDicomWebBinaryMode_Ignore, "");
795 }
796
797 buffer.RestApiPut("/instances/" + orthancId + "/attachments/4444", dicomweb, false);
798 writer.AddDicomWebSerializedJson(dicomweb.c_str(), dicomweb.size());
799 }
800 }
801 #endif
802 }
803
804
805
LocateStudy(OrthancPluginRestOutput * output,std::string & orthancId,std::string & studyInstanceUid,const OrthancPluginHttpRequest * request)806 bool LocateStudy(OrthancPluginRestOutput* output,
807 std::string& orthancId,
808 std::string& studyInstanceUid,
809 const OrthancPluginHttpRequest* request)
810 {
811 OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
812
813 if (request->method != OrthancPluginHttpMethod_Get)
814 {
815 OrthancPluginSendMethodNotAllowed(context, output, "GET");
816 return false;
817 }
818
819 studyInstanceUid = request->groups[0];
820
821 try
822 {
823 OrthancPlugins::OrthancString tmp;
824 tmp.Assign(OrthancPluginLookupStudy(context, studyInstanceUid.c_str()));
825
826 if (tmp.GetContent() != NULL)
827 {
828 tmp.ToString(orthancId);
829 return true;
830 }
831 }
832 catch (Orthanc::OrthancException&)
833 {
834 }
835
836 throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem,
837 "Accessing an inexistent study with WADO-RS: " + studyInstanceUid);
838 }
839
840
LocateSeries(OrthancPluginRestOutput * output,std::string & orthancId,std::string & studyInstanceUid,std::string & seriesInstanceUid,const OrthancPluginHttpRequest * request)841 bool LocateSeries(OrthancPluginRestOutput* output,
842 std::string& orthancId,
843 std::string& studyInstanceUid,
844 std::string& seriesInstanceUid,
845 const OrthancPluginHttpRequest* request)
846 {
847 OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
848
849 if (request->method != OrthancPluginHttpMethod_Get)
850 {
851 OrthancPluginSendMethodNotAllowed(context, output, "GET");
852 return false;
853 }
854
855 studyInstanceUid = request->groups[0];
856 seriesInstanceUid = request->groups[1];
857
858 bool found = false;
859
860 try
861 {
862 OrthancPlugins::OrthancString tmp;
863 tmp.Assign(OrthancPluginLookupSeries(context, seriesInstanceUid.c_str()));
864
865 if (tmp.GetContent() != NULL)
866 {
867 tmp.ToString(orthancId);
868 found = true;
869 }
870 }
871 catch (Orthanc::OrthancException&)
872 {
873 }
874
875 if (!found)
876 {
877 throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem,
878 "Accessing an inexistent series with WADO-RS: " + seriesInstanceUid);
879 }
880
881 Json::Value study;
882 if (!OrthancPlugins::RestApiGet(study, "/series/" + orthancId + "/study", false))
883 {
884 OrthancPluginSendHttpStatusCode(context, output, 404);
885 return false;
886 }
887 else if (study[MAIN_DICOM_TAGS]["StudyInstanceUID"].asString() != studyInstanceUid)
888 {
889 throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem,
890 "No series " + seriesInstanceUid + " in study " + studyInstanceUid);
891 }
892 else
893 {
894 return true;
895 }
896 }
897
898
LocateInstance(OrthancPluginRestOutput * output,std::string & orthancId,std::string & studyInstanceUid,std::string & seriesInstanceUid,std::string & sopInstanceUid,const OrthancPluginHttpRequest * request)899 bool LocateInstance(OrthancPluginRestOutput* output,
900 std::string& orthancId,
901 std::string& studyInstanceUid,
902 std::string& seriesInstanceUid,
903 std::string& sopInstanceUid,
904 const OrthancPluginHttpRequest* request)
905 {
906 OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
907
908 if (request->method != OrthancPluginHttpMethod_Get)
909 {
910 OrthancPluginSendMethodNotAllowed(context, output, "GET");
911 return false;
912 }
913
914 studyInstanceUid = request->groups[0];
915 seriesInstanceUid = request->groups[1];
916 sopInstanceUid = request->groups[2];
917
918 {
919 OrthancPlugins::OrthancString tmp;
920 tmp.Assign(OrthancPluginLookupInstance(context, sopInstanceUid.c_str()));
921
922 if (tmp.GetContent() == NULL)
923 {
924 throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem,
925 "Accessing an inexistent instance with WADO-RS: " + sopInstanceUid);
926 }
927
928 tmp.ToString(orthancId);
929 }
930
931 Json::Value study, series;
932 if (!OrthancPlugins::RestApiGet(series, "/instances/" + orthancId + "/series", false) ||
933 !OrthancPlugins::RestApiGet(study, "/instances/" + orthancId + "/study", false))
934 {
935 OrthancPluginSendHttpStatusCode(context, output, 404);
936 return false;
937 }
938 else if (study[MAIN_DICOM_TAGS]["StudyInstanceUID"].asString() != studyInstanceUid ||
939 series[MAIN_DICOM_TAGS]["SeriesInstanceUID"].asString() != seriesInstanceUid)
940 {
941 throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem,
942 "Instance " + sopInstanceUid +
943 " is not both in study " + studyInstanceUid +
944 " and in series " + seriesInstanceUid);
945 }
946 else
947 {
948 return true;
949 }
950 }
951
952
RetrieveDicomStudy(OrthancPluginRestOutput * output,const char * url,const OrthancPluginHttpRequest * request)953 void RetrieveDicomStudy(OrthancPluginRestOutput* output,
954 const char* url,
955 const OrthancPluginHttpRequest* request)
956 {
957 bool transcode;
958 Orthanc::DicomTransferSyntax targetSyntax;
959
960 AcceptMultipartDicom(transcode, targetSyntax, request);
961
962 std::string orthancId, studyInstanceUid;
963 if (LocateStudy(output, orthancId, studyInstanceUid, request))
964 {
965 AnswerListOfDicomInstances(output, Orthanc::ResourceType_Study, orthancId, transcode, targetSyntax);
966 }
967 }
968
969
RetrieveDicomSeries(OrthancPluginRestOutput * output,const char * url,const OrthancPluginHttpRequest * request)970 void RetrieveDicomSeries(OrthancPluginRestOutput* output,
971 const char* url,
972 const OrthancPluginHttpRequest* request)
973 {
974 bool transcode;
975 Orthanc::DicomTransferSyntax targetSyntax;
976
977 AcceptMultipartDicom(transcode, targetSyntax, request);
978
979 std::string orthancId, studyInstanceUid, seriesInstanceUid;
980 if (LocateSeries(output, orthancId, studyInstanceUid, seriesInstanceUid, request))
981 {
982 AnswerListOfDicomInstances(output, Orthanc::ResourceType_Series, orthancId, transcode, targetSyntax);
983 }
984 }
985
986
987
RetrieveDicomInstance(OrthancPluginRestOutput * output,const char * url,const OrthancPluginHttpRequest * request)988 void RetrieveDicomInstance(OrthancPluginRestOutput* output,
989 const char* url,
990 const OrthancPluginHttpRequest* request)
991 {
992 bool transcode;
993 Orthanc::DicomTransferSyntax targetSyntax;
994
995 AcceptMultipartDicom(transcode, targetSyntax, request);
996
997 std::string orthancId, studyInstanceUid, seriesInstanceUid, sopInstanceUid;
998 if (LocateInstance(output, orthancId, studyInstanceUid, seriesInstanceUid, sopInstanceUid, request))
999 {
1000 AnswerListOfDicomInstances(output, Orthanc::ResourceType_Instance, orthancId, transcode, targetSyntax);
1001 }
1002 }
1003
1004
1005
1006 namespace
1007 {
1008 class Identifier
1009 {
1010 private:
1011 std::string orthancId_;
1012 std::string dicomUid_;
1013
1014 public:
Identifier(const std::string & orthancId,const std::string & dicomUid)1015 Identifier(const std::string& orthancId,
1016 const std::string& dicomUid) :
1017 orthancId_(orthancId),
1018 dicomUid_(dicomUid)
1019 {
1020 }
1021
GetOrthancId() const1022 const std::string& GetOrthancId() const
1023 {
1024 return orthancId_;
1025 }
1026
GetDicomUid() const1027 const std::string& GetDicomUid() const
1028 {
1029 return dicomUid_;
1030 }
1031 };
1032 }
1033
1034
GetChildrenIdentifiers(std::list<Identifier> & target,Orthanc::ResourceType level,const std::string & orthancId)1035 static void GetChildrenIdentifiers(std::list<Identifier>& target,
1036 Orthanc::ResourceType level,
1037 const std::string& orthancId)
1038 {
1039 static const char* const ID = "ID";
1040 static const char* const SERIES_INSTANCE_UID = "SeriesInstanceUID";
1041 static const char* const SOP_INSTANCE_UID = "SOPInstanceUID";
1042
1043 target.clear();
1044
1045 const char* tag = NULL;
1046 std::string uri;
1047
1048 switch (level)
1049 {
1050 case Orthanc::ResourceType_Study:
1051 uri = "/studies/" + orthancId + "/series";
1052 tag = SERIES_INSTANCE_UID;
1053 break;
1054
1055 case Orthanc::ResourceType_Series:
1056 uri = "/series/" + orthancId + "/instances";
1057 tag = SOP_INSTANCE_UID;
1058 break;
1059
1060 default:
1061 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
1062 }
1063
1064 assert(tag != NULL);
1065
1066 Json::Value children;
1067 if (OrthancPlugins::RestApiGet(children, uri, false))
1068 {
1069 if (children.type() != Json::arrayValue)
1070 {
1071 throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
1072 }
1073
1074 for (Json::Value::ArrayIndex i = 0; i < children.size(); i++)
1075 {
1076 if (children[i].type() != Json::objectValue ||
1077 !children[i].isMember(ID) ||
1078 !children[i].isMember(MAIN_DICOM_TAGS) ||
1079 children[i][ID].type() != Json::stringValue ||
1080 children[i][MAIN_DICOM_TAGS].type() != Json::objectValue ||
1081 !children[i][MAIN_DICOM_TAGS].isMember(tag) ||
1082 children[i][MAIN_DICOM_TAGS][tag].type() != Json::stringValue)
1083 {
1084 throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
1085 }
1086 else
1087 {
1088 target.push_back(Identifier(children[i][ID].asString(),
1089 children[i][MAIN_DICOM_TAGS][tag].asString()));
1090
1091 }
1092 }
1093 }
1094 }
1095
1096
1097
RetrieveStudyMetadata(OrthancPluginRestOutput * output,const char * url,const OrthancPluginHttpRequest * request)1098 void RetrieveStudyMetadata(OrthancPluginRestOutput* output,
1099 const char* url,
1100 const OrthancPluginHttpRequest* request)
1101 {
1102 bool isXml;
1103 if (!AcceptMetadata(request, isXml))
1104 {
1105 OrthancPluginSendHttpStatusCode(OrthancPlugins::GetGlobalContext(), output, 400 /* Bad request */);
1106 }
1107 else
1108 {
1109 const OrthancPlugins::MetadataMode mode =
1110 OrthancPlugins::Configuration::GetMetadataMode(Orthanc::ResourceType_Study);
1111
1112 MainDicomTagsCache cache;
1113
1114 std::string studyOrthancId, studyInstanceUid;
1115 if (LocateStudy(output, studyOrthancId, studyInstanceUid, request))
1116 {
1117 OrthancPlugins::DicomWebFormatter::HttpWriter writer(output, isXml);
1118
1119 std::list<Identifier> series;
1120 GetChildrenIdentifiers(series, Orthanc::ResourceType_Study, studyOrthancId);
1121
1122 for (std::list<Identifier>::const_iterator a = series.begin(); a != series.end(); ++a)
1123 {
1124 std::list<Identifier> instances;
1125 GetChildrenIdentifiers(instances, Orthanc::ResourceType_Series, a->GetOrthancId());
1126
1127 for (std::list<Identifier>::const_iterator b = instances.begin(); b != instances.end(); ++b)
1128 {
1129 WriteInstanceMetadata(writer, mode, cache, b->GetOrthancId(), studyInstanceUid, a->GetDicomUid(),
1130 b->GetDicomUid(), OrthancPlugins::Configuration::GetBaseUrl(request));
1131 }
1132 }
1133
1134 writer.Send();
1135 }
1136 }
1137 }
1138
1139
RetrieveSeriesMetadata(OrthancPluginRestOutput * output,const char * url,const OrthancPluginHttpRequest * request)1140 void RetrieveSeriesMetadata(OrthancPluginRestOutput* output,
1141 const char* url,
1142 const OrthancPluginHttpRequest* request)
1143 {
1144 OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
1145
1146 bool isXml;
1147 if (!AcceptMetadata(request, isXml))
1148 {
1149 OrthancPluginSendHttpStatusCode(context, output, 400 /* Bad request */);
1150 }
1151 else
1152 {
1153 const OrthancPlugins::MetadataMode mode =
1154 OrthancPlugins::Configuration::GetMetadataMode(Orthanc::ResourceType_Series);
1155
1156 MainDicomTagsCache cache;
1157
1158 std::string seriesOrthancId, studyInstanceUid, seriesInstanceUid;
1159 if (LocateSeries(output, seriesOrthancId, studyInstanceUid, seriesInstanceUid, request))
1160 {
1161 OrthancPlugins::DicomWebFormatter::HttpWriter writer(output, isXml);
1162
1163 std::list<Identifier> instances;
1164 GetChildrenIdentifiers(instances, Orthanc::ResourceType_Series, seriesOrthancId);
1165
1166 for (std::list<Identifier>::const_iterator a = instances.begin(); a != instances.end(); ++a)
1167 {
1168 WriteInstanceMetadata(writer, mode, cache, a->GetOrthancId(), studyInstanceUid, seriesInstanceUid,
1169 a->GetDicomUid(), OrthancPlugins::Configuration::GetBaseUrl(request));
1170 }
1171
1172 writer.Send();
1173 }
1174 }
1175 }
1176
1177
RetrieveInstanceMetadata(OrthancPluginRestOutput * output,const char * url,const OrthancPluginHttpRequest * request)1178 void RetrieveInstanceMetadata(OrthancPluginRestOutput* output,
1179 const char* url,
1180 const OrthancPluginHttpRequest* request)
1181 {
1182 bool isXml;
1183 if (!AcceptMetadata(request, isXml))
1184 {
1185 OrthancPluginSendHttpStatusCode(OrthancPlugins::GetGlobalContext(), output, 400 /* Bad request */);
1186 }
1187 else
1188 {
1189 MainDicomTagsCache cache;
1190
1191 std::string orthancId, studyInstanceUid, seriesInstanceUid, sopInstanceUid;
1192 if (LocateInstance(output, orthancId, studyInstanceUid, seriesInstanceUid, sopInstanceUid, request))
1193 {
1194 OrthancPlugins::DicomWebFormatter::HttpWriter writer(output, isXml);
1195 WriteInstanceMetadata(writer, OrthancPlugins::MetadataMode_Full, cache, orthancId, studyInstanceUid,
1196 seriesInstanceUid, sopInstanceUid, OrthancPlugins::Configuration::GetBaseUrl(request));
1197 writer.Send();
1198 }
1199 }
1200 }
1201
1202
RetrieveBulkData(OrthancPluginRestOutput * output,const char * url,const OrthancPluginHttpRequest * request)1203 void RetrieveBulkData(OrthancPluginRestOutput* output,
1204 const char* url,
1205 const OrthancPluginHttpRequest* request)
1206 {
1207 OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
1208
1209 if (!AcceptBulkData(request))
1210 {
1211 OrthancPluginSendHttpStatusCode(context, output, 400 /* Bad request */);
1212 return;
1213 }
1214
1215 std::string orthancId, studyInstanceUid, seriesInstanceUid, sopInstanceUid;
1216 OrthancPlugins::MemoryBuffer content;
1217 if (LocateInstance(output, orthancId, studyInstanceUid, seriesInstanceUid, sopInstanceUid, request) &&
1218 content.RestApiGet("/instances/" + orthancId + "/file", false))
1219 {
1220 std::string bulk(request->groups[3]);
1221
1222 std::vector<std::string> path;
1223 Orthanc::Toolbox::TokenizeString(path, bulk, '/');
1224
1225 // Map the bulk data URI to the Orthanc "/instances/.../content/..." built-in URI
1226 std::string orthanc = "/instances/" + orthancId + "/content";
1227
1228 Orthanc::DicomTag tmp(0, 0);
1229
1230 if (path.size() == 1 &&
1231 Orthanc::DicomTag::ParseHexadecimal(tmp, path[0].c_str()) &&
1232 tmp == Orthanc::DICOM_TAG_PIXEL_DATA)
1233 {
1234 // Accessing pixel data: Return the raw content of the fragments in a multipart stream.
1235 // TODO - Is this how DICOMweb should work?
1236 orthanc += "/" + Orthanc::DICOM_TAG_PIXEL_DATA.Format();
1237
1238 Json::Value frames;
1239 if (OrthancPlugins::RestApiGet(frames, orthanc, false))
1240 {
1241 if (frames.type() != Json::arrayValue ||
1242 OrthancPluginStartMultipartAnswer(context, output, "related", "application/octet-stream") != 0)
1243 {
1244 throw Orthanc::OrthancException(Orthanc::ErrorCode_Plugin);
1245 }
1246
1247 for (Json::Value::ArrayIndex i = 0; i < frames.size(); i++)
1248 {
1249 std::string frame;
1250
1251 if (frames[i].type() != Json::stringValue ||
1252 !OrthancPlugins::RestApiGetString(frame, orthanc + "/" + frames[i].asString(), false) ||
1253 OrthancPluginSendMultipartItem(context, output, frame.c_str(), frame.size()) != 0)
1254 {
1255 throw Orthanc::OrthancException(Orthanc::ErrorCode_Plugin);
1256 }
1257 }
1258 }
1259 else
1260 {
1261 throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem);
1262 }
1263 }
1264 else
1265 {
1266 if (path.size() % 2 != 1)
1267 {
1268 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest,
1269 "Bulk data URI in WADO-RS should have an odd number of items: " + bulk);
1270 }
1271
1272 for (size_t i = 0; i < path.size() / 2; i++)
1273 {
1274 int index;
1275
1276 try
1277 {
1278 index = boost::lexical_cast<int>(path[2 * i + 1]);
1279 }
1280 catch (boost::bad_lexical_cast&)
1281 {
1282 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest,
1283 "Bad sequence index in bulk data URI: " + bulk);
1284 }
1285
1286 orthanc += "/" + path[2 * i] + "/" + boost::lexical_cast<std::string>(index - 1);
1287 }
1288
1289 orthanc += "/" + path.back();
1290
1291 std::string result;
1292 if (OrthancPlugins::RestApiGetString(result, orthanc, false))
1293 {
1294 if (OrthancPluginStartMultipartAnswer(context, output, "related", "application/octet-stream") != 0 ||
1295 OrthancPluginSendMultipartItem(context, output, result.c_str(), result.size()) != 0)
1296 {
1297 throw Orthanc::OrthancException(Orthanc::ErrorCode_Plugin);
1298 }
1299 }
1300 else
1301 {
1302 throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem);
1303 }
1304 }
1305 }
1306 }
1307