changeset 1865:4d83d27a955e

merge
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 24 Nov 2021 12:25:32 +0100
parents 35ac2be493e2 (current diff) ff03eb63e847 (diff)
children c26c18e876e3
files TODO
diffstat 16 files changed, 278 insertions(+), 32 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Wed Nov 24 12:25:19 2021 +0100
+++ b/.hgignore	Wed Nov 24 12:25:32 2021 +0100
@@ -22,6 +22,7 @@
 Applications/Samples/Deprecated/WebAssembly/ThirdPartyDownloads/
 Applications/Samples/Deprecated/WebAssembly/installDir/
 StoneWebViewer/Plugin/ThirdPartyDownloads/
+StoneWebViewer/Resources/package-lock.json
 StoneWebViewer/WebAssembly/ThirdPartyDownloads/
 UnitTestsSources/ThirdPartyDownloads/
 node_modules/
--- a/Applications/Samples/RtViewerPlugin/CMakeLists.txt	Wed Nov 24 12:25:19 2021 +0100
+++ b/Applications/Samples/RtViewerPlugin/CMakeLists.txt	Wed Nov 24 12:25:32 2021 +0100
@@ -27,7 +27,7 @@
   set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "mainline")
   set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
 else()
-  set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.9.4")
+  set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.9.7")
   set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
 endif()
 
--- a/Applications/Samples/WebAssembly/CMakeLists.txt	Wed Nov 24 12:25:19 2021 +0100
+++ b/Applications/Samples/WebAssembly/CMakeLists.txt	Wed Nov 24 12:25:32 2021 +0100
@@ -28,7 +28,7 @@
   set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "mainline")
   set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
 else()
