comparison OrthancServer/OrthancRestApi/OrthancRestArchive.cpp @ 1778:776573e592da

create media refactoring
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 13 Nov 2015 13:12:21 +0100
parents 0f5c416969dc
children 559956d5ceb2
comparison
equal deleted inserted replaced
1777:0f5c416969dc 1778:776573e592da
52 52
53 namespace Orthanc 53 namespace Orthanc
54 { 54 {
55 // Download of ZIP files ---------------------------------------------------- 55 // Download of ZIP files ----------------------------------------------------
56 56
57 static std::string GetDirectoryNameInArchive(const Json::Value& resource,
58 ResourceType resourceType)
59 {
60 std::string s;
61 const Json::Value& tags = resource["MainDicomTags"];
62
63 switch (resourceType)
64 {
65 case ResourceType_Patient:
66 {
67 std::string p = tags["PatientID"].asString();
68 std::string n = tags["PatientName"].asString();
69 s = p + " " + n;
70 break;
71 }
72
73 case ResourceType_Study:
74 {
75 std::string p;
76 if (tags.isMember("AccessionNumber"))
77 {
78 p = tags["AccessionNumber"].asString() + " ";
79 }
80
81 s = p + tags["StudyDescription"].asString();
82 break;
83 }
84
85 case ResourceType_Series:
86 {
87 std::string d = tags["SeriesDescription"].asString();
88 std::string m = tags["Modality"].asString();
89 s = m + " " + d;
90 break;
91 }
92
93 default:
94 throw OrthancException(ErrorCode_InternalError);
95 }
96
97 // Get rid of special characters
98 return Toolbox::ConvertToAscii(s);
99 }
100
101 static bool CreateRootDirectoryInArchive(HierarchicalZipWriter& writer,
102 ServerContext& context,
103 const Json::Value& resource,
104 ResourceType resourceType)
105 {
106 if (resourceType == ResourceType_Patient)
107 {
108 return true;
109 }
110
111 ResourceType parentType = GetParentResourceType(resourceType);
112 Json::Value parent;
113
114 switch (resourceType)
115 {
116 case ResourceType_Study:
117 {
118 if (!context.GetIndex().LookupResource(parent, resource["ParentPatient"].asString(), parentType))
119 {
120 return false;
121 }
122
123 break;
124 }
125
126 case ResourceType_Series:
127 if (!context.GetIndex().LookupResource(parent, resource["ParentStudy"].asString(), parentType) ||
128 !CreateRootDirectoryInArchive(writer, context, parent, parentType))
129 {
130 return false;
131 }
132 break;
133
134 default:
135 throw OrthancException(ErrorCode_NotImplemented);
136 }
137
138 writer.OpenDirectory(GetDirectoryNameInArchive(parent, parentType).c_str());
139 return true;
140 }
141
142 static bool ArchiveInstance(HierarchicalZipWriter& writer,
143 ServerContext& context,
144 const std::string& instancePublicId,
145 const char* filename)
146 {
147 writer.OpenFile(filename);
148
149 std::string dicom;
150 context.ReadFile(dicom, instancePublicId, FileContentType_Dicom);
151 writer.Write(dicom);
152
153 return true;
154 }
155
156 static bool ArchiveInternal(HierarchicalZipWriter& writer,
157 ServerContext& context,
158 const std::string& publicId,
159 ResourceType resourceType,
160 bool isFirstLevel)
161 {
162 Json::Value resource;
163 if (!context.GetIndex().LookupResource(resource, publicId, resourceType))
164 {
165 return false;
166 }
167
168 if (isFirstLevel &&
169 !CreateRootDirectoryInArchive(writer, context, resource, resourceType))
170 {
171 return false;
172 }
173
174 writer.OpenDirectory(GetDirectoryNameInArchive(resource, resourceType).c_str());
175
176 switch (resourceType)
177 {
178 case ResourceType_Patient:
179 for (Json::Value::ArrayIndex i = 0; i < resource["Studies"].size(); i++)
180 {
181 std::string studyId = resource["Studies"][i].asString();
182 if (!ArchiveInternal(writer, context, studyId, ResourceType_Study, false))
183 {
184 return false;
185 }
186 }
187 break;
188
189 case ResourceType_Study:
190 for (Json::Value::ArrayIndex i = 0; i < resource["Series"].size(); i++)
191 {
192 std::string seriesId = resource["Series"][i].asString();
193 if (!ArchiveInternal(writer, context, seriesId, ResourceType_Series, false))
194 {
195 return false;
196 }
197 }
198 break;
199
200 case ResourceType_Series:
201 {
202 // Create a filename prefix, depending on the modality
203 char format[24] = "%08d.dcm";
204
205 if (resource["MainDicomTags"].isMember("Modality"))
206 {
207 std::string modality = resource["MainDicomTags"]["Modality"].asString();
208
209 if (modality.size() == 1)
210 {
211 snprintf(format, sizeof(format) - 1, "%c%%07d.dcm", toupper(modality[0]));
212 }
213 else if (modality.size() >= 2)
214 {
215 snprintf(format, sizeof(format) - 1, "%c%c%%06d.dcm", toupper(modality[0]), toupper(modality[1]));
216 }
217 }
218
219 char filename[24];
220
221 for (Json::Value::ArrayIndex i = 0; i < resource["Instances"].size(); i++)
222 {
223 snprintf(filename, sizeof(filename) - 1, format, i);
224
225 std::string publicId = resource["Instances"][i].asString();
226
227 // This was the implementation up to Orthanc 0.7.0:
228 // std::string filename = instance["MainDicomTags"]["SOPInstanceUID"].asString() + ".dcm";
229
230 if (!ArchiveInstance(writer, context, publicId, filename))
231 {
232 return false;
233 }
234 }
235
236 break;
237 }
238
239 default:
240 throw OrthancException(ErrorCode_InternalError);
241 }
242
243 writer.CloseDirectory();
244 return true;
245 }
246
247
248 static bool IsZip64Required(uint64_t uncompressedSize, 57 static bool IsZip64Required(uint64_t uncompressedSize,
249 unsigned int countInstances) 58 unsigned int countInstances)
250 { 59 {
251 static const uint64_t SAFETY_MARGIN = 64 * MEGA_BYTES; 60 static const uint64_t SAFETY_MARGIN = 64 * MEGA_BYTES;
252 61
264 << (uncompressedSize / MEGA_BYTES) << "MB using the " 73 << (uncompressedSize / MEGA_BYTES) << "MB using the "
265 << (isZip64 ? "ZIP64" : "ZIP32") << " file format"; 74 << (isZip64 ? "ZIP64" : "ZIP32") << " file format";
266 75
267 return isZip64; 76 return isZip64;
268 } 77 }
269
270
271 static bool IsZip64Required(ServerIndex& index,
272 const std::string& id)
273 {
274 uint64_t uncompressedSize;
275 uint64_t compressedSize;
276 unsigned int countStudies;
277 unsigned int countSeries;
278 unsigned int countInstances;
279
280 index.GetStatistics(compressedSize, uncompressedSize,
281 countStudies, countSeries, countInstances, id);
282
283 return IsZip64Required(uncompressedSize, countInstances);
284 }
285
286
287 template <enum ResourceType resourceType>
288 static void GetArchive(RestApiGetCall& call)
289 {
290 ServerContext& context = OrthancRestApi::GetContext(call);
291
292 std::string id = call.GetUriComponent("id", "");
293 bool isZip64 = IsZip64Required(context.GetIndex(), id);
294
295 // Create a RAII for the temporary file to manage the ZIP file
296 Toolbox::TemporaryFile tmp;
297
298 {
299 // Create a ZIP writer
300 HierarchicalZipWriter writer(tmp.GetPath().c_str());
301 writer.SetZip64(isZip64);
302
303 // Store the requested resource into the ZIP
304 if (!ArchiveInternal(writer, context, id, resourceType, true))
305 {
306 return;
307 }
308 }
309
310 // Prepare the sending of the ZIP file
311 FilesystemHttpSender sender(tmp.GetPath());
312 sender.SetContentType("application/zip");
313 sender.SetContentFilename(id + ".zip");
314
315 // Send the ZIP
316 call.GetOutput().AnswerStream(sender);
317
318 // The temporary file is automatically removed thanks to the RAII
319 }
320
321
322 static void GetMediaArchive(RestApiGetCall& call)
323 {
324 ServerContext& context = OrthancRestApi::GetContext(call);
325
326 std::string id = call.GetUriComponent("id", "");
327 bool isZip64 = IsZip64Required(context.GetIndex(), id);
328
329 // Create a RAII for the temporary file to manage the ZIP file
330 Toolbox::TemporaryFile tmp;
331
332 {
333 // Create a ZIP writer
334 HierarchicalZipWriter writer(tmp.GetPath().c_str());
335 writer.SetZip64(isZip64);
336 writer.OpenDirectory("IMAGES");
337
338 // Create the DICOMDIR writer
339 DicomDirWriter dicomDir;
340
341 // Retrieve the list of the instances
342 std::list<std::string> instances;
343 context.GetIndex().GetChildInstances(instances, id);
344
345 size_t pos = 0;
346 for (std::list<std::string>::const_iterator
347 it = instances.begin(); it != instances.end(); ++it, ++pos)
348 {
349 // "DICOM restricts the filenames on DICOM media to 8
350 // characters (some systems wrongly use 8.3, but this does not
351 // conform to the standard)."
352 std::string filename = "IM" + boost::lexical_cast<std::string>(pos);
353 writer.OpenFile(filename.c_str());
354
355 std::string dicom;
356 context.ReadFile(dicom, *it, FileContentType_Dicom);
357 writer.Write(dicom);
358
359 ParsedDicomFile parsed(dicom);
360 dicomDir.Add("IMAGES", filename, parsed);
361 }
362
363 // Add the DICOMDIR
364 writer.CloseDirectory();
365 writer.OpenFile("DICOMDIR");
366 std::string s;
367 dicomDir.Encode(s);
368 writer.Write(s);
369 }
370
371 // Prepare the sending of the ZIP file
372 FilesystemHttpSender sender(tmp.GetPath());
373 sender.SetContentType("application/zip");
374 sender.SetContentFilename(id + ".zip");
375
376 // Send the ZIP
377 call.GetOutput().AnswerStream(sender);
378
379 // The temporary file is automatically removed thanks to the RAII
380 }
381
382
383 78
384 79
385 namespace 80 namespace
386 { 81 {
387 class ResourceIdentifiers 82 class ResourceIdentifiers
724 const FileInfo& dicom) 419 const FileInfo& dicom)
725 { 420 {
726 out_ << " " << instanceId << std::endl; 421 out_ << " " << instanceId << std::endl;
727 } 422 }
728 }; 423 };
424
425
426 class ArchiveWriterVisitor : public IArchiveVisitor
427 {
428 private:
429 HierarchicalZipWriter& writer_;
430 ServerContext& context_;
431 char instanceFormat_[24];
432 unsigned int countInstances_;
433
434 static std::string GetTag(const DicomMap& tags,
435 const DicomTag& tag)
436 {
437 const DicomValue* v = tags.TestAndGetValue(tag);
438 if (v != NULL &&
439 !v->IsBinary() &&
440 !v->IsNull())
441 {
442 return v->GetContent();
443 }
444 else
445 {
446 return "";
447 }
448 }
449
450 public:
451 ArchiveWriterVisitor(HierarchicalZipWriter& writer,
452 ServerContext& context) :
453 writer_(writer),
454 context_(context)
455 {
456 }
457
458 virtual void Open(ResourceType level,
459 const std::string& publicId)
460 {
461 std::string path;
462
463 DicomMap tags;
464 if (context_.GetIndex().GetMainDicomTags(tags, publicId, level, level))
465 {
466 switch (level)
467 {
468 case ResourceType_Patient:
469 path = GetTag(tags, DICOM_TAG_PATIENT_ID) + " " + GetTag(tags, DICOM_TAG_PATIENT_NAME);
470 break;
471
472 case ResourceType_Study:
473 path = GetTag(tags, DICOM_TAG_ACCESSION_NUMBER) + " " + GetTag(tags, DICOM_TAG_STUDY_DESCRIPTION);
474 break;
475
476 case ResourceType_Series:
477 {
478 std::string modality = GetTag(tags, DICOM_TAG_MODALITY);
479 path = modality + " " + GetTag(tags, DICOM_TAG_SERIES_DESCRIPTION);
480
481 if (modality.size() == 0)
482 {
483 snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%%08d.dcm");
484 }
485 else if (modality.size() == 1)
486 {
487 snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%c%%07d.dcm",
488 toupper(modality[0]));
489 }
490 else if (modality.size() >= 2)
491 {
492 snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%c%c%%06d.dcm",
493 toupper(modality[0]), toupper(modality[1]));
494 }
495
496 countInstances_ = 0;
497
498 break;
499 }
500
501 default:
502 throw OrthancException(ErrorCode_InternalError);
503 }
504 }
505
506 path = Toolbox::StripSpaces(Toolbox::ConvertToAscii(path));
507
508 if (path.empty())
509 {
510 path = std::string("Unknown ") + EnumerationToString(level);
511 }
512
513 writer_.OpenDirectory(path.c_str());
514 }
515
516 virtual void Close()
517 {
518 writer_.CloseDirectory();
519 }
520
521 virtual void AddInstance(const std::string& instanceId,
522 const FileInfo& dicom)
523 {
524 std::string content;
525 context_.ReadFile(content, dicom);
526
527 char filename[24];
528 snprintf(filename, sizeof(filename) - 1, instanceFormat_, countInstances_);
529 countInstances_ ++;
530
531 writer_.OpenFile(filename);
532 writer_.Write(content);
533 }
534
535 static void Apply(RestApiOutput& output,
536 ServerContext& context,
537 ArchiveIndex& archive,
538 const std::string& filename)
539 {
540 archive.Expand(context.GetIndex());
541
542 StatisticsVisitor stats;
543 archive.Apply(stats);
544
545 const bool isZip64 = IsZip64Required(stats.GetUncompressedSize(), stats.GetInstancesCount());
546
547 // Create a RAII for the temporary file to manage the ZIP file
548 Toolbox::TemporaryFile tmp;
549
550 {
551 // Create a ZIP writer
552 HierarchicalZipWriter writer(tmp.GetPath().c_str());
553 writer.SetZip64(isZip64);
554
555 ArchiveWriterVisitor v(writer, context);
556 archive.Apply(v);
557 }
558
559 // Prepare the sending of the ZIP file
560 FilesystemHttpSender sender(tmp.GetPath());
561 sender.SetContentType("application/zip");
562 sender.SetContentFilename(filename);
563
564 // Send the ZIP
565 output.AnswerStream(sender);
566
567 // The temporary file is automatically removed thanks to the RAII
568 }
569 };
570
571
572 class MediaWriterVisitor : public IArchiveVisitor
573 {
574 private:
575 HierarchicalZipWriter& writer_;
576 DicomDirWriter dicomDir_;
577 ServerContext& context_;
578 unsigned int countInstances_;
579
580 public:
581 MediaWriterVisitor(HierarchicalZipWriter& writer,
582 ServerContext& context) :
583 writer_(writer),
584 context_(context),
585 countInstances_(0)
586 {
587 }
588
589 void EncodeDicomDir(std::string& result)
590 {
591 dicomDir_.Encode(result);
592 }
593
594 virtual void Open(ResourceType level,
595 const std::string& publicId)
596 {
597 }
598
599 virtual void Close()
600 {
601 }
602
603 virtual void AddInstance(const std::string& instanceId,
604 const FileInfo& dicom)
605 {
606 // "DICOM restricts the filenames on DICOM media to 8
607 // characters (some systems wrongly use 8.3, but this does not
608 // conform to the standard)."
609 std::string filename = "IM" + boost::lexical_cast<std::string>(countInstances_);
610 writer_.OpenFile(filename.c_str());
611
612 std::string content;
613 context_.ReadFile(content, dicom);
614 writer_.Write(content);
615
616 ParsedDicomFile parsed(content);
617 dicomDir_.Add("IMAGES", filename, parsed);
618
619 countInstances_ ++;
620 }
621
622 static void Apply(RestApiOutput& output,
623 ServerContext& context,
624 ArchiveIndex& archive,
625 const std::string& filename)
626 {
627 archive.Expand(context.GetIndex());
628
629 StatisticsVisitor stats;
630 archive.Apply(stats);
631
632 const bool isZip64 = IsZip64Required(stats.GetUncompressedSize(), stats.GetInstancesCount());
633
634 // Create a RAII for the temporary file to manage the ZIP file
635 Toolbox::TemporaryFile tmp;
636
637 {
638 // Create a ZIP writer
639 HierarchicalZipWriter writer(tmp.GetPath().c_str());
640 writer.SetZip64(isZip64);
641 writer.OpenDirectory("IMAGES");
642
643 // Create the DICOMDIR writer
644 DicomDirWriter dicomDir;
645
646 MediaWriterVisitor v(writer, context);
647 archive.Apply(v);
648
649 // Add the DICOMDIR
650 writer.CloseDirectory();
651 writer.OpenFile("DICOMDIR");
652 std::string s;
653 v.EncodeDicomDir(s);
654 writer.Write(s);
655 }
656
657 // Prepare the sending of the ZIP file
658 FilesystemHttpSender sender(tmp.GetPath());
659 sender.SetContentType("application/zip");
660 sender.SetContentFilename(filename);
661
662 // Send the ZIP
663 output.AnswerStream(sender);
664
665 // The temporary file is automatically removed thanks to the RAII
666 }
667 };
729 } 668 }
730 669
731 670
732 static void CreateBatchArchive(RestApiPostCall& call) 671 static bool AddResourcesOfInterest(ArchiveIndex& archive,
672 RestApiPostCall& call)
733 { 673 {
734 ServerIndex& index = OrthancRestApi::GetIndex(call); 674 ServerIndex& index = OrthancRestApi::GetIndex(call);
735 675
736 Json::Value resources; 676 Json::Value resources;
737 if (call.ParseJsonRequest(resources) && 677 if (call.ParseJsonRequest(resources) &&
738 resources.type() == Json::arrayValue) 678 resources.type() == Json::arrayValue)
739 { 679 {
740 ArchiveIndex archive(ResourceType_Patient); // root
741
742 for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++) 680 for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++)
743 { 681 {
744 if (resources[i].type() != Json::stringValue) 682 if (resources[i].type() != Json::stringValue)
745 { 683 {
746 return; // Bad request 684 return false; // Bad request
747 } 685 }
748 686
749 ResourceIdentifiers resource(index, resources[i].asString()); 687 ResourceIdentifiers resource(index, resources[i].asString());
750 archive.Add(index, resource); 688 archive.Add(index, resource);
751 } 689 }
752 690
753 archive.Expand(index); 691 return true;
754 692 }
755 PrintVisitor v(std::cout); 693 else
756 archive.Apply(v); 694 {
757 695 return false;
758 StatisticsVisitor s; 696 }
759 archive.Apply(s); 697 }
760 698
761 std::cout << s.GetUncompressedSize() << " " << s.GetInstancesCount() << std::endl; 699
700 static void CreateBatchArchive(RestApiPostCall& call)
701 {
702 ArchiveIndex archive(ResourceType_Patient); // root
703
704 if (AddResourcesOfInterest(archive, call))
705 {
706 ArchiveWriterVisitor::Apply(call.GetOutput(),
707 OrthancRestApi::GetContext(call),
708 archive,
709 "Archive.zip");
762 } 710 }
763 } 711 }
764 712
765 713
714 static void CreateBatchMedia(RestApiPostCall& call)
715 {
716 ArchiveIndex archive(ResourceType_Patient); // root
717
718 if (AddResourcesOfInterest(archive, call))
719 {
720 MediaWriterVisitor::Apply(call.GetOutput(),
721 OrthancRestApi::GetContext(call),
722 archive,
723 "Archive.zip");
724 }
725 }
726
727
728 static void CreateArchive(RestApiGetCall& call)
729 {
730 ServerIndex& index = OrthancRestApi::GetIndex(call);
731
732 std::string id = call.GetUriComponent("id", "");
733 ResourceIdentifiers resource(index, id);
734
735 ArchiveIndex archive(ResourceType_Patient); // root
736 archive.Add(OrthancRestApi::GetIndex(call), resource);
737
738 ArchiveWriterVisitor::Apply(call.GetOutput(),
739 OrthancRestApi::GetContext(call),
740 archive,
741 id + ".zip");
742 }
743
744
745 static void CreateMedia(RestApiGetCall& call)
746 {
747 ServerIndex& index = OrthancRestApi::GetIndex(call);
748
749 std::string id = call.GetUriComponent("id", "");
750 ResourceIdentifiers resource(index, id);
751
752 ArchiveIndex archive(ResourceType_Patient); // root
753 archive.Add(OrthancRestApi::GetIndex(call), resource);
754
755 MediaWriterVisitor::Apply(call.GetOutput(),
756 OrthancRestApi::GetContext(call),
757 archive,
758 id + ".zip");
759 }
760
766 761
767 void OrthancRestApi::RegisterArchive() 762 void OrthancRestApi::RegisterArchive()
768 { 763 {
769 Register("/patients/{id}/archive", GetArchive<ResourceType_Patient>); 764 Register("/patients/{id}/archive", CreateArchive);
770 Register("/studies/{id}/archive", GetArchive<ResourceType_Study>); 765 Register("/studies/{id}/archive", CreateArchive);
771 Register("/series/{id}/archive", GetArchive<ResourceType_Series>); 766 Register("/series/{id}/archive", CreateArchive);
772 767
773 Register("/patients/{id}/media", GetMediaArchive); 768 Register("/patients/{id}/media", CreateMedia);
774 Register("/studies/{id}/media", GetMediaArchive); 769 Register("/studies/{id}/media", CreateMedia);
775 Register("/series/{id}/media", GetMediaArchive); 770 Register("/series/{id}/media", CreateMedia);
776 771
777 Register("/tools/archive", CreateBatchArchive); 772 Register("/tools/create-archive", CreateBatchArchive);
773 Register("/tools/create-media", CreateBatchMedia);
778 } 774 }
779 } 775 }