comparison OrthancServer/Sources/OrthancRestApi/OrthancRestResources.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 OrthancServer/OrthancRestApi/OrthancRestResources.cpp@d86bddb50972
children 05b8fd21089c
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 "../PrecompiledHeadersServer.h"
35 #include "OrthancRestApi.h"
36
37 #include "../../Core/Compression/GzipCompressor.h"
38 #include "../../Core/DicomFormat/DicomImageInformation.h"
39 #include "../../Core/DicomParsing/DicomWebJsonVisitor.h"
40 #include "../../Core/DicomParsing/FromDcmtkBridge.h"
41 #include "../../Core/DicomParsing/Internals/DicomImageDecoder.h"
42 #include "../../Core/HttpServer/HttpContentNegociation.h"
43 #include "../../Core/Images/Image.h"
44 #include "../../Core/Images/ImageProcessing.h"
45 #include "../../Core/Logging.h"
46 #include "../../Core/MultiThreading/Semaphore.h"
47 #include "../OrthancConfiguration.h"
48 #include "../Search/DatabaseLookup.h"
49 #include "../ServerContext.h"
50 #include "../ServerToolbox.h"
51 #include "../SliceOrdering.h"
52
53 #include "../../Plugins/Engine/OrthancPlugins.h"
54
55 // This "include" is mandatory for Release builds using Linux Standard Base
56 #include <boost/math/special_functions/round.hpp>
57
58
59 /**
60 * This semaphore is used to limit the number of concurrent HTTP
61 * requests on CPU-intensive routes of the REST API, in order to
62 * prevent exhaustion of resources (new in Orthanc 1.7.0).
63 **/
64 static Orthanc::Semaphore throttlingSemaphore_(4); // TODO => PARAMETER?
65
66
67 namespace Orthanc
68 {
69 static void AnswerDicomAsJson(RestApiCall& call,
70 const Json::Value& dicom,
71 DicomToJsonFormat mode)
72 {
73 if (mode != DicomToJsonFormat_Full)
74 {
75 Json::Value simplified;
76 ServerToolbox::SimplifyTags(simplified, dicom, mode);
77 call.GetOutput().AnswerJson(simplified);
78 }
79 else
80 {
81 call.GetOutput().AnswerJson(dicom);
82 }
83 }
84
85
86 static DicomToJsonFormat GetDicomFormat(const RestApiGetCall& call)
87 {
88 if (call.HasArgument("simplify"))
89 {
90 return DicomToJsonFormat_Human;
91 }
92 else if (call.HasArgument("short"))
93 {
94 return DicomToJsonFormat_Short;
95 }
96 else
97 {
98 return DicomToJsonFormat_Full;
99 }
100 }
101
102
103 static void AnswerDicomAsJson(RestApiGetCall& call,
104 const Json::Value& dicom)
105 {
106 AnswerDicomAsJson(call, dicom, GetDicomFormat(call));
107 }
108
109
110 static void ParseSetOfTags(std::set<DicomTag>& target,
111 const RestApiGetCall& call,
112 const std::string& argument)
113 {
114 target.clear();
115
116 if (call.HasArgument(argument))
117 {
118 std::vector<std::string> tags;
119 Toolbox::TokenizeString(tags, call.GetArgument(argument, ""), ',');
120
121 for (size_t i = 0; i < tags.size(); i++)
122 {
123 target.insert(FromDcmtkBridge::ParseTag(tags[i]));
124 }
125 }
126 }
127
128
129 // List all the patients, studies, series or instances ----------------------
130
131 static void AnswerListOfResources(RestApiOutput& output,
132 ServerIndex& index,
133 const std::list<std::string>& resources,
134 ResourceType level,
135 bool expand)
136 {
137 Json::Value answer = Json::arrayValue;
138
139 for (std::list<std::string>::const_iterator
140 resource = resources.begin(); resource != resources.end(); ++resource)
141 {
142 if (expand)
143 {
144 Json::Value item;
145 if (index.LookupResource(item, *resource, level))
146 {
147 answer.append(item);
148 }
149 }
150 else
151 {
152 answer.append(*resource);
153 }
154 }
155
156 output.AnswerJson(answer);
157 }
158
159
160 template <enum ResourceType resourceType>
161 static void ListResources(RestApiGetCall& call)
162 {
163 ServerIndex& index = OrthancRestApi::GetIndex(call);
164
165 std::list<std::string> result;
166
167 if (call.HasArgument("limit") ||
168 call.HasArgument("since"))
169 {
170 if (!call.HasArgument("limit"))
171 {
172 throw OrthancException(ErrorCode_BadRequest,
173 "Missing \"limit\" argument for GET request against: " +
174 call.FlattenUri());
175 }
176
177 if (!call.HasArgument("since"))
178 {
179 throw OrthancException(ErrorCode_BadRequest,
180 "Missing \"since\" argument for GET request against: " +
181 call.FlattenUri());
182 }
183
184 size_t since = boost::lexical_cast<size_t>(call.GetArgument("since", ""));
185 size_t limit = boost::lexical_cast<size_t>(call.GetArgument("limit", ""));
186 index.GetAllUuids(result, resourceType, since, limit);
187 }
188 else
189 {
190 index.GetAllUuids(result, resourceType);
191 }
192
193
194 AnswerListOfResources(call.GetOutput(), index, result, resourceType, call.HasArgument("expand"));
195 }
196
197 template <enum ResourceType resourceType>
198 static void GetSingleResource(RestApiGetCall& call)
199 {
200 Json::Value result;
201 if (OrthancRestApi::GetIndex(call).LookupResource(result, call.GetUriComponent("id", ""), resourceType))
202 {
203 call.GetOutput().AnswerJson(result);
204 }
205 }
206
207 template <enum ResourceType resourceType>
208 static void DeleteSingleResource(RestApiDeleteCall& call)
209 {
210 Json::Value result;
211 if (OrthancRestApi::GetContext(call).DeleteResource(result, call.GetUriComponent("id", ""), resourceType))
212 {
213 call.GetOutput().AnswerJson(result);
214 }
215 }
216
217
218 // Get information about a single patient -----------------------------------
219
220 static void IsProtectedPatient(RestApiGetCall& call)
221 {
222 std::string publicId = call.GetUriComponent("id", "");
223 bool isProtected = OrthancRestApi::GetIndex(call).IsProtectedPatient(publicId);
224 call.GetOutput().AnswerBuffer(isProtected ? "1" : "0", MimeType_PlainText);
225 }
226
227
228 static void SetPatientProtection(RestApiPutCall& call)
229 {
230 ServerContext& context = OrthancRestApi::GetContext(call);
231
232 std::string publicId = call.GetUriComponent("id", "");
233
234 std::string body;
235 call.BodyToString(body);
236 body = Toolbox::StripSpaces(body);
237
238 if (body == "0")
239 {
240 context.GetIndex().SetProtectedPatient(publicId, false);
241 call.GetOutput().AnswerBuffer("", MimeType_PlainText);
242 }
243 else if (body == "1")
244 {
245 context.GetIndex().SetProtectedPatient(publicId, true);
246 call.GetOutput().AnswerBuffer("", MimeType_PlainText);
247 }
248 else
249 {
250 // Bad request
251 }
252 }
253
254
255 // Get information about a single instance ----------------------------------
256
257 static void GetInstanceFile(RestApiGetCall& call)
258 {
259 ServerContext& context = OrthancRestApi::GetContext(call);
260
261 std::string publicId = call.GetUriComponent("id", "");
262
263 IHttpHandler::Arguments::const_iterator accept = call.GetHttpHeaders().find("accept");
264 if (accept != call.GetHttpHeaders().end())
265 {
266 // New in Orthanc 1.5.4
267 try
268 {
269 MimeType mime = StringToMimeType(accept->second.c_str());
270
271 if (mime == MimeType_DicomWebJson ||
272 mime == MimeType_DicomWebXml)
273 {
274 DicomWebJsonVisitor visitor;
275
276 {
277 ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), publicId);
278 locker.GetDicom().Apply(visitor);
279 }
280
281 if (mime == MimeType_DicomWebJson)
282 {
283 std::string s = visitor.GetResult().toStyledString();
284 call.GetOutput().AnswerBuffer(s, MimeType_DicomWebJson);
285 }
286 else
287 {
288 std::string xml;
289 visitor.FormatXml(xml);
290 call.GetOutput().AnswerBuffer(xml, MimeType_DicomWebXml);
291 }
292
293 return;
294 }
295 }
296 catch (OrthancException&)
297 {
298 }
299 }
300
301 context.AnswerAttachment(call.GetOutput(), publicId, FileContentType_Dicom);
302 }
303
304
305 static void ExportInstanceFile(RestApiPostCall& call)
306 {
307 ServerContext& context = OrthancRestApi::GetContext(call);
308
309 std::string publicId = call.GetUriComponent("id", "");
310
311 std::string dicom;
312 context.ReadDicom(dicom, publicId);
313
314 std::string target;
315 call.BodyToString(target);
316 SystemToolbox::WriteFile(dicom, target);
317
318 call.GetOutput().AnswerBuffer("{}", MimeType_Json);
319 }
320
321
322 template <DicomToJsonFormat format>
323 static void GetInstanceTags(RestApiGetCall& call)
324 {
325 ServerContext& context = OrthancRestApi::GetContext(call);
326
327 std::string publicId = call.GetUriComponent("id", "");
328
329 std::set<DicomTag> ignoreTagLength;
330 ParseSetOfTags(ignoreTagLength, call, "ignore-length");
331
332 if (format != DicomToJsonFormat_Full ||
333 !ignoreTagLength.empty())
334 {
335 Json::Value full;
336 context.ReadDicomAsJson(full, publicId, ignoreTagLength);
337 AnswerDicomAsJson(call, full, format);
338 }
339 else
340 {
341 // This path allows one to avoid the JSON decoding if no
342 // simplification is asked, and if no "ignore-length" argument
343 // is present
344 std::string full;
345 context.ReadDicomAsJson(full, publicId);
346 call.GetOutput().AnswerBuffer(full, MimeType_Json);
347 }
348 }
349
350
351 static void GetInstanceTagsBis(RestApiGetCall& call)
352 {
353 switch (GetDicomFormat(call))
354 {
355 case DicomToJsonFormat_Human:
356 GetInstanceTags<DicomToJsonFormat_Human>(call);
357 break;
358
359 case DicomToJsonFormat_Short:
360 GetInstanceTags<DicomToJsonFormat_Short>(call);
361 break;
362
363 case DicomToJsonFormat_Full:
364 GetInstanceTags<DicomToJsonFormat_Full>(call);
365 break;
366
367 default:
368 throw OrthancException(ErrorCode_InternalError);
369 }
370 }
371
372
373 static void ListFrames(RestApiGetCall& call)
374 {
375 std::string publicId = call.GetUriComponent("id", "");
376
377 unsigned int numberOfFrames;
378
379 {
380 ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), publicId);
381 numberOfFrames = locker.GetDicom().GetFramesCount();
382 }
383
384 Json::Value result = Json::arrayValue;
385 for (unsigned int i = 0; i < numberOfFrames; i++)
386 {
387 result.append(i);
388 }
389
390 call.GetOutput().AnswerJson(result);
391 }
392
393
394 namespace
395 {
396 class ImageToEncode
397 {
398 private:
399 std::unique_ptr<ImageAccessor>& image_;
400 ImageExtractionMode mode_;
401 bool invert_;
402 MimeType format_;
403 std::string answer_;
404
405 public:
406 ImageToEncode(std::unique_ptr<ImageAccessor>& image,
407 ImageExtractionMode mode,
408 bool invert) :
409 image_(image),
410 mode_(mode),
411 invert_(invert)
412 {
413 }
414
415 void Answer(RestApiOutput& output)
416 {
417 output.AnswerBuffer(answer_, format_);
418 }
419
420 void EncodeUsingPng()
421 {
422 format_ = MimeType_Png;
423 DicomImageDecoder::ExtractPngImage(answer_, image_, mode_, invert_);
424 }
425
426 void EncodeUsingPam()
427 {
428 format_ = MimeType_Pam;
429 DicomImageDecoder::ExtractPamImage(answer_, image_, mode_, invert_);
430 }
431
432 void EncodeUsingJpeg(uint8_t quality)
433 {
434 format_ = MimeType_Jpeg;
435 DicomImageDecoder::ExtractJpegImage(answer_, image_, mode_, invert_, quality);
436 }
437 };
438
439 class EncodePng : public HttpContentNegociation::IHandler
440 {
441 private:
442 ImageToEncode& image_;
443
444 public:
445 EncodePng(ImageToEncode& image) : image_(image)
446 {
447 }
448
449 virtual void Handle(const std::string& type,
450 const std::string& subtype)
451 {
452 assert(type == "image");
453 assert(subtype == "png");
454 image_.EncodeUsingPng();
455 }
456 };
457
458 class EncodePam : public HttpContentNegociation::IHandler
459 {
460 private:
461 ImageToEncode& image_;
462
463 public:
464 EncodePam(ImageToEncode& image) : image_(image)
465 {
466 }
467
468 virtual void Handle(const std::string& type,
469 const std::string& subtype)
470 {
471 assert(type == "image");
472 assert(subtype == "x-portable-arbitrarymap");
473 image_.EncodeUsingPam();
474 }
475 };
476
477 class EncodeJpeg : public HttpContentNegociation::IHandler
478 {
479 private:
480 ImageToEncode& image_;
481 unsigned int quality_;
482
483 public:
484 EncodeJpeg(ImageToEncode& image,
485 const RestApiGetCall& call) :
486 image_(image)
487 {
488 std::string v = call.GetArgument("quality", "90" /* default JPEG quality */);
489 bool ok = false;
490
491 try
492 {
493 quality_ = boost::lexical_cast<unsigned int>(v);
494 ok = (quality_ >= 1 && quality_ <= 100);
495 }
496 catch (boost::bad_lexical_cast&)
497 {
498 }
499
500 if (!ok)
501 {
502 throw OrthancException(
503 ErrorCode_BadRequest,
504 "Bad quality for a JPEG encoding (must be a number between 0 and 100): " + v);
505 }
506 }
507
508 virtual void Handle(const std::string& type,
509 const std::string& subtype)
510 {
511 assert(type == "image");
512 assert(subtype == "jpeg");
513 image_.EncodeUsingJpeg(quality_);
514 }
515 };
516 }
517
518
519 namespace
520 {
521 class IDecodedFrameHandler : public boost::noncopyable
522 {
523 public:
524 virtual ~IDecodedFrameHandler()
525 {
526 }
527
528 virtual void Handle(RestApiGetCall& call,
529 std::unique_ptr<ImageAccessor>& decoded,
530 const DicomMap& dicom) = 0;
531
532 virtual bool RequiresDicomTags() const = 0;
533
534 static void Apply(RestApiGetCall& call,
535 IDecodedFrameHandler& handler)
536 {
537 ServerContext& context = OrthancRestApi::GetContext(call);
538
539 std::string frameId = call.GetUriComponent("frame", "0");
540
541 unsigned int frame;
542 try
543 {
544 frame = boost::lexical_cast<unsigned int>(frameId);
545 }
546 catch (boost::bad_lexical_cast&)
547 {
548 return;
549 }
550
551 DicomMap dicom;
552 std::unique_ptr<ImageAccessor> decoded;
553
554 try
555 {
556 std::string publicId = call.GetUriComponent("id", "");
557
558 decoded.reset(context.DecodeDicomFrame(publicId, frame));
559
560 if (decoded.get() == NULL)
561 {
562 throw OrthancException(ErrorCode_NotImplemented,
563 "Cannot decode DICOM instance with ID: " + publicId);
564 }
565
566 if (handler.RequiresDicomTags())
567 {
568 /**
569 * Retrieve a summary of the DICOM tags, which is
570 * necessary to deal with MONOCHROME1 photometric
571 * interpretation, and with windowing parameters.
572 **/
573 ServerContext::DicomCacheLocker locker(context, publicId);
574 locker.GetDicom().ExtractDicomSummary(dicom);
575 }
576 }
577 catch (OrthancException& e)
578 {
579 if (e.GetErrorCode() == ErrorCode_ParameterOutOfRange ||
580 e.GetErrorCode() == ErrorCode_UnknownResource)
581 {
582 // The frame number is out of the range for this DICOM
583 // instance, the resource is not existent
584 }
585 else
586 {
587 std::string root = "";
588 for (size_t i = 1; i < call.GetFullUri().size(); i++)
589 {
590 root += "../";
591 }
592
593 call.GetOutput().Redirect(root + "app/images/unsupported.png");
594 }
595 return;
596 }
597
598 handler.Handle(call, decoded, dicom);
599 }
600
601
602 static void DefaultHandler(RestApiGetCall& call,
603 std::unique_ptr<ImageAccessor>& decoded,
604 ImageExtractionMode mode,
605 bool invert)
606 {
607 ImageToEncode image(decoded, mode, invert);
608
609 HttpContentNegociation negociation;
610 EncodePng png(image);
611 negociation.Register(MIME_PNG, png);
612
613 EncodeJpeg jpeg(image, call);
614 negociation.Register(MIME_JPEG, jpeg);
615
616 EncodePam pam(image);
617 negociation.Register(MIME_PAM, pam);
618
619 if (negociation.Apply(call.GetHttpHeaders()))
620 {
621 image.Answer(call.GetOutput());
622 }
623 }
624 };
625
626
627 class GetImageHandler : public IDecodedFrameHandler
628 {
629 private:
630 ImageExtractionMode mode_;
631
632 public:
633 GetImageHandler(ImageExtractionMode mode) :
634 mode_(mode)
635 {
636 }
637
638 virtual void Handle(RestApiGetCall& call,
639 std::unique_ptr<ImageAccessor>& decoded,
640 const DicomMap& dicom) ORTHANC_OVERRIDE
641 {
642 bool invert = false;
643
644 if (mode_ == ImageExtractionMode_Preview)
645 {
646 DicomImageInformation info(dicom);
647 invert = (info.GetPhotometricInterpretation() == PhotometricInterpretation_Monochrome1);
648 }
649
650 DefaultHandler(call, decoded, mode_, invert);
651 }
652
653 virtual bool RequiresDicomTags() const ORTHANC_OVERRIDE
654 {
655 return mode_ == ImageExtractionMode_Preview;
656 }
657 };
658
659
660 class RenderedFrameHandler : public IDecodedFrameHandler
661 {
662 private:
663 static void GetDicomParameters(bool& invert,
664 float& rescaleSlope,
665 float& rescaleIntercept,
666 float& windowWidth,
667 float& windowCenter,
668 const DicomMap& dicom)
669 {
670 DicomImageInformation info(dicom);
671
672 invert = (info.GetPhotometricInterpretation() == PhotometricInterpretation_Monochrome1);
673
674 rescaleSlope = 1.0f;
675 rescaleIntercept = 0.0f;
676
677 if (dicom.HasTag(Orthanc::DICOM_TAG_RESCALE_SLOPE) &&
678 dicom.HasTag(Orthanc::DICOM_TAG_RESCALE_INTERCEPT))
679 {
680 dicom.ParseFloat(rescaleSlope, Orthanc::DICOM_TAG_RESCALE_SLOPE);
681 dicom.ParseFloat(rescaleIntercept, Orthanc::DICOM_TAG_RESCALE_INTERCEPT);
682 }
683
684 windowWidth = static_cast<float>(1 << info.GetBitsStored()) * rescaleSlope;
685 windowCenter = windowWidth / 2.0f + rescaleIntercept;
686
687 if (dicom.HasTag(Orthanc::DICOM_TAG_WINDOW_CENTER) &&
688 dicom.HasTag(Orthanc::DICOM_TAG_WINDOW_WIDTH))
689 {
690 dicom.ParseFirstFloat(windowCenter, Orthanc::DICOM_TAG_WINDOW_CENTER);
691 dicom.ParseFirstFloat(windowWidth, Orthanc::DICOM_TAG_WINDOW_WIDTH);
692 }
693 }
694
695 static void GetUserArguments(float& windowWidth /* inout */,
696 float& windowCenter /* inout */,
697 unsigned int& argWidth,
698 unsigned int& argHeight,
699 bool& smooth,
700 RestApiGetCall& call)
701 {
702 static const char* ARG_WINDOW_CENTER = "window-center";
703 static const char* ARG_WINDOW_WIDTH = "window-width";
704 static const char* ARG_WIDTH = "width";
705 static const char* ARG_HEIGHT = "height";
706 static const char* ARG_SMOOTH = "smooth";
707
708 if (call.HasArgument(ARG_WINDOW_WIDTH))
709 {
710 try
711 {
712 windowWidth = boost::lexical_cast<float>(call.GetArgument(ARG_WINDOW_WIDTH, ""));
713 }
714 catch (boost::bad_lexical_cast&)
715 {
716 throw OrthancException(ErrorCode_ParameterOutOfRange,
717 "Bad value for argument: " + std::string(ARG_WINDOW_WIDTH));
718 }
719 }
720
721 if (call.HasArgument(ARG_WINDOW_CENTER))
722 {
723 try
724 {
725 windowCenter = boost::lexical_cast<float>(call.GetArgument(ARG_WINDOW_CENTER, ""));
726 }
727 catch (boost::bad_lexical_cast&)
728 {
729 throw OrthancException(ErrorCode_ParameterOutOfRange,
730 "Bad value for argument: " + std::string(ARG_WINDOW_CENTER));
731 }
732 }
733
734 argWidth = 0;
735 argHeight = 0;
736
737 if (call.HasArgument(ARG_WIDTH))
738 {
739 try
740 {
741 int tmp = boost::lexical_cast<int>(call.GetArgument(ARG_WIDTH, ""));
742 if (tmp < 0)
743 {
744 throw OrthancException(ErrorCode_ParameterOutOfRange,
745 "Argument cannot be negative: " + std::string(ARG_WIDTH));
746 }
747 else
748 {
749 argWidth = static_cast<unsigned int>(tmp);
750 }
751 }
752 catch (boost::bad_lexical_cast&)
753 {
754 throw OrthancException(ErrorCode_ParameterOutOfRange,
755 "Bad value for argument: " + std::string(ARG_WIDTH));
756 }
757 }
758
759 if (call.HasArgument(ARG_HEIGHT))
760 {
761 try
762 {
763 int tmp = boost::lexical_cast<int>(call.GetArgument(ARG_HEIGHT, ""));
764 if (tmp < 0)
765 {
766 throw OrthancException(ErrorCode_ParameterOutOfRange,
767 "Argument cannot be negative: " + std::string(ARG_HEIGHT));
768 }
769 else
770 {
771 argHeight = static_cast<unsigned int>(tmp);
772 }
773 }
774 catch (boost::bad_lexical_cast&)
775 {
776 throw OrthancException(ErrorCode_ParameterOutOfRange,
777 "Bad value for argument: " + std::string(ARG_HEIGHT));
778 }
779 }
780
781 smooth = false;
782
783 if (call.HasArgument(ARG_SMOOTH))
784 {
785 std::string value = call.GetArgument(ARG_SMOOTH, "");
786 if (value == "0" ||
787 value == "false")
788 {
789 smooth = false;
790 }
791 else if (value == "1" ||
792 value == "true")
793 {
794 smooth = true;
795 }
796 else
797 {
798 throw OrthancException(ErrorCode_ParameterOutOfRange,
799 "Argument must be Boolean: " + std::string(ARG_SMOOTH));
800 }
801 }
802 }
803
804
805 public:
806 virtual void Handle(RestApiGetCall& call,
807 std::unique_ptr<ImageAccessor>& decoded,
808 const DicomMap& dicom) ORTHANC_OVERRIDE
809 {
810 bool invert;
811 float rescaleSlope, rescaleIntercept, windowWidth, windowCenter;
812 GetDicomParameters(invert, rescaleSlope, rescaleIntercept, windowWidth, windowCenter, dicom);
813
814 unsigned int argWidth, argHeight;
815 bool smooth;
816 GetUserArguments(windowWidth, windowCenter, argWidth, argHeight, smooth, call);
817
818 unsigned int targetWidth = decoded->GetWidth();
819 unsigned int targetHeight = decoded->GetHeight();
820
821 if (decoded->GetWidth() != 0 &&
822 decoded->GetHeight() != 0)
823 {
824 float ratio = 1;
825
826 if (argWidth != 0 &&
827 argHeight != 0)
828 {
829 float ratioX = static_cast<float>(argWidth) / static_cast<float>(decoded->GetWidth());
830 float ratioY = static_cast<float>(argHeight) / static_cast<float>(decoded->GetHeight());
831 ratio = std::min(ratioX, ratioY);
832 }
833 else if (argWidth != 0)
834 {
835 ratio = static_cast<float>(argWidth) / static_cast<float>(decoded->GetWidth());
836 }
837 else if (argHeight != 0)
838 {
839 ratio = static_cast<float>(argHeight) / static_cast<float>(decoded->GetHeight());
840 }
841
842 targetWidth = boost::math::iround(ratio * static_cast<float>(decoded->GetWidth()));
843 targetHeight = boost::math::iround(ratio * static_cast<float>(decoded->GetHeight()));
844 }
845
846 if (decoded->GetFormat() == PixelFormat_RGB24)
847 {
848 if (targetWidth == decoded->GetWidth() &&
849 targetHeight == decoded->GetHeight())
850 {
851 DefaultHandler(call, decoded, ImageExtractionMode_Preview, false);
852 }
853 else
854 {
855 std::unique_ptr<ImageAccessor> resized(
856 new Image(decoded->GetFormat(), targetWidth, targetHeight, false));
857
858 if (smooth &&
859 (targetWidth < decoded->GetWidth() ||
860 targetHeight < decoded->GetHeight()))
861 {
862 ImageProcessing::SmoothGaussian5x5(*decoded);
863 }
864
865 ImageProcessing::Resize(*resized, *decoded);
866 DefaultHandler(call, resized, ImageExtractionMode_Preview, false);
867 }
868 }
869 else
870 {
871 // Grayscale image: (1) convert to Float32, (2) apply
872 // windowing to get a Grayscale8, (3) possibly resize
873
874 Image converted(PixelFormat_Float32, decoded->GetWidth(), decoded->GetHeight(), false);
875 ImageProcessing::Convert(converted, *decoded);
876
877 // Avoid divisions by zero
878 if (windowWidth <= 1.0f)
879 {
880 windowWidth = 1;
881 }
882
883 if (std::abs(rescaleSlope) <= 0.1f)
884 {
885 rescaleSlope = 0.1f;
886 }
887
888 const float scaling = 255.0f * rescaleSlope / windowWidth;
889 const float offset = (rescaleIntercept - windowCenter + windowWidth / 2.0f) / rescaleSlope;
890
891 std::unique_ptr<ImageAccessor> rescaled(new Image(PixelFormat_Grayscale8, decoded->GetWidth(), decoded->GetHeight(), false));
892 ImageProcessing::ShiftScale(*rescaled, converted, offset, scaling, false);
893
894 if (targetWidth == decoded->GetWidth() &&
895 targetHeight == decoded->GetHeight())
896 {
897 DefaultHandler(call, rescaled, ImageExtractionMode_UInt8, invert);
898 }
899 else
900 {
901 std::unique_ptr<ImageAccessor> resized(
902 new Image(PixelFormat_Grayscale8, targetWidth, targetHeight, false));
903
904 if (smooth &&
905 (targetWidth < decoded->GetWidth() ||
906 targetHeight < decoded->GetHeight()))
907 {
908 ImageProcessing::SmoothGaussian5x5(*rescaled);
909 }
910
911 ImageProcessing::Resize(*resized, *rescaled);
912 DefaultHandler(call, resized, ImageExtractionMode_UInt8, invert);
913 }
914 }
915 }
916
917 virtual bool RequiresDicomTags() const ORTHANC_OVERRIDE
918 {
919 return true;
920 }
921 };
922 }
923
924
925 template <enum ImageExtractionMode mode>
926 static void GetImage(RestApiGetCall& call)
927 {
928 Semaphore::Locker locker(throttlingSemaphore_);
929
930 GetImageHandler handler(mode);
931 IDecodedFrameHandler::Apply(call, handler);
932 }
933
934
935 static void GetRenderedFrame(RestApiGetCall& call)
936 {
937 Semaphore::Locker locker(throttlingSemaphore_);
938
939 RenderedFrameHandler handler;
940 IDecodedFrameHandler::Apply(call, handler);
941 }
942
943
944 static void GetMatlabImage(RestApiGetCall& call)
945 {
946 Semaphore::Locker locker(throttlingSemaphore_);
947
948 ServerContext& context = OrthancRestApi::GetContext(call);
949
950 std::string frameId = call.GetUriComponent("frame", "0");
951
952 unsigned int frame;
953 try
954 {
955 frame = boost::lexical_cast<unsigned int>(frameId);
956 }
957 catch (boost::bad_lexical_cast&)
958 {
959 return;
960 }
961
962 std::string publicId = call.GetUriComponent("id", "");
963 std::unique_ptr<ImageAccessor> decoded(context.DecodeDicomFrame(publicId, frame));
964
965 if (decoded.get() == NULL)
966 {
967 throw OrthancException(ErrorCode_NotImplemented,
968 "Cannot decode DICOM instance with ID: " + publicId);
969 }
970 else
971 {
972 std::string result;
973 decoded->ToMatlabString(result);
974 call.GetOutput().AnswerBuffer(result, MimeType_PlainText);
975 }
976 }
977
978
979 template <bool GzipCompression>
980 static void GetRawFrame(RestApiGetCall& call)
981 {
982 std::string frameId = call.GetUriComponent("frame", "0");
983
984 unsigned int frame;
985 try
986 {
987 frame = boost::lexical_cast<unsigned int>(frameId);
988 }
989 catch (boost::bad_lexical_cast&)
990 {
991 return;
992 }
993
994 std::string publicId = call.GetUriComponent("id", "");
995 std::string raw;
996 MimeType mime;
997
998 {
999 ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), publicId);
1000 locker.GetDicom().GetRawFrame(raw, mime, frame);
1001 }
1002
1003 if (GzipCompression)
1004 {
1005 GzipCompressor gzip;
1006 std::string compressed;
1007 gzip.Compress(compressed, raw.empty() ? NULL : raw.c_str(), raw.size());
1008 call.GetOutput().AnswerBuffer(compressed, MimeType_Gzip);
1009 }
1010 else
1011 {
1012 call.GetOutput().AnswerBuffer(raw, mime);
1013 }
1014 }
1015
1016
1017 static void GetResourceStatistics(RestApiGetCall& call)
1018 {
1019 static const uint64_t MEGA_BYTES = 1024 * 1024;
1020
1021 std::string publicId = call.GetUriComponent("id", "");
1022
1023 ResourceType type;
1024 uint64_t diskSize, uncompressedSize, dicomDiskSize, dicomUncompressedSize;
1025 unsigned int countStudies, countSeries, countInstances;
1026 OrthancRestApi::GetIndex(call).GetResourceStatistics(
1027 type, diskSize, uncompressedSize, countStudies, countSeries,
1028 countInstances, dicomDiskSize, dicomUncompressedSize, publicId);
1029
1030 Json::Value result = Json::objectValue;
1031 result["DiskSize"] = boost::lexical_cast<std::string>(diskSize);
1032 result["DiskSizeMB"] = static_cast<unsigned int>(diskSize / MEGA_BYTES);
1033 result["UncompressedSize"] = boost::lexical_cast<std::string>(uncompressedSize);
1034 result["UncompressedSizeMB"] = static_cast<unsigned int>(uncompressedSize / MEGA_BYTES);
1035
1036 result["DicomDiskSize"] = boost::lexical_cast<std::string>(dicomDiskSize);
1037 result["DicomDiskSizeMB"] = static_cast<unsigned int>(dicomDiskSize / MEGA_BYTES);
1038 result["DicomUncompressedSize"] = boost::lexical_cast<std::string>(dicomUncompressedSize);
1039 result["DicomUncompressedSizeMB"] = static_cast<unsigned int>(dicomUncompressedSize / MEGA_BYTES);
1040
1041 switch (type)
1042 {
1043 // Do NOT add "break" below this point!
1044 case ResourceType_Patient:
1045 result["CountStudies"] = countStudies;
1046
1047 case ResourceType_Study:
1048 result["CountSeries"] = countSeries;
1049
1050 case ResourceType_Series:
1051 result["CountInstances"] = countInstances;
1052
1053 case ResourceType_Instance:
1054 default:
1055 break;
1056 }
1057
1058 call.GetOutput().AnswerJson(result);
1059 }
1060
1061
1062
1063 // Handling of metadata -----------------------------------------------------
1064
1065 static void CheckValidResourceType(RestApiCall& call)
1066 {
1067 std::string resourceType = call.GetUriComponent("resourceType", "");
1068 StringToResourceType(resourceType.c_str());
1069 }
1070
1071
1072 static void ListMetadata(RestApiGetCall& call)
1073 {
1074 CheckValidResourceType(call);
1075
1076 std::string publicId = call.GetUriComponent("id", "");
1077 std::map<MetadataType, std::string> metadata;
1078
1079 OrthancRestApi::GetIndex(call).GetAllMetadata(metadata, publicId);
1080
1081 Json::Value result;
1082
1083 if (call.HasArgument("expand"))
1084 {
1085 result = Json::objectValue;
1086
1087 for (std::map<MetadataType, std::string>::const_iterator
1088 it = metadata.begin(); it != metadata.end(); ++it)
1089 {
1090 std::string key = EnumerationToString(it->first);
1091 result[key] = it->second;
1092 }
1093 }
1094 else
1095 {
1096 result = Json::arrayValue;
1097
1098 for (std::map<MetadataType, std::string>::const_iterator
1099 it = metadata.begin(); it != metadata.end(); ++it)
1100 {
1101 result.append(EnumerationToString(it->first));
1102 }
1103 }
1104
1105 call.GetOutput().AnswerJson(result);
1106 }
1107
1108
1109 static void GetMetadata(RestApiGetCall& call)
1110 {
1111 CheckValidResourceType(call);
1112
1113 std::string publicId = call.GetUriComponent("id", "");
1114 std::string name = call.GetUriComponent("name", "");
1115 MetadataType metadata = StringToMetadata(name);
1116
1117 std::string value;
1118 if (OrthancRestApi::GetIndex(call).LookupMetadata(value, publicId, metadata))
1119 {
1120 call.GetOutput().AnswerBuffer(value, MimeType_PlainText);
1121 }
1122 }
1123
1124
1125 static void DeleteMetadata(RestApiDeleteCall& call)
1126 {
1127 CheckValidResourceType(call);
1128
1129 std::string publicId = call.GetUriComponent("id", "");
1130 std::string name = call.GetUriComponent("name", "");
1131 MetadataType metadata = StringToMetadata(name);
1132
1133 if (IsUserMetadata(metadata)) // It is forbidden to modify internal metadata
1134 {
1135 OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata);
1136 call.GetOutput().AnswerBuffer("", MimeType_PlainText);
1137 }
1138 else
1139 {
1140 call.GetOutput().SignalError(HttpStatus_403_Forbidden);
1141 }
1142 }
1143
1144
1145 static void SetMetadata(RestApiPutCall& call)
1146 {
1147 CheckValidResourceType(call);
1148
1149 std::string publicId = call.GetUriComponent("id", "");
1150 std::string name = call.GetUriComponent("name", "");
1151 MetadataType metadata = StringToMetadata(name);
1152
1153 std::string value;
1154 call.BodyToString(value);
1155
1156 if (IsUserMetadata(metadata)) // It is forbidden to modify internal metadata
1157 {
1158 // It is forbidden to modify internal metadata
1159 OrthancRestApi::GetIndex(call).SetMetadata(publicId, metadata, value);
1160 call.GetOutput().AnswerBuffer("", MimeType_PlainText);
1161 }
1162 else
1163 {
1164 call.GetOutput().SignalError(HttpStatus_403_Forbidden);
1165 }
1166 }
1167
1168
1169
1170
1171 // Handling of attached files -----------------------------------------------
1172
1173 static void ListAttachments(RestApiGetCall& call)
1174 {
1175 std::string resourceType = call.GetUriComponent("resourceType", "");
1176 std::string publicId = call.GetUriComponent("id", "");
1177 std::list<FileContentType> attachments;
1178 OrthancRestApi::GetIndex(call).ListAvailableAttachments(attachments, publicId, StringToResourceType(resourceType.c_str()));
1179
1180 Json::Value result = Json::arrayValue;
1181
1182 for (std::list<FileContentType>::const_iterator
1183 it = attachments.begin(); it != attachments.end(); ++it)
1184 {
1185 result.append(EnumerationToString(*it));
1186 }
1187
1188 call.GetOutput().AnswerJson(result);
1189 }
1190
1191
1192 static bool GetAttachmentInfo(FileInfo& info, RestApiCall& call)
1193 {
1194 CheckValidResourceType(call);
1195
1196 std::string publicId = call.GetUriComponent("id", "");
1197 std::string name = call.GetUriComponent("name", "");
1198 FileContentType contentType = StringToContentType(name);
1199
1200 return OrthancRestApi::GetIndex(call).LookupAttachment(info, publicId, contentType);
1201 }
1202
1203
1204 static void GetAttachmentOperations(RestApiGetCall& call)
1205 {
1206 FileInfo info;
1207 if (GetAttachmentInfo(info, call))
1208 {
1209 Json::Value operations = Json::arrayValue;
1210
1211 operations.append("compress");
1212 operations.append("compressed-data");
1213
1214 if (info.GetCompressedMD5() != "")
1215 {
1216 operations.append("compressed-md5");
1217 }
1218
1219 operations.append("compressed-size");
1220 operations.append("data");
1221 operations.append("is-compressed");
1222
1223 if (info.GetUncompressedMD5() != "")
1224 {
1225 operations.append("md5");
1226 }
1227
1228 operations.append("size");
1229 operations.append("uncompress");
1230
1231 if (info.GetCompressedMD5() != "" &&
1232 info.GetUncompressedMD5() != "")
1233 {
1234 operations.append("verify-md5");
1235 }
1236
1237 call.GetOutput().AnswerJson(operations);
1238 }
1239 }
1240
1241
1242 template <int uncompress>
1243 static void GetAttachmentData(RestApiGetCall& call)
1244 {
1245 ServerContext& context = OrthancRestApi::GetContext(call);
1246
1247 CheckValidResourceType(call);
1248
1249 std::string publicId = call.GetUriComponent("id", "");
1250 FileContentType type = StringToContentType(call.GetUriComponent("name", ""));
1251
1252 if (uncompress)
1253 {
1254 context.AnswerAttachment(call.GetOutput(), publicId, type);
1255 }
1256 else
1257 {
1258 // Return the raw data (possibly compressed), as stored on the filesystem
1259 std::string content;
1260 context.ReadAttachment(content, publicId, type, false);
1261 call.GetOutput().AnswerBuffer(content, MimeType_Binary);
1262 }
1263 }
1264
1265
1266 static void GetAttachmentSize(RestApiGetCall& call)
1267 {
1268 FileInfo info;
1269 if (GetAttachmentInfo(info, call))
1270 {
1271 call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetUncompressedSize()), MimeType_PlainText);
1272 }
1273 }
1274
1275
1276 static void GetAttachmentCompressedSize(RestApiGetCall& call)
1277 {
1278 FileInfo info;
1279 if (GetAttachmentInfo(info, call))
1280 {
1281 call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetCompressedSize()), MimeType_PlainText);
1282 }
1283 }
1284
1285
1286 static void GetAttachmentMD5(RestApiGetCall& call)
1287 {
1288 FileInfo info;
1289 if (GetAttachmentInfo(info, call) &&
1290 info.GetUncompressedMD5() != "")
1291 {
1292 call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetUncompressedMD5()), MimeType_PlainText);
1293 }
1294 }
1295
1296
1297 static void GetAttachmentCompressedMD5(RestApiGetCall& call)
1298 {
1299 FileInfo info;
1300 if (GetAttachmentInfo(info, call) &&
1301 info.GetCompressedMD5() != "")
1302 {
1303 call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetCompressedMD5()), MimeType_PlainText);
1304 }
1305 }
1306
1307
1308 static void VerifyAttachment(RestApiPostCall& call)
1309 {
1310 ServerContext& context = OrthancRestApi::GetContext(call);
1311 CheckValidResourceType(call);
1312
1313 std::string publicId = call.GetUriComponent("id", "");
1314 std::string name = call.GetUriComponent("name", "");
1315
1316 FileInfo info;
1317 if (!GetAttachmentInfo(info, call) ||
1318 info.GetCompressedMD5() == "" ||
1319 info.GetUncompressedMD5() == "")
1320 {
1321 // Inexistent resource, or no MD5 available
1322 return;
1323 }
1324
1325 bool ok = false;
1326
1327 // First check whether the compressed data is correctly stored in the disk
1328 std::string data;
1329 context.ReadAttachment(data, publicId, StringToContentType(name), false);
1330
1331 std::string actualMD5;
1332 Toolbox::ComputeMD5(actualMD5, data);
1333
1334 if (actualMD5 == info.GetCompressedMD5())
1335 {
1336 // The compressed data is OK. If a compression algorithm was
1337 // applied to it, now check the MD5 of the uncompressed data.
1338 if (info.GetCompressionType() == CompressionType_None)
1339 {
1340 ok = true;
1341 }
1342 else
1343 {
1344 context.ReadAttachment(data, publicId, StringToContentType(name), true);
1345 Toolbox::ComputeMD5(actualMD5, data);
1346 ok = (actualMD5 == info.GetUncompressedMD5());
1347 }
1348 }
1349
1350 if (ok)
1351 {
1352 LOG(INFO) << "The attachment " << name << " of resource " << publicId << " has the right MD5";
1353 call.GetOutput().AnswerBuffer("{}", MimeType_Json);
1354 }
1355 else
1356 {
1357 LOG(INFO) << "The attachment " << name << " of resource " << publicId << " has bad MD5!";
1358 }
1359 }
1360
1361
1362 static void UploadAttachment(RestApiPutCall& call)
1363 {
1364 ServerContext& context = OrthancRestApi::GetContext(call);
1365 CheckValidResourceType(call);
1366
1367 std::string publicId = call.GetUriComponent("id", "");
1368 std::string name = call.GetUriComponent("name", "");
1369
1370 FileContentType contentType = StringToContentType(name);
1371 if (IsUserContentType(contentType) && // It is forbidden to modify internal attachments
1372 context.AddAttachment(publicId, StringToContentType(name), call.GetBodyData(), call.GetBodySize()))
1373 {
1374 call.GetOutput().AnswerBuffer("{}", MimeType_Json);
1375 }
1376 else
1377 {
1378 call.GetOutput().SignalError(HttpStatus_403_Forbidden);
1379 }
1380 }
1381
1382
1383 static void DeleteAttachment(RestApiDeleteCall& call)
1384 {
1385 CheckValidResourceType(call);
1386
1387 std::string publicId = call.GetUriComponent("id", "");
1388 std::string name = call.GetUriComponent("name", "");
1389 FileContentType contentType = StringToContentType(name);
1390
1391 bool allowed;
1392 if (IsUserContentType(contentType))
1393 {
1394 allowed = true;
1395 }
1396 else
1397 {
1398 OrthancConfiguration::ReaderLock lock;
1399
1400 if (lock.GetConfiguration().GetBooleanParameter("StoreDicom", true) &&
1401 contentType == FileContentType_DicomAsJson)
1402 {
1403 allowed = true;
1404 }
1405 else
1406 {
1407 // It is forbidden to delete internal attachments, except for
1408 // the "DICOM as JSON" summary as of Orthanc 1.2.0 (this summary
1409 // would be automatically reconstructed on the next GET call)
1410 allowed = false;
1411 }
1412 }
1413
1414 if (allowed)
1415 {
1416 OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType);
1417 call.GetOutput().AnswerBuffer("{}", MimeType_Json);
1418 }
1419 else
1420 {
1421 call.GetOutput().SignalError(HttpStatus_403_Forbidden);
1422 }
1423 }
1424
1425
1426 template <enum CompressionType compression>
1427 static void ChangeAttachmentCompression(RestApiPostCall& call)
1428 {
1429 CheckValidResourceType(call);
1430
1431 std::string publicId = call.GetUriComponent("id", "");
1432 std::string name = call.GetUriComponent("name", "");
1433 FileContentType contentType = StringToContentType(name);
1434
1435 OrthancRestApi::GetContext(call).ChangeAttachmentCompression(publicId, contentType, compression);
1436 call.GetOutput().AnswerBuffer("{}", MimeType_Json);
1437 }
1438
1439
1440 static void IsAttachmentCompressed(RestApiGetCall& call)
1441 {
1442 FileInfo info;
1443 if (GetAttachmentInfo(info, call))
1444 {
1445 std::string answer = (info.GetCompressionType() == CompressionType_None) ? "0" : "1";
1446 call.GetOutput().AnswerBuffer(answer, MimeType_PlainText);
1447 }
1448 }
1449
1450
1451 // Raw access to the DICOM tags of an instance ------------------------------
1452
1453 static void GetRawContent(RestApiGetCall& call)
1454 {
1455 std::string id = call.GetUriComponent("id", "");
1456
1457 ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), id);
1458
1459 locker.GetDicom().SendPathValue(call.GetOutput(), call.GetTrailingUri());
1460 }
1461
1462
1463
1464 static bool ExtractSharedTags(Json::Value& shared,
1465 ServerContext& context,
1466 const std::string& publicId)
1467 {
1468 // Retrieve all the instances of this patient/study/series
1469 typedef std::list<std::string> Instances;
1470 Instances instances;
1471 context.GetIndex().GetChildInstances(instances, publicId); // (*)
1472
1473 // Loop over the instances
1474 bool isFirst = true;
1475 shared = Json::objectValue;
1476
1477 for (Instances::const_iterator it = instances.begin();
1478 it != instances.end(); ++it)
1479 {
1480 // Get the tags of the current instance, in the simplified format
1481 Json::Value tags;
1482
1483 try
1484 {
1485 context.ReadDicomAsJson(tags, *it);
1486 }
1487 catch (OrthancException&)
1488 {
1489 // Race condition: This instance has been removed since
1490 // (*). Ignore this instance.
1491 continue;
1492 }
1493
1494 if (tags.type() != Json::objectValue)
1495 {
1496 return false; // Error
1497 }
1498
1499 // Only keep the tags that are mapped to a string
1500 Json::Value::Members members = tags.getMemberNames();
1501 for (size_t i = 0; i < members.size(); i++)
1502 {
1503 const Json::Value& tag = tags[members[i]];
1504 if (tag.type() != Json::objectValue ||
1505 tag["Type"].type() != Json::stringValue ||
1506 tag["Type"].asString() != "String")
1507 {
1508 tags.removeMember(members[i]);
1509 }
1510 }
1511
1512 if (isFirst)
1513 {
1514 // This is the first instance, keep its tags as such
1515 shared = tags;
1516 isFirst = false;
1517 }
1518 else
1519 {
1520 // Loop over all the members of the shared tags extracted so
1521 // far. If the value of one of these tags does not match its
1522 // value in the current instance, remove it.
1523 members = shared.getMemberNames();
1524 for (size_t i = 0; i < members.size(); i++)
1525 {
1526 if (!tags.isMember(members[i]) ||
1527 tags[members[i]]["Value"].asString() != shared[members[i]]["Value"].asString())
1528 {
1529 shared.removeMember(members[i]);
1530 }
1531 }
1532 }
1533 }
1534
1535 return true;
1536 }
1537
1538
1539 static void GetSharedTags(RestApiGetCall& call)
1540 {
1541 ServerContext& context = OrthancRestApi::GetContext(call);
1542 std::string publicId = call.GetUriComponent("id", "");
1543
1544 Json::Value sharedTags;
1545 if (ExtractSharedTags(sharedTags, context, publicId))
1546 {
1547 // Success: Send the value of the shared tags
1548 AnswerDicomAsJson(call, sharedTags);
1549 }
1550 }
1551
1552
1553 static void GetModuleInternal(RestApiGetCall& call,
1554 ResourceType resourceType,
1555 DicomModule module)
1556 {
1557 if (!((resourceType == ResourceType_Patient && module == DicomModule_Patient) ||
1558 (resourceType == ResourceType_Study && module == DicomModule_Patient) ||
1559 (resourceType == ResourceType_Study && module == DicomModule_Study) ||
1560 (resourceType == ResourceType_Series && module == DicomModule_Series) ||
1561 (resourceType == ResourceType_Instance && module == DicomModule_Instance) ||
1562 (resourceType == ResourceType_Instance && module == DicomModule_Image)))
1563 {
1564 throw OrthancException(ErrorCode_NotImplemented);
1565 }
1566
1567 ServerContext& context = OrthancRestApi::GetContext(call);
1568 std::string publicId = call.GetUriComponent("id", "");
1569
1570 std::set<DicomTag> ignoreTagLength;
1571 ParseSetOfTags(ignoreTagLength, call, "ignore-length");
1572
1573 typedef std::set<DicomTag> ModuleTags;
1574 ModuleTags moduleTags;
1575 DicomTag::AddTagsForModule(moduleTags, module);
1576
1577 Json::Value tags;
1578
1579 if (resourceType != ResourceType_Instance)
1580 {
1581 // Retrieve all the instances of this patient/study/series
1582 typedef std::list<std::string> Instances;
1583 Instances instances;
1584 context.GetIndex().GetChildInstances(instances, publicId);
1585
1586 if (instances.empty())
1587 {
1588 return; // Error: No instance (should never happen)
1589 }
1590
1591 // Select one child instance
1592 publicId = instances.front();
1593 }
1594
1595 context.ReadDicomAsJson(tags, publicId, ignoreTagLength);
1596
1597 // Filter the tags of the instance according to the module
1598 Json::Value result = Json::objectValue;
1599 for (ModuleTags::const_iterator tag = moduleTags.begin(); tag != moduleTags.end(); ++tag)
1600 {
1601 std::string s = tag->Format();
1602 if (tags.isMember(s))
1603 {
1604 result[s] = tags[s];
1605 }
1606 }
1607
1608 AnswerDicomAsJson(call, result);
1609 }
1610
1611
1612
1613 template <enum ResourceType resourceType,
1614 enum DicomModule module>
1615 static void GetModule(RestApiGetCall& call)
1616 {
1617 GetModuleInternal(call, resourceType, module);
1618 }
1619
1620
1621 namespace
1622 {
1623 typedef std::list< std::pair<ResourceType, std::string> > LookupResults;
1624 }
1625
1626
1627 static void AccumulateLookupResults(LookupResults& result,
1628 ServerIndex& index,
1629 const DicomTag& tag,
1630 const std::string& value,
1631 ResourceType level)
1632 {
1633 std::vector<std::string> tmp;
1634 index.LookupIdentifierExact(tmp, level, tag, value);
1635
1636 for (size_t i = 0; i < tmp.size(); i++)
1637 {
1638 result.push_back(std::make_pair(level, tmp[i]));
1639 }
1640 }
1641
1642
1643 static void Lookup(RestApiPostCall& call)
1644 {
1645 std::string tag;
1646 call.BodyToString(tag);
1647
1648 LookupResults resources;
1649 ServerIndex& index = OrthancRestApi::GetIndex(call);
1650 AccumulateLookupResults(resources, index, DICOM_TAG_PATIENT_ID, tag, ResourceType_Patient);
1651 AccumulateLookupResults(resources, index, DICOM_TAG_STUDY_INSTANCE_UID, tag, ResourceType_Study);
1652 AccumulateLookupResults(resources, index, DICOM_TAG_SERIES_INSTANCE_UID, tag, ResourceType_Series);
1653 AccumulateLookupResults(resources, index, DICOM_TAG_SOP_INSTANCE_UID, tag, ResourceType_Instance);
1654
1655 Json::Value result = Json::arrayValue;
1656 for (LookupResults::const_iterator
1657 it = resources.begin(); it != resources.end(); ++it)
1658 {
1659 ResourceType type = it->first;
1660 const std::string& id = it->second;
1661
1662 Json::Value item = Json::objectValue;
1663 item["Type"] = EnumerationToString(type);
1664 item["ID"] = id;
1665 item["Path"] = GetBasePath(type, id);
1666
1667 result.append(item);
1668 }
1669
1670 call.GetOutput().AnswerJson(result);
1671 }
1672
1673
1674 namespace
1675 {
1676 class FindVisitor : public ServerContext::ILookupVisitor
1677 {
1678 private:
1679 bool isComplete_;
1680 std::list<std::string> resources_;
1681
1682 public:
1683 FindVisitor() :
1684 isComplete_(false)
1685 {
1686 }
1687
1688 virtual bool IsDicomAsJsonNeeded() const
1689 {
1690 return false; // (*)
1691 }
1692
1693 virtual void MarkAsComplete()
1694 {
1695 isComplete_ = true; // Unused information as of Orthanc 1.5.0
1696 }
1697
1698 virtual void Visit(const std::string& publicId,
1699 const std::string& instanceId /* unused */,
1700 const DicomMap& mainDicomTags /* unused */,
1701 const Json::Value* dicomAsJson /* unused (*) */)
1702 {
1703 resources_.push_back(publicId);
1704 }
1705
1706 void Answer(RestApiOutput& output,
1707 ServerIndex& index,
1708 ResourceType level,
1709 bool expand) const
1710 {
1711 AnswerListOfResources(output, index, resources_, level, expand);
1712 }
1713 };
1714 }
1715
1716
1717 static void Find(RestApiPostCall& call)
1718 {
1719 static const char* const KEY_CASE_SENSITIVE = "CaseSensitive";
1720 static const char* const KEY_EXPAND = "Expand";
1721 static const char* const KEY_LEVEL = "Level";
1722 static const char* const KEY_LIMIT = "Limit";
1723 static const char* const KEY_QUERY = "Query";
1724 static const char* const KEY_SINCE = "Since";
1725
1726 ServerContext& context = OrthancRestApi::GetContext(call);
1727
1728 Json::Value request;
1729 if (!call.ParseJsonRequest(request) ||
1730 request.type() != Json::objectValue)
1731 {
1732 throw OrthancException(ErrorCode_BadRequest,
1733 "The body must contain a JSON object");
1734 }
1735 else if (!request.isMember(KEY_LEVEL) ||
1736 request[KEY_LEVEL].type() != Json::stringValue)
1737 {
1738 throw OrthancException(ErrorCode_BadRequest,
1739 "Field \"" + std::string(KEY_LEVEL) + "\" is missing, or should be a string");
1740 }
1741 else if (!request.isMember(KEY_QUERY) &&
1742 request[KEY_QUERY].type() != Json::objectValue)
1743 {
1744 throw OrthancException(ErrorCode_BadRequest,
1745 "Field \"" + std::string(KEY_QUERY) + "\" is missing, or should be a JSON object");
1746 }
1747 else if (request.isMember(KEY_CASE_SENSITIVE) &&
1748 request[KEY_CASE_SENSITIVE].type() != Json::booleanValue)
1749 {
1750 throw OrthancException(ErrorCode_BadRequest,
1751 "Field \"" + std::string(KEY_CASE_SENSITIVE) + "\" should be a Boolean");
1752 }
1753 else if (request.isMember(KEY_LIMIT) &&
1754 request[KEY_LIMIT].type() != Json::intValue)
1755 {
1756 throw OrthancException(ErrorCode_BadRequest,
1757 "Field \"" + std::string(KEY_LIMIT) + "\" should be an integer");
1758 }
1759 else if (request.isMember(KEY_SINCE) &&
1760 request[KEY_SINCE].type() != Json::intValue)
1761 {
1762 throw OrthancException(ErrorCode_BadRequest,
1763 "Field \"" + std::string(KEY_SINCE) + "\" should be an integer");
1764 }
1765 else
1766 {
1767 bool expand = false;
1768 if (request.isMember(KEY_EXPAND))
1769 {
1770 expand = request[KEY_EXPAND].asBool();
1771 }
1772
1773 bool caseSensitive = false;
1774 if (request.isMember(KEY_CASE_SENSITIVE))
1775 {
1776 caseSensitive = request[KEY_CASE_SENSITIVE].asBool();
1777 }
1778
1779 size_t limit = 0;
1780 if (request.isMember(KEY_LIMIT))
1781 {
1782 int tmp = request[KEY_LIMIT].asInt();
1783 if (tmp < 0)
1784 {
1785 throw OrthancException(ErrorCode_ParameterOutOfRange,
1786 "Field \"" + std::string(KEY_LIMIT) + "\" should be a positive integer");
1787 }
1788
1789 limit = static_cast<size_t>(tmp);
1790 }
1791
1792 size_t since = 0;
1793 if (request.isMember(KEY_SINCE))
1794 {
1795 int tmp = request[KEY_SINCE].asInt();
1796 if (tmp < 0)
1797 {
1798 throw OrthancException(ErrorCode_ParameterOutOfRange,
1799 "Field \"" + std::string(KEY_SINCE) + "\" should be a positive integer");
1800 }
1801
1802 since = static_cast<size_t>(tmp);
1803 }
1804
1805 ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString());
1806
1807 DatabaseLookup query;
1808
1809 Json::Value::Members members = request[KEY_QUERY].getMemberNames();
1810 for (size_t i = 0; i < members.size(); i++)
1811 {
1812 if (request[KEY_QUERY][members[i]].type() != Json::stringValue)
1813 {
1814 throw OrthancException(ErrorCode_BadRequest,
1815 "Tag \"" + members[i] + "\" should be associated with a string");
1816 }
1817
1818 const std::string value = request[KEY_QUERY][members[i]].asString();
1819
1820 if (!value.empty())
1821 {
1822 // An empty string corresponds to an universal constraint,
1823 // so we ignore it. This mimics the behavior of class
1824 // "OrthancFindRequestHandler"
1825 query.AddRestConstraint(FromDcmtkBridge::ParseTag(members[i]),
1826 value, caseSensitive, true);
1827 }
1828 }
1829
1830 FindVisitor visitor;
1831 context.Apply(visitor, query, level, since, limit);
1832 visitor.Answer(call.GetOutput(), context.GetIndex(), level, expand);
1833 }
1834 }
1835
1836
1837 template <enum ResourceType start,
1838 enum ResourceType end>
1839 static void GetChildResources(RestApiGetCall& call)
1840 {
1841 ServerIndex& index = OrthancRestApi::GetIndex(call);
1842
1843 std::list<std::string> a, b, c;
1844 a.push_back(call.GetUriComponent("id", ""));
1845
1846 ResourceType type = start;
1847 while (type != end)
1848 {
1849 b.clear();
1850
1851 for (std::list<std::string>::const_iterator
1852 it = a.begin(); it != a.end(); ++it)
1853 {
1854 index.GetChildren(c, *it);
1855 b.splice(b.begin(), c);
1856 }
1857
1858 type = GetChildResourceType(type);
1859
1860 a.clear();
1861 a.splice(a.begin(), b);
1862 }
1863
1864 Json::Value result = Json::arrayValue;
1865
1866 for (std::list<std::string>::const_iterator
1867 it = a.begin(); it != a.end(); ++it)
1868 {
1869 Json::Value item;
1870
1871 if (OrthancRestApi::GetIndex(call).LookupResource(item, *it, end))
1872 {
1873 result.append(item);
1874 }
1875 }
1876
1877 call.GetOutput().AnswerJson(result);
1878 }
1879
1880
1881 static void GetChildInstancesTags(RestApiGetCall& call)
1882 {
1883 ServerContext& context = OrthancRestApi::GetContext(call);
1884 std::string publicId = call.GetUriComponent("id", "");
1885 DicomToJsonFormat format = GetDicomFormat(call);
1886
1887 std::set<DicomTag> ignoreTagLength;
1888 ParseSetOfTags(ignoreTagLength, call, "ignore-length");
1889
1890 // Retrieve all the instances of this patient/study/series
1891 typedef std::list<std::string> Instances;
1892 Instances instances;
1893
1894 context.GetIndex().GetChildInstances(instances, publicId); // (*)
1895
1896 Json::Value result = Json::objectValue;
1897
1898 for (Instances::const_iterator it = instances.begin();
1899 it != instances.end(); ++it)
1900 {
1901 Json::Value full;
1902 context.ReadDicomAsJson(full, *it, ignoreTagLength);
1903
1904 if (format != DicomToJsonFormat_Full)
1905 {
1906 Json::Value simplified;
1907 ServerToolbox::SimplifyTags(simplified, full, format);
1908 result[*it] = simplified;
1909 }
1910 else
1911 {
1912 result[*it] = full;
1913 }
1914 }
1915
1916 call.GetOutput().AnswerJson(result);
1917 }
1918
1919
1920
1921 template <enum ResourceType start,
1922 enum ResourceType end>
1923 static void GetParentResource(RestApiGetCall& call)
1924 {
1925 assert(start > end);
1926
1927 ServerIndex& index = OrthancRestApi::GetIndex(call);
1928
1929 std::string current = call.GetUriComponent("id", "");
1930 ResourceType currentType = start;
1931 while (currentType > end)
1932 {
1933 std::string parent;
1934 if (!index.LookupParent(parent, current))
1935 {
1936 // Error that could happen if the resource gets deleted by
1937 // another concurrent call
1938 return;
1939 }
1940
1941 current = parent;
1942 currentType = GetParentResourceType(currentType);
1943 }
1944
1945 assert(currentType == end);
1946
1947 Json::Value result;
1948 if (index.LookupResource(result, current, end))
1949 {
1950 call.GetOutput().AnswerJson(result);
1951 }
1952 }
1953
1954
1955 static void ExtractPdf(RestApiGetCall& call)
1956 {
1957 const std::string id = call.GetUriComponent("id", "");
1958
1959 std::string pdf;
1960 ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), id);
1961
1962 if (locker.GetDicom().ExtractPdf(pdf))
1963 {
1964 call.GetOutput().AnswerBuffer(pdf, MimeType_Pdf);
1965 return;
1966 }
1967 }
1968
1969
1970 static void OrderSlices(RestApiGetCall& call)
1971 {
1972 const std::string id = call.GetUriComponent("id", "");
1973
1974 ServerIndex& index = OrthancRestApi::GetIndex(call);
1975 SliceOrdering ordering(index, id);
1976
1977 Json::Value result;
1978 ordering.Format(result);
1979 call.GetOutput().AnswerJson(result);
1980 }
1981
1982
1983 static void GetInstanceHeader(RestApiGetCall& call)
1984 {
1985 ServerContext& context = OrthancRestApi::GetContext(call);
1986
1987 std::string publicId = call.GetUriComponent("id", "");
1988
1989 std::string dicomContent;
1990 context.ReadDicom(dicomContent, publicId);
1991
1992 // TODO Consider using "DicomMap::ParseDicomMetaInformation()" to
1993 // speed up things here
1994
1995 ParsedDicomFile dicom(dicomContent);
1996
1997 Json::Value header;
1998 dicom.HeaderToJson(header, DicomToJsonFormat_Full);
1999
2000 AnswerDicomAsJson(call, header);
2001 }
2002
2003
2004 static void InvalidateTags(RestApiPostCall& call)
2005 {
2006 ServerIndex& index = OrthancRestApi::GetIndex(call);
2007
2008 // Loop over the instances, grouping them by parent studies so as
2009 // to avoid large memory consumption
2010 std::list<std::string> studies;
2011 index.GetAllUuids(studies, ResourceType_Study);
2012
2013 for (std::list<std::string>::const_iterator
2014 study = studies.begin(); study != studies.end(); ++study)
2015 {
2016 std::list<std::string> instances;
2017 index.GetChildInstances(instances, *study);
2018
2019 for (std::list<std::string>::const_iterator
2020 instance = instances.begin(); instance != instances.end(); ++instance)
2021 {
2022 index.DeleteAttachment(*instance, FileContentType_DicomAsJson);
2023 }
2024 }
2025
2026 call.GetOutput().AnswerBuffer("", MimeType_PlainText);
2027 }
2028
2029
2030 template <enum ResourceType type>
2031 static void ReconstructResource(RestApiPostCall& call)
2032 {
2033 ServerContext& context = OrthancRestApi::GetContext(call);
2034 ServerToolbox::ReconstructResource(context, call.GetUriComponent("id", ""));
2035 call.GetOutput().AnswerBuffer("", MimeType_PlainText);
2036 }
2037
2038
2039 static void ReconstructAllResources(RestApiPostCall& call)
2040 {
2041 ServerContext& context = OrthancRestApi::GetContext(call);
2042
2043 std::list<std::string> studies;
2044 context.GetIndex().GetAllUuids(studies, ResourceType_Study);
2045
2046 for (std::list<std::string>::const_iterator
2047 study = studies.begin(); study != studies.end(); ++study)
2048 {
2049 ServerToolbox::ReconstructResource(context, *study);
2050 }
2051
2052 call.GetOutput().AnswerBuffer("", MimeType_PlainText);
2053 }
2054
2055
2056 void OrthancRestApi::RegisterResources()
2057 {
2058 Register("/instances", ListResources<ResourceType_Instance>);
2059 Register("/patients", ListResources<ResourceType_Patient>);
2060 Register("/series", ListResources<ResourceType_Series>);
2061 Register("/studies", ListResources<ResourceType_Study>);
2062
2063 Register("/instances/{id}", DeleteSingleResource<ResourceType_Instance>);
2064 Register("/instances/{id}", GetSingleResource<ResourceType_Instance>);
2065 Register("/patients/{id}", DeleteSingleResource<ResourceType_Patient>);
2066 Register("/patients/{id}", GetSingleResource<ResourceType_Patient>);
2067 Register("/series/{id}", DeleteSingleResource<ResourceType_Series>);
2068 Register("/series/{id}", GetSingleResource<ResourceType_Series>);
2069 Register("/studies/{id}", DeleteSingleResource<ResourceType_Study>);
2070 Register("/studies/{id}", GetSingleResource<ResourceType_Study>);
2071
2072 Register("/instances/{id}/statistics", GetResourceStatistics);
2073 Register("/patients/{id}/statistics", GetResourceStatistics);
2074 Register("/studies/{id}/statistics", GetResourceStatistics);
2075 Register("/series/{id}/statistics", GetResourceStatistics);
2076
2077 Register("/patients/{id}/shared-tags", GetSharedTags);
2078 Register("/series/{id}/shared-tags", GetSharedTags);
2079 Register("/studies/{id}/shared-tags", GetSharedTags);
2080
2081 Register("/instances/{id}/module", GetModule<ResourceType_Instance, DicomModule_Instance>);
2082 Register("/patients/{id}/module", GetModule<ResourceType_Patient, DicomModule_Patient>);
2083 Register("/series/{id}/module", GetModule<ResourceType_Series, DicomModule_Series>);
2084 Register("/studies/{id}/module", GetModule<ResourceType_Study, DicomModule_Study>);
2085 Register("/studies/{id}/module-patient", GetModule<ResourceType_Study, DicomModule_Patient>);
2086
2087 Register("/instances/{id}/file", GetInstanceFile);
2088 Register("/instances/{id}/export", ExportInstanceFile);
2089 Register("/instances/{id}/tags", GetInstanceTagsBis);
2090 Register("/instances/{id}/simplified-tags", GetInstanceTags<DicomToJsonFormat_Human>);
2091 Register("/instances/{id}/frames", ListFrames);
2092
2093 Register("/instances/{id}/frames/{frame}/preview", GetImage<ImageExtractionMode_Preview>);
2094 Register("/instances/{id}/frames/{frame}/rendered", GetRenderedFrame);
2095 Register("/instances/{id}/frames/{frame}/image-uint8", GetImage<ImageExtractionMode_UInt8>);
2096 Register("/instances/{id}/frames/{frame}/image-uint16", GetImage<ImageExtractionMode_UInt16>);
2097 Register("/instances/{id}/frames/{frame}/image-int16", GetImage<ImageExtractionMode_Int16>);
2098 Register("/instances/{id}/frames/{frame}/matlab", GetMatlabImage);
2099 Register("/instances/{id}/frames/{frame}/raw", GetRawFrame<false>);
2100 Register("/instances/{id}/frames/{frame}/raw.gz", GetRawFrame<true>);
2101 Register("/instances/{id}/pdf", ExtractPdf);
2102 Register("/instances/{id}/preview", GetImage<ImageExtractionMode_Preview>);
2103 Register("/instances/{id}/rendered", GetRenderedFrame);
2104 Register("/instances/{id}/image-uint8", GetImage<ImageExtractionMode_UInt8>);
2105 Register("/instances/{id}/image-uint16", GetImage<ImageExtractionMode_UInt16>);
2106 Register("/instances/{id}/image-int16", GetImage<ImageExtractionMode_Int16>);
2107 Register("/instances/{id}/matlab", GetMatlabImage);
2108 Register("/instances/{id}/header", GetInstanceHeader);
2109
2110 Register("/patients/{id}/protected", IsProtectedPatient);
2111 Register("/patients/{id}/protected", SetPatientProtection);
2112
2113 Register("/{resourceType}/{id}/metadata", ListMetadata);
2114 Register("/{resourceType}/{id}/metadata/{name}", DeleteMetadata);
2115 Register("/{resourceType}/{id}/metadata/{name}", GetMetadata);
2116 Register("/{resourceType}/{id}/metadata/{name}", SetMetadata);
2117
2118 Register("/{resourceType}/{id}/attachments", ListAttachments);
2119 Register("/{resourceType}/{id}/attachments/{name}", DeleteAttachment);
2120 Register("/{resourceType}/{id}/attachments/{name}", GetAttachmentOperations);
2121 Register("/{resourceType}/{id}/attachments/{name}", UploadAttachment);
2122 Register("/{resourceType}/{id}/attachments/{name}/compress", ChangeAttachmentCompression<CompressionType_ZlibWithSize>);
2123 Register("/{resourceType}/{id}/attachments/{name}/compressed-data", GetAttachmentData<0>);
2124 Register("/{resourceType}/{id}/attachments/{name}/compressed-md5", GetAttachmentCompressedMD5);
2125 Register("/{resourceType}/{id}/attachments/{name}/compressed-size", GetAttachmentCompressedSize);
2126 Register("/{resourceType}/{id}/attachments/{name}/data", GetAttachmentData<1>);
2127 Register("/{resourceType}/{id}/attachments/{name}/is-compressed", IsAttachmentCompressed);
2128 Register("/{resourceType}/{id}/attachments/{name}/md5", GetAttachmentMD5);
2129 Register("/{resourceType}/{id}/attachments/{name}/size", GetAttachmentSize);
2130 Register("/{resourceType}/{id}/attachments/{name}/uncompress", ChangeAttachmentCompression<CompressionType_None>);
2131 Register("/{resourceType}/{id}/attachments/{name}/verify-md5", VerifyAttachment);
2132
2133 Register("/tools/invalidate-tags", InvalidateTags);
2134 Register("/tools/lookup", Lookup);
2135 Register("/tools/find", Find);
2136
2137 Register("/patients/{id}/studies", GetChildResources<ResourceType_Patient, ResourceType_Study>);
2138 Register("/patients/{id}/series", GetChildResources<ResourceType_Patient, ResourceType_Series>);
2139 Register("/patients/{id}/instances", GetChildResources<ResourceType_Patient, ResourceType_Instance>);
2140 Register("/studies/{id}/series", GetChildResources<ResourceType_Study, ResourceType_Series>);
2141 Register("/studies/{id}/instances", GetChildResources<ResourceType_Study, ResourceType_Instance>);
2142 Register("/series/{id}/instances", GetChildResources<ResourceType_Series, ResourceType_Instance>);
2143
2144 Register("/studies/{id}/patient", GetParentResource<ResourceType_Study, ResourceType_Patient>);
2145 Register("/series/{id}/patient", GetParentResource<ResourceType_Series, ResourceType_Patient>);
2146 Register("/series/{id}/study", GetParentResource<ResourceType_Series, ResourceType_Study>);
2147 Register("/instances/{id}/patient", GetParentResource<ResourceType_Instance, ResourceType_Patient>);
2148 Register("/instances/{id}/study", GetParentResource<ResourceType_Instance, ResourceType_Study>);
2149 Register("/instances/{id}/series", GetParentResource<ResourceType_Instance, ResourceType_Series>);
2150
2151 Register("/patients/{id}/instances-tags", GetChildInstancesTags);
2152 Register("/studies/{id}/instances-tags", GetChildInstancesTags);
2153 Register("/series/{id}/instances-tags", GetChildInstancesTags);
2154
2155 Register("/instances/{id}/content/*", GetRawContent);
2156
2157 Register("/series/{id}/ordered-slices", OrderSlices);
2158
2159 Register("/patients/{id}/reconstruct", ReconstructResource<ResourceType_Patient>);
2160 Register("/studies/{id}/reconstruct", ReconstructResource<ResourceType_Study>);
2161 Register("/series/{id}/reconstruct", ReconstructResource<ResourceType_Series>);
2162 Register("/instances/{id}/reconstruct", ReconstructResource<ResourceType_Instance>);
2163 Register("/tools/reconstruct", ReconstructAllResources);
2164 }
2165 }