-  set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.9.4")
+  set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.9.7")
   set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
 endif()
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/StoneWebViewer/BuildInstructions.txt	Wed Nov 24 12:25:32 2021 +0100
@@ -0,0 +1,73 @@
+Build & develop instructions
+============================
+
+Prerequisites:
+-------------
+
+- Install the usual build tools to build an Orthanc plugin (a good reference is the orthanc base image Dockerfile): https://github.com/jodogne/OrthancDocker/blob/master/base/Dockerfile
+- Install EMSDK (a good reference is the wasm-builder image Dockerfile): https://github.com/jodogne/OrthancDocker/tree/master/wasm-builder
+
+Create your build environment:
+-----------------------------
+
+mkdir ~/dev
+cd ~/dev
+hg clone https://hg.orthanc-server.com/orthanc/
+hg clone https://hg.orthanc-server.com/orthanc-stone/
+hg clone https://hg.orthanc-server.com/orthanc-dicomweb/
+mkdir -p ~/dev/build/orthanc
+mkdir -p ~/dev/build/orthanc-dicomweb
+mkdir -p ~/dev/build/wasm-stone-viewer
+mkdir -p ~/dev/build/stone-viewer-plugin
+
+# build orthanc
+cd ~/dev/build/orthanc
+cmake -DCMAKE_BUILD_TYPE=Release -DUSE_SYSTEM_CIVETWEB=OFF -DALLOW_DOWNLOADS=ON ../../orthanc/OrthancServer
+make -j 6
+
+# build orthanc-dicomweb
+cd ~/dev/build/orthanc-dicomweb
+cmake -DCMAKE_BUILD_TYPE=Release -DSTATIC_BUILD -DALLOW_DOWNLOADS=ON ../../orthanc-dicomweb
+make -j 6
+
+# build the StoneViewer WASM code
+# note: for fast link time: use -DCMAKE_BUILD_TYPE=RelWithDebInfo.  However, this produces an output that is too large to be embedded in the plugin
+# therefore, when you build the plugin, build the WASM code with -DCMAKE_BUILD_TYPE=Release
+cd ~/dev/build/wasm-stone-viewer
+cmake ../../orthanc-stone/Applications/StoneWebViewer/WebAssembly -DLIBCLANG=/usr/lib/x86_64-linux-gnu/libclang-10.so -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=${EMSDK}/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake -DALLOW_DOWNLOADS=ON -G Ninja
+ninja install
+
+# build the StoneViewer Plugin
+cd ~/dev/build/stone-viewer-plugin
+cmake ../../orthanc-stone/Applications/StoneWebViewer/Plugin -DUSE -DCMAKE_BUILD_TYPE=Release -DSTATIC_BUILD=ON -DALLOW_DOWNLOADS=ON
+make -j 6
+
+
+Updating the code:
+-----------------
+
+To avoid rebuilding the plugin everytime you update the HTML/CSS/JS or C++ code from the WebAssembly, you can use the ServeFolders plugin
+to host this part of the code.  Here's a sample configuration file that works well for StoneViewer development:
+
+{
+    "AuthenticationEnabled": false,
+    "HttpPort": 8043,
+    "Plugins": [
+        "/home/alain/dev/build/stone-viewer-plugin/libStoneWebViewer.so",
+        "/home/alain/dev/build/orthanc-dicomweb/libOrthancDicomWeb.so",
+        "/home/alain/dev/build/orthanc/libServeFolders.so"
+    ],
+
+    "ServeFolders": {
+        "/stone-webviewer-live" : "/home/alain/o/orthanc-stone/wasm-binaries/StoneWebViewer/"
+    }
+}
+
+Everytime you modify the HTML/CSS/JS or C++ code from the WebAssembly, you don't need to restart Orthanc and can simply run
+cd ~/dev/build/wasm-stone-viewer && ninja install
+Then, the viewer is available on http://localhost:8043/stone-webviewer-live/index.html?study=1.2.3.4.5
+
+If you modify the plugin code, you must of course rebuild the plugin and restart Orthanc
+
+If you modify the scss files, you'll need to run:
+- Applications/StoneWebViewer/Resources$ ./node_modules/node-sass/bin/node-sass ./Styles/styles.scss > ../WebApplication/app.css as explained in Applications/StoneWebViewer/Resources/Styles.txt
\ No newline at end of file
--- a/Applications/StoneWebViewer/NEWS	Wed Nov 24 12:25:19 2021 +0100
+++ b/Applications/StoneWebViewer/NEWS	Wed Nov 24 12:25:32 2021 +0100
@@ -1,6 +1,21 @@
 Pending changes in the mainline
 ===============================
 
+* SeriesList: 
+  - display the SeriesNumber tag in front of image count.
+  - order series by SeriesNumber
+  - don't show non displayable series (see "SkipSeriesFromModalities")
+* In the top right overlay, display ContentDate/ContentTime if they are 
+  available in the instance.  If not, StudyDate is displayed (previous
+  behavior)
+* New configuration options:
+  - "TimeFormat" to control the way Dicom Times are displayed.
+  - "SkipSeriesFromModalities" to ignore series from given modality types.
+
+
+Version 2.2 (2021-08-31)
+========================
+
 * Support detection of windowing and rescale in Philips multiframe images
 * Fix values reported in "ww/wc" info panel (windowing width and center)
 
--- a/Applications/StoneWebViewer/Resources/Styles/_serieslist.scss	Wed Nov 24 12:25:19 2021 +0100
+++ b/Applications/StoneWebViewer/Resources/Styles/_serieslist.scss	Wed Nov 24 12:25:32 2021 +0100
@@ -80,11 +80,14 @@
     line-height:15px;
     width:15px;
     height:15px;
-    border-radius: 100%;
+    border-radius: 5px;
     background-color: $gray;
     vertical-align: middle;
     text-align: center;
     font-weight: bold;
