changeset 265:dab414e5b520 iiif

reorganization
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 10 Jul 2023 13:22:10 +0200
parents 75d933374805
children ca605878dc73
files Applications/Dicomizer.cpp Framework/Enumerations.h Framework/Inputs/DicomPyramidInstance.cpp ViewerPlugin/IIIF.cpp ViewerPlugin/OrthancExplorer.js
diffstat 5 files changed, 159 insertions(+), 60 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/Dicomizer.cpp	Mon Jul 10 12:24:46 2023 +0200
+++ b/Applications/Dicomizer.cpp	Mon Jul 10 13:22:10 2023 +0200
@@ -344,7 +344,7 @@
   }
 
   // VL Whole Slide Microscopy Image IOD
-  OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_SOPClassUID, "1.2.840.10008.5.1.4.1.1.77.1.6");
+  OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_SOPClassUID, VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE_IOD);
 
   // Slide Microscopy
   OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_Modality, "SM");  
--- a/Framework/Enumerations.h	Mon Jul 10 12:24:46 2023 +0200
+++ b/Framework/Enumerations.h	Mon Jul 10 13:22:10 2023 +0200
@@ -29,6 +29,8 @@
 
 namespace OrthancWSI
 {
+  static const char* const VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE_IOD = "1.2.840.10008.5.1.4.1.1.77.1.6";
+
   // WARNING - Don't change the enum values below, as this would break
   // serialization of "DicomPyramidInstance"
   enum ImageCompression
--- a/Framework/Inputs/DicomPyramidInstance.cpp	Mon Jul 10 12:24:46 2023 +0200
+++ b/Framework/Inputs/DicomPyramidInstance.cpp	Mon Jul 10 13:22:10 2023 +0200
@@ -160,7 +160,7 @@
     FullOrthancDataset dataset(orthanc, "/instances/" + instanceId + "/tags");
     DicomDatasetReader reader(dataset);
 
-    if (reader.GetMandatoryStringValue(Orthanc::DicomPath(Orthanc::DICOM_TAG_SOP_CLASS_UID)) != "1.2.840.10008.5.1.4.1.1.77.1.6" ||
+    if (reader.GetMandatoryStringValue(Orthanc::DicomPath(Orthanc::DICOM_TAG_SOP_CLASS_UID)) != VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE_IOD ||
         reader.GetMandatoryStringValue(Orthanc::DicomPath(Orthanc::DICOM_TAG_MODALITY)) != "SM")
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
--- a/ViewerPlugin/IIIF.cpp	Mon Jul 10 12:24:46 2023 +0200
+++ b/ViewerPlugin/IIIF.cpp	Mon Jul 10 13:22:10 2023 +0200
@@ -30,6 +30,7 @@
 #include <Images/Image.h>
 #include <Images/ImageProcessing.h>
 #include <Logging.h>
+#include <SerializationToolbox.h>
 
 #include <boost/regex.hpp>
 #include <boost/math/special_functions/round.hpp>
@@ -135,8 +136,8 @@
 
 
 static void ServeIIIFTiledImageTile(OrthancPluginRestOutput* output,
-                               const char* url,
-                               const OrthancPluginHttpRequest* request)
+                                    const char* url,
+                                    const OrthancPluginHttpRequest* request)
 {
   std::string seriesId(request->groups[0]);
   std::string region(request->groups[1]);
@@ -328,15 +329,62 @@
 }
 
 
+static void AddCanvas(Json::Value& manifest,
+                      const std::string& seriesId,
+                      const std::string& imageService,
+                      unsigned int page,
+                      unsigned int width,
+                      unsigned int height,
+                      const std::string& description)
+{
+  const std::string base = iiifPublicUrl_ + seriesId;
+
+  Json::Value service;
+  service["id"] = iiifPublicUrl_ + imageService;
+  service["profile"] = "level0";
+  service["type"] = "ImageService3";
+
+  Json::Value body;
+  body["id"] = iiifPublicUrl_ + imageService + "/full/max/0/default.jpg";
+  body["type"] = "Image";
+  body["format"] = Orthanc::EnumerationToString(Orthanc::MimeType_Jpeg);
+  body["height"] = height;
+  body["width"] = width;
+  body["service"].append(service);
+
+  Json::Value annotation;
+  annotation["id"] = base + "/annotation/p" + boost::lexical_cast<std::string>(page) + "-image";
+  annotation["type"] = "Annotation";
+  annotation["motivation"] = "painting";
+  annotation["body"] = body;
+  annotation["target"] = base + "/canvas/p" + boost::lexical_cast<std::string>(page);
+
+  Json::Value annotationPage;
+  annotationPage["id"] = base + "/page/p" + boost::lexical_cast<std::string>(page) + "/1";
+  annotationPage["type"] = "AnnotationPage";
+  annotationPage["items"].append(annotation);
+
+  Json::Value canvas;
+  canvas["id"] = annotation["target"];
+  canvas["type"] = "Canvas";
+  canvas["width"] = width;
+  canvas["height"] = height;
+  canvas["label"]["en"].append(description);
+  canvas["items"].append(annotationPage);
+
+  manifest["items"].append(canvas);
+}
+
+
 static void ServeIIIFManifest(OrthancPluginRestOutput* output,
                               const char* url,
                               const OrthancPluginHttpRequest* request)
 {
-  /**
-   * This is based on IIIF cookbook: "Support Deep Viewing with Basic
-   * Use of a IIIF Image Service."
-   * https://iiif.io/api/cookbook/recipe/0005-image-service/
-   **/
+  static const char* const KEY_INSTANCES = "Instances";
+  static const char* const SOP_CLASS_UID = "0008,0016";
+  static const char* const SLICES_SHORT = "SlicesShort";
+  static const char* const ROWS = "0028,0010";
+  static const char* const COLUMNS = "0028,0011";
 
   std::string seriesId(request->groups[0]);
 
@@ -349,65 +397,112 @@
     throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
   }
 
-  unsigned int width, height;
-
+  if (study.type() != Json::objectValue ||
+      series.type() != Json::objectValue ||
+      !series.isMember(KEY_INSTANCES) ||
+      series[KEY_INSTANCES].type() != Json::arrayValue ||
+      series[KEY_INSTANCES].size() == 0u ||
+      series[KEY_INSTANCES][0].type() != Json::stringValue)
   {
-    OrthancWSI::DicomPyramidCache::Locker locker(seriesId);
-    width = locker.GetPyramid().GetLevelWidth(0);
-    height = locker.GetPyramid().GetLevelHeight(0);
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
   }
 
+  Json::Value oneInstance;
+  if (!OrthancPlugins::RestApiGet(oneInstance, "/instances/" + series[KEY_INSTANCES][0].asString() + "/tags?short", false) ||
+      oneInstance.type() != Json::objectValue ||
+      !oneInstance.isMember(SOP_CLASS_UID) ||
+      oneInstance[SOP_CLASS_UID].type() != Json::stringValue)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+  }
+
+  const std::string sopClassUid = Orthanc::Toolbox::StripSpaces(oneInstance[SOP_CLASS_UID].asString());
+
   const std::string base = iiifPublicUrl_ + seriesId;
 
