changeset 1393:27e0a00bd3e8

RtViewer SingleFrameViewer OK : wasm SDL single viewport other viewports ongoing
author Benjamin Golinvaux <bgo@osimis.io>
date Wed, 29 Apr 2020 15:54:18 +0200
parents ffdb82850e98
children a8ac7e3de0e8
files Samples/Common/RtViewer.cpp Samples/Common/RtViewer.h Samples/Sdl/RtViewer/RtViewerSdl.cpp Samples/Sdl/SingleFrameViewer/SimpleViewer.cpp Samples/WebAssembly/RtViewer/CMakeLists.txt Samples/WebAssembly/RtViewer/RtViewerWasm.cpp Samples/WebAssembly/RtViewer/RtViewerWasm.html Samples/WebAssembly/RtViewer/RtViewerWasmApp.js Samples/WebAssembly/SingleFrameViewer/CMakeLists.txt Samples/WebAssembly/SingleFrameViewer/SingleFrameViewerApp.js Samples/WebAssembly/SingleFrameViewer/index.html
diffstat 11 files changed, 403 insertions(+), 508 deletions(-) [+]
line wrap: on
line diff
--- a/Samples/Common/RtViewer.cpp	Tue Apr 28 13:52:21 2020 +0200
+++ b/Samples/Common/RtViewer.cpp	Wed Apr 29 15:54:18 2020 +0200
@@ -20,6 +20,7 @@
 
 // Sample app
 #include "RtViewer.h"
+#include "SampleHelpers.h"
 
 // Stone of Orthanc
 #include <Framework/StoneInitialization.h>
@@ -39,16 +40,12 @@
 #include <Framework/Volumes/VolumeSceneLayerSource.h>
 
 #include <Framework/Oracle/GetOrthancWebViewerJpegCommand.h>
-#include <Framework/Oracle/ThreadedOracle.h>
 #include <Framework/Scene2D/GrayscaleStyleConfigurator.h>
 #include <Framework/Scene2D/LookupTableStyleConfigurator.h>
 #include <Framework/Volumes/DicomVolumeImageMPRSlicer.h>
 #include <Framework/StoneException.h>
 
 // Orthanc
-#include <Core/Images/Image.h>
-#include <Core/Images/ImageProcessing.h>
-#include <Core/Images/PngWriter.h>
 #include <Core/Logging.h>
 #include <Core/OrthancException.h>
 
@@ -249,8 +246,8 @@
     , currentPlane_(0)
     , projection_(VolumeProjection_Coronal)
   {
-    // False means we do NOT let Windows treat this as a legacy application that needs to be scaled
-    viewport_ = SdlOpenGLViewport::Create("CT RTDOSE RTSTRUCT viewer", 1024, 1024, false);
+    // the viewport hosts the scene
+    CreateViewport();
 
     std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
     ViewportController& controller = lock->GetController();
@@ -326,35 +323,7 @@
       activeTracker_.reset();
     }
   }