+    width: max-content;
+    padding: 1px 5px;
+    line-height: 12px;
 }
 .wvSerieslist__information{
     font-size: 14px;
--- a/Applications/StoneWebViewer/Version.cmake	Wed Nov 24 12:25:19 2021 +0100
+++ b/Applications/StoneWebViewer/Version.cmake	Wed Nov 24 12:25:32 2021 +0100
@@ -23,6 +23,6 @@
   set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "mainline")
   set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
 else()
-  set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.9.4")
+  set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.9.7")
   set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
 endif()
--- a/Applications/StoneWebViewer/WebApplication/app.css	Wed Nov 24 12:25:19 2021 +0100
+++ b/Applications/StoneWebViewer/WebApplication/app.css	Wed Nov 24 12:25:32 2021 +0100
@@ -2330,11 +2330,14 @@
   line-height: 15px;
   width: 15px;
   height: 15px;
-  border-radius: 100%;
+  border-radius: 5px;
   background-color: gray;
   vertical-align: middle;
   text-align: center;
-  font-weight: bold; }
+  font-weight: bold;
+  width: max-content;
+  padding: 1px 5px;
+  line-height: 12px; }
 
 .wvSerieslist__information {
   font-size: 14px;
--- a/Applications/StoneWebViewer/WebApplication/app.js	Wed Nov 24 12:25:19 2021 +0100
+++ b/Applications/StoneWebViewer/WebApplication/app.js	Wed Nov 24 12:25:32 2021 +0100
@@ -150,7 +150,9 @@
       videoUri: '',
       windowingCenter: 0, 
       windowingWidth: 0,
-      instanceNumber: 0
+      instanceNumber: 0,
+      contentDate: '',
+      contentTime: '',
     }
   },
   watch: {
@@ -171,7 +173,9 @@
       this.windowingCenter = 0;
       this.windowingWidth = 0;
       this.instanceNumber = 0;
-      
+      this.contentDate = '';
+      this.contentTime = ''
+
       if (this.cineTimeoutId !== null) {
         clearTimeout(this.cineTimeoutId);
         this.cineTimeoutId = null;
@@ -255,6 +259,8 @@
         that.numberOfFrames = args.detail.numberOfFrames;
         that.quality = args.detail.quality;
         that.instanceNumber = args.detail.instanceNumber;
+        that.contentDate = args.detail.contentDate;
+        that.contentTime = args.detail.contentTime;
       }
     });
 
@@ -561,6 +567,9 @@
       var studies = [];
       var posColor = 0;
 
+      // order series by SeriesNumber
+      sourceSeries.sort((a, b) => {return a[SERIES_NUMBER] - b[SERIES_NUMBER];})
+
       for (var i = 0; i < sourceStudies.length; i++) {
         var studyInstanceUid = sourceStudies[i][STUDY_INSTANCE_UID];
         if (studyInstanceUid !== undefined) {
@@ -938,6 +947,43 @@
       }
     },
 
+    FormatTime: function(time)
+    {
+      if (time === undefined ||
+        time.length == 0) {
+        return '';
+      }
+      else {
+        var format = this.globalConfiguration['TimeFormat'];
+        if (format === undefined) {
+          // No configuration for the date format, use the DICOM tag as such
+          return time;
+        }
+        else {
+          var timeRegexHMS = /([0-9]{2})([0-9]{2})([0-9]{2})/;
+          var timeRegexHMSms = /([0-9]{2})([0-9]{2})([0-9]{2}).([0-9]*)/
+          var m = time.match(timeRegexHMSms);
+          if (m) {
+            format = format.replace(/hh/g, m[1]).replace(/mm/g, m[2]).replace(/ss/g, m[3]);
+            if (format.indexOf('f') != -1) { // format expects ms
+              return format.replace(/f/g, m[4])
+            } else {
+              return format;
+            }
+          }
+          var m = time.match(timeRegexHMS);
+          if (m) {
+            format = format.replace(/hh/g, m[1]).replace(/mm/g, m[2]).replace(/ss/g, m[3]);
+            if (format.indexOf('f') != -1) { // format expects ms but we could not capture one
+              return format.replace(/.f/g, '')
+            }
+          }
+
+          return time;
+        }
+      }
+    },
+
     DownloadJpeg: function()
     {
       var canvas = document.getElementById(this.GetActiveCanvas());
@@ -1097,6 +1143,10 @@
     stone.SetDicomCacheSize(app.globalConfiguration.DicomCacheSize);
   }
 