-  Json::Value service;
-  service["id"] = iiifPublicUrl_ + "tiles/" + seriesId;
-  service["profile"] = "level0";
-  service["type"] = "ImageService3";
-
-  Json::Value body;
-  body["id"] = iiifPublicUrl_ + "tiles/" + seriesId + "/full/max/0/default.jpg";
-  body["type"] = "Image";
-  body["format"] = Orthanc::EnumerationToString(Orthanc::MimeType_Jpeg);
-  body["height"] = height;
-  body["width"] = width;
-  body["service"].append(service);
-
-  Json::Value annotation;
-  annotation["id"] = base + "/annotation/p0001-image";
-  annotation["type"] = "Annotation";
-  annotation["motivation"] = "painting";
-  annotation["body"] = body;
-  annotation["target"] = base + "/canvas/p1";
-
-  Json::Value annotationPage;
-  annotationPage["id"] = base + "/page/p1/1";
-  annotationPage["type"] = "AnnotationPage";
-  annotationPage["items"].append(annotation);
-
-  Json::Value canvas;
-  canvas["id"] = annotation["target"];
-  canvas["type"] = "Canvas";
-  canvas["width"] = width;
-  canvas["height"] = height;
-
-  Json::Value labels = Json::arrayValue;
-  labels.append(series["MainDicomTags"]["SeriesDate"].asString() + " - " +
-                series["MainDicomTags"]["SeriesDescription"].asString());
-  canvas["label"]["en"] = labels;
-
-  canvas["items"].append(annotationPage);
-
   Json::Value manifest;
   manifest["@context"] = "http://iiif.io/api/presentation/3/context.json";
   manifest["id"] = base + "/manifest.json";
   manifest["type"] = "Manifest";
+  manifest["label"]["en"].append(study["MainDicomTags"]["StudyDate"].asString() + " - " +
+                                 study["MainDicomTags"]["StudyDescription"].asString());
 