-
-  void RtViewerApp::TakeScreenshot(const std::string& target,
-                                                unsigned int canvasWidth,
-                                                unsigned int canvasHeight)
-  {
-    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
-    ViewportController& controller = lock->GetController();
-    Scene2D& scene = controller.GetScene();
-
-    CairoCompositor compositor(canvasWidth, canvasHeight);
-    compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, FONT_SIZE_0, Orthanc::Encoding_Latin1);
-    compositor.Refresh(scene);
-
-    Orthanc::ImageAccessor canvas;
-    compositor.GetCanvas().GetReadOnlyAccessor(canvas);
-
-    Orthanc::Image png(Orthanc::PixelFormat_RGB24, canvas.GetWidth(), canvas.GetHeight(), false);
-    Orthanc::ImageProcessing::Convert(png, canvas);
-
-    Orthanc::PngWriter writer;
-    writer.WriteToFile(target, png);
-  }
-
-  boost::shared_ptr<IFlexiblePointerTracker> RtViewerApp::TrackerHitTest(const PointerEvent& e)
-  {
-    // std::vector<boost::shared_ptr<MeasureTool>> measureTools_;
-    return boost::shared_ptr<IFlexiblePointerTracker>();
-  }
-    
+  
   void RtViewerApp::PrepareLoadersAndSlicers()
   {
 
@@ -369,7 +338,7 @@
       // "true" means use progressive quality (jpeg 50 --> jpeg 90 --> 16-bit raw)
       // "false" means only using hi quality
       // TODO: add flag for quality
-      ctLoader_ = OrthancSeriesVolumeProgressiveLoader::Create(*loadersContext_, ctVolume_, false);
+      ctLoader_ = OrthancSeriesVolumeProgressiveLoader::Create(*loadersContext_, ctVolume_, true);
 
       // we need to store the CT loader to ask from geometry details later on when geometry is loaded
       geometryProvider_ = ctLoader_;
@@ -421,13 +390,15 @@
     this->SetStructureSet(LAYER_POSITION + 2, rtstructLoader_);
 
 #if 1 
+    ORTHANC_ASSERT(HasArgument("ctseries") && HasArgument("rtdose") && HasArgument("rtstruct"));
+
     LOG(INFO) << "About to load:";
-    LOG(INFO) << "  CT       : " << ctSeriesId_;;
-    LOG(INFO) << "  RTDOSE   : " << doseInstanceId_;
-    LOG(INFO) << "  RTSTRUCT : " << rtStructInstanceId_;
-    ctLoader_->LoadSeries(ctSeriesId_);
-    doseLoader_->LoadInstance(doseInstanceId_);
-    rtstructLoader_->LoadInstanceFullVisibility(rtStructInstanceId_);
+    LOG(INFO) << "  CT       : " << GetArgument("ctseries");
+    LOG(INFO) << "  RTDOSE   : " << GetArgument("rtdose");
+    LOG(INFO) << "  RTSTRUCT : " << GetArgument("rtstruct");
+    ctLoader_->LoadSeries(GetArgument("ctseries"));
+    doseLoader_->LoadInstance(GetArgument("rtdose"));
+    rtstructLoader_->LoadInstanceFullVisibility(GetArgument("rtstruct"));
 
 #elif 0
     /*
@@ -547,6 +518,23 @@
     structLayerSource_.reset(new OrthancStone::VolumeSceneLayerSource(scene, depth, volume));
   }
 
+  void RtViewerApp::SetArgument(const std::string& key, const std::string& value)
+  {
+    if (key == "loglevel")
+      OrthancStoneHelpers::SetLogLevel(value);
+    else
+      arguments_[key] = value;
+  }
+
+  const std::string& RtViewerApp::GetArgument(const std::string& key) const
+  {
+    ORTHANC_ASSERT(HasArgument(key));
+    return arguments_.at(key);
+  }
+  bool RtViewerApp::HasArgument(const std::string& key) const
+  {
+    return (arguments_.find(key) != arguments_.end());
+  }
 
   void RtViewerApp::SetInfoDisplayMessage(
     std::string key, std::string value)
--- a/Samples/Common/RtViewer.h	Tue Apr 28 13:52:21 2020 +0200
+++ b/Samples/Common/RtViewer.h	Wed Apr 29 15:54:18 2020 +0200
@@ -18,19 +18,18 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
 
-#include <Framework/Viewport/SdlViewport.h>
-#include <Framework/Loaders/GenericLoadersContext.h>
+#include <Framework/Viewport/IViewport.h>
+
+#include <Framework/Loaders/DicomStructureSetLoader.h>
+#include <Framework/Loaders/OrthancMultiframeVolumeLoader.h>
+#include <Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h>
+#include <Framework/Loaders/ILoadersContext.h>
+#include <Framework/Messages/IMessageEmitter.h>
 #include <Framework/Messages/IObserver.h>
-#include <Framework/Messages/IMessageEmitter.h>
+#include <Framework/Messages/ObserverBase.h>
 #include <Framework/Oracle/OracleCommandExceptionMessage.h>
 #include <Framework/Scene2DViewport/ViewportController.h>
 #include <Framework/Volumes/DicomVolumeImage.h>
-#include <Framework/Oracle/ThreadedOracle.h>
-#include <Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h>
-#include <Framework/Loaders/OrthancMultiframeVolumeLoader.h>
-#include <Framework/Loaders/DicomStructureSetLoader.h>
-
-#include <Framework/Messages/ObserverBase.h>
 
 #include <boost/enable_shared_from_this.hpp>
 #include <boost/thread.hpp>
@@ -88,6 +87,10 @@
     void ProcessOptions(int argc, char* argv[]);
     void HandleApplicationEvent(const SDL_Event& event);
 #elif ORTHANC_ENABLE_WASM
+  public:
+    void RunWasm();
+#else
+#  error Either ORTHANC_ENABLE_SDL or ORTHANC_ENABLE_WASM must be enabled
 #endif
 
   public:
@@ -95,6 +98,12 @@
     void DisableTracker();
 
     /**
+    Called by command-line option processing or when parsing the URL 
+    parameters.
+    */
+    void SetArgument(const std::string& key, const std::string& value);
+
+    /**
     This method is called when the scene transform changes. It allows to
     recompute the visual elements whose content depend upon the scene transform
     */
@@ -136,56 +145,14 @@
 
   private:
     void PrepareLoadersAndSlicers();
-
-    /**
-    Url of the Orthanc instance
-    Typically, in a native application (Qt, SDL), it will be an absolute URL like "http://localhost:8042". In 
-    wasm on the browser, it could be an absolute URL, provided you do not have cross-origin problems, or a relative
-    URL. In our wasm samples, it is set to "..", because we set up either a reverse proxy or an Orthanc ServeFolders
-    plugin that serves the main web application from an URL like "http://localhost:8042/rtviewer" (with ".." leading 
-    to the main Orthanc root URL)
-    */
-    std::string orthancUrl_;
-
-    /**
-    Orthanc ID of the CT series to load. Only used between startup and loading time.
-    */
-    std::string ctSeriesId_;
-
-    /**
-    Orthanc ID of the RTDOSE instance to load. Only used between startup and loading time.
-    */
-    std::string doseInstanceId_;
-
-    /**
-    Orthanc ID of the RTSTRUCT instance to load. Only used between startup and loading time.
-    */
-    std::string rtStructInstanceId_;
-
-
-#if ORTHANC_ENABLE_SDL
-    // if threaded (not wasm)
-    //IObservable oracleObservable_;
-    //ThreadedOracle oracle_;
-    //boost::shared_mutex mutex_; // to serialize messages from the ThreadedOracle
-#elif ORTHANC_ENABLE_WASM
-
-
-#endif
-
     void SelectNextTool();
 
-    /**
-    This returns a random point in the canvas part of the scene, but in
-    scene coordinates
-    */
-    ScenePoint2D GetRandomPointInScene() const;
+    // argument handling
+    // SetArgument is above (public section)
+    std::map<std::string, std::string> arguments_;
 
-    boost::shared_ptr<IFlexiblePointerTracker> TrackerHitTest(const PointerEvent& e);
-
-    boost::shared_ptr<IFlexiblePointerTracker> CreateSuitableTracker(
-      const SDL_Event& event,
-      const PointerEvent& e);
+    const std::string& GetArgument(const std::string& key) const;
+    bool HasArgument(const std::string& key) const;
 
     void TakeScreenshot(
       const std::string& target,
@@ -224,6 +191,7 @@
       const boost::shared_ptr<DicomStructureSetLoader>& volume);
 
   private:
+    void CreateViewport();
     void DisplayFloatingCtrlInfoText(const PointerEvent& e);
     void DisplayInfoText();
     void HideInfoText();
@@ -239,7 +207,8 @@
     boost::shared_ptr<DicomStructureSetLoader>  rtstructLoader_;
 
     /** encapsulates resources shared by loaders */
-    boost::shared_ptr<GenericLoadersContext>            loadersContext_;
+    boost::shared_ptr<ILoadersContext>                  loadersContext_;
+
     boost::shared_ptr<VolumeSceneLayerSource>           ctVolumeLayerSource_, doseVolumeLayerSource_, structLayerSource_;
     
     /**
@@ -270,7 +239,7 @@
 
     RtViewerGuiTool currentTool_;
     boost::shared_ptr<UndoStack> undoStack_;
-    boost::shared_ptr<SdlOpenGLViewport> viewport_;
+    boost::shared_ptr<IViewport> viewport_;
   };
 
 }
--- a/Samples/Sdl/RtViewer/RtViewerSdl.cpp	Tue Apr 28 13:52:21 2020 +0200
+++ b/Samples/Sdl/RtViewer/RtViewerSdl.cpp	Wed Apr 29 15:54:18 2020 +0200
@@ -20,14 +20,24 @@
 
 #include "RtViewer.h"
 #include "../SdlHelpers.h"
-#include "SampleHelpers.h"
 
+// Stone of Orthanc includes
+#include <Framework/Loaders/GenericLoadersContext.h>
+#include <Framework/OpenGL/SdlOpenGLContext.h>
 #include <Framework/StoneException.h>
 #include <Framework/StoneInitialization.h>
 
-#include <Framework/OpenGL/SdlOpenGLContext.h>
+// Orthanc (a.o. for screenshot capture)
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Core/Images/PngWriter.h>
+
 
 #include <boost/program_options.hpp>
+#include <boost/shared_ptr.hpp>
+
+// #include <boost/pointer_cast.hpp> this include might be necessary in more recent boost versions
+
 #include <SDL.h>
 
 #include <string>
@@ -51,6 +61,12 @@
 
 namespace OrthancStone
 {
+  void RtViewerApp::CreateViewport()
+  {
+    // False means we do NOT let Windows treat this as a legacy application that needs to be scaled
+    viewport_ = SdlOpenGLViewport::Create("CT RTDOSE RTSTRUCT viewer", 1024, 1024, false);
+  }
+
   void RtViewerApp::ProcessOptions(int argc, char* argv[])
   {
     namespace po = boost::program_options;
@@ -84,31 +100,12 @@
       std::cerr << "Please check your command line options! (\"" << e.what() << "\")" << std::endl;
     }
 
-    if (vm.count("loglevel") > 0)
-    {
-      std::string logLevel = vm["loglevel"].as<std::string>();
-      OrthancStoneHelpers::SetLogLevel(logLevel);
-    }
-
-    if (vm.count("orthanc") > 0)
+    for (po::variables_map::iterator it = vm.begin(); it != vm.end(); ++it)
     {
-      // maybe check URL validity here
-      orthancUrl_ = vm["orthanc"].as<std::string>();
-    }
-
-    if (vm.count("ctseries") > 0)
-    {
-      ctSeriesId_ = vm["ctseries"].as<std::string>();
-    }
-
-    if (vm.count("rtdose") > 0)
-    {
-      doseInstanceId_ = vm["rtdose"].as<std::string>();
-    }
-
-    if (vm.count("rtstruct") > 0)
-    {
-      rtStructInstanceId_ = vm["rtstruct"].as<std::string>();
+      std::string key = it->first;
+      const po::variable_value& value = it->second;
+      const std::string& strValue = value.as<std::string>();
+      SetArgument(key, strValue);
     }
   }
 
@@ -139,7 +136,39 @@
     Create the shared loaders context
     */
     loadersContext_.reset(new GenericLoadersContext(1, 4, 1));
-    loadersContext_->StartOracle();
+
+    // we are in SDL --> downcast to concrete type
+    boost::shared_ptr<GenericLoadersContext> loadersContext = boost::dynamic_pointer_cast<GenericLoadersContext>(loadersContext_);
+
+    /**
+      Url of the Orthanc instance
+      Typically, in a native application (Qt, SDL), it will be an absolute URL like "http://localhost:8042". In 
+      wasm on the browser, it could be an absolute URL, provided you do not have cross-origin problems, or a relative
+      URL. In our wasm samples, it is set to "..", because we set up either a reverse proxy or an Orthanc ServeFolders
+      plugin that serves the main web application from an URL like "http://localhost:8042/rtviewer" (with ".." leading 
+      to the main Orthanc root URL)
+    */
+    std::string orthancUrl = arguments_["orthanc"];
+
+    {
+      Orthanc::WebServiceParameters p;
+      if (HasArgument("orthanc"))
+      {
+        p.SetUrl(orthancUrl);
+      }
+      if (HasArgument("user"))
+      {
+        ORTHANC_ASSERT(HasArgument("password"));
+        p.SetCredentials(GetArgument("user"), GetArgument("password"));
+      } 
+      else
+      {
+        ORTHANC_ASSERT(!HasArgument("password"));
+      }
+      loadersContext->SetOrthancParameters(p);
+    }
+
+    loadersContext->StartOracle();
 
     /**
     It is very important that the Oracle (responsible for network I/O) be started before creating and firing the 
@@ -147,13 +176,38 @@
     */
     PrepareLoadersAndSlicers();
 
-    OrthancStone::DefaultViewportInteractor interactor;
+    DefaultViewportInteractor interactor;
+
+    boost::shared_ptr<SdlViewport> viewport = boost::dynamic_pointer_cast<SdlViewport>(viewport_);
+
+    OrthancStoneHelpers::SdlRunLoop(viewport, interactor);
+
+    loadersContext->StopOracle();
+  }
 