+  if ('SkipSeriesFromModalities' in app.globalConfiguration) {
+    stone.SetSkipSeriesFromModalities(JSON.stringify(app.globalConfiguration.SkipSeriesFromModalities));
+  }
+  
   // Bearer token is new in Stone Web viewer 2.0
   var token = getParameterFromUrl('token');
   if (token !== undefined)
@@ -1161,7 +1211,7 @@
 
 
 window.addEventListener('ResourcesLoaded', function() {
-  console.log('resources loaded');
+  console.log('resources loaded: ', stone.GetStudiesCount(), 'studies &', stone.GetSeriesCount(), 'series');
 
   var studies = [];
   for (var i = 0; i < stone.GetStudiesCount(); i++) {
--- a/Applications/StoneWebViewer/WebApplication/configuration.json	Wed Nov 24 12:25:19 2021 +0100
+++ b/Applications/StoneWebViewer/WebApplication/configuration.json	Wed Nov 24 12:25:32 2021 +0100
@@ -9,6 +9,15 @@
     // "DateFormat" : "DD/MM/YYYY",
 
     /**
+     * Defines how times are displayed in the UI. If this option is not
+     * set, the DICOM tags will be displayed as such. "hh" will be
+     * replaced by the hour, "mm" by the minutes, "ss" by the seconds
+     * and ".f" by the fractions of seconds.
+     **/
+    // "TimeFormat" : "hh:mm:ss.f",
+
+
+    /**
      * This option allows you to define windowing presets.
      * For each preset, you must provide a name, the window width
      * and window center.
@@ -105,6 +114,11 @@
      * image, this logo will be displayed at the bottom-left of the
      * Stone Web viewer.
      **/
-    "InstitutionLogo" : ""
+    "InstitutionLogo" : "",
+
+    /**
+     * Define a list of modality type that the viewer will ignore.
+     **/
+    "SkipSeriesFromModalities": ["SR", "SEG", "PR"]
   }
 }
--- a/Applications/StoneWebViewer/WebApplication/index.html	Wed Nov 24 12:25:19 2021 +0100
+++ b/Applications/StoneWebViewer/WebApplication/index.html	Wed Nov 24 12:25:32 2021 +0100
@@ -243,7 +243,11 @@
                                      />
                                 
                                 <div v-bind:class="'wvSerieslist__badge--' + study.color"
-                                     v-if="series[seriesIndex].numberOfFrames != 0">{{ series[seriesIndex].numberOfFrames }}</div>
+                                     v-if="series[seriesIndex].numberOfFrames != 0 || series[seriesIndex].tags[SERIES_NUMBER] !== undefined">
+                                     <span v-if="series[seriesIndex].tags[SERIES_NUMBER] !== undefined">#{{ series[seriesIndex].tags[SERIES_NUMBER] }}</span>
+                                     <span v-if="series[seriesIndex].numberOfFrames != 0 && series[seriesIndex].tags[SERIES_NUMBER] !== undefined"> - </span>
+                                     <span v-if="series[seriesIndex].numberOfFrames != 0">{{ series[seriesIndex].numberOfFrames }}</span>
+                                </div>
                               </div>
                             </div>
 
@@ -672,7 +676,8 @@
                   </div>
                   <div v-if="'tags' in content.series" class="wv-overlay-topright">
                     {{ content.series.tags[STUDY_DESCRIPTION] }}<br/>
-                    {{ app.FormatDate(content.series.tags[STUDY_DATE]) }}<br/>
+                    <span v-if="contentDate !== undefined && contentDate != ''">{{ app.FormatDate(contentDate) }} <span v-show="contentTime != ''">{{ app.FormatTime(contentTime) }}</span><br/></span>
+                    <span v-else="contentDate === undefined || contentDate == ''">{{ app.FormatDate(content.series.tags[STUDY_DATE]) }}<br/></span>
                     {{ content.series.tags[SERIES_NUMBER] }} | {{ content.series.tags[SERIES_DESCRIPTION] }}
                   </div>
                   <div class="wv-overlay-bottomleft wvPrintExclude" style="bottom: 0px">
@@ -813,9 +818,6 @@
         </div>
       </div>
     </script>
-
-    
-
     <script src="js/jquery-3.4.1.min.js"></script>
     <script src="js/bootstrap.min.js"></script>
     <script src="js/vue.min.js"></script>
--- a/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Wed Nov 24 12:25:19 2021 +0100
+++ b/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Wed Nov 24 12:25:32 2021 +0100
@@ -98,7 +98,7 @@
 #include <boost/math/special_functions/round.hpp>
 #include <boost/make_shared.hpp>
 #include <stdio.h>
-
+#include <algorithm>
 
 #if !defined(STONE_WEB_VIEWER_EXPORT)
 // We are not running ParseWebAssemblyExports.py, but we're compiling the wasm
@@ -321,6 +321,7 @@
   boost::shared_ptr<OrthancStone::SeriesMetadataLoader>    metadataLoader_;
   std::set<std::string>                                    scheduledVirtualSeriesThumbnails_;
   VirtualSeries                                            virtualSeries_;
+  std::vector<std::string>                                 skipSeriesFromModalities_;
 
   explicit ResourcesLoader(OrthancStone::ILoadersContext& context,
                            const OrthancStone::DicomSource& source) :
@@ -342,20 +343,41 @@
     LOG(INFO) << "resources loaded: " << dicom.GetSize()
               << ", " << Orthanc::EnumerationToString(payload.GetValue());
 
+    std::vector<std::string> seriesIdsToRemove;
+
     if (payload.GetValue() == Orthanc::ResourceType_Series)
     {
+      // the 'dicom' var is actually equivalent to the 'series_' member in this case
+
       for (size_t i = 0; i < dicom.GetSize(); i++)
       {
-        std::string studyInstanceUid, seriesInstanceUid;
+        std::string studyInstanceUid, seriesInstanceUid, modality;
         if (dicom.GetResource(i).LookupStringValue(
               studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) &&
             dicom.GetResource(i).LookupStringValue(
-              seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false))
+              seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false) &&
+            dicom.GetResource(i).LookupStringValue(
+              modality, Orthanc::DICOM_TAG_MODALITY, false))
         {
-          thumbnailsLoader_->ScheduleLoadThumbnail(source_, "", studyInstanceUid, seriesInstanceUid);
-          metadataLoader_->ScheduleLoadSeries(PRIORITY_LOW + 1, source_, studyInstanceUid, seriesInstanceUid);
+          // skip series that should not be displayed
+          if (std::find(skipSeriesFromModalities_.begin(), skipSeriesFromModalities_.end(), modality) == skipSeriesFromModalities_.end())
+          {
+            thumbnailsLoader_->ScheduleLoadThumbnail(source_, "", studyInstanceUid, seriesInstanceUid);
+            metadataLoader_->ScheduleLoadSeries(PRIORITY_LOW + 1, source_, studyInstanceUid, seriesInstanceUid);
+          }
+
+          else
+          {
+            seriesIdsToRemove.push_back(seriesInstanceUid);
+          }
         }
       }
