Mercurial > hg > orthanc
comparison OrthancServer/FromDcmtkBridge.cpp @ 948:e57e08ed510f dicom-rt
integration mainline -> dicom-rt
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Wed, 25 Jun 2014 13:57:05 +0200 |
parents | 8cfc6119a5bd 87791ebc1f50 |
children | 98d6ba37c7dc |
comparison
equal
deleted
inserted
replaced
767:c19552f604d5 | 948:e57e08ed510f |
---|---|
29 * along with this program. If not, see <http://www.gnu.org/licenses/>. | 29 * along with this program. If not, see <http://www.gnu.org/licenses/>. |
30 **/ | 30 **/ |
31 | 31 |
32 | 32 |
33 | 33 |
34 /*========================================================================= | 34 #include "PrecompiledHeadersServer.h" |
35 | |
36 This file is based on portions of the following project: | |
37 | |
38 Program: GDCM (Grassroots DICOM). A DICOM library | |
39 Module: http://gdcm.sourceforge.net/Copyright.html | |
40 | |
41 Copyright (c) 2006-2011 Mathieu Malaterre | |
42 Copyright (c) 1993-2005 CREATIS | |
43 (CREATIS = Centre de Recherche et d'Applications en Traitement de l'Image) | |
44 All rights reserved. | |
45 | |
46 Redistribution and use in source and binary forms, with or without | |
47 modification, are permitted provided that the following conditions are met: | |
48 | |
49 * Redistributions of source code must retain the above copyright notice, | |
50 this list of conditions and the following disclaimer. | |
51 | |
52 * Redistributions in binary form must reproduce the above copyright notice, | |
53 this list of conditions and the following disclaimer in the documentation | |
54 and/or other materials provided with the distribution. | |
55 | |
56 * Neither name of Mathieu Malaterre, or CREATIS, nor the names of any | |
57 contributors (CNRS, INSERM, UCB, Universite Lyon I), may be used to | |
58 endorse or promote products derived from this software without specific | |
59 prior written permission. | |
60 | |
61 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' | |
62 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
63 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
64 ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR | |
65 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
66 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
67 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
68 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |
69 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
70 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
71 | |
72 =========================================================================*/ | |
73 | |
74 | 35 |
75 #ifndef NOMINMAX | 36 #ifndef NOMINMAX |
76 #define NOMINMAX | 37 #define NOMINMAX |
77 #endif | 38 #endif |
78 | 39 |
40 #include "Internals/DicomImageDecoder.h" | |
41 | |
79 #include "FromDcmtkBridge.h" | 42 #include "FromDcmtkBridge.h" |
80 | |
81 #include "ToDcmtkBridge.h" | 43 #include "ToDcmtkBridge.h" |
82 #include "../Core/Toolbox.h" | 44 #include "../Core/Toolbox.h" |
83 #include "../Core/OrthancException.h" | 45 #include "../Core/OrthancException.h" |
84 #include "../Core/FileFormats/PngWriter.h" | 46 #include "../Core/ImageFormats/PngWriter.h" |
85 #include "../Core/Uuid.h" | 47 #include "../Core/Uuid.h" |
86 #include "../Core/DicomFormat/DicomString.h" | 48 #include "../Core/DicomFormat/DicomString.h" |
87 #include "../Core/DicomFormat/DicomNullValue.h" | 49 #include "../Core/DicomFormat/DicomNullValue.h" |
88 #include "../Core/DicomFormat/DicomIntegerPixelAccessor.h" | 50 #include "../Core/DicomFormat/DicomIntegerPixelAccessor.h" |
89 | 51 |
129 #include <boost/math/special_functions/round.hpp> | 91 #include <boost/math/special_functions/round.hpp> |
130 #include <glog/logging.h> | 92 #include <glog/logging.h> |
131 #include <dcmtk/dcmdata/dcostrmb.h> | 93 #include <dcmtk/dcmdata/dcostrmb.h> |
132 | 94 |
133 | 95 |
134 static const char* CONTENT_TYPE_OCTET_STREAM = "application/octet-stream"; | |
135 | |
136 | |
137 | |
138 namespace Orthanc | 96 namespace Orthanc |
139 { | 97 { |
140 void ParsedDicomFile::Setup(const char* buffer, size_t size) | |
141 { | |
142 DcmInputBufferStream is; | |
143 if (size > 0) | |
144 { | |
145 is.setBuffer(buffer, size); | |
146 } | |
147 is.setEos(); | |
148 | |
149 file_.reset(new DcmFileFormat); | |
150 file_->transferInit(); | |
151 if (!file_->read(is).good()) | |
152 { | |
153 throw OrthancException(ErrorCode_BadFileFormat); | |
154 } | |
155 file_->loadAllDataIntoMemory(); | |
156 file_->transferEnd(); | |
157 } | |
158 | |
159 | |
160 static void SendPathValueForDictionary(RestApiOutput& output, | |
161 DcmItem& dicom) | |
162 { | |
163 Json::Value v = Json::arrayValue; | |
164 | |
165 for (unsigned long i = 0; i < dicom.card(); i++) | |
166 { | |
167 DcmElement* element = dicom.getElement(i); | |
168 if (element) | |
169 { | |
170 char buf[16]; | |
171 sprintf(buf, "%04x-%04x", element->getTag().getGTag(), element->getTag().getETag()); | |
172 v.append(buf); | |
173 } | |
174 } | |
175 | |
176 output.AnswerJson(v); | |
177 } | |
178 | |
179 static inline uint16_t GetCharValue(char c) | 98 static inline uint16_t GetCharValue(char c) |
180 { | 99 { |
181 if (c >= '0' && c <= '9') | 100 if (c >= '0' && c <= '9') |
182 return c - '0'; | 101 return c - '0'; |
183 else if (c >= 'a' && c <= 'f') | 102 else if (c >= 'a' && c <= 'f') |
193 return ((GetCharValue(c[0]) << 12) + | 112 return ((GetCharValue(c[0]) << 12) + |
194 (GetCharValue(c[1]) << 8) + | 113 (GetCharValue(c[1]) << 8) + |
195 (GetCharValue(c[2]) << 4) + | 114 (GetCharValue(c[2]) << 4) + |
196 GetCharValue(c[3])); | 115 GetCharValue(c[3])); |
197 } | 116 } |
198 | |
199 static void ParseTagAndGroup(DcmTagKey& key, | |
200 const std::string& tag) | |
201 { | |
202 DicomTag t = FromDcmtkBridge::ParseTag(tag); | |
203 key = DcmTagKey(t.GetGroup(), t.GetElement()); | |
204 } | |
205 | |
206 | |
207 static void SendSequence(RestApiOutput& output, | |
208 DcmSequenceOfItems& sequence) | |
209 { | |
210 // This element is a sequence | |
211 Json::Value v = Json::arrayValue; | |
212 | |
213 for (unsigned long i = 0; i < sequence.card(); i++) | |
214 { | |
215 v.append(boost::lexical_cast<std::string>(i)); | |
216 } | |
217 | |
218 output.AnswerJson(v); | |
219 } | |
220 | |
221 | |
222 static unsigned int GetPixelDataBlockCount(DcmPixelData& pixelData, | |
223 E_TransferSyntax transferSyntax) | |
224 { | |
225 DcmPixelSequence* pixelSequence = NULL; | |
226 if (pixelData.getEncapsulatedRepresentation | |
227 (transferSyntax, NULL, pixelSequence).good() && pixelSequence) | |
228 { | |
229 return pixelSequence->card(); | |
230 } | |
231 else | |
232 { | |
233 return 1; | |
234 } | |
235 } | |
236 | |
237 | |
238 static void AnswerDicomField(RestApiOutput& output, | |
239 DcmElement& element, | |
240 E_TransferSyntax transferSyntax) | |
241 { | |
242 // This element is nor a sequence, neither a pixel-data | |
243 std::string buffer; | |
244 buffer.resize(65536); | |
245 Uint32 length = element.getLength(transferSyntax); | |
246 Uint32 offset = 0; | |
247 | |
248 output.GetLowLevelOutput().SendOkHeader(CONTENT_TYPE_OCTET_STREAM, true, length, NULL); | |
249 | |
250 while (offset < length) | |
251 { | |
252 Uint32 nbytes; | |
253 if (length - offset < buffer.size()) | |
254 { | |
255 nbytes = length - offset; | |
256 } | |
257 else | |
258 { | |
259 nbytes = buffer.size(); | |
260 } | |
261 | |
262 OFCondition cond = element.getPartialValue(&buffer[0], offset, nbytes); | |
263 | |
264 if (cond.good()) | |
265 { | |
266 output.GetLowLevelOutput().Send(&buffer[0], nbytes); | |
267 offset += nbytes; | |
268 } | |
269 else | |
270 { | |
271 LOG(ERROR) << "Error while sending a DICOM field: " << cond.text(); | |
272 return; | |
273 } | |
274 } | |
275 | |
276 output.MarkLowLevelOutputDone(); | |
277 } | |
278 | |
279 | |
280 static bool AnswerPixelData(RestApiOutput& output, | |
281 DcmItem& dicom, | |
282 E_TransferSyntax transferSyntax, | |
283 const std::string* blockUri) | |
284 { | |
285 DcmTag k(DICOM_TAG_PIXEL_DATA.GetGroup(), | |
286 DICOM_TAG_PIXEL_DATA.GetElement()); | |
287 | |
288 DcmElement *element = NULL; | |
289 if (!dicom.findAndGetElement(k, element).good() || | |
290 element == NULL) | |
291 { | |
292 return false; | |
293 } | |
294 | |
295 try | |
296 { | |
297 DcmPixelData& pixelData = dynamic_cast<DcmPixelData&>(*element); | |
298 if (blockUri == NULL) | |
299 { | |
300 // The user asks how many blocks are presents in this pixel data | |
301 unsigned int blocks = GetPixelDataBlockCount(pixelData, transferSyntax); | |
302 | |
303 Json::Value result(Json::arrayValue); | |
304 for (unsigned int i = 0; i < blocks; i++) | |
305 { | |
306 result.append(boost::lexical_cast<std::string>(i)); | |
307 } | |
308 | |
309 output.AnswerJson(result); | |
310 return true; | |
311 } | |
312 | |
313 | |
314 unsigned int block = boost::lexical_cast<unsigned int>(*blockUri); | |
315 | |
316 if (block < GetPixelDataBlockCount(pixelData, transferSyntax)) | |
317 { | |
318 DcmPixelSequence* pixelSequence = NULL; | |
319 if (pixelData.getEncapsulatedRepresentation | |
320 (transferSyntax, NULL, pixelSequence).good() && pixelSequence) | |
321 { | |
322 // This is the case for JPEG transfer syntaxes | |
323 if (block < pixelSequence->card()) | |
324 { | |
325 DcmPixelItem* pixelItem = NULL; | |
326 if (pixelSequence->getItem(pixelItem, block).good() && pixelItem) | |
327 { | |
328 if (pixelItem->getLength() == 0) | |
329 { | |
330 output.AnswerBuffer(NULL, 0, CONTENT_TYPE_OCTET_STREAM); | |
331 return true; | |
332 } | |
333 | |
334 Uint8* buffer = NULL; | |
335 if (pixelItem->getUint8Array(buffer).good() && buffer) | |
336 { | |
337 output.AnswerBuffer(buffer, pixelItem->getLength(), CONTENT_TYPE_OCTET_STREAM); | |
338 return true; | |
339 } | |
340 } | |
341 } | |
342 } | |
343 else | |
344 { | |
345 // This is the case for raw, uncompressed image buffers | |
346 assert(*blockUri == "0"); | |
347 AnswerDicomField(output, *element, transferSyntax); | |
348 } | |
349 } | |
350 } | |
351 catch (boost::bad_lexical_cast&) | |
352 { | |
353 // The URI entered by the user is not a number | |
354 } | |
355 catch (std::bad_cast&) | |
356 { | |
357 // This should never happen | |
358 } | |
359 | |
360 return false; | |
361 } | |
362 | |
363 | |
364 | |
365 static void SendPathValueForLeaf(RestApiOutput& output, | |
366 const std::string& tag, | |
367 DcmItem& dicom, | |
368 E_TransferSyntax transferSyntax) | |
369 { | |
370 DcmTagKey k; | |
371 ParseTagAndGroup(k, tag); | |
372 | |
373 DcmSequenceOfItems* sequence = NULL; | |
374 if (dicom.findAndGetSequence(k, sequence).good() && | |
375 sequence != NULL && | |
376 sequence->getVR() == EVR_SQ) | |
377 { | |
378 SendSequence(output, *sequence); | |
379 return; | |
380 } | |
381 | |
382 DcmElement* element = NULL; | |
383 if (dicom.findAndGetElement(k, element).good() && | |
384 element != NULL && | |
385 //element->getVR() != EVR_UNKNOWN && // This would forbid private tags | |
386 element->getVR() != EVR_SQ) | |
387 { | |
388 AnswerDicomField(output, *element, transferSyntax); | |
389 } | |
390 } | |
391 | |
392 void ParsedDicomFile::SendPathValue(RestApiOutput& output, | |
393 const UriComponents& uri) | |
394 { | |
395 DcmItem* dicom = file_->getDataset(); | |
396 E_TransferSyntax transferSyntax = file_->getDataset()->getOriginalXfer(); | |
397 | |
398 // Special case: Accessing the pixel data | |
399 if (uri.size() == 1 || | |
400 uri.size() == 2) | |
401 { | |
402 DcmTagKey tag; | |
403 ParseTagAndGroup(tag, uri[0]); | |
404 | |
405 if (tag.getGroup() == DICOM_TAG_PIXEL_DATA.GetGroup() && | |
406 tag.getElement() == DICOM_TAG_PIXEL_DATA.GetElement()) | |
407 { | |
408 AnswerPixelData(output, *dicom, transferSyntax, uri.size() == 1 ? NULL : &uri[1]); | |
409 return; | |
410 } | |
411 } | |
412 | |
413 // Go down in the tag hierarchy according to the URI | |
414 for (size_t pos = 0; pos < uri.size() / 2; pos++) | |
415 { | |
416 size_t index; | |
417 try | |
418 { | |
419 index = boost::lexical_cast<size_t>(uri[2 * pos + 1]); | |
420 } | |
421 catch (boost::bad_lexical_cast&) | |
422 { | |
423 return; | |
424 } | |
425 | |
426 DcmTagKey k; | |
427 DcmItem *child = NULL; | |
428 ParseTagAndGroup(k, uri[2 * pos]); | |
429 if (!dicom->findAndGetSequenceItem(k, child, index).good() || | |
430 child == NULL) | |
431 { | |
432 return; | |
433 } | |
434 | |
435 dicom = child; | |
436 } | |
437 | |
438 // We have reached the end of the URI | |
439 if (uri.size() % 2 == 0) | |
440 { | |
441 SendPathValueForDictionary(output, *dicom); | |
442 } | |
443 else | |
444 { | |
445 SendPathValueForLeaf(output, uri.back(), *dicom, transferSyntax); | |
446 } | |
447 } | |
448 | |
449 | |
450 | |
451 | |
452 | |
453 static DcmElement* CreateElementForTag(const DicomTag& tag) | |
454 { | |
455 DcmTag key(tag.GetGroup(), tag.GetElement()); | |
456 | |
457 switch (key.getEVR()) | |
458 { | |
459 // http://support.dcmtk.org/docs/dcvr_8h-source.html | |
460 | |
461 /** | |
462 * TODO. | |
463 **/ | |
464 | |
465 case EVR_OB: // other byte | |
466 case EVR_OF: // other float | |
467 case EVR_OW: // other word | |
468 case EVR_AT: // attribute tag | |
469 throw OrthancException(ErrorCode_NotImplemented); | |
470 | |
471 case EVR_UN: // unknown value representation | |
472 throw OrthancException(ErrorCode_ParameterOutOfRange); | |
473 | |
474 | |
475 /** | |
476 * String types. | |
477 * http://support.dcmtk.org/docs/classDcmByteString.html | |
478 **/ | |
479 | |
480 case EVR_AS: // age string | |
481 return new DcmAgeString(key); | |
482 | |
483 case EVR_AE: // application entity title | |
484 return new DcmApplicationEntity(key); | |
485 | |
486 case EVR_CS: // code string | |
487 return new DcmCodeString(key); | |
488 | |
489 case EVR_DA: // date string | |
490 return new DcmDate(key); | |
491 | |
492 case EVR_DT: // date time string | |
493 return new DcmDateTime(key); | |
494 | |
495 case EVR_DS: // decimal string | |
496 return new DcmDecimalString(key); | |
497 | |
498 case EVR_IS: // integer string | |
499 return new DcmIntegerString(key); | |
500 | |
501 case EVR_TM: // time string | |
502 return new DcmTime(key); | |
503 | |
504 case EVR_UI: // unique identifier | |
505 return new DcmUniqueIdentifier(key); | |
506 | |
507 case EVR_ST: // short text | |
508 return new DcmShortText(key); | |
509 | |
510 case EVR_LO: // long string | |
511 return new DcmLongString(key); | |
512 | |
513 case EVR_LT: // long text | |
514 return new DcmLongText(key); | |
515 | |
516 case EVR_UT: // unlimited text | |
517 return new DcmUnlimitedText(key); | |
518 | |
519 case EVR_SH: // short string | |
520 return new DcmShortString(key); | |
521 | |
522 case EVR_PN: // person name | |
523 return new DcmPersonName(key); | |
524 | |
525 | |
526 /** | |
527 * Numerical types | |
528 **/ | |
529 | |
530 case EVR_SL: // signed long | |
531 return new DcmSignedLong(key); | |
532 | |
533 case EVR_SS: // signed short | |
534 return new DcmSignedShort(key); | |
535 | |
536 case EVR_UL: // unsigned long | |
537 return new DcmUnsignedLong(key); | |
538 | |
539 case EVR_US: // unsigned short | |
540 return new DcmUnsignedShort(key); | |
541 | |
542 case EVR_FL: // float single-precision | |
543 return new DcmFloatingPointSingle(key); | |
544 | |
545 case EVR_FD: // float double-precision | |
546 return new DcmFloatingPointDouble(key); | |
547 | |
548 | |
549 /** | |
550 * Sequence types, should never occur at this point. | |
551 **/ | |
552 | |
553 case EVR_SQ: // sequence of items | |
554 throw OrthancException(ErrorCode_ParameterOutOfRange); | |
555 | |
556 | |
557 /** | |
558 * Internal to DCMTK. | |
559 **/ | |
560 | |
561 case EVR_ox: // OB or OW depending on context | |
562 case EVR_xs: // SS or US depending on context | |
563 case EVR_lt: // US, SS or OW depending on context, used for LUT Data (thus the name) | |
564 case EVR_na: // na="not applicable", for data which has no VR | |
565 case EVR_up: // up="unsigned pointer", used internally for DICOMDIR suppor | |
566 case EVR_item: // used internally for items | |
567 case EVR_metainfo: // used internally for meta info datasets | |
568 case EVR_dataset: // used internally for datasets | |
569 case EVR_fileFormat: // used internally for DICOM files | |
570 case EVR_dicomDir: // used internally for DICOMDIR objects | |
571 case EVR_dirRecord: // used internally for DICOMDIR records | |
572 case EVR_pixelSQ: // used internally for pixel sequences in a compressed image | |
573 case EVR_pixelItem: // used internally for pixel items in a compressed image | |
574 case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR) | |
575 case EVR_PixelData: // used internally for uncompressed pixeld data | |
576 case EVR_OverlayData: // used internally for overlay data | |
577 case EVR_UNKNOWN2B: // used internally for elements with unknown VR with 2-byte length field in explicit VR | |
578 default: | |
579 break; | |
580 } | |
581 | |
582 throw OrthancException(ErrorCode_InternalError); | |
583 } | |
584 | |
585 | |
586 | |
587 static void FillElementWithString(DcmElement& element, | |
588 const DicomTag& tag, | |
589 const std::string& value) | |
590 { | |
591 DcmTag key(tag.GetGroup(), tag.GetElement()); | |
592 bool ok = false; | |
593 | |
594 try | |
595 { | |
596 switch (key.getEVR()) | |
597 { | |
598 // http://support.dcmtk.org/docs/dcvr_8h-source.html | |
599 | |
600 /** | |
601 * TODO. | |
602 **/ | |
603 | |
604 case EVR_OB: // other byte | |
605 case EVR_OF: // other float | |
606 case EVR_OW: // other word | |
607 case EVR_AT: // attribute tag | |
608 throw OrthancException(ErrorCode_NotImplemented); | |
609 | |
610 case EVR_UN: // unknown value representation | |
611 throw OrthancException(ErrorCode_ParameterOutOfRange); | |
612 | |
613 | |
614 /** | |
615 * String types. | |
616 **/ | |
617 | |
618 case EVR_DS: // decimal string | |
619 case EVR_IS: // integer string | |
620 case EVR_AS: // age string | |
621 case EVR_DA: // date string | |
622 case EVR_DT: // date time string | |
623 case EVR_TM: // time string | |
624 case EVR_AE: // application entity title | |
625 case EVR_CS: // code string | |
626 case EVR_SH: // short string | |
627 case EVR_LO: // long string | |
628 case EVR_ST: // short text | |
629 case EVR_LT: // long text | |
630 case EVR_UT: // unlimited text | |
631 case EVR_PN: // person name | |
632 case EVR_UI: // unique identifier | |
633 { | |
634 ok = element.putString(value.c_str()).good(); | |
635 break; | |
636 } | |
637 | |
638 | |
639 /** | |
640 * Numerical types | |
641 **/ | |
642 | |
643 case EVR_SL: // signed long | |
644 { | |
645 ok = element.putSint32(boost::lexical_cast<Sint32>(value)).good(); | |
646 break; | |
647 } | |
648 | |
649 case EVR_SS: // signed short | |
650 { | |
651 ok = element.putSint16(boost::lexical_cast<Sint16>(value)).good(); | |
652 break; | |
653 } | |
654 | |
655 case EVR_UL: // unsigned long | |
656 { | |
657 ok = element.putUint32(boost::lexical_cast<Uint32>(value)).good(); | |
658 break; | |
659 } | |
660 | |
661 case EVR_US: // unsigned short | |
662 { | |
663 ok = element.putUint16(boost::lexical_cast<Uint16>(value)).good(); | |
664 break; | |
665 } | |
666 | |
667 case EVR_FL: // float single-precision | |
668 { | |
669 ok = element.putFloat32(boost::lexical_cast<float>(value)).good(); | |
670 break; | |
671 } | |
672 | |
673 case EVR_FD: // float double-precision | |
674 { | |
675 ok = element.putFloat64(boost::lexical_cast<double>(value)).good(); | |
676 break; | |
677 } | |
678 | |
679 | |
680 /** | |
681 * Sequence types, should never occur at this point. | |
682 **/ | |
683 | |
684 case EVR_SQ: // sequence of items | |
685 { | |
686 ok = false; | |
687 break; | |
688 } | |
689 | |
690 | |
691 /** | |
692 * Internal to DCMTK. | |
693 **/ | |
694 | |
695 case EVR_ox: // OB or OW depending on context | |
696 case EVR_xs: // SS or US depending on context | |
697 case EVR_lt: // US, SS or OW depending on context, used for LUT Data (thus the name) | |
698 case EVR_na: // na="not applicable", for data which has no VR | |
699 case EVR_up: // up="unsigned pointer", used internally for DICOMDIR suppor | |
700 case EVR_item: // used internally for items | |
701 case EVR_metainfo: // used internally for meta info datasets | |
702 case EVR_dataset: // used internally for datasets | |
703 case EVR_fileFormat: // used internally for DICOM files | |
704 case EVR_dicomDir: // used internally for DICOMDIR objects | |
705 case EVR_dirRecord: // used internally for DICOMDIR records | |
706 case EVR_pixelSQ: // used internally for pixel sequences in a compressed image | |
707 case EVR_pixelItem: // used internally for pixel items in a compressed image | |
708 case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR) | |
709 case EVR_PixelData: // used internally for uncompressed pixeld data | |
710 case EVR_OverlayData: // used internally for overlay data | |
711 case EVR_UNKNOWN2B: // used internally for elements with unknown VR with 2-byte length field in explicit VR | |
712 default: | |
713 break; | |
714 } | |
715 } | |
716 catch (boost::bad_lexical_cast&) | |
717 { | |
718 ok = false; | |
719 } | |
720 | |
721 if (!ok) | |
722 { | |
723 throw OrthancException(ErrorCode_InternalError); | |
724 } | |
725 } | |
726 | |
727 | |
728 void ParsedDicomFile::Remove(const DicomTag& tag) | |
729 { | |
730 DcmTagKey key(tag.GetGroup(), tag.GetElement()); | |
731 DcmElement* element = file_->getDataset()->remove(key); | |
732 if (element != NULL) | |
733 { | |
734 delete element; | |
735 } | |
736 } | |
737 | |
738 | |
739 | |
740 void ParsedDicomFile::RemovePrivateTags() | |
741 { | |
742 typedef std::list<DcmElement*> Tags; | |
743 | |
744 Tags privateTags; | |
745 | |
746 DcmDataset& dataset = *file_->getDataset(); | |
747 for (unsigned long i = 0; i < dataset.card(); i++) | |
748 { | |
749 DcmElement* element = dataset.getElement(i); | |
750 DcmTag tag(element->getTag()); | |
751 if (!strcmp("PrivateCreator", tag.getTagName()) || // TODO - This may change with future versions of DCMTK | |
752 tag.getPrivateCreator() != NULL) | |
753 { | |
754 privateTags.push_back(element); | |
755 } | |
756 } | |
757 | |
758 for (Tags::iterator it = privateTags.begin(); | |
759 it != privateTags.end(); ++it) | |
760 { | |
761 DcmElement* tmp = dataset.remove(*it); | |
762 if (tmp != NULL) | |
763 { | |
764 delete tmp; | |
765 } | |
766 } | |
767 } | |
768 | |
769 | |
770 | |
771 void ParsedDicomFile::Insert(const DicomTag& tag, | |
772 const std::string& value) | |
773 { | |
774 std::auto_ptr<DcmElement> element(CreateElementForTag(tag)); | |
775 FillElementWithString(*element, tag, value); | |
776 | |
777 if (!file_->getDataset()->insert(element.release(), false, false).good()) | |
778 { | |
779 // This field already exists | |
780 throw OrthancException(ErrorCode_InternalError); | |
781 } | |
782 } | |
783 | |
784 | |
785 void ParsedDicomFile::Replace(const DicomTag& tag, | |
786 const std::string& value, | |
787 DicomReplaceMode mode) | |
788 { | |
789 DcmTagKey key(tag.GetGroup(), tag.GetElement()); | |
790 DcmElement* element = NULL; | |
791 | |
792 if (!file_->getDataset()->findAndGetElement(key, element).good() || | |
793 element == NULL) | |
794 { | |
795 // This field does not exist, act wrt. the specified "mode" | |
796 switch (mode) | |
797 { | |
798 case DicomReplaceMode_InsertIfAbsent: | |
799 Insert(tag, value); | |
800 break; | |
801 | |
802 case DicomReplaceMode_ThrowIfAbsent: | |
803 throw OrthancException(ErrorCode_InexistentItem); | |
804 | |
805 case DicomReplaceMode_IgnoreIfAbsent: | |
806 return; | |
807 } | |
808 } | |
809 else | |
810 { | |
811 FillElementWithString(*element, tag, value); | |
812 } | |
813 | |
814 | |
815 /** | |
816 * dcmodify will automatically correct 'Media Storage SOP Class | |
817 * UID' and 'Media Storage SOP Instance UID' in the metaheader, if | |
818 * you make changes to the related tags in the dataset ('SOP Class | |
819 * UID' and 'SOP Instance UID') via insert or modify mode | |
820 * options. You can disable this behaviour by using the -nmu | |
821 * option. | |
822 **/ | |
823 if (tag == DICOM_TAG_SOP_CLASS_UID) | |
824 Replace(DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID, value, DicomReplaceMode_InsertIfAbsent); | |
825 | |
826 if (tag == DICOM_TAG_SOP_INSTANCE_UID) | |
827 Replace(DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID, value, DicomReplaceMode_InsertIfAbsent); | |
828 } | |
829 | |
830 | |
831 void ParsedDicomFile::Answer(RestApiOutput& output) | |
832 { | |
833 std::string serialized; | |
834 if (FromDcmtkBridge::SaveToMemoryBuffer(serialized, file_->getDataset())) | |
835 { | |
836 output.AnswerBuffer(serialized, CONTENT_TYPE_OCTET_STREAM); | |
837 } | |
838 } | |
839 | |
840 | |
841 | |
842 static bool GetTagValueInternal(std::string& value, | |
843 DcmItem& item, | |
844 const DicomTag& tag) | |
845 { | |
846 DcmTagKey k(tag.GetGroup(), tag.GetElement()); | |
847 DcmElement* element = NULL; | |
848 if (!item.findAndGetElement(k, element).good() || | |
849 element == NULL) | |
850 { | |
851 return false; | |
852 } | |
853 | |
854 std::auto_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement(*element)); | |
855 | |
856 if (v.get() == NULL) | |
857 { | |
858 value = ""; | |
859 } | |
860 else | |
861 { | |
862 value = v->AsString(); | |
863 } | |
864 | |
865 return true; | |
866 } | |
867 | |
868 | |
869 bool ParsedDicomFile::GetTagValue(std::string& value, | |
870 const DicomTag& tag) | |
871 { | |
872 DcmDataset& dataset = *file_->getDataset(); | |
873 return GetTagValueInternal(value, dataset, tag); | |
874 } | |
875 | |
876 | |
877 | |
878 bool ParsedDicomFile::GetTagValue(std::string& value, | |
879 const SequencePath& path, | |
880 const DicomTag& tag) | |
881 { | |
882 if (path.size() == 0) | |
883 { | |
884 return GetTagValue(value, tag); | |
885 } | |
886 | |
887 DcmItem* current = file_->getDataset(); | |
888 assert(current != NULL); | |
889 | |
890 for (SequencePath::const_iterator it = path.begin(); it != path.end(); it++) | |
891 { | |
892 DcmTagKey k(it->first.GetGroup(), it->first.GetElement()); | |
893 | |
894 DcmSequenceOfItems* sequence = NULL; | |
895 if (!current->findAndGetSequence(k, sequence).good() || | |
896 sequence == NULL || | |
897 sequence->getVR() != EVR_SQ) | |
898 { | |
899 return false; | |
900 } | |
901 | |
902 if (it->second < 0 || it->second > sequence->card()) | |
903 { | |
904 return false; | |
905 } | |
906 | |
907 current = sequence->getItem(it->second); | |
908 | |
909 if (current == NULL) | |
910 { | |
911 return false; | |
912 } | |
913 } | |
914 | |
915 return GetTagValueInternal(value, *current, tag); | |
916 } | |
917 | |
918 | |
919 | |
920 DicomInstanceHasher ParsedDicomFile::GetHasher() | |
921 { | |
922 std::string patientId, studyUid, seriesUid, instanceUid; | |
923 | |
924 if (!GetTagValue(patientId, DICOM_TAG_PATIENT_ID) || | |
925 !GetTagValue(studyUid, DICOM_TAG_STUDY_INSTANCE_UID) || | |
926 !GetTagValue(seriesUid, DICOM_TAG_SERIES_INSTANCE_UID) || | |
927 !GetTagValue(instanceUid, DICOM_TAG_SOP_INSTANCE_UID)) | |
928 { | |
929 throw OrthancException(ErrorCode_BadFileFormat); | |
930 } | |
931 | |
932 return DicomInstanceHasher(patientId, studyUid, seriesUid, instanceUid); | |
933 } | |
934 | |
935 | 117 |
936 void FromDcmtkBridge::Convert(DicomMap& target, DcmDataset& dataset) | 118 void FromDcmtkBridge::Convert(DicomMap& target, DcmDataset& dataset) |
937 { | 119 { |
938 target.Clear(); | 120 target.Clear(); |
939 for (unsigned long i = 0; i < dataset.card(); i++) | 121 for (unsigned long i = 0; i < dataset.card(); i++) |
1244 FromDcmtkBridge::ToJson(target, *dicom.getDataset(), maxStringLength); | 426 FromDcmtkBridge::ToJson(target, *dicom.getDataset(), maxStringLength); |
1245 } | 427 } |
1246 } | 428 } |
1247 | 429 |
1248 | 430 |
1249 static void ExtractPngImageColorPreview(std::string& result, | |
1250 DicomIntegerPixelAccessor& accessor) | |
1251 { | |
1252 assert(accessor.GetChannelCount() == 3); | |
1253 PngWriter w; | |
1254 | |
1255 std::vector<uint8_t> image(accessor.GetWidth() * accessor.GetHeight() * 3, 0); | |
1256 uint8_t* pixel = &image[0]; | |
1257 | |
1258 for (unsigned int y = 0; y < accessor.GetHeight(); y++) | |
1259 { | |
1260 for (unsigned int x = 0; x < accessor.GetWidth(); x++) | |
1261 { | |
1262 for (unsigned int c = 0; c < 3; c++, pixel++) | |
1263 { | |
1264 int32_t v = accessor.GetValue(x, y, c); | |
1265 if (v < 0) | |
1266 *pixel = 0; | |
1267 else if (v > 255) | |
1268 *pixel = 255; | |
1269 else | |
1270 *pixel = v; | |
1271 } | |
1272 } | |
1273 } | |
1274 | |
1275 w.WriteToMemory(result, accessor.GetWidth(), accessor.GetHeight(), | |
1276 accessor.GetWidth() * 3, PixelFormat_RGB24, &image[0]); | |
1277 } | |
1278 | |
1279 | |
1280 static void ExtractPngImageGrayscalePreview(std::string& result, | |
1281 DicomIntegerPixelAccessor& accessor) | |
1282 { | |
1283 assert(accessor.GetChannelCount() == 1); | |
1284 PngWriter w; | |
1285 | |
1286 int32_t min, max; | |
1287 accessor.GetExtremeValues(min, max); | |
1288 | |
1289 std::vector<uint8_t> image(accessor.GetWidth() * accessor.GetHeight(), 0); | |
1290 if (min != max) | |
1291 { | |
1292 uint8_t* pixel = &image[0]; | |
1293 for (unsigned int y = 0; y < accessor.GetHeight(); y++) | |
1294 { | |
1295 for (unsigned int x = 0; x < accessor.GetWidth(); x++, pixel++) | |
1296 { | |
1297 int32_t v = accessor.GetValue(x, y); | |
1298 *pixel = static_cast<uint8_t>( | |
1299 boost::math::lround(static_cast<float>(v - min) / | |
1300 static_cast<float>(max - min) * 255.0f)); | |
1301 } | |
1302 } | |
1303 } | |
1304 | |
1305 w.WriteToMemory(result, accessor.GetWidth(), accessor.GetHeight(), | |
1306 accessor.GetWidth(), PixelFormat_Grayscale8, &image[0]); | |
1307 } | |
1308 | |
1309 | |
1310 template <typename T> | |
1311 static void ExtractPngImageTruncate(std::string& result, | |
1312 DicomIntegerPixelAccessor& accessor, | |
1313 PixelFormat format) | |
1314 { | |
1315 assert(accessor.GetChannelCount() == 1); | |
1316 | |
1317 PngWriter w; | |
1318 | |
1319 std::vector<T> image(accessor.GetWidth() * accessor.GetHeight(), 0); | |
1320 T* pixel = &image[0]; | |
1321 for (unsigned int y = 0; y < accessor.GetHeight(); y++) | |
1322 { | |
1323 for (unsigned int x = 0; x < accessor.GetWidth(); x++, pixel++) | |
1324 { | |
1325 int32_t v = accessor.GetValue(x, y); | |
1326 if (v < static_cast<int32_t>(std::numeric_limits<T>::min())) | |
1327 *pixel = std::numeric_limits<T>::min(); | |
1328 else if (v > static_cast<int32_t>(std::numeric_limits<T>::max())) | |
1329 *pixel = std::numeric_limits<T>::max(); | |
1330 else | |
1331 *pixel = static_cast<T>(v); | |
1332 } | |
1333 } | |
1334 | |
1335 w.WriteToMemory(result, accessor.GetWidth(), accessor.GetHeight(), | |
1336 accessor.GetWidth() * sizeof(T), format, &image[0]); | |
1337 } | |
1338 | |
1339 | |
1340 static bool DecodePsmctRle1(std::string& output, | |
1341 DcmDataset& dataset) | |
1342 { | |
1343 static const DicomTag tagContent(0x07a1, 0x100a); | |
1344 static const DicomTag tagCompressionType(0x07a1, 0x1011); | |
1345 | |
1346 DcmElement* e; | |
1347 char* c; | |
1348 | |
1349 // Check whether the DICOM instance contains an image encoded with | |
1350 // the PMSCT_RLE1 scheme. | |
1351 if (!dataset.findAndGetElement(ToDcmtkBridge::Convert(tagCompressionType), e).good() || | |
1352 e == NULL || | |
1353 !e->isaString() || | |
1354 !e->getString(c).good() || | |
1355 c == NULL || | |
1356 strcmp("PMSCT_RLE1", c)) | |
1357 { | |
1358 return false; | |
1359 } | |
1360 | |
1361 // OK, this is a custom RLE encoding from Philips. Get the pixel | |
1362 // data from the appropriate private DICOM tag. | |
1363 Uint8* pixData = NULL; | |
1364 if (!dataset.findAndGetElement(ToDcmtkBridge::Convert(tagContent), e).good() || | |
1365 e == NULL || | |
1366 e->getUint8Array(pixData) != EC_Normal) | |
1367 { | |
1368 return false; | |
1369 } | |
1370 | |
1371 // The "unsigned" below IS VERY IMPORTANT | |
1372 const uint8_t* inbuffer = reinterpret_cast<const uint8_t*>(pixData); | |
1373 const size_t length = e->getLength(); | |
1374 | |
1375 /** | |
1376 * The code below is an adaptation of a sample code for GDCM by | |
1377 * Mathieu Malaterre (under a BSD license). | |
1378 * http://gdcm.sourceforge.net/html/rle2img_8cxx-example.html | |
1379 **/ | |
1380 | |
1381 // RLE pass | |
1382 std::vector<uint8_t> temp; | |
1383 temp.reserve(length); | |
1384 for (size_t i = 0; i < length; i++) | |
1385 { | |
1386 if (inbuffer[i] == 0xa5) | |
1387 { | |
1388 temp.push_back(inbuffer[i+2]); | |
1389 for (uint8_t repeat = inbuffer[i + 1]; repeat != 0; repeat--) | |
1390 { | |
1391 temp.push_back(inbuffer[i+2]); | |
1392 } | |
1393 i += 2; | |
1394 } | |
1395 else | |
1396 { | |
1397 temp.push_back(inbuffer[i]); | |
1398 } | |
1399 } | |
1400 | |
1401 // Delta encoding pass | |
1402 uint16_t delta = 0; | |
1403 output.clear(); | |
1404 output.reserve(temp.size()); | |
1405 for (size_t i = 0; i < temp.size(); i++) | |
1406 { | |
1407 uint16_t value; | |
1408 | |
1409 if (temp[i] == 0x5a) | |
1410 { | |
1411 uint16_t v1 = temp[i + 1]; | |
1412 uint16_t v2 = temp[i + 2]; | |
1413 value = (v2 << 8) + v1; | |
1414 i += 2; | |
1415 } | |
1416 else | |
1417 { | |
1418 value = delta + (int8_t) temp[i]; | |
1419 } | |
1420 | |
1421 output.push_back(value & 0xff); | |
1422 output.push_back(value >> 8); | |
1423 delta = value; | |
1424 } | |
1425 | |
1426 if (output.size() % 2) | |
1427 { | |
1428 output.resize(output.size() - 1); | |
1429 } | |
1430 | |
1431 return true; | |
1432 } | |
1433 | |
1434 | |
1435 void FromDcmtkBridge::ExtractPngImage(std::string& result, | |
1436 DcmDataset& dataset, | |
1437 unsigned int frame, | |
1438 ImageExtractionMode mode) | |
1439 { | |
1440 // See also: http://support.dcmtk.org/wiki/dcmtk/howto/accessing-compressed-data | |
1441 | |
1442 std::auto_ptr<DicomIntegerPixelAccessor> accessor; | |
1443 | |
1444 DicomMap m; | |
1445 FromDcmtkBridge::Convert(m, dataset); | |
1446 | |
1447 std::string privateContent; | |
1448 | |
1449 DcmElement* e; | |
1450 if (dataset.findAndGetElement(ToDcmtkBridge::Convert(DICOM_TAG_PIXEL_DATA), e).good() && | |
1451 e != NULL) | |
1452 { | |
1453 Uint8* pixData = NULL; | |
1454 if (e->getUint8Array(pixData) == EC_Normal) | |
1455 { | |
1456 accessor.reset(new DicomIntegerPixelAccessor(m, pixData, e->getLength())); | |
1457 accessor->SetCurrentFrame(frame); | |
1458 } | |
1459 } | |
1460 else if (DecodePsmctRle1(privateContent, dataset)) | |
1461 { | |
1462 LOG(INFO) << "The PMSCT_RLE1 decoding has succeeded"; | |
1463 Uint8* pixData = NULL; | |
1464 if (privateContent.size() > 0) | |
1465 pixData = reinterpret_cast<Uint8*>(&privateContent[0]); | |
1466 accessor.reset(new DicomIntegerPixelAccessor(m, pixData, privateContent.size())); | |
1467 accessor->SetCurrentFrame(frame); | |
1468 } | |
1469 | |
1470 if (accessor.get() == NULL) | |
1471 { | |
1472 throw OrthancException(ErrorCode_BadFileFormat); | |
1473 } | |
1474 | |
1475 PixelFormat format; | |
1476 bool supported = false; | |
1477 | |
1478 if (accessor->GetChannelCount() == 1) | |
1479 { | |
1480 switch (mode) | |
1481 { | |
1482 case ImageExtractionMode_Preview: | |
1483 supported = true; | |
1484 format = PixelFormat_Grayscale8; | |
1485 break; | |
1486 | |
1487 case ImageExtractionMode_UInt8: | |
1488 supported = true; | |
1489 format = PixelFormat_Grayscale8; | |
1490 break; | |
1491 | |
1492 case ImageExtractionMode_UInt16: | |
1493 supported = true; | |
1494 format = PixelFormat_Grayscale16; | |
1495 break; | |
1496 | |
1497 case ImageExtractionMode_Int16: | |
1498 supported = true; | |
1499 format = PixelFormat_SignedGrayscale16; | |
1500 break; | |
1501 | |
1502 default: | |
1503 supported = false; | |
1504 break; | |
1505 } | |
1506 } | |
1507 else if (accessor->GetChannelCount() == 3) | |
1508 { | |
1509 switch (mode) | |
1510 { | |
1511 case ImageExtractionMode_Preview: | |
1512 supported = true; | |
1513 format = PixelFormat_RGB24; | |
1514 break; | |
1515 | |
1516 default: | |
1517 supported = false; | |
1518 break; | |
1519 } | |
1520 } | |
1521 | |
1522 if (!supported) | |
1523 { | |
1524 throw OrthancException(ErrorCode_NotImplemented); | |
1525 } | |
1526 | |
1527 if (accessor.get() == NULL || | |
1528 accessor->GetWidth() == 0 || | |
1529 accessor->GetHeight() == 0) | |
1530 { | |
1531 PngWriter w; | |
1532 w.WriteToMemory(result, 0, 0, 0, format, NULL); | |
1533 } | |
1534 else | |
1535 { | |
1536 switch (mode) | |
1537 { | |
1538 case ImageExtractionMode_Preview: | |
1539 if (format == PixelFormat_Grayscale8) | |
1540 ExtractPngImageGrayscalePreview(result, *accessor); | |
1541 else | |
1542 ExtractPngImageColorPreview(result, *accessor); | |
1543 break; | |
1544 | |
1545 case ImageExtractionMode_UInt8: | |
1546 ExtractPngImageTruncate<uint8_t>(result, *accessor, format); | |
1547 break; | |
1548 | |
1549 case ImageExtractionMode_UInt16: | |
1550 ExtractPngImageTruncate<uint16_t>(result, *accessor, format); | |
1551 break; | |
1552 | |
1553 case ImageExtractionMode_Int16: | |
1554 ExtractPngImageTruncate<int16_t>(result, *accessor, format); | |
1555 break; | |
1556 | |
1557 default: | |
1558 throw OrthancException(ErrorCode_NotImplemented); | |
1559 } | |
1560 } | |
1561 } | |
1562 | |
1563 | |
1564 void FromDcmtkBridge::ExtractPngImage(std::string& result, | |
1565 const std::string& dicomContent, | |
1566 unsigned int frame, | |
1567 ImageExtractionMode mode) | |
1568 { | |
1569 DcmInputBufferStream is; | |
1570 if (dicomContent.size() > 0) | |
1571 { | |
1572 is.setBuffer(&dicomContent[0], dicomContent.size()); | |
1573 } | |
1574 is.setEos(); | |
1575 | |
1576 DcmFileFormat dicom; | |
1577 if (dicom.read(is).good()) | |
1578 { | |
1579 ExtractPngImage(result, *dicom.getDataset(), frame, mode); | |
1580 } | |
1581 else | |
1582 { | |
1583 throw OrthancException(ErrorCode_BadFileFormat); | |
1584 } | |
1585 } | |
1586 | |
1587 | |
1588 | 431 |
1589 std::string FromDcmtkBridge::GetName(const DicomTag& t) | 432 std::string FromDcmtkBridge::GetName(const DicomTag& t) |
1590 { | 433 { |
1591 // Some patches for important tags because of different DICOM | 434 // Some patches for important tags because of different DICOM |
1592 // dictionaries between DCMTK versions | 435 // dictionaries between DCMTK versions |
1701 result[GetName(it->first)] = it->second->AsString(); | 544 result[GetName(it->first)] = it->second->AsString(); |
1702 } | 545 } |
1703 } | 546 } |
1704 | 547 |
1705 | 548 |
1706 std::string FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel level) | 549 std::string FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType level) |
1707 { | 550 { |
1708 char uid[100]; | 551 char uid[100]; |
1709 | 552 |
1710 switch (level) | 553 switch (level) |
1711 { | 554 { |
1712 case DicomRootLevel_Patient: | 555 case ResourceType_Patient: |
1713 // The "PatientID" field is of type LO (Long String), 64 | 556 // The "PatientID" field is of type LO (Long String), 64 |
1714 // Bytes Maximum. An UUID is of length 36, thus it can be used | 557 // Bytes Maximum. An UUID is of length 36, thus it can be used |
1715 // as a random PatientID. | 558 // as a random PatientID. |
1716 return Toolbox::GenerateUuid(); | 559 return Toolbox::GenerateUuid(); |
1717 | 560 |
1718 case DicomRootLevel_Instance: | 561 case ResourceType_Instance: |
1719 return dcmGenerateUniqueIdentifier(uid, SITE_INSTANCE_UID_ROOT); | 562 return dcmGenerateUniqueIdentifier(uid, SITE_INSTANCE_UID_ROOT); |
1720 | 563 |
1721 case DicomRootLevel_Series: | 564 case ResourceType_Series: |
1722 return dcmGenerateUniqueIdentifier(uid, SITE_SERIES_UID_ROOT); | 565 return dcmGenerateUniqueIdentifier(uid, SITE_SERIES_UID_ROOT); |
1723 | 566 |
1724 case DicomRootLevel_Study: | 567 case ResourceType_Study: |
1725 return dcmGenerateUniqueIdentifier(uid, SITE_STUDY_UID_ROOT); | 568 return dcmGenerateUniqueIdentifier(uid, SITE_STUDY_UID_ROOT); |
1726 | 569 |
1727 default: | 570 default: |
1728 throw OrthancException(ErrorCode_ParameterOutOfRange); | 571 throw OrthancException(ErrorCode_ParameterOutOfRange); |
1729 } | 572 } |