-    OrthancStoneHelpers::SdlRunLoop(viewport_, interactor);
+  void RtViewerApp::TakeScreenshot(const std::string& target,
+                                   unsigned int canvasWidth,
+                                   unsigned int canvasHeight)
+  {
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
+    Scene2D& scene = controller.GetScene();
 
-    loadersContext_->StopOracle();
+    CairoCompositor compositor(canvasWidth, canvasHeight);
+    compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, FONT_SIZE_0, Orthanc::Encoding_Latin1);
+    compositor.Refresh(scene);
+
+    Orthanc::ImageAccessor canvas;
+    compositor.GetCanvas().GetReadOnlyAccessor(canvas);
+
+    Orthanc::Image png(Orthanc::PixelFormat_RGB24, canvas.GetWidth(), canvas.GetHeight(), false);
+    Orthanc::ImageProcessing::Convert(png, canvas);
+
+    Orthanc::PngWriter writer;
+    writer.WriteToFile(target, png);
   }
 
+
 #if 0
   void RtViewerApp::HandleApplicationEvent(
     const SDL_Event& event)
--- a/Samples/Sdl/SingleFrameViewer/SimpleViewer.cpp	Tue Apr 28 13:52:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,293 +0,0 @@
-#error TO BE DELETED
-
-#include "SdlSimpleViewerApplication.h"
-
-#include <Core/OrthancException.h>
-
-#include <Framework/Loaders/GenericLoadersContext.h>
-#include <Framework/StoneException.h>
-#include <Framework/StoneEnumerations.h>
-#include <Framework/StoneInitialization.h>
-#include <Framework/Viewport/SdlViewport.h>
-
-#include <SDL.h>
-
-namespace OrthancStone
-{
-  static KeyboardModifiers GetKeyboardModifiers(const uint8_t* keyboardState,
-                                                const int scancodeCount)
-  {
-    int result = KeyboardModifiers_None;
-
-    if (keyboardState != NULL)
-    {
-      if (SDL_SCANCODE_LSHIFT < scancodeCount &&
-          keyboardState[SDL_SCANCODE_LSHIFT])
-      {
-        result |= KeyboardModifiers_Shift;
-      }
-
-      if (SDL_SCANCODE_RSHIFT < scancodeCount &&
-          keyboardState[SDL_SCANCODE_RSHIFT])
-      {
-        result |= KeyboardModifiers_Shift;
-      }
-
-      if (SDL_SCANCODE_LCTRL < scancodeCount &&
-          keyboardState[SDL_SCANCODE_LCTRL])
-      {
-        result |= KeyboardModifiers_Control;
-      }
-
-      if (SDL_SCANCODE_RCTRL < scancodeCount &&
-          keyboardState[SDL_SCANCODE_RCTRL])
-      {
-        result |= KeyboardModifiers_Control;
-      }
-
-      if (SDL_SCANCODE_LALT < scancodeCount &&
-          keyboardState[SDL_SCANCODE_LALT])
-      {
-        result |= KeyboardModifiers_Alt;
-      }
-
-      if (SDL_SCANCODE_RALT < scancodeCount &&
-          keyboardState[SDL_SCANCODE_RALT])
-      {
-        result |= KeyboardModifiers_Alt;
-      }
-    }
-
-    return static_cast<KeyboardModifiers>(result);
-  }
-
-
-  static void GetPointerEvent(PointerEvent& p,
-                              const ICompositor& compositor,
-                              SDL_Event event,
-                              const uint8_t* keyboardState,
-                              const int scancodeCount)
-  {
-    KeyboardModifiers modifiers = GetKeyboardModifiers(keyboardState, scancodeCount);
-
-    switch (event.button.button)
-    {
-    case SDL_BUTTON_LEFT:
-      p.SetMouseButton(OrthancStone::MouseButton_Left);
-      break;
-
-    case SDL_BUTTON_RIGHT:
-      p.SetMouseButton(OrthancStone::MouseButton_Right);
-      break;
-
-    case SDL_BUTTON_MIDDLE:
-      p.SetMouseButton(OrthancStone::MouseButton_Middle);
-      break;
-
-    default:
-      p.SetMouseButton(OrthancStone::MouseButton_None);
-      break;
-    }
-
-    p.AddPosition(compositor.GetPixelCenterCoordinates(event.button.x, event.button.y));
-    p.SetAltModifier(modifiers & KeyboardModifiers_Alt);
-    p.SetControlModifier(modifiers & KeyboardModifiers_Control);
-    p.SetShiftModifier(modifiers & KeyboardModifiers_Shift);
-  }
-
-}
-
-/**
- * IMPORTANT: The full arguments to "main()" are needed for SDL on
- * Windows. Otherwise, one gets the linking error "undefined reference
- * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
- **/
-int main(int argc, char* argv[])
-{
-  try
-  {
-    OrthancStone::StoneInitialize();
-    Orthanc::Logging::EnableInfoLevel(true);
-    //Orthanc::Logging::EnableTraceLevel(true);
-
-    {
-
-#if 1
-      boost::shared_ptr<OrthancStone::SdlViewport> viewport = 
-        OrthancStone::SdlOpenGLViewport::Create("Stone of Orthanc", 800, 600);
-#else
-      boost::shared_ptr<OrthancStone::SdlViewport> viewport =
-        OrthancStone::SdlCairoViewport::Create("Stone of Orthanc", 800, 600);
-#endif
-
-      OrthancStone::GenericLoadersContext context(1, 4, 1);
-      
-      context.StartOracle();
-
-      {
-
-        boost::shared_ptr<SdlSimpleViewerApplication> application(
-          SdlSimpleViewerApplication::Create(context, viewport));
-
-        OrthancStone::DicomSource source;
-
-        // Default and command-line parameters
-        const char* instanceId = "285dece8-e1956b38-cdc7d084-6ce3371e-536a9ffc";
-        unsigned int frameIndex = 0;
-
-        if (argc == 1)
-        {
-          LOG(ERROR) << "No instanceId supplied. The default of " << instanceId << " will be used. "
-            << "Please supply the Orthanc instance ID of the frame you wish to display then, optionally, "
-            << "the zero-based index of the frame (for multi-frame instances)";
-          // TODO: frame number as second argument...
-        }
-
-        if (argc >= 2)
-          instanceId = argv[1];
-
-        if (argc >= 3)
-          frameIndex = atoi(argv[1]);
-
-        if (argc > 3)
-        {
-          LOG(ERROR) << "Extra arguments ignored!";
-        }
-         
-
-        application->LoadOrthancFrame(source, instanceId, frameIndex);
-
-        OrthancStone::DefaultViewportInteractor interactor;
-
-        {
-          int scancodeCount = 0;
-          const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
-
-          bool stop = false;
-          while (!stop)
-          {
-            bool paint = false;
-            SDL_Event event;
-            while (SDL_PollEvent(&event))
-            {
-              if (event.type == SDL_QUIT)
-              {
-                stop = true;
-                break;
-              }
-              else if (viewport->IsRefreshEvent(event))
-              {
-                paint = true;
-              }
-              else if (event.type == SDL_WINDOWEVENT &&
-                (event.window.event == SDL_WINDOWEVENT_RESIZED ||
-                 event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED))
-              {
-                viewport->UpdateSize(event.window.data1, event.window.data2);
-              }
-              else if (event.type == SDL_WINDOWEVENT &&
-                (event.window.event == SDL_WINDOWEVENT_SHOWN ||
-                 event.window.event == SDL_WINDOWEVENT_EXPOSED))
-              {
-                paint = true;
-              }
-              else if (event.type == SDL_KEYDOWN &&
-                       event.key.repeat == 0 /* Ignore key bounce */)
-              {
-                switch (event.key.keysym.sym)
-                {
-                case SDLK_f:
-                  viewport->ToggleMaximize();
-                  break;
-
-                case SDLK_s:
-                  application->FitContent();
-                  break;
-
-                case SDLK_q:
-                  stop = true;
-                  break;
-
-                default:
-                  break;
-                }
-              }
-              else if (event.type == SDL_MOUSEBUTTONDOWN ||
-                       event.type == SDL_MOUSEMOTION ||
-                       event.type == SDL_MOUSEBUTTONUP)
-              {
-                std::auto_ptr<OrthancStone::IViewport::ILock> lock(viewport->Lock());
-                if (lock->HasCompositor())
-                {
-                  OrthancStone::PointerEvent p;
-                  OrthancStone::GetPointerEvent(p, lock->GetCompositor(),
-                                                event, keyboardState, scancodeCount);
-
-                  switch (event.type)
-                  {
-                  case SDL_MOUSEBUTTONDOWN:
-                    lock->GetController().HandleMousePress(interactor, p,
-                                                           lock->GetCompositor().GetCanvasWidth(),
-                                                           lock->GetCompositor().GetCanvasHeight());
-                    lock->Invalidate();
-                    break;
-
-                  case SDL_MOUSEMOTION:
-                    if (lock->GetController().HandleMouseMove(p))
-                    {
-                      lock->Invalidate();
-                    }
-                    break;
-
-                  case SDL_MOUSEBUTTONUP:
-                    lock->GetController().HandleMouseRelease(p);
-                    lock->Invalidate();
-                    break;
-
-                  default:
-                    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-                  }
-                }
-              }
-            }
-
-            if (paint)
-            {
-              viewport->Paint();
-            }
-
-            // Small delay to avoid using 100% of CPU
-            SDL_Delay(1);
-          }
-        }
-
-        context.StopOracle();
-      }
-    }
-
-    OrthancStone::StoneFinalize();
-    return 0;
-  }
-  catch (Orthanc::OrthancException & e)
-  {
-    auto test = e.What();
-    fprintf(stdout, test);
-    LOG(ERROR) << "OrthancException: " << e.What();
-    return -1;
-  }
-  catch (OrthancStone::StoneException & e)
-  {
-    LOG(ERROR) << "StoneException: " << e.What();
-    return -1;
-  }
-  catch (std::runtime_error & e)
-  {
-    LOG(ERROR) << "Runtime error: " << e.what();
-    return -1;
-  }
-  catch (...)
-  {
-    LOG(ERROR) << "Native exception";
-    return -1;
-  }
-}
--- a/Samples/WebAssembly/RtViewer/CMakeLists.txt	Tue Apr 28 13:52:21 2020 +0200
+++ b/Samples/WebAssembly/RtViewer/CMakeLists.txt	Wed Apr 29 15:54:18 2020 +0200
@@ -33,6 +33,7 @@
 set(ORTHANC_FRAMEWORK_SOURCE "path")
 
 include(${STONE_ROOT}/Resources/CMake/OrthancStoneParameters.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake)
 
 SET(ENABLE_DCMTK ON)
 SET(ENABLE_GOOGLE_TEST OFF)