+
+      for (size_t i = 0; i < seriesIdsToRemove.size(); i++)
+      {
+        LOG(INFO) << "series to hide: " << seriesIdsToRemove[i];
+        dicom.RemoveResource(seriesIdsToRemove[i]);  
+      }
     }
 
     if (pending_ == 0)
@@ -515,6 +537,11 @@
   }
 
 public:
+  void SetSkipSeriesFromModalities(const std::vector<std::string>& skipSeriesFromModalities)
+  {
+    skipSeriesFromModalities_ = skipSeriesFromModalities;
+  }
+
   static boost::shared_ptr<ResourcesLoader> Create(OrthancStone::ILoadersContext::ILock& lock,
                                                    const OrthancStone::DicomSource& source)
   {
@@ -1450,7 +1477,9 @@
                                     size_t currentFrame,
                                     size_t countFrames,
                                     DisplayedFrameQuality quality,
-                                    unsigned int instanceNumber) = 0;
+                                    unsigned int instanceNumber,
+                                    const std::string& contentDate,
+                                    const std::string& contentTime) = 0;
 
     // "click" is a 3D vector in world coordinates
     virtual void SignalCrosshair(const ViewerViewport& viewport,
@@ -1972,13 +2001,15 @@
     {
       const Orthanc::DicomMap& instance = frames_->GetInstanceOfFrame(cursor_->GetCurrentIndex()).GetTags();
 
-      uint32_t instanceNumber;
-      if (!instance.ParseUnsignedInteger32(instanceNumber, Orthanc::DICOM_TAG_INSTANCE_NUMBER))
-      {
-        instanceNumber = 0;
-      }
-      
-      observer_->SignalFrameUpdated(*this, cursorIndex, frames_->GetFramesCount(), quality, instanceNumber);
+      uint32_t instanceNumber = 0;
+      std::string contentDate;
+      std::string contentTime;
+
+      instance.ParseUnsignedInteger32(instanceNumber, Orthanc::DICOM_TAG_INSTANCE_NUMBER);
+      instance.LookupStringValue(contentDate, Orthanc::DicomTag(0x0008, 0x0023), false);
+      instance.LookupStringValue(contentTime, Orthanc::DicomTag(0x0008, 0x0033), false);
+
+      observer_->SignalFrameUpdated(*this, cursorIndex, frames_->GetFramesCount(), quality, instanceNumber, contentDate, contentTime);
     }
   }
   
