comparison Sources/Plugin.cpp @ 5:8c1fe0ca24f5

added the possibility to replace DICOMweb by DICOM JSON
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 16 Jun 2023 15:51:40 +0200
parents c34ecc350b32
children e8e7ba4371e3
comparison
equal deleted inserted replaced
4:c34ecc350b32 5:8c1fe0ca24f5
22 **/ 22 **/
23 23
24 24
25 #include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h" 25 #include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"
26 26
27 #include <Compression/GzipCompressor.h>
28 #include <DicomFormat/DicomInstanceHasher.h>
27 #include <DicomFormat/DicomMap.h> 29 #include <DicomFormat/DicomMap.h>
28 #include <Logging.h> 30 #include <Logging.h>
29 #include <SerializationToolbox.h> 31 #include <SerializationToolbox.h>
30 #include <SystemToolbox.h> 32 #include <SystemToolbox.h>
31 #include <Toolbox.h> 33 #include <Toolbox.h>
32 #include <Compression/GzipCompressor.h>
33 34
34 #include <EmbeddedResources.h> 35 #include <EmbeddedResources.h>
35 36
36 #include <boost/thread/shared_mutex.hpp> 37 #include <boost/thread/shared_mutex.hpp>
37 38
91 static void InitializeOhifTags() 92 static void InitializeOhifTags()
92 { 93 {
93 ohifStudyTags_[Orthanc::DICOM_TAG_STUDY_INSTANCE_UID] = TagInformation(DataType_String, "StudyInstanceUID"); 94 ohifStudyTags_[Orthanc::DICOM_TAG_STUDY_INSTANCE_UID] = TagInformation(DataType_String, "StudyInstanceUID");
94 ohifStudyTags_[Orthanc::DICOM_TAG_STUDY_DATE] = TagInformation(DataType_String, "StudyDate"); 95 ohifStudyTags_[Orthanc::DICOM_TAG_STUDY_DATE] = TagInformation(DataType_String, "StudyDate");
95 ohifStudyTags_[Orthanc::DICOM_TAG_STUDY_TIME] = TagInformation(DataType_String, "StudyTime"); 96 ohifStudyTags_[Orthanc::DICOM_TAG_STUDY_TIME] = TagInformation(DataType_String, "StudyTime");
97 ohifStudyTags_[Orthanc::DICOM_TAG_STUDY_DESCRIPTION] = TagInformation(DataType_String, "StudyDescription");
96 ohifStudyTags_[Orthanc::DICOM_TAG_PATIENT_NAME] = TagInformation(DataType_String, "PatientName"); 98 ohifStudyTags_[Orthanc::DICOM_TAG_PATIENT_NAME] = TagInformation(DataType_String, "PatientName");
97 ohifStudyTags_[Orthanc::DICOM_TAG_PATIENT_ID] = TagInformation(DataType_String, "PatientID"); 99 ohifStudyTags_[Orthanc::DICOM_TAG_PATIENT_ID] = TagInformation(DataType_String, "PatientID");
98 ohifStudyTags_[Orthanc::DICOM_TAG_ACCESSION_NUMBER] = TagInformation(DataType_String, "AccessionNumber"); 100 ohifStudyTags_[Orthanc::DICOM_TAG_ACCESSION_NUMBER] = TagInformation(DataType_String, "AccessionNumber");
99 ohifStudyTags_[Orthanc::DicomTag(0x0010, 0x1010)] = TagInformation(DataType_String, "PatientAge"); 101 ohifStudyTags_[Orthanc::DicomTag(0x0010, 0x1010)] = TagInformation(DataType_String, "PatientAge");
100 ohifStudyTags_[Orthanc::DICOM_TAG_PATIENT_SEX] = TagInformation(DataType_String, "PatientSex"); 102 ohifStudyTags_[Orthanc::DICOM_TAG_PATIENT_SEX] = TagInformation(DataType_String, "PatientSex");
101 103
102 ohifSeriesTags_[Orthanc::DICOM_TAG_SERIES_INSTANCE_UID] = TagInformation(DataType_String, "SeriesInstanceUID"); 104 ohifSeriesTags_[Orthanc::DICOM_TAG_SERIES_INSTANCE_UID] = TagInformation(DataType_String, "SeriesInstanceUID");
103 ohifSeriesTags_[Orthanc::DICOM_TAG_SERIES_NUMBER] = TagInformation(DataType_Integer, "SeriesNumber"); 105 ohifSeriesTags_[Orthanc::DICOM_TAG_SERIES_NUMBER] = TagInformation(DataType_Integer, "SeriesNumber");
106 ohifSeriesTags_[Orthanc::DICOM_TAG_SERIES_DESCRIPTION] = TagInformation(DataType_String, "SeriesDescription");
104 ohifSeriesTags_[Orthanc::DICOM_TAG_MODALITY] = TagInformation(DataType_String, "Modality"); 107 ohifSeriesTags_[Orthanc::DICOM_TAG_MODALITY] = TagInformation(DataType_String, "Modality");
105 ohifSeriesTags_[Orthanc::DICOM_TAG_SLICE_THICKNESS] = TagInformation(DataType_Float, "SliceThickness"); 108 ohifSeriesTags_[Orthanc::DICOM_TAG_SLICE_THICKNESS] = TagInformation(DataType_Float, "SliceThickness");
106 109
107 ohifInstanceTags_[Orthanc::DICOM_TAG_COLUMNS] = TagInformation(DataType_Integer, "Columns"); 110 ohifInstanceTags_[Orthanc::DICOM_TAG_COLUMNS] = TagInformation(DataType_Integer, "Columns");
108 ohifInstanceTags_[Orthanc::DICOM_TAG_ROWS] = TagInformation(DataType_Integer, "Rows"); 111 ohifInstanceTags_[Orthanc::DICOM_TAG_ROWS] = TagInformation(DataType_Integer, "Rows");
215 } 218 }
216 } 219 }
217 }; 220 };
218 221
219 222
220 static void GetOhifDicomTags(Json::Value& target, 223 static bool GetOhifDicomTags(Json::Value& target,
221 const std::string& instanceId) 224 const std::string& instanceId)
222 { 225 {
223 Json::Value source; 226 Json::Value source;
224 if (!OrthancPlugins::RestApiGet(source, "/instances/" + instanceId + "/tags?short", false)) 227 if (!OrthancPlugins::RestApiGet(source, "/instances/" + instanceId + "/tags?short", false))
225 { 228 {
226 throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); 229 return false;
227 } 230 }
228 231
229 if (source.type() != Json::objectValue) 232 if (source.type() != Json::objectValue)
230 { 233 {
231 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); 234 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
319 // This should never happen 322 // This should never happen
320 break; 323 break;
321 } 324 }
322 } 325 }
323 } 326 }
327
328 return true;
324 } 329 }
325 330
326 331
327 static ResourcesCache cache_; 332 static ResourcesCache cache_;
328 static std::string routerBasename_; 333 static std::string routerBasename_;
334 static bool useDicomWeb_;
329 335
330 void ServeFile(OrthancPluginRestOutput* output, 336 void ServeFile(OrthancPluginRestOutput* output,
331 const char* url, 337 const char* url,
332 const OrthancPluginHttpRequest* request) 338 const OrthancPluginHttpRequest* request)
333 { 339 {
337 // (https://web.dev/coop-coep/) 343 // (https://web.dev/coop-coep/)
338 OrthancPluginSetHttpHeader(context, output, "Cross-Origin-Embedder-Policy", "require-corp"); 344 OrthancPluginSetHttpHeader(context, output, "Cross-Origin-Embedder-Policy", "require-corp");
339 OrthancPluginSetHttpHeader(context, output, "Cross-Origin-Opener-Policy", "same-origin"); 345 OrthancPluginSetHttpHeader(context, output, "Cross-Origin-Opener-Policy", "same-origin");
340 OrthancPluginSetHttpHeader(context, output, "Cross-Origin-Resource-Policy", "same-origin"); 346 OrthancPluginSetHttpHeader(context, output, "Cross-Origin-Resource-Policy", "same-origin");
341 347
342 std::string uri = request->groups[0]; 348 const std::string uri = request->groups[0];
343 349
344 if (uri == "app-config.js") 350 if (uri == "app-config.js")
345 { 351 {
346 std::string system, user; 352 std::string system, user;
347 Orthanc::EmbeddedResources::GetFileResource(system, Orthanc::EmbeddedResources::APP_CONFIG_SYSTEM); 353 Orthanc::EmbeddedResources::GetFileResource(system, Orthanc::EmbeddedResources::APP_CONFIG_SYSTEM);
348 Orthanc::EmbeddedResources::GetFileResource(user, Orthanc::EmbeddedResources::APP_CONFIG_USER); 354 Orthanc::EmbeddedResources::GetFileResource(user, Orthanc::EmbeddedResources::APP_CONFIG_USER);
349 355
350 std::map<std::string, std::string> dictionary; 356 std::map<std::string, std::string> dictionary;
351 dictionary["ROUTER_BASENAME"] = routerBasename_; 357 dictionary["ROUTER_BASENAME"] = routerBasename_;
358 dictionary["USE_DICOM_WEB"] = (useDicomWeb_ ? "true" : "false");
352 359
353 system = Orthanc::Toolbox::SubstituteVariables(system, dictionary); 360 system = Orthanc::Toolbox::SubstituteVariables(system, dictionary);
354 361
355 std::string s = (user + "\n" + system); 362 std::string s = (user + "\n" + system);
356 OrthancPluginAnswerBuffer(context, output, s.c_str(), s.size(), "application/json"); 363 OrthancPluginAnswerBuffer(context, output, s.c_str(), s.size(), "application/json");
364 cache_.Answer(context, output, uri); 371 cache_.Answer(context, output, uri);
365 } 372 }
366 } 373 }
367 374
368 375
376 static void GenerateOhifStudy(Json::Value& target,
377 const std::string& studyId)
378 {
379 // https://v3-docs.ohif.org/configuration/dataSources/dicom-json
380 static const char* const KEY_ID = "ID";
381 const std::string KEY_PATIENT_ID = Orthanc::DICOM_TAG_PATIENT_ID.Format();
382 const std::string KEY_STUDY_INSTANCE_UID = Orthanc::DICOM_TAG_STUDY_INSTANCE_UID.Format();
383 const std::string KEY_SERIES_INSTANCE_UID = Orthanc::DICOM_TAG_SERIES_INSTANCE_UID.Format();
384 const std::string KEY_SOP_INSTANCE_UID = Orthanc::DICOM_TAG_SOP_INSTANCE_UID.Format();
385
386 Json::Value instancesIds;
387 if (!OrthancPlugins::RestApiGet(instancesIds, "/studies/" + studyId + "/instances", false))
388 {
389 throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
390 }
391
392 if (instancesIds.type() != Json::arrayValue)
393 {
394 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
395 }
396
397 std::vector<Json::Value> instancesTags;
398 instancesTags.reserve(instancesIds.size());
399
400 for (Json::ArrayIndex i = 0; i < instancesIds.size(); i++)
401 {
402 if (instancesIds[i].type() != Json::objectValue ||
403 !instancesIds[i].isMember(KEY_ID) ||
404 instancesIds[i][KEY_ID].type() != Json::stringValue)
405 {
406 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
407 }
408
409 Json::Value t;
410 if (GetOhifDicomTags(t, instancesIds[i][KEY_ID].asString()))
411 {
412 instancesTags.push_back(t);
413 }
414 }
415
416 typedef std::list<const Json::Value*> ListOfResources;
417 typedef std::map<std::string, ListOfResources> MapOfResources;
418
419 MapOfResources studies;
420 for (Json::ArrayIndex i = 0; i < instancesTags.size(); i++)
421 {
422 if (instancesTags[i].isMember(KEY_STUDY_INSTANCE_UID))
423 {
424 if (instancesTags[i][KEY_STUDY_INSTANCE_UID].type() != Json::stringValue)
425 {
426 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
427 }
428 else
429 {
430 const std::string& studyInstanceUid = instancesTags[i][KEY_STUDY_INSTANCE_UID].asString();
431 studies[studyInstanceUid].push_back(&instancesTags[i]);
432 }
433 }
434 }
435
436 target["studies"] = Json::arrayValue;
437
438 for (MapOfResources::const_iterator it = studies.begin(); it != studies.end(); ++it)
439 {
440 if (!it->second.empty())
441 {
442 assert(it->second.front() != NULL);
443 const Json::Value& firstInstanceInStudy = *it->second.front();
444
445 Json::Value study = Json::objectValue;
446 for (TagsDictionary::const_iterator tag = ohifStudyTags_.begin(); tag != ohifStudyTags_.end(); ++tag)
447 {
448 if (firstInstanceInStudy.isMember(tag->first.Format()))
449 {
450 study[tag->second.GetName()] = firstInstanceInStudy[tag->first.Format()];
451 }
452 }
453
454 MapOfResources seriesInStudy;
455 for (ListOfResources::const_iterator it2 = it->second.begin(); it2 != it->second.end(); ++it2)
456 {
457 assert(*it2 != NULL);
458 const Json::Value& instanceInStudy = **it2;
459
460 if (instanceInStudy.isMember(KEY_SERIES_INSTANCE_UID))
461 {
462 if (instanceInStudy[KEY_SERIES_INSTANCE_UID].type() != Json::stringValue)
463 {
464 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
465 }
466 else
467 {
468 const std::string& seriesInstanceUid = instanceInStudy[KEY_SERIES_INSTANCE_UID].asString();
469 seriesInStudy[seriesInstanceUid].push_back(&instanceInStudy);
470 }
471 }
472 }
473
474 study["series"] = Json::arrayValue;
475
476 for (MapOfResources::const_iterator it3 = seriesInStudy.begin(); it3 != seriesInStudy.end(); ++it3)
477 {
478 if (!it3->second.empty())
479 {
480 assert(it3->second.front() != NULL);
481 const Json::Value& firstInstanceInSeries = *it3->second.front();
482
483 Json::Value series = Json::objectValue;
484 for (TagsDictionary::const_iterator tag = ohifSeriesTags_.begin(); tag != ohifSeriesTags_.end(); ++tag)
485 {
486 if (firstInstanceInSeries.isMember(tag->first.Format()))
487 {
488 series[tag->second.GetName()] = firstInstanceInSeries[tag->first.Format()];
489 }
490 }
491
492 series["instances"] = Json::arrayValue;
493
494 for (ListOfResources::const_iterator it4 = it3->second.begin(); it4 != it3->second.end(); ++it4)
495 {
496 assert(*it4 != NULL);
497 const Json::Value& instanceInSeries = **it4;
498
499 Json::Value metadata;
500 for (TagsDictionary::const_iterator tag = ohifInstanceTags_.begin(); tag != ohifInstanceTags_.end(); ++tag)
501 {
502 if (instanceInSeries.isMember(tag->first.Format()))
503 {
504 metadata[tag->second.GetName()] = instanceInSeries[tag->first.Format()];
505 }
506 }
507
508 Orthanc::DicomInstanceHasher hasher(instanceInSeries[KEY_PATIENT_ID].asString(),
509 instanceInSeries[KEY_STUDY_INSTANCE_UID].asString(),
510 instanceInSeries[KEY_SERIES_INSTANCE_UID].asString(),
511 instanceInSeries[KEY_SOP_INSTANCE_UID].asString());
512
513 Json::Value instance = Json::objectValue;
514 instance["metadata"] = metadata;
515 instance["url"] = "dicomweb:../instances/" + hasher.HashInstance() + "/file";
516
517 series["instances"].append(instance);
518 }
519
520 study["series"].append(series);
521 }
522 }
523
524 target["studies"].append(study);
525 }
526 }
527 }
528
529
530 void GetOhifStudy(OrthancPluginRestOutput* output,
531 const char* url,
532 const OrthancPluginHttpRequest* request)
533 {
534 OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
535
536 const std::string studyId = request->groups[0];
537
538 Json::Value v;
539 GenerateOhifStudy(v, studyId);
540
541 std::string s;
542 Orthanc::Toolbox::WriteFastJson(s, v);
543
544 OrthancPluginAnswerBuffer(context, output, s.c_str(), s.size(), "application/json");
545 }
546
547
548 static OrthancPluginErrorCode RedirectRoot(OrthancPluginRestOutput* output,
549 const char* url,
550 const OrthancPluginHttpRequest* request)
551 {
552 OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
553
554 if (request->method != OrthancPluginHttpMethod_Get)
555 {
556 OrthancPluginSendMethodNotAllowed(context, output, "GET");
557 }
558 else
559 {
560 // TODO - Is there a better way to go to the list of studies in DICOMweb?
561 // TODO - What if not using DICOMweb?
562 OrthancPluginRedirect(context, output, "ohif/viewer?StudyInstanceUIDs=");
563 }
564
565 return OrthancPluginErrorCode_Success;
566 }
567
568
369 OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType, 569 OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType,
370 OrthancPluginResourceType resourceType, 570 OrthancPluginResourceType resourceType,
371 const char* resourceId) 571 const char* resourceId)
372 { 572 {
373 try 573 try
374 { 574 {
375 if (changeType == OrthancPluginChangeType_OrthancStarted) 575 if (changeType == OrthancPluginChangeType_OrthancStarted)
376 { 576 {
377 /*{ 577 if (useDicomWeb_)
378 Json::Value v; 578 {
379 GetOhifDicomTags(v, "54bfd747-407e46b1-ef106fdd-dc19e482-ff8dbe02"); 579 Json::Value info;
380 std::cout << v.toStyledString(); 580 if (!OrthancPlugins::RestApiGet(info, "/plugins/dicom-web", false))
381 std::string s; 581 {
382 Orthanc::Toolbox::WriteFastJson(s, v); 582 throw Orthanc::OrthancException(
383 std::cout << s.size() << std::endl; 583 Orthanc::ErrorCode_InternalError,
384 584 "The OHIF plugin requires the DICOMweb plugin to be installed");
385 Orthanc::GzipCompressor c; 585 }
386 std::string ss; 586
387 Orthanc::IBufferCompressor::Compress(ss, c, s); 587 if (info.type() != Json::objectValue ||
388 std::cout << ss.size() << std::endl; 588 !info.isMember("ID") ||
389 589 !info.isMember("Version") ||
390 std::string sss; 590 info["ID"].type() != Json::stringValue ||
391 Orthanc::Toolbox::EncodeBase64(sss, ss); 591 info["Version"].type() != Json::stringValue ||
392 std::cout << sss.size() << std::endl; 592 info["ID"].asString() != "dicom-web")
393 }*/ 593 {
394 594 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
395 Json::Value info; 595 "The DICOMweb plugin is required by OHIF, but is not properly installed");
396 if (!OrthancPlugins::RestApiGet(info, "/plugins/dicom-web", false)) 596 }
397 {
398 throw Orthanc::OrthancException(
399 Orthanc::ErrorCode_InternalError,
400 "The OHIF plugin requires the DICOMweb plugin to be installed");
401 }
402
403 if (info.type() != Json::objectValue ||
404 !info.isMember("ID") ||
405 !info.isMember("Version") ||
406 info["ID"].type() != Json::stringValue ||
407 info["Version"].type() != Json::stringValue ||
408 info["ID"].asString() != "dicom-web")
409 {
410 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
411 "The DICOMweb plugin is not properly installed");
412 } 597 }
413 } 598 }
414 } 599 }
415 catch (Orthanc::OrthancException& e) 600 catch (Orthanc::OrthancException& e)
416 { 601 {
455 OrthancPlugins::OrthancConfiguration globalConfiguration; 640 OrthancPlugins::OrthancConfiguration globalConfiguration;
456 globalConfiguration.GetSection(configuration, "OHIF"); 641 globalConfiguration.GetSection(configuration, "OHIF");
457 } 642 }
458 643
459 routerBasename_ = configuration.GetStringValue("RouterBasename", "/ohif"); 644 routerBasename_ = configuration.GetStringValue("RouterBasename", "/ohif");
645 useDicomWeb_ = configuration.GetBooleanValue("DicomWeb", false);
460 646
461 OrthancPluginSetDescription(context, "OHIF plugin for Orthanc."); 647 OrthancPluginSetDescription(context, "OHIF plugin for Orthanc.");
462 648
649 OrthancPluginRegisterRestCallback(context, "/ohif", RedirectRoot);
463 OrthancPlugins::RegisterRestCallback<ServeFile>("/ohif/(.*)", true); 650 OrthancPlugins::RegisterRestCallback<ServeFile>("/ohif/(.*)", true);
651 OrthancPlugins::RegisterRestCallback<GetOhifStudy>("/ohif-source/(.*)", true);
464 652
465 OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback); 653 OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
466 654
467 // Extend the default Orthanc Explorer with custom JavaScript for OHIF 655 {
468 std::string explorer; 656 // Extend the default Orthanc Explorer with custom JavaScript for OHIF
469 Orthanc::EmbeddedResources::GetFileResource(explorer, Orthanc::EmbeddedResources::ORTHANC_EXPLORER); 657 std::string explorer;
470 OrthancPluginExtendOrthancExplorer(context, explorer.c_str()); 658 Orthanc::EmbeddedResources::GetFileResource(explorer, Orthanc::EmbeddedResources::ORTHANC_EXPLORER);
659
660 std::map<std::string, std::string> dictionary;
661 dictionary["USE_DICOM_WEB"] = (useDicomWeb_ ? "true" : "false");
662 explorer = Orthanc::Toolbox::SubstituteVariables(explorer, dictionary);
663
664 OrthancPluginExtendOrthancExplorer(context, explorer.c_str());
665 }
471 666
472 return 0; 667 return 0;
473 } 668 }
474 669
475 670