@@ -40,11 +41,25 @@
 SET(ENABLE_WASM ON)
 SET(ORTHANC_SANDBOXED ON)
 
+
+# We embed a font to be used for on-screen overlays
+# ---------------------------------------------------------------
+
+DownloadPackage(
+  "a24b8136b8f3bb93f166baf97d9328de"
+  "http://orthanc.osimis.io/ThirdPartyDownloads/ubuntu-font-family-0.83.zip"
+  "${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83")
+
+set(ORTHANC_STONE_APPLICATION_RESOURCES
+  UBUNTU_FONT  ${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83/Ubuntu-R.ttf
+  )
+
 # 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})
+include_directories(../../Common)
 
 # Define the WASM module
 # ---------------------------------------------------------------
@@ -71,9 +86,8 @@
 # ---------------------------------------------------------------
 install(
   FILES
-  ${CMAKE_SOURCE_DIR}/RtViewerApp.js
+  ${CMAKE_SOURCE_DIR}/RtViewerWasmApp.js
   ${CMAKE_SOURCE_DIR}/index.html
   ${CMAKE_CURRENT_BINARY_DIR}/RtViewerWasm.wasm
-  ${CMAKE_SOURCE_DIR}/WasmWrapper.js
   DESTINATION ${CMAKE_INSTALL_PREFIX}
   )
--- a/Samples/WebAssembly/RtViewer/RtViewerWasm.cpp	Tue Apr 28 13:52:21 2020 +0200
+++ b/Samples/WebAssembly/RtViewer/RtViewerWasm.cpp	Wed Apr 29 15:54:18 2020 +0200
@@ -0,0 +1,170 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "RtViewer.h"
+#include "SampleHelpers.h"
+
+// Stone of Orthanc includes
+#include <Framework/Loaders/WebAssemblyLoadersContext.h>
+//#include <Framework/OpenGL/WebAssemblyOpenGLContext.h>
+#include <Framework/Viewport/WebGLViewport.h>
+#include <Framework/StoneException.h>
+#include <Framework/StoneInitialization.h>
+
+#include <Framework/Loaders/WebAssemblyLoadersContext.h>
+
+#include <Framework/StoneException.h>
+#include <Framework/StoneInitialization.h>
+
+#include <Core/Toolbox.h>
+
+#include <boost/program_options.hpp>
+#include <boost/shared_ptr.hpp>
+// #include <boost/pointer_cast.hpp> this include might be necessary in more recent boost versions
+
+#include <emscripten.h>
+#include <emscripten/html5.h>
+
+
+#define DISPATCH_JAVASCRIPT_EVENT(name)                         \
+  EM_ASM(                                                       \
+    const customEvent = document.createEvent("CustomEvent");    \
+    customEvent.initCustomEvent(name, false, false, undefined); \
+    window.dispatchEvent(customEvent);                          \
+    );
+
+#define EXTERN_CATCH_EXCEPTIONS                         \
+  catch (Orthanc::OrthancException& e)                  \
+  {                                                     \
+    LOG(ERROR) << "OrthancException: " << e.What();     \
+    DISPATCH_JAVASCRIPT_EVENT("StoneException");        \
+  }                                                     \
+  catch (OrthancStone::StoneException& e)               \
+  {                                                     \
+    LOG(ERROR) << "StoneException: " << e.What();       \
+    DISPATCH_JAVASCRIPT_EVENT("StoneException");        \
+  }                                                     \
+  catch (std::exception& e)                             \
+  {                                                     \
+    LOG(ERROR) << "Runtime error: " << e.what();        \
+    DISPATCH_JAVASCRIPT_EVENT("StoneException");        \
+  }                                                     \
+  catch (...)                                           \
+  {                                                     \
+    LOG(ERROR) << "Native exception";                   \
+    DISPATCH_JAVASCRIPT_EVENT("StoneException");        \
+  }
+
+namespace OrthancStone
+{
+  void RtViewerApp::CreateViewport()
+  {
+    viewport_ = WebGLViewport::Create("mycanvas1");
+  }
+
+  void RtViewerApp::TakeScreenshot(const std::string& target,
+                                   unsigned int canvasWidth,
+                                   unsigned int canvasHeight)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+  }
+
+
+  void RtViewerApp::RunWasm()
+  {
+    {
+      std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+      ViewportController& controller = lock->GetController();
+      Scene2D& scene = controller.GetScene();
+      ICompositor& compositor = lock->GetCompositor();
+
+      controller.FitContent(compositor.GetCanvasWidth(), compositor.GetCanvasHeight());
+
+      compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT,
+                         FONT_SIZE_0, Orthanc::Encoding_Latin1);
+      compositor.SetFont(1, Orthanc::EmbeddedResources::UBUNTU_FONT,
+                         FONT_SIZE_1, Orthanc::Encoding_Latin1);
+    }
+
+    loadersContext_.reset(new OrthancStone::WebAssemblyLoadersContext(1, 4, 1));
+
+    // we are in WASM --> downcast to concrete type
+    boost::shared_ptr<WebAssemblyLoadersContext> loadersContext = 
+      boost::dynamic_pointer_cast<WebAssemblyLoadersContext>(loadersContext_);
+
+    if (HasArgument("orthanc"))
+      loadersContext->SetLocalOrthanc(GetArgument("orthanc"));
+    else 
+      loadersContext->SetLocalOrthanc("..");
+
+    loadersContext->SetDicomCacheSize(128 * 1024 * 1024);  // 128MB
+
+    PrepareLoadersAndSlicers();
+
+    DefaultViewportInteractor interactor;
+  }
+}
+
+extern "C"
+{
+  boost::shared_ptr<OrthancStone::RtViewerApp> g_app;
+
+  int main(int argc, char const *argv[]) 
+  {
+    try
+    {
+      OrthancStone::StoneInitialize();
+      Orthanc::Logging::Initialize();
+      Orthanc::Logging::EnableTraceLevel(true);
+
+      LOG(WARNING) << "Initializing native Stone";
+
+      LOG(WARNING) << "Compiled with Emscripten " << __EMSCRIPTEN_major__
+                   << "." << __EMSCRIPTEN_minor__
+                   << "." << __EMSCRIPTEN_tiny__;
+
+      LOG(INFO) << "Endianness: " << Orthanc::EnumerationToString(Orthanc::Toolbox::DetectEndianness());
+
+      g_app = OrthancStone::RtViewerApp::Create();
+  
+      DISPATCH_JAVASCRIPT_EVENT("WasmModuleInitialized");
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }
+
+  EMSCRIPTEN_KEEPALIVE
+  void Initialize(const char* canvasId)
+  {
+    try
+    {
+      g_app->RunWasm();
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }
+
+  EMSCRIPTEN_KEEPALIVE
+  void SetArgument(const char* key, const char* value)
+  {
+    // This is called for each GET argument (cf. "app.js")
+    LOG(INFO) << "Received GET argument: [" << key << "] = [" << value << "]";
+    g_app->SetArgument(key, value);
+  }
+
+}
--- a/Samples/WebAssembly/RtViewer/RtViewerWasm.html	Tue Apr 28 13:52:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,59 +0,0 @@
-<!doctype html>
-<html lang="en-us">
-  <head>
-    <meta charset="utf-8">
-    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-
-    <!-- Disable pinch zoom on mobile devices -->
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
-    <meta name="HandheldFriendly" content="true" />
-    
-    
-    <title>Stone of Orthanc</title>
-
-    <style>
-      html, body {
-      width: 100%;
-      height: 100%;
-      margin: 0px;
-      border: 0;
-      overflow: hidden; /*  Disable scrollbars */
-      display: block;  /* No floating content on sides */
-      }
-
-      #mycanvas1 {
-      position:absolute;
-      left:0%;
-      top:0%;
-      background-color: red;
-      width: 50%;
-      height: 100%;
-      }
-
-      #mycanvas2 {
-      position:absolute;
-      left:50%;
-      top:0%;
-      background-color: green;
-      width: 50%;
-      height: 50%;
-      }
-
-      #mycanvas3 {
-      position:absolute;
-      left:50%;
-      top:50%;
-      background-color: blue;
-      width: 50%;
-      height: 50%;
-      }
-    </style>
-  </head>
-  <body>
-    <canvas id="mycanvas1" oncontextmenu="return false;"></canvas>
-    <canvas id="mycanvas2" oncontextmenu="return false;"></canvas>
-    <canvas id="mycanvas3" oncontextmenu="return false;"></canvas>
-
-    <script src="RtViewerWasmApp.js"></script>
-  </body>
-</html>
--- a/Samples/WebAssembly/RtViewer/RtViewerWasmApp.js	Tue Apr 28 13:52:21 2020 +0200
+++ b/Samples/WebAssembly/RtViewer/RtViewerWasmApp.js	Wed Apr 29 15:54:18 2020 +0200
@@ -1,11 +1,33 @@
-// Check support for WebAssembly
-if (!('WebAssembly' in window)) {
-  alert('Sorry, your browser does not support WebAssembly :(');
-} else {
+
+// This object wraps the functions exposed by the wasm module
+
+const WasmModuleWrapper = function() {
+  this._InitializeViewport = undefined;
+};
+
+WasmModuleWrapper.prototype.Setup = function(Module) {
+  this._SetArgument = Module.cwrap('SetArgument', null, [ 'string', 'string' ]);
+  this._Initialize = Module.cwrap('Initialize', null, [ 'string' ]);
+};
 
-  // Wait for the module to be loaded (the event "WebAssemblyLoaded"
-  // must be emitted by the "main" function)
-  window.addEventListener('WebAssemblyLoaded', function() {
+WasmModuleWrapper.prototype.SetArgument = function(key, value) {
+  this._SetArgument(key, value);
+};
+
+WasmModuleWrapper.prototype.Initialize = function(canvasId) {
+  this._Initialize(canvasId);
+};
+
+var wasmModuleWrapper = new WasmModuleWrapper();
+
+$(document).ready(function() {
+
+  window.addEventListener('WasmModuleInitialized', function() {
+
+    // bind the C++ global functions
+    wasmModuleWrapper.Setup(Module);
+
+    console.warn('Native C++ module initialized');
 
     // Loop over the GET arguments
     var parameters = window.location.search.substr(1);
@@ -14,15 +36,50 @@
       for (var i = 0; i < tokens.length; i++) {
         var arg = tokens[i].split('=');
         if (arg.length == 2) {
-
           // Send each GET argument to WebAssembly
-          Module.ccall('SetArgument', null, [ 'string', 'string' ],
-                       [ arg[0], decodeURIComponent(arg[1]) ]);
+          wasmModuleWrapper.SetArgument(arg[0], decodeURIComponent(arg[1]));
         }
       }
     }
+    wasmModuleWrapper.Initialize();
+  });
 
-    // Inform the WebAssembly module that it can start
-    Module.ccall('Initialize', null, null, null);
-  });
-}
+  window.addEventListener('StoneException', function() {
+    alert('Exception caught in C++ code');
+  });    
+
+  var scriptSource;
+
+  if ('WebAssembly' in window) {
+    console.warn('Loading WebAssembly');
+    scriptSource = 'RtViewerWasm.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');
+    });
+});
+
+// http://localhost:9979/rtviewer/index.html?loglevel=trace&ctseries=CTSERIES&rtdose=RTDOSE&rtstruct=RTSTRUCT
+
--- a/Samples/WebAssembly/SingleFrameViewer/CMakeLists.txt	Tue Apr 28 13:52:21 2020 +0200
+++ b/Samples/WebAssembly/SingleFrameViewer/CMakeLists.txt	Wed Apr 29 15:54:18 2020 +0200
@@ -61,9 +61,10 @@
   )
 
 # Declare installation files for the companion files (web scaffolding)