@@ -2575,7 +2606,7 @@
     if (observer_.get() != NULL)
     {
       observer_->SignalFrameUpdated(*this, cursor_->GetCurrentIndex(),
-                                    frames_->GetFramesCount(), DisplayedFrameQuality_None, 0);
+                                    frames_->GetFramesCount(), DisplayedFrameQuality_None, 0, "", "");
     }
 
     centralPhysicalWidth_ = 1;
@@ -3257,7 +3288,9 @@
                                   size_t currentFrame,
                                   size_t countFrames,
                                   DisplayedFrameQuality quality,
-                                  unsigned int instanceNumber) ORTHANC_OVERRIDE
+                                  unsigned int instanceNumber,
+                                  const std::string& contentDate,
+                                  const std::string& contentTime) ORTHANC_OVERRIDE
   {
     EM_ASM({
         const customEvent = document.createEvent("CustomEvent");
@@ -3266,13 +3299,19 @@
                                         "currentFrame" : $1,
                                         "numberOfFrames" : $2,
                                         "quality" : $3,
-                                        "instanceNumber" : $4 });
+                                        "instanceNumber" : $4,
+                                        "contentDate" : UTF8ToString($5),
+                                        "contentTime" : UTF8ToString($6),
+                                         });
         window.dispatchEvent(customEvent);
       },
       viewport.GetCanvasId().c_str(),
       static_cast<int>(currentFrame),
       static_cast<int>(countFrames),
-      quality, instanceNumber);
+      quality, 
+      instanceNumber, 
+      contentDate.c_str(),
+      contentTime.c_str());
 
     UpdateReferenceLines();
   }
@@ -3560,6 +3599,27 @@
   
 
   EMSCRIPTEN_KEEPALIVE
