changeset 1354:c0e4eb14c912 broker

SingleFrameViewer WASM working OK
author Benjamin Golinvaux <bgo@osimis.io>
date Wed, 15 Apr 2020 12:59:15 +0200
parents af65bce18951
children 4971b1c5dfa6
files Samples/WebAssembly/SingleFrameViewer/CMakeLists.txt Samples/WebAssembly/SingleFrameViewer/SingleFrameViewerApp.js Samples/WebAssembly/SingleFrameViewer/SingleFrameViewerApplication.h Samples/WebAssembly/SingleFrameViewer/WasmWrapper.js Samples/WebAssembly/SingleFrameViewer/index.html
diffstat 5 files changed, 740 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/WebAssembly/SingleFrameViewer/CMakeLists.txt	Wed Apr 15 12:59:15 2020 +0200
@@ -0,0 +1,70 @@
+cmake_minimum_required(VERSION 2.8.3)
+
+# Configuration of the Emscripten compiler for WebAssembly target
+# ---------------------------------------------------------------
+set(USE_WASM ON CACHE BOOL "")
+set(ORTHANC_FRAMEWORK_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../../orthanc CACHE STRING "")
+set(STONE_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../../orthanc-stone)
+
+set(EMSCRIPTEN_SET_LLVM_WASM_BACKEND ON CACHE BOOL "")
+
+set(WASM_FLAGS "-s WASM=1 -s FETCH=1")
+if (CMAKE_BUILD_TYPE STREQUAL "Debug")
+  set(WASM_FLAGS "${WASM_FLAGS} -s SAFE_HEAP=1")
+endif()
+
+set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WASM_FLAGS}")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_FLAGS}")
+
+set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]'")
+set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ERROR_ON_UNDEFINED_SYMBOLS=1")
+set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ASSERTIONS=1 -s DISABLE_EXCEPTION_CATCHING=0")
+set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1 -s TOTAL_MEMORY=268435456")  # 256MB + resize
+set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1")
+
+# Stone of Orthanc configuration
+# ---------------------------------------------------------------
+set(ALLOW_DOWNLOADS ON)
+set(ORTHANC_FRAMEWORK_SOURCE "path")
+
+include(${STONE_ROOT}/Resources/CMake/OrthancStoneParameters.cmake)
+
+SET(ENABLE_DCMTK ON)
+SET(ENABLE_GOOGLE_TEST OFF)
+SET(ENABLE_LOCALE ON)  # Necessary for text rendering
+SET(ENABLE_WASM ON)
+SET(ORTHANC_SANDBOXED ON)
+
+# this will set up the build system for Stone of Orthanc and will
+# populate the ORTHANC_STONE_SOURCES CMake variable
+include(${STONE_ROOT}/Resources/CMake/OrthancStoneConfiguration.cmake)
+
+include_directories(${STONE_ROOT})
+
+# Define the WASM module
+# ---------------------------------------------------------------
+add_executable(SingleFrameViewerWasm
+  test.cpp
+  ${ORTHANC_STONE_SOURCES}
+  )
+
+# Declare installation files for the module
+# ---------------------------------------------------------------
+install(
+  TARGETS SingleFrameViewerWasm
+  RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}
+  )
+
+# Declare installation files for the companion files (web scaffolding)
+# please note that ${CMAKE_CURRENT_BINARY_DIR}/SingleFrameViewerWasm.js 
+# (the generated JS loader for the WASM module) is handled by the `install1`
+# section above
+# ---------------------------------------------------------------
+install(
+  FILES
+  ${CMAKE_SOURCE_DIR}/SingleFrameViewerApp.js
+  ${CMAKE_SOURCE_DIR}/index.html
+  ${CMAKE_CURRENT_BINARY_DIR}/SingleFrameViewerWasm.wasm
+  ${CMAKE_SOURCE_DIR}/WasmWrapper.js
+  DESTINATION ${CMAKE_INSTALL_PREFIX}
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/WebAssembly/SingleFrameViewer/SingleFrameViewerApp.js	Wed Apr 15 12:59:15 2020 +0200
@@ -0,0 +1,89 @@
+
+// This object wraps the functions exposed by the wasm module
+
+const WasmModuleWrapper = function() {
+  this._InitializeViewport = undefined;
+  this._LoadOrthanc = undefined;
+  this._LoadDicomWeb = undefined;
+};
+
+WasmModuleWrapper.prototype.Setup = function(Module) {
+  this._InitializeViewport = Module.cwrap('InitializeViewport', null, [ 'string' ]);
+  this._LoadOrthanc = Module.cwrap('LoadOrthanc', null, [ 'string', 'int' ]);
+  this._LoadDicomWeb = Module.cwrap('LoadDicomWeb', null, [ 'string', 'string', 'string', 'string', 'int' ]);
+};
+
+WasmModuleWrapper.prototype.InitializeViewport = function(canvasId) {
+  this._InitializeViewport(canvasId);
+};
+
+WasmModuleWrapper.prototype.LoadOrthanc = function(instance, frame) {
+  this._LoadOrthanc(instance, frame);
+};
+
+WasmModuleWrapper.prototype.LoadDicomWeb = function(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid, frame) {
+  this._LoadDicomWeb(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid, frame);
+};
+
+var moduleWrapper = new WasmModuleWrapper();
+
+$(document).ready(function() {
+
+  window.addEventListener('StoneInitialized', function() {
+    stone.Setup(Module);
+    console.warn('Native Stone properly intialized');
+
+    stone.InitializeViewport('viewport');
+  });
+
+  window.addEventListener('StoneException', function() {
+    alert('Exception caught in Stone');
+  });    
+
+  var scriptSource;
+
+  if ('WebAssembly' in window) {
+    console.warn('Loading WebAssembly');
+    scriptSource = 'SingleFrameViewerWasm.js';
+  } else {
+    console.error('Your browser does not support WebAssembly!');
+  }
+
+  // Option 1: Loading script using plain HTML
+  
+  /*
+    var script = document.createElement('script');
+    script.src = scriptSource;
+    script.type = 'text/javascript';
+    document.body.appendChild(script);
+  */
+
+  // Option 2: Loading script using AJAX (gives the opportunity to
+  // report explicit errors)
+  
+  axios.get(scriptSource)
+    .then(function (response) {
+      var script = document.createElement('script');
+      script.innerHTML = response.data;
+      script.type = 'text/javascript';
+      document.body.appendChild(script);
+    })
+    .catch(function (error) {
+      alert('Cannot load the WebAssembly framework');
+    });
+});
+
+
+$('#orthancLoad').click(function() {
+  stone.LoadOrthanc($('#orthancInstance').val(),
+                    $('#orthancFrame').val());
+});
+
+
+$('#dicomWebLoad').click(function() {
+  stone.LoadDicomWeb($('#dicomWebServer').val(),
+                     $('#dicomWebStudy').val(),
+                     $('#dicomWebSeries').val(),
+                     $('#dicomWebInstance').val(),
+                     $('#dicomWebFrame').val());
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/WebAssembly/SingleFrameViewer/SingleFrameViewerApplication.h	Wed Apr 15 12:59:15 2020 +0200
@@ -0,0 +1,483 @@
+#pragma once
+
+#include <Framework/Viewport/IViewport.h>
+#include <Framework/Loaders/DicomResourcesLoader.h>
+#include <Framework/Loaders/ILoadersContext.h>
+#include <Framework/Loaders/SeriesFramesLoader.h>
+#include <Framework/Loaders/SeriesThumbnailsLoader.h>
+
+#include <boost/make_shared.hpp>
+
+
+namespace OrthancStone
+{
+  class Application : public ObserverBase<Application>
+  {
+  private:
+    ILoadersContext&                         context_;
+    boost::shared_ptr<IViewport>             viewport_;
+    boost::shared_ptr<DicomResourcesLoader>  dicomLoader_;
+    boost::shared_ptr<SeriesFramesLoader>    framesLoader_;
+
+    Application(ILoadersContext& context,
+                boost::shared_ptr<IViewport> viewport) : 
+      context_(context),
+      viewport_(viewport)
+    {
+    }
+
+    void Handle(const SeriesFramesLoader::FrameLoadedMessage& message)
+    {
+      LOG(INFO) << "Frame decoded! "
+                << message.GetImage().GetWidth() << "x" << message.GetImage().GetHeight()
+                << " " << Orthanc::EnumerationToString(message.GetImage().GetFormat());
+
+      std::auto_ptr<TextureBaseSceneLayer> layer(
+        message.GetInstanceParameters().CreateTexture(message.GetImage()));
+      layer->SetLinearInterpolation(true);
+
+      {
+        std::auto_ptr<IViewport::ILock> lock(viewport_->Lock());
+        lock->GetController().GetScene().SetLayer(0, layer.release());
+        lock->GetCompositor().FitContent(lock->GetController().GetScene());
+        lock->Invalidate();
+      }
+    }
+
+    void Handle(const DicomResourcesLoader::SuccessMessage& message)
+    {
+      if (message.GetResources()->GetSize() != 1)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+   
+      //message.GetResources()->GetResource(0).Print(stdout);
+
+      {
+        std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+        SeriesFramesLoader::Factory f(*message.GetResources());
+
+        framesLoader_ = boost::dynamic_pointer_cast<SeriesFramesLoader>(f.Create(*lock));
+        Register<SeriesFramesLoader::FrameLoadedMessage>(*framesLoader_, &Application::Handle);
+
+        assert(message.HasUserPayload());
+        const Orthanc::SingleValueObject<unsigned int>& payload =
+          dynamic_cast<const Orthanc::SingleValueObject<unsigned int>&>(message.GetUserPayload());
+
+        LOG(INFO) << "Loading pixel data of frame: " << payload.GetValue();
+        framesLoader_->ScheduleLoadFrame(
+          0, message.GetDicomSource(), payload.GetValue(),
+          message.GetDicomSource().GetQualityCount() - 1 /* download best quality available */,
+          NULL);
+      }
+    }
+
+  public:
+    static boost::shared_ptr<Application> Create(ILoadersContext& context,
+                                                 boost::shared_ptr<IViewport> viewport)
+    {
+      boost::shared_ptr<Application> application(new Application(context, viewport));
+
+      {
+        std::auto_ptr<ILoadersContext::ILock> lock(context.Lock());
+        DicomResourcesLoader::Factory f;
+        application->dicomLoader_ = boost::dynamic_pointer_cast<DicomResourcesLoader>(f.Create(*lock));
+      }
+
+      application->Register<DicomResourcesLoader::SuccessMessage>(*application->dicomLoader_, &Application::Handle);
+
+      return application;
+    }
+
+    void LoadOrthancFrame(const DicomSource& source,
+                          const std::string& instanceId,
+                          unsigned int frame)
+    {
+      std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+
+      dicomLoader_->ScheduleLoadOrthancResource(
+        boost::make_shared<LoadedDicomResources>(Orthanc::DICOM_TAG_SOP_INSTANCE_UID), 
+        0, source, Orthanc::ResourceType_Instance, instanceId,
+        new Orthanc::SingleValueObject<unsigned int>(frame));
+    }
+
+    void LoadDicomWebFrame(const DicomSource& source,
+                           const std::string& studyInstanceUid,
+                           const std::string& seriesInstanceUid,
+                           const std::string& sopInstanceUid,
+                           unsigned int frame)
+    {
+      std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+
+      // We first must load the "/metadata" to know the number of frames
+      dicomLoader_->ScheduleGetDicomWeb(
+        boost::make_shared<LoadedDicomResources>(Orthanc::DICOM_TAG_SOP_INSTANCE_UID), 0, source,
+        "/studies/" + studyInstanceUid + "/series/" + seriesInstanceUid + "/instances/" + sopInstanceUid + "/metadata",
+        new Orthanc::SingleValueObject<unsigned int>(frame));
+    }
+
+    void FitContent()
+    {
+      std::auto_ptr<IViewport::ILock> lock(viewport_->Lock());
+      lock->GetCompositor().FitContent(lock->GetController().GetScene());
+      lock->Invalidate();
+    }
+  };
+
+
+
+  class IWebViewerLoadersObserver : public boost::noncopyable
+  {
+  public:
+    virtual ~IWebViewerLoadersObserver()
+    {
+    }
+
+    virtual void SignalSeriesUpdated(LoadedDicomResources& series) = 0;
+
+    virtual void SignalThumbnailLoaded(const std::string& studyInstanceUid,
+                                       const std::string& seriesInstanceUid,
+                                       SeriesThumbnailType type) = 0;
+  };
+  
+
+  class WebViewerLoaders : public ObserverBase<WebViewerLoaders>
+  {
+  private:
+    static const int PRIORITY_ADD_RESOURCES = 0;
+    static const int PRIORITY_THUMBNAILS = OracleScheduler::PRIORITY_LOW + 100;
+
+    enum Type
+    {
+      Type_Orthanc = 1,
+      Type_DicomWeb = 2
+    };
+
+    ILoadersContext&                           context_;
+    std::auto_ptr<IWebViewerLoadersObserver>   observer_;
+    bool                                       loadThumbnails_;
+    DicomSource                                source_;
+    std::set<std::string>                      scheduledSeries_;
+    std::set<std::string>                      scheduledThumbnails_;
+    std::set<std::string>                      scheduledStudies_;
+    boost::shared_ptr<LoadedDicomResources>    loadedSeries_;
+    boost::shared_ptr<LoadedDicomResources>    loadedStudies_;
+    boost::shared_ptr<DicomResourcesLoader>    resourcesLoader_;
+    boost::shared_ptr<SeriesThumbnailsLoader>  thumbnailsLoader_;
+
+    WebViewerLoaders(ILoadersContext& context,
+                     IWebViewerLoadersObserver* observer) :
+      context_(context),
+      observer_(observer),
+      loadThumbnails_(false)
+    {
+      loadedSeries_ = boost::make_shared<LoadedDicomResources>(Orthanc::DICOM_TAG_SERIES_INSTANCE_UID);
+      loadedStudies_ = boost::make_shared<LoadedDicomResources>(Orthanc::DICOM_TAG_STUDY_INSTANCE_UID);
+    }
+
+    static Orthanc::IDynamicObject* CreatePayload(Type type)
+    {
+      return new Orthanc::SingleValueObject<Type>(type);
+    }
+    
+    void HandleThumbnail(const SeriesThumbnailsLoader::ThumbnailLoadedMessage& message)
+    {
+      if (observer_.get() != NULL)
+      {
+        observer_->SignalThumbnailLoaded(message.GetStudyInstanceUid(),
+                                         message.GetSeriesInstanceUid(),
+                                         message.GetType());
+      }
+    }
+    
+    void HandleLoadedResources(const DicomResourcesLoader::SuccessMessage& message)
+    {
+      LoadedDicomResources series(Orthanc::DICOM_TAG_SERIES_INSTANCE_UID);
+
+      switch (dynamic_cast<const Orthanc::SingleValueObject<Type>&>(message.GetUserPayload()).GetValue())
+      {
+        case Type_DicomWeb:
+        {          
+          for (size_t i = 0; i < loadedSeries_->GetSize(); i++)
+          {
+            std::string study;
+            if (loadedSeries_->GetResource(i).LookupStringValue(
+                  study, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) &&
+                loadedStudies_->HasResource(study))
+            {
+              Orthanc::DicomMap m;
+              m.Assign(loadedSeries_->GetResource(i));
+              loadedStudies_->MergeResource(m, study);
+              series.AddResource(m);
+            }
+          }
+
+          break;
+        }
+
+        case Type_Orthanc:
+        {          
+          for (size_t i = 0; i < message.GetResources()->GetSize(); i++)
+          {
+            series.AddResource(message.GetResources()->GetResource(i));
+          }
+
+          break;
+        }
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+
+      if (loadThumbnails_ &&
+          (!source_.IsDicomWeb() ||
+           source_.HasDicomWebRendered()))
+      {
+        for (size_t i = 0; i < series.GetSize(); i++)
+        {
+          std::string patientId, studyInstanceUid, seriesInstanceUid;
+          if (series.GetResource(i).LookupStringValue(patientId, Orthanc::DICOM_TAG_PATIENT_ID, false) &&
+              series.GetResource(i).LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) &&
+              series.GetResource(i).LookupStringValue(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false) &&
+              scheduledThumbnails_.find(seriesInstanceUid) == scheduledThumbnails_.end())
+          {
+            scheduledThumbnails_.insert(seriesInstanceUid);
+            thumbnailsLoader_->ScheduleLoadThumbnail(source_, patientId, studyInstanceUid, seriesInstanceUid);
+          }
+        }
+      }
+
+      if (observer_.get() != NULL &&
+          series.GetSize() > 0)
+      {
+        observer_->SignalSeriesUpdated(series);
+      }
+    }
+
+    void HandleOrthancRestApi(const OrthancRestApiCommand::SuccessMessage& message)
+    {
+      Json::Value body;
+      message.ParseJsonBody(body);
+
+      if (body.type() != Json::arrayValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+      }
+      else
+      {
+        for (Json::Value::ArrayIndex i = 0; i < body.size(); i++)
+        {
+          if (body[i].type() == Json::stringValue)
+          {
+            AddOrthancSeries(body[i].asString());
+          }
+          else
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+          }
+        }
+      }
+    }
+
+  public:
+    static boost::shared_ptr<WebViewerLoaders> Create(ILoadersContext& context,
+                                                      const DicomSource& source,
+                                                      bool loadThumbnails,
+                                                      IWebViewerLoadersObserver* observer)
+    {
+      boost::shared_ptr<WebViewerLoaders> application(new WebViewerLoaders(context, observer));
+      application->source_ = source;
+      application->loadThumbnails_ = loadThumbnails;
+
+      {
+        std::auto_ptr<ILoadersContext::ILock> lock(context.Lock());
+
+        {
+          DicomResourcesLoader::Factory f;
+          application->resourcesLoader_ = boost::dynamic_pointer_cast<DicomResourcesLoader>(f.Create(*lock));
+        }
+
+        {
+          SeriesThumbnailsLoader::Factory f;
+          f.SetPriority(PRIORITY_THUMBNAILS);
+          application->thumbnailsLoader_ = boost::dynamic_pointer_cast<SeriesThumbnailsLoader>(f.Create(*lock));
+        }
+
+        application->Register<OrthancRestApiCommand::SuccessMessage>(
+          lock->GetOracleObservable(), &WebViewerLoaders::HandleOrthancRestApi);
+
+        application->Register<DicomResourcesLoader::SuccessMessage>(
+          *application->resourcesLoader_, &WebViewerLoaders::HandleLoadedResources);
+
+        application->Register<SeriesThumbnailsLoader::ThumbnailLoadedMessage>(
+          *application->thumbnailsLoader_, &WebViewerLoaders::HandleThumbnail);
+
+        lock->AddLoader(application);
+      }
+
+      return application;
+    }
+    
+    void AddDicomAllSeries()
+    {
+      std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+
+      if (source_.IsDicomWeb())
+      {
+        resourcesLoader_->ScheduleGetDicomWeb(loadedSeries_, PRIORITY_ADD_RESOURCES, source_,
+                                              "/series", CreatePayload(Type_DicomWeb));
+        resourcesLoader_->ScheduleGetDicomWeb(loadedStudies_, PRIORITY_ADD_RESOURCES, source_,
+                                              "/studies", CreatePayload(Type_DicomWeb));
+      }
+      else if (source_.IsOrthanc())
+      {
+        std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+        command->SetMethod(Orthanc::HttpMethod_Get);
+        command->SetUri("/series");
+        lock->Schedule(GetSharedObserver(), PRIORITY_ADD_RESOURCES, command.release());
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+    }
+    
+    void AddDicomStudy(const std::string& studyInstanceUid)
+    {
+      // Avoid adding twice the same study
+      if (scheduledStudies_.find(studyInstanceUid) == scheduledStudies_.end())
+      {
+        scheduledStudies_.insert(studyInstanceUid);
+
+        if (source_.IsDicomWeb())
+        {
+          Orthanc::DicomMap filter;
+          filter.SetValue(Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, studyInstanceUid, false);
+          
+          std::set<Orthanc::DicomTag> tags;
+          
+          {
+            std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());      
+            
+            resourcesLoader_->ScheduleQido(loadedStudies_, PRIORITY_ADD_RESOURCES, source_,
+                                           Orthanc::ResourceType_Study, filter, tags, CreatePayload(Type_DicomWeb));
+            
+            resourcesLoader_->ScheduleQido(loadedSeries_, PRIORITY_ADD_RESOURCES, source_,
+                                           Orthanc::ResourceType_Series, filter, tags, CreatePayload(Type_DicomWeb));
+          }
+        }
+        else if (source_.IsOrthanc())
+        {
+          std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+          command->SetMethod(Orthanc::HttpMethod_Post);
+          command->SetUri("/tools/find");
+
+          Json::Value body;
+          body["Level"] = "Series";
+          body["Query"] = Json::objectValue;
+          body["Query"]["StudyInstanceUID"] = studyInstanceUid;
+          command->SetBody(body);
+
+          {
+            std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());      
+            lock->Schedule(GetSharedObserver(), PRIORITY_ADD_RESOURCES, command.release());
+          }
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+        }        
+      }
+    }
+    
+    void AddDicomSeries(const std::string& studyInstanceUid,
+                        const std::string& seriesInstanceUid)
+    {
+      std::set<Orthanc::DicomTag> tags;
+
+      std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());      
+
+      if (scheduledStudies_.find(studyInstanceUid) == scheduledStudies_.end())
+      {
+        scheduledStudies_.insert(studyInstanceUid);
+          
+        if (source_.IsDicomWeb())
+        {
+          Orthanc::DicomMap filter;
+          filter.SetValue(Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, studyInstanceUid, false);
+          
+          resourcesLoader_->ScheduleQido(loadedStudies_, PRIORITY_ADD_RESOURCES, source_,
+                                         Orthanc::ResourceType_Study, filter, tags, CreatePayload(Type_DicomWeb));
+        }
+      }
+
+      if (scheduledSeries_.find(seriesInstanceUid) == scheduledSeries_.end())
+      {
+        scheduledSeries_.insert(seriesInstanceUid);
+
+        if (source_.IsDicomWeb())
+        {
+          Orthanc::DicomMap filter;
+          filter.SetValue(Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, studyInstanceUid, false);
+          filter.SetValue(Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, seriesInstanceUid, false);
+          
+          resourcesLoader_->ScheduleQido(loadedSeries_, PRIORITY_ADD_RESOURCES, source_,
+                                         Orthanc::ResourceType_Series, filter, tags, CreatePayload(Type_DicomWeb));
+        }
+        else if (source_.IsOrthanc())
+        {
+          std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+          command->SetMethod(Orthanc::HttpMethod_Post);
+          command->SetUri("/tools/find");
+
+          Json::Value body;
+          body["Level"] = "Series";
+          body["Query"] = Json::objectValue;
+          body["Query"]["StudyInstanceUID"] = studyInstanceUid;
+          body["Query"]["SeriesInstanceUID"] = seriesInstanceUid;
+          command->SetBody(body);
+
+          lock->Schedule(GetSharedObserver(), PRIORITY_ADD_RESOURCES, command.release());
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+        }
+      }
+    }
+
+    void AddOrthancStudy(const std::string& orthancId)
+    {
+      if (source_.IsOrthanc())
+      {
+        std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());      
+        resourcesLoader_->ScheduleLoadOrthancResources(
+          loadedSeries_, PRIORITY_ADD_RESOURCES, source_,
+          Orthanc::ResourceType_Study, orthancId, Orthanc::ResourceType_Series,
+          CreatePayload(Type_Orthanc));
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType,
+                                        "Only applicable to Orthanc DICOM sources");
+      }
+    }
+
+    void AddOrthancSeries(const std::string& orthancId)
+    {
+      if (source_.IsOrthanc())
+      {
+        std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());      
+        resourcesLoader_->ScheduleLoadOrthancResource(
+          loadedSeries_, PRIORITY_ADD_RESOURCES,
+          source_, Orthanc::ResourceType_Series, orthancId,
+          CreatePayload(Type_Orthanc));
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType,
+                                        "Only applicable to Orthanc DICOM sources");
+      }
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/WebAssembly/SingleFrameViewer/WasmWrapper.js	Wed Apr 15 12:59:15 2020 +0200
@@ -0,0 +1,27 @@
+
+const Stone = function() {
+  this._InitializeViewport = undefined;
+  this._LoadOrthanc = undefined;
+  this._LoadDicomWeb = undefined;
+};
+
+Stone.prototype.Setup = function(Module) {
+  this._InitializeViewport = Module.cwrap('InitializeViewport', null, [ 'string' ]);
+  this._LoadOrthanc = Module.cwrap('LoadOrthanc', null, [ 'string', 'int' ]);
+  this._LoadDicomWeb = Module.cwrap('LoadDicomWeb', null, [ 'string', 'string', 'string', 'string', 'int' ]);
+};
+
+Stone.prototype.InitializeViewport = function(canvasId) {
+  this._InitializeViewport(canvasId);
+};
+
+Stone.prototype.LoadOrthanc = function(instance, frame) {
+  this._LoadOrthanc(instance, frame);
+};
+
+Stone.prototype.LoadDicomWeb = function(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid, frame) {
+  this._LoadDicomWeb(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid, frame);
+};
+
+var stone = new Stone();
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/WebAssembly/SingleFrameViewer/index.html	Wed Apr 15 12:59:15 2020 +0200
@@ -0,0 +1,71 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <title>Osimis' Web Viewer</title>
+    <meta charset="utf-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1" />
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
+    <meta name="apple-mobile-web-app-capable" content="yes" />
+    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
+    <link rel="icon" href="data:;base64,iVBORw0KGgo=">
+
+    <style>
+      canvas {
+      background-color: green;
+      width : 100%;
+      height : 512px;
+      }
+    </style>
+  </head>
+  <body>
+    <h1>Viewport</h1>
+
+    <canvas id="viewport" >
+    </canvas>
+
+    <h1>Load from Orthanc</h1>
+    <p>
+      Orthanc instance: <input type="text" id="orthancInstance" size="80"
+                               value="5eb2dd5f-3fca21a8-fa7565fd-63e112ae-344830a4">
+      <!-- Orthanc instance: <input type="text" id="orthancInstance" size="80"
+                               value="61f3143e-96f34791-ad6bbb8d-62559e75-45943e1b"> -->
+      <!-- Orthanc instance: <input type="text" id="orthancInstance" size="80"
+                               value="8dddbd75-f4095768-6e66f851-22305751-18782bdd"> -->
+    </p>
+    <p>
+      Frame number: <input type="text" id="orthancFrame" value="0">
+    </p>
+    <p>
+      <button id="orthancLoad">Load</button>
+    </p>
+
+    <h1>Load from DICOMweb</h1>
+    <p>
+      Server name in Orthanc: <input type="text" id="dicomWebServer" value="self">
+    </p>
+    <p>
+      Study instance UID: <input type="text" id="dicomWebStudy" size="80"
+                                 value="1.2.840.113543.6.6.4.7.64067529866380271256212683512383713111129">
+    </p>
+    <p>
+      Series instance UID: <input type="text" id="dicomWebSeries" size="80"
+                                  value="1.2.840.113543.6.6.4.7.63556916880112768082712975118701689357177">
+    </p>
+    <p>
+      SOP instance UID: <input type="text" id="dicomWebInstance" size="80"
+                               value="1.2.840.113543.6.6.4.7.64234348190163144631511103849051737563212">
+    </p>
+    <p>
+      Frame number: <input type="text" id="dicomWebFrame" value="0">
+    </p>
+    <p>
+      <button id="dicomWebLoad">Load</button>
+    </p>
+
+    <script src="https://code.jquery.com/jquery-3.4.1.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.0/axios.js"></script>
+    
+    <script src="WasmWrapper.js"></script>
+    <script src="SingleFrameViewerApp.js"></script>
+  </body>
+</html>