-# please note that ${CMAKE_CURRENT_BINARY_DIR}/SingleFrameViewerWasm.js 
+# please note that ${CMAKE_CURRENT_BINARY_DIR}/RtViewerWasm.js
 # (the generated JS loader for the WASM module) is handled by the `install1`
-# section above
+# section above: it is considered to be the binary output of 
+# the linker.
 # ---------------------------------------------------------------
 install(
   FILES
--- a/Samples/WebAssembly/SingleFrameViewer/SingleFrameViewerApp.js	Tue Apr 28 13:52:21 2020 +0200
+++ b/Samples/WebAssembly/SingleFrameViewer/SingleFrameViewerApp.js	Wed Apr 29 15:54:18 2020 +0200
@@ -3,14 +3,12 @@
 
 const WasmModuleWrapper = function() {
   this._InitializeViewport = undefined;
-  this._LoadOrthanc = undefined;
-  this._LoadDicomWeb = undefined;
+  this._LoadFromOrthanc = undefined;
 };
 
 WasmModuleWrapper.prototype.Setup = function(Module) {
   this._InitializeViewport = Module.cwrap('InitializeViewport', null, [ 'string' ]);
   this._LoadFromOrthanc = Module.cwrap('LoadFromOrthanc', null, [ 'string', 'int' ]);
-  this._LoadFromDicomWeb = Module.cwrap('LoadFromDicomWeb', null, [ 'string', 'string', 'string', 'string', 'int' ]);
 };
 
 WasmModuleWrapper.prototype.InitializeViewport = function(canvasId) {
@@ -21,10 +19,6 @@
   this._LoadFromOrthanc(instance, frame);
 };
 
-WasmModuleWrapper.prototype.LoadFromDicomWeb = function(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid, frame) {
-  this._LoadFromDicomWeb(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid, frame);
-};
-
 var wasmModuleWrapper = new WasmModuleWrapper();
 
 $(document).ready(function() {
--- a/Samples/WebAssembly/SingleFrameViewer/index.html	Tue Apr 28 13:52:21 2020 +0200
+++ b/Samples/WebAssembly/SingleFrameViewer/index.html	Wed Apr 29 15:54:18 2020 +0200
@@ -1,7 +1,7 @@
 <!doctype html>
 <html lang="en">
   <head>
-    <title>Osimis' Web Viewer</title>
+    <title>Stone of Orthanc Single Frame 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" />