-  labels = Json::arrayValue;
-  labels.append(study["MainDicomTags"]["StudyDate"].asString() + " - " +
-                study["MainDicomTags"]["StudyDescription"].asString());
-  manifest["label"]["en"] = labels;
+  if (sopClassUid == OrthancWSI::VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE_IOD)
+  {
+    /**
+     * This is based on IIIF cookbook: "Support Deep Viewing with Basic
+     * Use of a IIIF Image Service."
+     * https://iiif.io/api/cookbook/recipe/0005-image-service/
+     **/
+    unsigned int width, height;
+
+    {
+      OrthancWSI::DicomPyramidCache::Locker locker(seriesId);
+      width = locker.GetPyramid().GetLevelWidth(0);
+      height = locker.GetPyramid().GetLevelHeight(0);
+    }
+
+    const std::string description = (series["MainDicomTags"]["SeriesDate"].asString() + " - " +
+                                     series["MainDicomTags"]["SeriesDescription"].asString());
+
+    AddCanvas(manifest, seriesId, "tiles/" + seriesId, 1, width, height, description);
+  }
+  else
+  {
+    /**
+     * This is based on IIIF cookbook: "Simple Manifest - Book"
+     * https://iiif.io/api/cookbook/recipe/0009-book-1/
+     **/
+
+    uint32_t width, height;
+    if (!oneInstance.isMember(COLUMNS) ||
+        !oneInstance.isMember(ROWS) ||
+        oneInstance[COLUMNS].type() != Json::stringValue ||
+        oneInstance[ROWS].type() != Json::stringValue ||
+        !Orthanc::SerializationToolbox::ParseFirstUnsignedInteger32(width, oneInstance[COLUMNS].asString()) ||
+        !Orthanc::SerializationToolbox::ParseFirstUnsignedInteger32(height, oneInstance[ROWS].asString()))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+    }
 
-  manifest["items"].append(canvas);
+    Json::Value orderedSlices;
+    if (!OrthancPlugins::RestApiGet(orderedSlices, "/series/" + seriesId + "/ordered-slices", false))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+    }
+
+    if (orderedSlices.type() != Json::objectValue ||
+        !orderedSlices.isMember(SLICES_SHORT) ||
+        orderedSlices[SLICES_SHORT].type() != Json::arrayValue)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    const Json::Value& slicesShort = orderedSlices[SLICES_SHORT];
+
+    unsigned int page = 1;
+    for (Json::ArrayIndex instance = 0; instance < slicesShort.size(); instance++)
+    {
+      if (slicesShort[instance].type() != Json::arrayValue ||
+          slicesShort[instance].size() != 3u ||
+          slicesShort[instance][0].type() != Json::stringValue ||
+          slicesShort[instance][1].type() != Json::intValue ||
+          slicesShort[instance][2].type() != Json::intValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      const std::string instanceId = slicesShort[instance][0].asString();
+      const unsigned int start = slicesShort[instance][1].asUInt();
+      const unsigned int count = slicesShort[instance][2].asUInt();
+
+      for (unsigned int frame = start; frame < start + count; frame++, page++)
+      {
+        const std::string description = "Page " + boost::lexical_cast<std::string>(page);
+        AddCanvas(manifest, instanceId, "instances/" + instanceId, page, width, height, description);
+      }
+    }
+  }
 
   std::string s = manifest.toStyledString();
   OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), "application/json");
--- a/ViewerPlugin/OrthancExplorer.js	Mon Jul 10 12:24:46 2023 +0200
+++ b/ViewerPlugin/OrthancExplorer.js	Mon Jul 10 13:22:10 2023 +0200
@@ -24,9 +24,11 @@
   var seriesId = $.mobile.pageData.uuid;
 
   $('#mirador-button').remove();
-  $('#series-iiif').remove();
   $('#wsi-button').remove();
 
+  $('#series-iiif-button').remove();
+  $('#series-access').listview("refresh");
+
   // Test whether this is a whole-slide image by check the SOP Class
   // UID of one instance of the series
   GetResource('/series/' + seriesId, function(series) {
@@ -56,16 +58,16 @@
 
       if (${SERVE_IIIF}) {
         var b = $('<a>')
-            .attr('id', 'series-iiif-button')
             .attr('data-role', 'button')
             .attr('href', '#')
             .text('Copy link to IIIF manifest');
 
         var li = $('<li>')
+            .attr('id', 'series-iiif-button')
             .attr('data-icon', 'gear')
             .append(b);
 
-        $('#series-access').append(li);
+        $('#series-access').append(li).listview("refresh");
 
         b.click(function(e) {
           if ($.mobile.pageData) {