+  void SetSkipSeriesFromModalities(const char* value)
+  {
+    try
+    {
+      LOG(WARNING) << "SetSkipSeriesFromModalities " << value;
+      
+      Json::Value modalities;
+      Orthanc::Toolbox::ReadJson(modalities, value);
+      std::vector<std::string> skipSeriesFromModalities;
+
+      for (Json::Value::ArrayIndex i = 0; i < modalities.size(); i++)
+      {
+        skipSeriesFromModalities.push_back(modalities[i].asString());
+      }
+      GetResourcesLoader().SetSkipSeriesFromModalities(skipSeriesFromModalities);
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }
+
+
+  EMSCRIPTEN_KEEPALIVE
   void FetchAllStudies()
   {
     try
--- a/OrthancStone/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake	Wed Nov 24 12:25:19 2021 +0100
+++ b/OrthancStone/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake	Wed Nov 24 12:25:32 2021 +0100
@@ -132,6 +132,12 @@
         set(ORTHANC_FRAMEWORK_MD5 "9b86e6f00e03278293cd15643cc0233f")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.4")
         set(ORTHANC_FRAMEWORK_MD5 "6d5ca4a73ac7d42445041ca79de1624d")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.5")
+        set(ORTHANC_FRAMEWORK_MD5 "10fc64de1254a095e5d3ed3931f0cfbb")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.6")
+        set(ORTHANC_FRAMEWORK_MD5 "4b5d05683d747c29b2860ad79d11e62e")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.7")
+        set(ORTHANC_FRAMEWORK_MD5 "c912bbb860d640d3ae3003b5c9698205")
 
       # Below this point are development snapshots that were used to
       # release some plugin, before an official release of the Orthanc
--- a/OrthancStone/Sources/Loaders/LoadedDicomResources.h	Wed Nov 24 12:25:19 2021 +0100
+++ b/OrthancStone/Sources/Loaders/LoadedDicomResources.h	Wed Nov 24 12:25:32 2021 +0100
@@ -109,6 +109,17 @@
       return resources_.find(id) != resources_.end();
     }
 
+    void RemoveResource(const std::string& id)
+    {
+      if (HasResource(id))
+      {
+        Resource* resource = resources_[id];
+        delete resource;
+        resources_.erase(id);
+        flattened_.clear();   // Invalidate the flattened version 
+      }
+    }
+
     void MergeResource(Orthanc::DicomMap& target,
                        const std::string& id) const;
   
--- a/OrthancStone/Sources/Messages/IObservable.cpp	Wed Nov 24 12:25:19 2021 +0100
+++ b/OrthancStone/Sources/Messages/IObservable.cpp	Wed Nov 24 12:25:32 2021 +0100
@@ -27,6 +27,7 @@
 #include <Logging.h>
 
 #include <cassert>
+#include <stdexcept>
 
 namespace OrthancStone 
 {
@@ -88,6 +89,10 @@
             {
               LOG(ERROR) << "Exception on callable: " << e.What();
             }
+            catch (std::exception& e)
+            {
+              LOG(ERROR) << "C++ exception on callable: " << e.what();
+            }
             catch (...)
             {
               LOG(ERROR) << "Native exception on callable";
--- a/TODO	Wed Nov 24 12:25:19 2021 +0100
+++ b/TODO	Wed Nov 24 12:25:32 2021 +0100
@@ -41,6 +41,9 @@
 * Vertical "timeline" to see the position of the current frame in the
   series, and to change the current frame by clicking on the timeline.
 
+* Display a pixel probe with the Hounsfield Unit.
+  https://groups.google.com/g/orthanc-users/c/m7S0wbYYW5s/m/MBaxIQ_IAAAJ
+
 * Display video files even if the Orthanc REST API is not available
   (using pure DICOMweb). This could possible be done using the
   DICOMweb Bulk Data URI, and/or a dedicated JavaScript video player.