Mercurial > hg > orthanc-ohif
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 |