changeset 1899:917500c46fe0

moved the Platform folder from the Applications folder to the Stone library itself
author Alain Mazy <am@osimis.io>
date Sat, 29 Jan 2022 12:47:32 +0100
parents a5e54bd87b25
children 563f6aa8e24c
files Applications/Platforms/Sdl/OrthancStoneSdlConfiguration.cmake Applications/Platforms/Sdl/OrthancStoneSdlParameters.cmake Applications/Platforms/Sdl/SdlConfiguration.cmake Applications/Platforms/Sdl/SdlOpenGLContext.cpp Applications/Platforms/Sdl/SdlOpenGLContext.h Applications/Platforms/Sdl/SdlViewport.cpp Applications/Platforms/Sdl/SdlViewport.h Applications/Platforms/Sdl/SdlWindow.cpp Applications/Platforms/Sdl/SdlWindow.h Applications/Platforms/WebAssembly/OrthancStoneWebAssemblyConfiguration.cmake Applications/Platforms/WebAssembly/OrthancStoneWebAssemblyParameters.cmake Applications/Platforms/WebAssembly/SharedLibrary/CMakeLists.txt Applications/Platforms/WebAssembly/SharedLibrary/NOTES.txt Applications/Platforms/WebAssembly/UnitTests/CMakeLists.txt Applications/Platforms/WebAssembly/UnitTests/NOTES.txt Applications/Platforms/WebAssembly/UnitTests/index.html Applications/Platforms/WebAssembly/WebAssemblyCairoViewport.cpp Applications/Platforms/WebAssembly/WebAssemblyCairoViewport.h Applications/Platforms/WebAssembly/WebAssemblyLoadersContext.cpp Applications/Platforms/WebAssembly/WebAssemblyLoadersContext.h Applications/Platforms/WebAssembly/WebAssemblyOpenGLContext.cpp Applications/Platforms/WebAssembly/WebAssemblyOpenGLContext.h Applications/Platforms/WebAssembly/WebAssemblyOracle.cpp Applications/Platforms/WebAssembly/WebAssemblyOracle.h Applications/Platforms/WebAssembly/WebAssemblyOracle_Includes.h Applications/Platforms/WebAssembly/WebAssemblyViewport.cpp Applications/Platforms/WebAssembly/WebAssemblyViewport.h Applications/Platforms/WebAssembly/WebGLViewport.cpp Applications/Platforms/WebAssembly/WebGLViewport.h Applications/Platforms/WebAssembly/WebGLViewportsRegistry.cpp Applications/Platforms/WebAssembly/WebGLViewportsRegistry.h OrthancStone/Sources/Platforms/Sdl/OrthancStoneSdlConfiguration.cmake OrthancStone/Sources/Platforms/Sdl/OrthancStoneSdlParameters.cmake OrthancStone/Sources/Platforms/Sdl/SdlConfiguration.cmake OrthancStone/Sources/Platforms/Sdl/SdlOpenGLContext.cpp OrthancStone/Sources/Platforms/Sdl/SdlOpenGLContext.h OrthancStone/Sources/Platforms/Sdl/SdlViewport.cpp OrthancStone/Sources/Platforms/Sdl/SdlViewport.h OrthancStone/Sources/Platforms/Sdl/SdlWindow.cpp OrthancStone/Sources/Platforms/Sdl/SdlWindow.h OrthancStone/Sources/Platforms/WebAssembly/OrthancStoneWebAssemblyConfiguration.cmake OrthancStone/Sources/Platforms/WebAssembly/OrthancStoneWebAssemblyParameters.cmake OrthancStone/Sources/Platforms/WebAssembly/SharedLibrary/CMakeLists.txt OrthancStone/Sources/Platforms/WebAssembly/SharedLibrary/NOTES.txt OrthancStone/Sources/Platforms/WebAssembly/UnitTests/CMakeLists.txt OrthancStone/Sources/Platforms/WebAssembly/UnitTests/NOTES.txt OrthancStone/Sources/Platforms/WebAssembly/UnitTests/index.html OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyCairoViewport.cpp OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyCairoViewport.h OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyLoadersContext.cpp OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyLoadersContext.h OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyOpenGLContext.cpp OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyOpenGLContext.h OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyOracle.cpp OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyOracle.h OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyOracle_Includes.h OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyViewport.cpp OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyViewport.h OrthancStone/Sources/Platforms/WebAssembly/WebGLViewport.cpp OrthancStone/Sources/Platforms/WebAssembly/WebGLViewport.h OrthancStone/Sources/Platforms/WebAssembly/WebGLViewportsRegistry.cpp OrthancStone/Sources/Platforms/WebAssembly/WebGLViewportsRegistry.h
diffstat 62 files changed, 4638 insertions(+), 4603 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/Platforms/Sdl/OrthancStoneSdlConfiguration.cmake	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-# Stone of Orthanc
-# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
-# Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
-
-
-
-#####################################################################
-## Sanity check of the configuration
-#####################################################################
-
-include(${ORTHANC_STONE_ROOT}/../Resources/CMake/OrthancStoneConfiguration.cmake)
-
-if (ORTHANC_SANDBOXED)
-  message(FATAL_ERROR "Cannot enable SDL in sandboxed environments")
-endif()
-
-if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten" OR
-    CMAKE_SYSTEM_NAME STREQUAL "PNaCl" OR
-    CMAKE_SYSTEM_NAME STREQUAL "NaCl32" OR
-    CMAKE_SYSTEM_NAME STREQUAL "NaCl64")
-  message(FATAL_ERROR "Trying to use a Web compiler for a native build")
-endif()
-
-
-
-#####################################################################
-## Configure SDL
-#####################################################################
-
-message("SDL is enabled")
-include(${CMAKE_CURRENT_LIST_DIR}/SdlConfiguration.cmake)
-
-add_definitions(
-  -DORTHANC_ENABLE_SDL=1
-  -DORTHANC_ENABLE_WASM=0
-  )
-
-if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows" AND
-    NOT MSVC)
-  # This is necessary when compiling EXE for Windows using MinGW
-  link_libraries(mingw32)
-endif()
-
-
-
-#####################################################################
-## Additional source files for SDL
-#####################################################################
-
-list(APPEND ORTHANC_STONE_SOURCES
-  ${CMAKE_CURRENT_LIST_DIR}/SdlViewport.cpp
-  ${CMAKE_CURRENT_LIST_DIR}/SdlWindow.cpp
-  ${SDL_SOURCES}
-  )
-
-if (ENABLE_OPENGL)
-  list(APPEND ORTHANC_STONE_SOURCES
-    ${CMAKE_CURRENT_LIST_DIR}/SdlOpenGLContext.cpp
-    )
-endif()
--- a/Applications/Platforms/Sdl/OrthancStoneSdlParameters.cmake	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-# Stone of Orthanc
-# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
-# Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
-
-
-
-#####################################################################
-## Load generic parameters
-#####################################################################
-
-include(${CMAKE_CURRENT_LIST_DIR}/../../../OrthancStone/Resources/CMake/OrthancStoneParameters.cmake)
-
-
-#####################################################################
-## CMake parameters tunable by the user
-#####################################################################
-
-# Advanced parameters to fine-tune linking against system libraries
-set(USE_SYSTEM_SDL ON CACHE BOOL "Use the system version of SDL2")
-
-
-#####################################################################
-## Internal CMake parameters to enable the optional subcomponents of
-## the Stone of Orthanc
-#####################################################################
-
-set(ENABLE_THREADS ON CACHE INTERNAL "")
--- a/Applications/Platforms/Sdl/SdlConfiguration.cmake	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,225 +0,0 @@
-# Stone of Orthanc
-# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
-# Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
-
-
-if (STATIC_BUILD OR NOT USE_SYSTEM_SDL)
-  SET(SDL_SOURCES_DIR ${CMAKE_BINARY_DIR}/SDL2-2.0.4)
-  SET(SDL_URL "http://orthanc.osimis.io/ThirdPartyDownloads/SDL2-2.0.4.tar.gz")
-  SET(SDL_MD5 "44fc4a023349933e7f5d7a582f7b886e")
-  DownloadPackage(${SDL_MD5} ${SDL_URL} "${SDL_SOURCES_DIR}")
-
-  include_directories(${SDL_SOURCES_DIR}/include)
-
-  set(TMP "${SDL_SOURCES_DIR}/include/SDL_config_premake.h")
-  if (NOT EXISTS "${TMP}")
-    file(WRITE "${TMP}" "
-#include \"SDL_platform.h\"
-#define HAVE_STDARG_H 1
-#define HAVE_STDDEF_H 1
-#define HAVE_STDINT_H 1
-")
-  endif()
-
-  # General source files
-  file(GLOB SDL_SOURCES
-    ${SDL_SOURCES_DIR}/src/*.c
-    ${SDL_SOURCES_DIR}/src/atomic/*.c
-    ${SDL_SOURCES_DIR}/src/audio/*.c
-    ${SDL_SOURCES_DIR}/src/cpuinfo/*.c
-    ${SDL_SOURCES_DIR}/src/dynapi/*.c
-    ${SDL_SOURCES_DIR}/src/events/*.c
-    ${SDL_SOURCES_DIR}/src/file/*.c
-    ${SDL_SOURCES_DIR}/src/haptic/*.c
-    ${SDL_SOURCES_DIR}/src/joystick/*.c
-    ${SDL_SOURCES_DIR}/src/libm/*.c
-    ${SDL_SOURCES_DIR}/src/power/*.c
-    ${SDL_SOURCES_DIR}/src/render/*.c
-    ${SDL_SOURCES_DIR}/src/stdlib/*.c
-    ${SDL_SOURCES_DIR}/src/thread/*.c
-    ${SDL_SOURCES_DIR}/src/timer/*.c
-    ${SDL_SOURCES_DIR}/src/video/*.c
-
-    ${SDL_SOURCES_DIR}/src/loadso/dummy/*.c
-    #${SDL_SOURCES_DIR}/src/timer/dummy/*.c
-    ${SDL_SOURCES_DIR}/src/audio/dummy/*.c
-    ${SDL_SOURCES_DIR}/src/filesystem/dummy/*.c
-    ${SDL_SOURCES_DIR}/src/haptic/dummy/*.c
-    ${SDL_SOURCES_DIR}/src/joystick/dummy/*.c
-    #${SDL_SOURCES_DIR}/src/main/dummy/*.c
-    ${SDL_SOURCES_DIR}/src/video/dummy/*.c
-    )
-
-  add_definitions(
-    -DUSING_PREMAKE_CONFIG_H=1
-
-    -DSDL_AUDIO_DISABLED=1
-    -DSDL_AUDIO_DRIVER_DUMMY=1
-    -DSDL_FILESYSTEM_DISABLED=1
-    -DSDL_FILESYSTEM_DUMMY=1
-    -DSDL_FILE_DISABLED=1
-    -DSDL_HAPTIC_DISABLED=1
-    -DSDL_JOYSTICK_DISABLED=1
-
-    #-DSDL_THREADS_DISABLED=1
-    )
-
-  if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
-    file(GLOB TMP
-      ${SDL_SOURCES_DIR}/src/core/linux/*.c
-      ${SDL_SOURCES_DIR}/src/loadso/dlopen/*.c
-      ${SDL_SOURCES_DIR}/src/render/software/*.c
-      ${SDL_SOURCES_DIR}/src/thread/pthread/*.c
-      ${SDL_SOURCES_DIR}/src/timer/unix/*.c
-      ${SDL_SOURCES_DIR}/src/video/x11/*.c
-      )
-
-    list(APPEND SDL_SOURCES ${TMP})
-
-    add_definitions(
-      -DSDL_LOADSO_DLOPEN=1
-      -DSDL_THREAD_PTHREAD=1
-      -DSDL_TIMER_UNIX=1
-      -DSDL_POWER_DISABLED=1
-
-      -DSDL_VIDEO_DRIVER_X11=1
-
-      -DSDL_ASSEMBLY_ROUTINES=1
-      -DSDL_THREAD_PTHREAD_RECURSIVE_MUTEX=1
-      -DSDL_VIDEO_DRIVER_X11_SUPPORTS_GENERIC_EVENTS=1
-      -DHAVE_GCC_SYNC_LOCK_TEST_AND_SET=1
-      )
-
-    link_libraries(X11 Xext)
-
-    if (NOT CMAKE_SYSTEM_VERSION STREQUAL "Raspberry")
-      # Raspberry Pi has no support for OpenGL
-      file(GLOB TMP
-        ${SDL_SOURCES_DIR}/src/render/opengl/*.c
-        ${SDL_SOURCES_DIR}/src/render/opengles2/*.c
-        )
-
-      list(APPEND SDL_SOURCES ${TMP})
-
-      add_definitions(
-        -DSDL_VIDEO_OPENGL=1
-        -DSDL_VIDEO_OPENGL_ES2=1
-        -DSDL_VIDEO_RENDER_OGL=1
-        -DSDL_VIDEO_RENDER_OGL_ES2=1
-        -DSDL_VIDEO_OPENGL_GLX=1
-        -DSDL_VIDEO_OPENGL_EGL=1
-        )
-    endif()
-
-  elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows")
-    file(GLOB TMP
-      ${SDL_SOURCES_DIR}/src/audio/directsound/*.c
-      ${SDL_SOURCES_DIR}/src/audio/disk/*.c
-      ${SDL_SOURCES_DIR}/src/audio/winmm/*.c
-      ${SDL_SOURCES_DIR}/src/joystick/windows/*.c
-      ${SDL_SOURCES_DIR}/src/haptic/windows/*.c
-      ${SDL_SOURCES_DIR}/src/power/windows/*.c
-
-      ${SDL_SOURCES_DIR}/src/main/windows/*.c
-      ${SDL_SOURCES_DIR}/src/core/windows/*.c
-      ${SDL_SOURCES_DIR}/src/loadso/windows/*.c
-      ${SDL_SOURCES_DIR}/src/render/direct3d/*.c
-      ${SDL_SOURCES_DIR}/src/render/direct3d11/*.c
-      ${SDL_SOURCES_DIR}/src/render/opengl/*.c
-      ${SDL_SOURCES_DIR}/src/render/psp/*.c
-      ${SDL_SOURCES_DIR}/src/render/opengles/*.c
-      ${SDL_SOURCES_DIR}/src/render/opengles2/*.c
-      ${SDL_SOURCES_DIR}/src/render/software/*.c
-      ${SDL_SOURCES_DIR}/src/thread/generic/SDL_syscond.c   # Don't include more files from "thread/generic/*.c"!
-      ${SDL_SOURCES_DIR}/src/thread/windows/*.c
-      ${SDL_SOURCES_DIR}/src/timer/windows/*.c
-      ${SDL_SOURCES_DIR}/src/video/windows/*.c
-      ${SDL_SOURCES_DIR}/src/windows/dlopen/*.c
-      )
-
-    list(APPEND SDL_SOURCES ${TMP})
-
-    # NB: OpenGL ES headers are not available in MinGW-W64
-    add_definitions(
-      -DSDL_LOADSO_WINDOWS=1
-      -DSDL_THREAD_WINDOWS=1
-      -DSDL_TIMER_WINDOWS=1
-      -DSDL_POWER_WINDOWS=1
-
-      -DSDL_VIDEO_OPENGL=1
-      -DSDL_VIDEO_OPENGL_WGL=1
-      -DSDL_VIDEO_RENDER_D3D=1
-      -DSDL_VIDEO_RENDER_OGL=1
-      -DSDL_VIDEO_DRIVER_WINDOWS=1
-      )
-
-    if (MSVC)
-      add_definitions(
-        -D__FLTUSED__
-        -DHAVE_LIBC=1
-      )
-    else()
-      add_definitions(
-        -DHAVE_GCC_ATOMICS=1
-        -DSDL_ASSEMBLY_ROUTINES=1
-        )
-    endif()
-    
-    link_libraries(imm32 winmm version)
-
-  elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
-    file(GLOB TMP
-      ${SDL_SOURCES_DIR}/src/loadso/dlopen/*.c
-      ${SDL_SOURCES_DIR}/src/render/opengl/*.c
-      ${SDL_SOURCES_DIR}/src/render/opengles2/*.c
-      ${SDL_SOURCES_DIR}/src/render/software/*.c
-      ${SDL_SOURCES_DIR}/src/thread/pthread/*.c
-      ${SDL_SOURCES_DIR}/src/timer/unix/*.c
-      ${SDL_SOURCES_DIR}/src/video/cocoa/*.m
-      )
-
-    list(APPEND SDL_SOURCES ${TMP})
-
-    add_definitions(
-      -DSDL_LOADSO_DLOPEN=1
-      -DSDL_THREAD_PTHREAD=1
-      -DSDL_TIMER_UNIX=1
-      -DSDL_POWER_DISABLED=1
-
-      -DSDL_VIDEO_DRIVER_COCOA=1
-      -DSDL_VIDEO_OPENGL=1
-      -DSDL_VIDEO_OPENGL_CGL=1
-      -DSDL_VIDEO_RENDER_OGL=1
-      
-      -DSDL_ASSEMBLY_ROUTINES=1
-      -DSDL_THREAD_PTHREAD_RECURSIVE_MUTEX=1
-      )
-
-    find_library(CARBON_LIBRARY Carbon)
-    find_library(COCOA_LIBRARY Cocoa)
-    find_library(IOKIT_LIBRARY IOKit)
-    find_library(QUARTZ_LIBRARY QuartzCore)
-    link_libraries(${CARBON_LIBRARY} ${COCOA_LIBRARY} ${IOKIT_LIBRARY} ${QUARTZ_LIBRARY})
-
-  endif()
-
-else()
-  pkg_search_module(SDL2 REQUIRED sdl2)
-  include_directories(${SDL2_INCLUDE_DIRS})
-  link_libraries(${SDL2_LIBRARIES})
-endif()
--- a/Applications/Platforms/Sdl/SdlOpenGLContext.cpp	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,131 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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 "SdlOpenGLContext.h"
-#include "../../../OrthancStone/Sources/StoneException.h"
-
-#if ORTHANC_ENABLE_SDL == 1
-
-#if !defined(ORTHANC_ENABLE_GLEW)
-#  error Macro ORTHANC_ENABLE_GLEW must be defined
-#endif
-
-#if ORTHANC_ENABLE_GLEW == 1
-#  include <GL/glew.h>
-#endif
-
-#include <Logging.h>
-#include <OrthancException.h>
-
-#include <boost/thread/mutex.hpp>
-
-namespace OrthancStone
-{
-  SdlOpenGLContext::SdlOpenGLContext(const char* title,
-                                     unsigned int width,
-                                     unsigned int height,
-                                     bool allowDpiScaling) 
-    : window_(title, width, height, true /* enable OpenGL */, allowDpiScaling)
-    , context_(NULL)
-  {
-    context_ = SDL_GL_CreateContext(window_.GetObject());
-    
-    if (context_ == NULL)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
-                                      "Cannot initialize OpenGL");
-    }
-
-#if ORTHANC_ENABLE_GLEW == 1
-    // The initialization function of glew (i.e. "glewInit()") can
-    // only be called once an OpenGL is setup.
-    // https://stackoverflow.com/a/45033669/881731
-    {
-      static boost::mutex  mutex_;
-      static bool          isGlewInitialized_ = false;
-  
-      boost::mutex::scoped_lock lock(mutex_);
-
-      if (!isGlewInitialized_)
-      {
-        LOG(INFO) << "Initializing glew";
-        
-        GLenum err = glewInit();
-        if (GLEW_OK != err)
-        {
-          LOG(ERROR) << glewGetErrorString(err);
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
-                                          "Cannot initialize glew");
-        }
-
-        isGlewInitialized_ = true;
-      }
-    }    
-#endif
-  }
-
-  
-  SdlOpenGLContext::~SdlOpenGLContext()
-  {
-    SDL_GL_DeleteContext(context_);
-  }
-
-  void SdlOpenGLContext::MakeCurrent()
-  {
-    if (SDL_GL_MakeCurrent(window_.GetObject(), context_) != 0)
-    {
-      const char* errText = SDL_GetError();
-      std::stringstream ss;
-      ss << "Cannot set current OpenGL context. SDL error text: " << errText;
-      std::string errStr = ss.str();
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, errStr.c_str());
-    }
-
-    // This makes our buffer swap synchronized with the monitor's vertical refresh
-    SDL_GL_SetSwapInterval(1);
-  }
-
-  
-  void SdlOpenGLContext::SwapBuffer()
-  {
-    // Swap our buffer to display the current contents of buffer on screen
-    SDL_GL_SwapWindow(window_.GetObject());
-  }
-
-  
-  unsigned int SdlOpenGLContext::GetCanvasWidth() const
-  {
-    int w = 0;
-    SDL_GL_GetDrawableSize(window_.GetObject(), &w, NULL);
-    return static_cast<unsigned int>(w);
-  }
-
-  
-  unsigned int SdlOpenGLContext::GetCanvasHeight() const
-  {
-    int h = 0;
-    SDL_GL_GetDrawableSize(window_.GetObject(), NULL, &h);
-    return static_cast<unsigned int>(h);
-  }
-}
-
-#endif
--- a/Applications/Platforms/Sdl/SdlOpenGLContext.h	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,78 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
- **/
-
-
-#pragma once
-
-#if ORTHANC_ENABLE_SDL == 1
-
-#include "../../../OrthancStone/Sources/OpenGL/IOpenGLContext.h"
-#include "SdlWindow.h"
-
-#include <Compatibility.h>  // For ORTHANC_OVERRIDE
-
-#include <SDL_render.h>
-
-#include <Enumerations.h>
-
-namespace OrthancStone
-{
-  class SdlOpenGLContext : public OpenGL::IOpenGLContext
-  {
-  private:
-    SdlWindow      window_;
-    SDL_GLContext  context_;
-
-  public:
-    SdlOpenGLContext(const char* title,
-                     unsigned int width,
-                     unsigned int height,
-                     bool allowDpiScaling = true);
-
-    ~SdlOpenGLContext();
-
-    SdlWindow& GetWindow()
-    {
-      return window_;
-    }
-
-    virtual bool IsContextLost() ORTHANC_OVERRIDE
-    {
-      // On desktop applications, an OpenGL context should never be lost
-      return false;
-    }
-
-    virtual void MakeCurrent() ORTHANC_OVERRIDE;
-
-    virtual void SwapBuffer() ORTHANC_OVERRIDE;
-
-    unsigned int GetCanvasWidth() const;
-
-    unsigned int GetCanvasHeight() const;
-
-    void ToggleMaximize()
-    {
-      window_.ToggleMaximize();
-    }
-  };
-}
-
-#endif
--- a/Applications/Platforms/Sdl/SdlViewport.cpp	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,263 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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 "SdlViewport.h"
-
-#include "../../../OrthancStone/Sources/Scene2DViewport/ViewportController.h"
-
-#include <OrthancException.h>
-
-#include <boost/make_shared.hpp>
-
-namespace OrthancStone
-{
-  ICompositor& SdlViewport::SdlLock::GetCompositor()
-  {
-    if (that_.compositor_.get() == NULL)
-    {
-      // The derived class should have called "AcquireCompositor()"
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-    }
-    else
-    {
-      return *that_.compositor_;
-    }
-  }
-
-
-  void SdlViewport::AcquireCompositor(ICompositor* compositor /* takes ownership */)
-  {
-    if (compositor == NULL)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-    }
-
-    compositor_.reset(compositor);
-  }
-
-
-  SdlViewport::SdlViewport()
-  {
-    refreshEvent_ = SDL_RegisterEvents(1);
-
-    if (refreshEvent_ == static_cast<uint32_t>(-1))
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-    }
-  }
-  
-  void SdlViewport::PostConstructor()
-  {
-    controller_ = boost::make_shared<ViewportController>(shared_from_this());
-  }
-    
-  void SdlViewport::SendRefreshEvent()
-  {
-    SDL_Event event;
-    SDL_memset(&event, 0, sizeof(event));
-    event.type = refreshEvent_;
-    SDL_PushEvent(&event);  // This function is thread-safe, and can be called from other threads safely.
-  }
-
-  
-  void SdlViewport::UpdateSize(unsigned int width, unsigned int height)
-  {
-    SdlLock lock(*this);
-    lock.GetCompositor().SetCanvasSize(width, height);
-    lock.Invalidate();
-  }
-
-
-#if ORTHANC_ENABLE_OPENGL == 1
-  SdlOpenGLViewport::SdlOpenGLViewport(const std::string& title,
-                                       unsigned int width,
-                                       unsigned int height,
-                                       bool allowDpiScaling) :
-    context_(title.c_str(), width, height, allowDpiScaling)
-  {
-    AcquireCompositor(new OpenGLCompositor(context_));  // (*)
-  }
-#endif
-
-
-#if ORTHANC_ENABLE_OPENGL == 1
-  void SdlOpenGLViewport::RefreshCanvasSize()
-  {
-    UpdateSize(context_.GetCanvasWidth(), context_.GetCanvasHeight());
-  }
-#endif
-
-
-#if ORTHANC_ENABLE_OPENGL == 1
-  boost::shared_ptr<SdlOpenGLViewport> SdlOpenGLViewport::Create(const std::string& title,
-                                                                 unsigned int width,
-                                                                 unsigned int height,
-                                                                 bool allowDpiScaling)
-  {
-    boost::shared_ptr<SdlOpenGLViewport> that =
-      boost::shared_ptr<SdlOpenGLViewport>(new SdlOpenGLViewport(title, width, height, allowDpiScaling));
-    that->SdlViewport::PostConstructor();
-    return that;
-  }
-#endif
-
-
-#if ORTHANC_ENABLE_OPENGL == 1
-  uint32_t SdlOpenGLViewport::GetSdlWindowId()
-  {
-    const SdlWindow& sdlWindowWrapper = context_.GetWindow();
-    SDL_Window* sdlWindow = sdlWindowWrapper.GetObject();
-    Uint32 sdlWindowId = SDL_GetWindowID(sdlWindow);
-    return sdlWindowId;
-  }
-#endif
-  
-
-#if ORTHANC_ENABLE_OPENGL == 1
-  SdlOpenGLViewport::~SdlOpenGLViewport()
-  {
-    // Make sure that the "OpenGLCompositor" is destroyed BEFORE the
-    // "OpenGLContext" it references (*)
-    ClearCompositor();
-  }
-#endif
-
-
-#if ORTHANC_ENABLE_OPENGL == 1
-  void SdlOpenGLViewport::Paint()
-  {
-    SdlLock lock(*this);
-    lock.GetCompositor().Refresh(lock.GetController().GetScene());
-  }
-#endif
-
-
-#if ORTHANC_ENABLE_OPENGL == 1
-  void SdlOpenGLViewport::ToggleMaximize()
-  {
-    // No need to call "Invalidate()" here, as "UpdateSize()" will
-    // be invoked after event "SDL_WINDOWEVENT_SIZE_CHANGED"
-    SdlLock lock(*this);
-    context_.ToggleMaximize();
-  }
-#endif
-
-
-  void SdlCairoViewport::RefreshCanvasSize()
-  {
-    UpdateSize(window_.GetWidth(), window_.GetHeight());
-  }
-
-  SdlCairoViewport::SdlCairoViewport(const std::string& title,
-                                     unsigned int width,
-                                     unsigned int height,
-                                     bool allowDpiScaling) :
-    window_(title, width, height, false /* enable OpenGL */, allowDpiScaling),
-    sdlSurface_(NULL)
-  {
-    AcquireCompositor(new CairoCompositor(width, height));
-  }
-
-  SdlCairoViewport::~SdlCairoViewport()
-  {
-    if (sdlSurface_)
-    {
-      SDL_FreeSurface(sdlSurface_);
-    }
-  }
-  
-  uint32_t SdlCairoViewport::GetSdlWindowId()
-  {
-    SDL_Window* sdlWindow = window_.GetObject();
-    Uint32 sdlWindowId = SDL_GetWindowID(sdlWindow);
-    return sdlWindowId;
-  }
-
-  void SdlCairoViewport::Paint()
-  {
-    SdlLock lock(*this);
-
-    lock.GetCompositor().Refresh(lock.GetController().GetScene());
-    CreateSdlSurfaceFromCompositor(dynamic_cast<CairoCompositor&>(lock.GetCompositor()));
-    
-    if (sdlSurface_ != NULL)
-    {
-      window_.Render(sdlSurface_);
-    }
-  }
-
-
-  void SdlCairoViewport::ToggleMaximize()
-  {
-    // No need to call "Invalidate()" here, as "UpdateSize()" will
-    // be invoked after event "SDL_WINDOWEVENT_SIZE_CHANGED"
-    SdlLock lock(*this);
-    window_.ToggleMaximize();
-  }
-
-  
-  // Assumes that the mutex is locked
-  void SdlCairoViewport::CreateSdlSurfaceFromCompositor(const CairoCompositor& compositor)
-  {
-    static const uint32_t rmask = 0x00ff0000;
-    static const uint32_t gmask = 0x0000ff00;
-    static const uint32_t bmask = 0x000000ff;
-
-    const unsigned int width = compositor.GetCanvas().GetWidth();
-    const unsigned int height = compositor.GetCanvas().GetHeight();
-
-    if (sdlSurface_ != NULL)
-    {
-      if (sdlSurface_->pixels == compositor.GetCanvas().GetBuffer() &&
-          sdlSurface_->w == static_cast<int>(width) &&
-          sdlSurface_->h == static_cast<int>(height) &&
-          sdlSurface_->pitch == static_cast<int>(compositor.GetCanvas().GetPitch()))
-      {
-        // The image from the compositor has not changed, no need to update the surface
-        return;
-      }
-      else
-      {
-        SDL_FreeSurface(sdlSurface_);
-      }
-    }
-
-    sdlSurface_ = SDL_CreateRGBSurfaceFrom((void*)(compositor.GetCanvas().GetBuffer()), width, height, 32,
-                                           compositor.GetCanvas().GetPitch(), rmask, gmask, bmask, 0);
-    if (!sdlSurface_)
-    {
-      LOG(ERROR) << "Cannot create a SDL surface from a Cairo surface";
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-    }
-  }
-
-
-  boost::shared_ptr<SdlCairoViewport> SdlCairoViewport::Create(const std::string& title,
-                                                               unsigned int width,
-                                                               unsigned int height,
-                                                               bool allowDpiScaling)
-  {
-    boost::shared_ptr<SdlCairoViewport> that =
-      boost::shared_ptr<SdlCairoViewport>(new SdlCairoViewport(title, width, height, allowDpiScaling));
-    that->SdlViewport::PostConstructor();
-    return that;
-  }
-}
--- a/Applications/Platforms/Sdl/SdlViewport.h	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,204 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
- **/
-
-#pragma once
-
-#if !defined(ORTHANC_ENABLE_SDL)
-#  error Macro ORTHANC_ENABLE_SDL must be defined
-#endif
-
-#if ORTHANC_ENABLE_SDL != 1
-#  error SDL must be enabled to use this file
-#endif
-
-#if !defined(ORTHANC_ENABLE_OPENGL)
-#  error The macro ORTHANC_ENABLE_OPENGL must be defined
-#endif
-
-#include "SdlOpenGLContext.h"
-#include "../../../OrthancStone/Sources/Scene2D/CairoCompositor.h"
-#include "../../../OrthancStone/Sources/Viewport/IViewport.h"
-
-#if ORTHANC_ENABLE_OPENGL == 1
-#  include "../../../OrthancStone/Sources/Scene2D/OpenGLCompositor.h"
-#endif
-
-#include <SDL_events.h>
-
-// TODO: required for UndoStack injection
-// I don't like it either :)
-#include <boost/weak_ptr.hpp>
-
-#include <boost/thread/recursive_mutex.hpp>
-#include <boost/enable_shared_from_this.hpp>
-
-namespace OrthancStone
-{
-  class UndoStack;
-
-  class SdlViewport : public IViewport,
-                      public boost::enable_shared_from_this<SdlViewport>
-  {
-  private:
-    boost::recursive_mutex                 mutex_;
-    uint32_t                               refreshEvent_;
-    boost::shared_ptr<ViewportController>  controller_;
-    std::unique_ptr<ICompositor>           compositor_;
-
-    void SendRefreshEvent();
-
-  protected:
-    class SdlLock : public ILock
-    {
-    private:
-      SdlViewport&                        that_;
-      boost::recursive_mutex::scoped_lock lock_;
-
-    public:
-      explicit SdlLock(SdlViewport& that) :
-        that_(that),
-        lock_(that.mutex_)
-      {
-      }
-
-      virtual bool HasCompositor() const ORTHANC_OVERRIDE
-      {
-        return true;
-      }
-
-      virtual ICompositor& GetCompositor() ORTHANC_OVERRIDE;
-      
-      virtual ViewportController& GetController() ORTHANC_OVERRIDE
-      {
-        return *that_.controller_;
-      }
-
-      virtual void Invalidate() ORTHANC_OVERRIDE
-      {
-        that_.SendRefreshEvent();
-      }
-      
-      virtual void RefreshCanvasSize() ORTHANC_OVERRIDE
-      {
-        that_.RefreshCanvasSize();
-      }
-    };
-
-    void ClearCompositor()
-    {
-      compositor_.reset();
-    }
-
-    void AcquireCompositor(ICompositor* compositor /* takes ownership */);
-
-    virtual void RefreshCanvasSize() = 0;
-    
-  protected:
-    SdlViewport();
-
-    void PostConstructor();
-
-  public:
-    bool IsRefreshEvent(const SDL_Event& event) const
-    {
-      return (event.type == refreshEvent_);
-    }
-
-    virtual ILock* Lock() ORTHANC_OVERRIDE
-    {
-      return new SdlLock(*this);
-    }
-
-    virtual uint32_t GetSdlWindowId() = 0;
-
-    void UpdateSize(unsigned int width,
-                    unsigned int height);
-
-    virtual void ToggleMaximize() = 0;
-
-    // Must be invoked from the main SDL thread
-    virtual void Paint() = 0;
-  };
-
-
-#if ORTHANC_ENABLE_OPENGL == 1
-  class SdlOpenGLViewport : public SdlViewport
-  {
-  private:
-    SdlOpenGLContext  context_;
-
-    SdlOpenGLViewport(const std::string& title,
-                      unsigned int       width,
-                      unsigned int       height,
-                      bool               allowDpiScaling = true);
-
-  protected:
-    virtual void RefreshCanvasSize() ORTHANC_OVERRIDE;
-    
-  public:
-    static boost::shared_ptr<SdlOpenGLViewport> Create(const std::string& title,
-                                                       unsigned int width,
-                                                       unsigned int height,
-                                                       bool allowDpiScaling = true);
-
-
-    virtual ~SdlOpenGLViewport();
-
-    virtual uint32_t GetSdlWindowId() ORTHANC_OVERRIDE;
-
-    virtual void Paint() ORTHANC_OVERRIDE;
-
-    virtual void ToggleMaximize() ORTHANC_OVERRIDE;
-  };
-#endif
-
-
-  class SdlCairoViewport : public SdlViewport
-  {
-  private:
-    SdlWindow     window_;
-    SDL_Surface*  sdlSurface_;
-
-    void CreateSdlSurfaceFromCompositor(const CairoCompositor& compositor);
-
-    SdlCairoViewport(const std::string& title,
-                     unsigned int width,
-                     unsigned int height,
-                     bool allowDpiScaling);
-
-  protected:
-    virtual void RefreshCanvasSize() ORTHANC_OVERRIDE;
-    
-  public:
-    static boost::shared_ptr<SdlCairoViewport> Create(const std::string& title,
-                                                      unsigned int width,
-                                                      unsigned int height,
-                                                      bool allowDpiScaling = true);
-
-    virtual ~SdlCairoViewport();
-
-    virtual uint32_t GetSdlWindowId() ORTHANC_OVERRIDE;
-
-    virtual void Paint() ORTHANC_OVERRIDE;
-
-    virtual void ToggleMaximize() ORTHANC_OVERRIDE;
-  };
-}
--- a/Applications/Platforms/Sdl/SdlWindow.cpp	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,211 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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 "SdlWindow.h"
-
-#if ORTHANC_ENABLE_SDL == 1
-
-#include <Logging.h>
-#include <OrthancException.h>
-
-#ifdef WIN32 
-#include <windows.h> // for SetProcessDpiAware
-#endif 
-// WIN32
-
-#include <SDL_render.h>
-#include <SDL_video.h>
-#include <SDL.h>
-
-namespace OrthancStone
-{
-  SdlWindow::SdlWindow(const std::string& title,
-                       unsigned int width,
-                       unsigned int height,
-                       bool enableOpenGl,
-                       bool allowDpiScaling) :
-    maximized_(false)
-  {
-    // TODO Understand why, with SDL_WINDOW_OPENGL + MinGW32 + Release
-    // build mode, the application crashes whenever the SDL window is
-    // resized or maximized
-
-    uint32_t windowFlags, rendererFlags;
-    if (enableOpenGl)
-    {
-      windowFlags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE;
-      rendererFlags = SDL_RENDERER_ACCELERATED;
-    }
-    else
-    {
-      windowFlags = SDL_WINDOW_RESIZABLE;
-      rendererFlags = SDL_RENDERER_SOFTWARE;
-    }
-
-// TODO: probably required on MacOS X, too
-#if defined(WIN32) && (_WIN32_WINNT >= 0x0600)
-    if (!allowDpiScaling)
-    {
-      // if we do NOT allow DPI scaling, it means an SDL pixel will be a real
-      // monitor pixel. This is needed for high-DPI applications
-
-      // Enable high-DPI support on Windows
-
-      // THE FOLLOWING HAS BEEN COMMENTED OUT BECAUSE IT WILL CRASH UNDER 
-      // OLD WINDOWS VERSIONS
-      // ADD THIS AT THE TOP TO ENABLE IT:
-      // 
-      //#pragma comment(lib, "Shcore.lib") THIS IS ONLY REQUIRED FOR SetProcessDpiAwareness
-      //#include <windows.h>
-      //#include <ShellScalingAPI.h> THIS IS ONLY REQUIRED FOR SetProcessDpiAwareness
-      //#include <comdef.h> THIS IS ONLY REQUIRED FOR SetProcessDpiAwareness
-      // SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE);
-      
-      // This is supported on Vista+
-      SetProcessDPIAware();
-
-      windowFlags |= SDL_WINDOW_ALLOW_HIGHDPI;
-    }
-#endif 
-// WIN32
-    
-    window_ = SDL_CreateWindow(title.c_str(),
-                               SDL_WINDOWPOS_UNDEFINED,
-                               SDL_WINDOWPOS_UNDEFINED,
-                               width, height, windowFlags);
-
-    if (window_ == NULL) 
-    {
-      LOG(ERROR) << "Cannot create the SDL window: " << SDL_GetError();
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-    }
-
-    renderer_ = SDL_CreateRenderer(window_, -1, rendererFlags);
-    if (!renderer_)
-    {
-      LOG(ERROR) << "Cannot create the SDL renderer: " << SDL_GetError();
-      SDL_DestroyWindow(window_);
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-    }
-  }
-
-
-  SdlWindow::~SdlWindow()
-  {
-    if (renderer_ != NULL)
-    { 
-      SDL_DestroyRenderer(renderer_);
-    }
-
-    if (window_ != NULL)
-    { 
-      SDL_DestroyWindow(window_);
-    }
-  }
-
-
-  unsigned int SdlWindow::GetWidth() const
-  {
-    int w = -1;
-    SDL_GetWindowSize(window_, &w, NULL);
-
-    if (w < 0)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-    }
-    else
-    {
-      return static_cast<unsigned int>(w);
-    }
-  }
-
-
-  unsigned int SdlWindow::GetHeight() const
-  {
-    int h = -1;
-    SDL_GetWindowSize(window_, NULL, &h);
-
-    if (h < 0)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-    }
-    else
-    {
-      return static_cast<unsigned int>(h);
-    }
-  }
-
-
-  void SdlWindow::Render(SDL_Surface* surface)
-  {
-    /**
-     * "You are strongly encouraged to call SDL_RenderClear() to
-     * initialize the backbuffer before starting each new frame's
-     * drawing, even if you plan to overwrite every pixel."
-     * https://wiki.libsdl.org/SDL_RenderPresent
-     **/
-    SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255);
-    SDL_RenderClear(renderer_);  // Clear the entire screen to our selected color
-
-    SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer_, surface);
-    if (texture != NULL)
-    {
-      SDL_RenderCopy(renderer_, texture, NULL, NULL);
-      SDL_DestroyTexture(texture);
-    }
-
-    SDL_RenderPresent(renderer_);
-  }
-
-
-  void SdlWindow::ToggleMaximize()
-  {
-    if (maximized_)
-    {
-      SDL_RestoreWindow(window_);
-      maximized_ = false;
-    }
-    else
-    {
-      SDL_MaximizeWindow(window_);
-      maximized_ = true;
-    }
-  }
-
-
-  void SdlWindow::GlobalInitialize()
-  {
-    if (SDL_Init(SDL_INIT_VIDEO) != 0)
-    {
-      LOG(ERROR) << "Cannot initialize SDL";
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-    }
-  }
-
-
-  void SdlWindow::GlobalFinalize()
-  {
-    SDL_Quit();
-  }
-}
-
-#endif
--- a/Applications/Platforms/Sdl/SdlWindow.h	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,78 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
- **/
-
-
-#pragma once
-
-#if ORTHANC_ENABLE_SDL == 1
-
-#include <boost/noncopyable.hpp>
-#include <string>
-
-// Forward declaration of SDL type to avoid clashes with DCMTK headers
-// on "typedef Sint8", in "StoneInitialization.cpp"
-struct SDL_Window;
-struct SDL_Renderer;
-struct SDL_Surface;
-
-namespace OrthancStone
-{
-  class SdlWindow : public boost::noncopyable
-  {
-  private:
-    struct SDL_Window   *window_;
-    struct SDL_Renderer *renderer_;
-    bool                 maximized_;
-
-  public:
-    SdlWindow(const std::string& title,
-              unsigned int width,
-              unsigned int height,
-              bool enableOpenGl,
-              bool allowDpiScaling = true);
-
-    ~SdlWindow();
-
-    SDL_Window *GetObject() const
-    {
-      return window_;
-    }
-
-    unsigned int GetWidth() const;
-
-    unsigned int GetHeight() const;
-
-    /**
-     * WARNING: "Refresh()" cannot only be called from the main SDL
-     * thread, in which the window was created. Otherwise, the
-     * renderer displays nothing!
-     **/
-    void Render(struct SDL_Surface* surface);
-
-    void ToggleMaximize();
-
-    static void GlobalInitialize();
-
-    static void GlobalFinalize();
-  };
-}
-
-#endif
--- a/Applications/Platforms/WebAssembly/OrthancStoneWebAssemblyConfiguration.cmake	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-# Stone of Orthanc
-# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
-# Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
-
-
-
-#####################################################################
-## Sanity check of the configuration
-#####################################################################
-
-set(ENABLE_WEB_CLIENT OFF)
-include(${ORTHANC_STONE_ROOT}/../Resources/CMake/OrthancStoneConfiguration.cmake)
-
-if (NOT ORTHANC_SANDBOXED)
-  message(FATAL_ERROR "WebAssembly target must me configured as sandboxed")
-endif()
-
-if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
-  message(FATAL_ERROR "WebAssembly target requires the emscripten compiler")    
-endif()
-
-add_definitions(
-  -DORTHANC_ENABLE_SDL=0
-  -DORTHANC_ENABLE_WASM=1
-  )
-
-
-#####################################################################
-## Additional source files for WebAssembly
-#####################################################################
-
-list(APPEND ORTHANC_STONE_SOURCES
-  ${CMAKE_CURRENT_LIST_DIR}/WebAssemblyCairoViewport.cpp
-  ${CMAKE_CURRENT_LIST_DIR}/WebAssemblyLoadersContext.cpp
-  ${CMAKE_CURRENT_LIST_DIR}/WebAssemblyOracle.cpp
-  ${CMAKE_CURRENT_LIST_DIR}/WebAssemblyViewport.cpp
-  )
-
-if (ENABLE_OPENGL)
-  list(APPEND ORTHANC_STONE_SOURCES
-    ${CMAKE_CURRENT_LIST_DIR}/WebAssemblyOpenGLContext.cpp
-    ${CMAKE_CURRENT_LIST_DIR}/WebGLViewport.cpp
-    ${CMAKE_CURRENT_LIST_DIR}/WebGLViewportsRegistry.cpp
-    )
-endif()
--- a/Applications/Platforms/WebAssembly/OrthancStoneWebAssemblyParameters.cmake	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,41 +0,0 @@
-# Stone of Orthanc
-# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
-# Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
-
-
-
-#####################################################################
-## Load generic parameters
-#####################################################################
-
-include(${CMAKE_CURRENT_LIST_DIR}/../../../OrthancStone/Resources/CMake/OrthancStoneParameters.cmake)
-
-
-#####################################################################
-## CMake parameters tunable by the user
-#####################################################################
-
-# None for now, but might contain stuff like memory settings
-
-
-#####################################################################
-## Internal CMake parameters to enable the optional subcomponents of
-## the Stone of Orthanc
-#####################################################################
-
-set(ENABLE_THREADS OFF CACHE INTERNAL "")
--- a/Applications/Platforms/WebAssembly/SharedLibrary/CMakeLists.txt	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,211 +0,0 @@
-# Stone of Orthanc
-# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
-# Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
-
-
-cmake_minimum_required(VERSION 2.8.3)
-cmake_policy(SET CMP0058 NEW)
-
-project(OrthancStoneModule)
-
-
-# Warning message related to WebAssembly modules: We know that 1.38.41
-# DOES NOT work, but that 1.39.17 works.
-message("")
-message("=== IMPORTANT: Make sure to use a recent version of Emscripten (preferably >= 2.0.0) ===")
-message("")
-
-
-set(ORTHANC_STONE_INSTALL_PREFIX "${CMAKE_SOURCE_DIR}/../../../../wasm-binaries/OrthancStoneModule" CACHE PATH "Where to put the WebAssembly binaries")
-
-
-# Ask for the generation of a side module
-set(WASM_FLAGS "-s SIDE_MODULE=1 -s EXPORT_ALL=1")  # Must be before "Compiler.cmake"
-
-
-# Configuration of the Emscripten compiler for WebAssembly target
-# ---------------------------------------------------------------
-
-set(USE_WASM ON CACHE BOOL "")
-
-set(WASM_FLAGS "${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(WASM_LINKER_FLAGS "${WASM_LINKER_FLAGS} -s ASSERTIONS=1 -s DISABLE_EXCEPTION_CATCHING=0")
-set(WASM_LINKER_FLAGS "${WASM_LINKER_FLAGS} -s DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1")
-add_definitions(
-  -DDISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1
-)
-
-
-# Stone of Orthanc configuration
-# ---------------------------------------------------------------
-
-include(${CMAKE_SOURCE_DIR}/../OrthancStoneWebAssemblyParameters.cmake)
-
-SET(ENABLE_DCMTK ON)
-SET(ENABLE_DCMTK_NETWORKING OFF)
-SET(ENABLE_DCMTK_TRANSCODING OFF)
-SET(ENABLE_GOOGLE_TEST OFF)
-SET(ENABLE_LOCALE ON)  # Necessary for text rendering
-SET(ORTHANC_SANDBOXED ON)
-
-include(${CMAKE_SOURCE_DIR}/../OrthancStoneWebAssemblyConfiguration.cmake)
-
-
-
-
-################################################################################
-
-# The source files that register a callback cannot be part of a side
-# module, and must be compiled in the main module. The following
-# command can be used to identify such files:
-#  $ grep -lrE 'emscripten_' ../../Sources/
-
-set(SOURCES_WITH_EMSCRIPTEN_CALLBACKS
-  ${CMAKE_SOURCE_DIR}/../WebAssemblyOracle.cpp
-  ${CMAKE_SOURCE_DIR}/../WebAssemblyViewport.cpp
-  ${CMAKE_SOURCE_DIR}/../WebAssemblyCairoViewport.cpp
-  )
-
-list(REMOVE_ITEM ORTHANC_STONE_SOURCES
-  ${SOURCES_WITH_EMSCRIPTEN_CALLBACKS}
-  )
-
-configure_file(
-  ${ORTHANC_STONE_ROOT}/../SharedLibrary/OrthancStone.h.in
-  ${CMAKE_CURRENT_BINARY_DIR}/Include/orthanc-stone/OrthancStone.h
-  )
-
-configure_file(
-  ${ORTHANC_FRAMEWORK_ROOT}/../SharedLibrary/OrthancFramework.h.in
-  ${CMAKE_CURRENT_BINARY_DIR}/Include/orthanc-framework/OrthancFramework.h
-  )
-
-file(
-  COPY ${CMAKE_SOURCE_DIR}/../../../../OrthancStone/Sources/
-  DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/orthanc-stone
-  NO_SOURCE_PERMISSIONS
-  FILES_MATCHING
-  PATTERN "*.h"
-  PATTERN OrthancStone.h EXCLUDE
-  PATTERN "Deprecated*" EXCLUDE
-  )
-
-file(
-  COPY ${ORTHANC_FRAMEWORK_ROOT}/
-  DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/orthanc-framework
-  NO_SOURCE_PERMISSIONS
-  FILES_MATCHING
-  PATTERN "*.h"
-  PATTERN OrthancFramework.h EXCLUDE
-  )
-
-add_executable(OrthancStoneModule
-  ${ORTHANC_STONE_SOURCES}
-  ${AUTOGENERATED_SOURCES}  
-  ${CAIRO_SOURCES}
-  ${PIXMAN_SOURCES}
-  ${FREETYPE_SOURCES}
-  )
-
-set_target_properties(OrthancStoneModule
-  PROPERTIES
-  COMPILE_FLAGS "${WASM_FLAGS}"
-  LINK_FLAGS "${WASM_LINKER_FLAGS}"
-  )
-
-# CMake does not natively handle SIDE_MODULE, and believes that
-# Emscripten produces a ".js" file (whereas it creates only the
-# ".wasm"). Create a dummy ".js" for target to work.
-add_custom_command(
-  TARGET OrthancStoneModule POST_BUILD
-  COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_CURRENT_BINARY_DIR}/OrthancStoneModule.js
-  )
-
-file(
-  COPY ${BOOST_SOURCES_DIR}/boost/
-  DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/boost/
-  NO_SOURCE_PERMISSIONS
-  FILES_MATCHING
-  PATTERN "*.h"
-  PATTERN "*.hpp"
-  PATTERN "*.ipp"
-  )
-
-file(
-  COPY ${JSONCPP_SOURCES_DIR}/include/json/
-  DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/json/
-  NO_SOURCE_PERMISSIONS
-  FILES_MATCHING
-  PATTERN "*.h"
-  )
-
-file(
-  COPY ${CAIRO_SOURCES_DIR}/src/
-  DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/cairo/
-  NO_SOURCE_PERMISSIONS
-  FILES_MATCHING
-  PATTERN "*.h"
-  )
-
-set(DCMTK_MODULES
-  dcmdata
-  config
-  ofstd
-  oflog
-  )
-
-foreach (module IN LISTS DCMTK_MODULES)
-  file(
-    COPY ${DCMTK_SOURCES_DIR}/ofstd/include/dcmtk/${module}/
-    DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/dcmtk/${module}/
-    NO_SOURCE_PERMISSIONS
-    FILES_MATCHING
-    PATTERN "*.h"
-    )
-endforeach()
-
-    
-install(
-  TARGETS OrthancStoneModule
-  DESTINATION ${ORTHANC_STONE_INSTALL_PREFIX}/lib
-  )
-
-install(FILES
-  ${CMAKE_CURRENT_BINARY_DIR}/OrthancStoneModule.wasm
-  DESTINATION ${ORTHANC_STONE_INSTALL_PREFIX}/lib
-  )
-
-install(
-  DIRECTORY
-  ${CMAKE_CURRENT_BINARY_DIR}/Include/boost
-  ${CMAKE_CURRENT_BINARY_DIR}/Include/cairo
-  ${CMAKE_CURRENT_BINARY_DIR}/Include/dcmtk
-  ${CMAKE_CURRENT_BINARY_DIR}/Include/json
-  ${CMAKE_CURRENT_BINARY_DIR}/Include/orthanc-framework
-  ${CMAKE_CURRENT_BINARY_DIR}/Include/orthanc-stone
-  DESTINATION ${ORTHANC_STONE_INSTALL_PREFIX}/include/
-  )
-
-install(FILES
-  ${SOURCES_WITH_EMSCRIPTEN_CALLBACKS}
-  DESTINATION ${ORTHANC_STONE_INSTALL_PREFIX}/src/orthanc-stone
-  )
--- a/Applications/Platforms/WebAssembly/SharedLibrary/NOTES.txt	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,18 +0,0 @@
-
-Install Emscripten:
-https://emscripten.org/docs/getting_started/downloads.html
-
-# cd ~/Downloads
-# git clone https://github.com/emscripten-core/emsdk.git
-# cd emsdk
-# ./emsdk install 2.0.0
-# ./emsdk activate 2.0.0
-
-
-Then, if the installation path was "~/Downloads/emsdk/":
-
-# source ~/Downloads/emsdk/emsdk_env.sh
-# cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=${EMSDK}/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake -DALLOW_DOWNLOADS=ON -G Ninja
-# ninja install
-
-=> The binaries will be put in "../../../../wasm-binaries/"
--- a/Applications/Platforms/WebAssembly/UnitTests/CMakeLists.txt	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,137 +0,0 @@
-# Stone of Orthanc
-# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
-# Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2022 Osimis S.A., Belgium
-# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
-
-
-cmake_minimum_required(VERSION 2.8.3)
-cmake_policy(SET CMP0058 NEW)
-
-project(OrthancStone)
-
-set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "mainline")
-set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
-set(ORTHANC_STONE_INSTALL_PREFIX "${CMAKE_SOURCE_DIR}/../../../../wasm-binaries/" CACHE PATH "Where to put the WebAssembly binaries")
-
-
-# Configuration of the Emscripten compiler for WebAssembly target
-# ---------------------------------------------------------------
-set(USE_WASM ON CACHE BOOL "")
-
-set(WASM_FLAGS "-s WASM=1 -s FETCH=1 -s ASSERTIONS=1 -s DISABLE_EXCEPTION_CATCHING=0")
-if (CMAKE_BUILD_TYPE STREQUAL "Debug")
-  set(WASM_FLAGS "${WASM_FLAGS} -s SAFE_HEAP=1")
-endif()
-
-set(WASM_LINKER_FLAGS "${WASM_LINKER_FLAGS} -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]'")
-set(WASM_LINKER_FLAGS "${WASM_LINKER_FLAGS} -s ERROR_ON_UNDEFINED_SYMBOLS=1")
-set(WASM_LINKER_FLAGS "${WASM_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1 -s TOTAL_MEMORY=268435456")  # 256MB + resize
-set(WASM_LINKER_FLAGS "${WASM_LINKER_FLAGS} -s DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1")
-add_definitions(
-  -DDISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1
-)
-
-
-# Stone of Orthanc configuration
-# ---------------------------------------------------------------
-set(ALLOW_DOWNLOADS ON)
-
-include(${CMAKE_SOURCE_DIR}/../OrthancStoneWebAssemblyParameters.cmake)
-
-SET(ENABLE_DCMTK ON)  # Necessary for the tests of the Orthanc framework
-SET(ENABLE_PUGIXML ON)  # Necessary for the tests of the Orthanc framework
-SET(ENABLE_GOOGLE_TEST OFF)
-SET(ENABLE_LOCALE ON)  # Necessary for text rendering
-SET(ENABLE_LUA ON)  # Necessary for the tests of the Orthanc framework
-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(${CMAKE_SOURCE_DIR}/../OrthancStoneWebAssemblyConfiguration.cmake)
-
-
-################################################################################
-
-# Define the WASM module
-# ---------------------------------------------------------------
-
-set(USE_SYSTEM_GOOGLE_TEST OFF CACHE BOOL "Use the system version of Google Test")
-set(USE_GOOGLE_TEST_DEBIAN_PACKAGE OFF CACHE BOOL "Use the sources of Google Test shipped with libgtest-dev (Debian only)")
-mark_as_advanced(USE_GOOGLE_TEST_DEBIAN_PACKAGE)
-include(${ORTHANC_STONE_ROOT}/../Resources/Orthanc/CMake/DownloadPackage.cmake)
-include(${ORTHANC_STONE_ROOT}/../Resources/Orthanc/CMake/GoogleTestConfiguration.cmake)
-
-
-include(${CMAKE_SOURCE_DIR}/../../../../OrthancStone/UnitTestsSources/UnitTestsSources.cmake)
-
-EmbedResources(
-  RT_STRUCT_00 ${CMAKE_SOURCE_DIR}/../../../../OrthancStone/UnitTestsSources/TestStructureSet.json
-  )
-
-add_executable(UnitTests
-  # Testing Stone
-  ${UNIT_TESTS_SOURCES}
-
-  # Testing the Orthanc Framework
-  ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/DicomMapTests.cpp
-  ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/FrameworkTests.cpp
-  ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/FromDcmtkTests.cpp
-  ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/ImageProcessingTests.cpp
-  ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/ImageTests.cpp
-  ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/JpegLosslessTests.cpp
-  ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/LoggingTests.cpp
-  ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/LuaTests.cpp
-  ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/RestApiTests.cpp
-  ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/StreamTests.cpp
-  ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/ToolboxTests.cpp
-
-  # Tests that are not compatible with WebAssembly
-
-  # ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/FileStorageTests.cpp
-  # ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/JobsTests.cpp
-  # ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/MemoryCacheTests.cpp
-  # ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/SQLiteChromiumTests.cpp
-  # ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/SQLiteTests.cpp
-  # ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/ZipTests.cpp
-
-  # Shared files
-  ${AUTOGENERATED_SOURCES}
-  ${BOOST_EXTENDED_SOURCES}
-  ${GOOGLE_TEST_SOURCES}
-  ${ORTHANC_STONE_SOURCES}
-  )
-
-  
-# Declare installation files for the module
-# ---------------------------------------------------------------
-install(
-  TARGETS UnitTests
-  RUNTIME DESTINATION ${ORTHANC_STONE_INSTALL_PREFIX}/UnitTests/
-  )
-
-# Declare installation files for the companion files (web scaffolding)
-# please note that ${CMAKE_CURRENT_BINARY_DIR}/RtViewerWasm.js
-# (the generated JS loader for the WASM module) is handled by the `install1`
-# section above: it is considered to be the binary output of 
-# the linker.
-# ---------------------------------------------------------------
-install(
-  FILES
-  ${CMAKE_SOURCE_DIR}/index.html
-  ${CMAKE_CURRENT_BINARY_DIR}/UnitTests.wasm
-  DESTINATION ${ORTHANC_STONE_INSTALL_PREFIX}/UnitTests
-  )
--- a/Applications/Platforms/WebAssembly/UnitTests/NOTES.txt	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,18 +0,0 @@
-Native compilation (without Docker)
-===================================
-
-Install Emscripten:
-https://emscripten.org/docs/getting_started/downloads.html
-
-Then, if the installation path is "~/Downloads/emsdk/":
-
-# source ~/Downloads/emsdk/emsdk_env.sh
-# mkdir Build && cd Build
-# cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=${EMSDK}/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake -DALLOW_DOWNLOADS=ON -G Ninja
-# ninja install
-
-=> The binaries will be put in "../../../../../wasm-binaries/UnitTests/"
-
-# cd `pwd`/../../../../../wasm-binaries/UnitTests
-# python3 -m http.server 8000
-# firefox http://localhost:8000/index.html
--- a/Applications/Platforms/WebAssembly/UnitTests/index.html	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-  <head>
-    <meta charset="utf-8">
-    <title>Stone of Orthanc</title>
-  </head>
-  <body>
-    <h1>Stone of Orthanc - Unit tests</h1>
-    <pre id="output">
-      Running the tests...
-    </pre>
-    <script src="UnitTests.js" async></script>
-  </body>
-</html>
--- a/Applications/Platforms/WebAssembly/WebAssemblyCairoViewport.cpp	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,118 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
- **/
-
-
-#if defined(ORTHANC_BUILDING_STONE_LIBRARY) && ORTHANC_BUILDING_STONE_LIBRARY == 1
-#  include "WebAssemblyCairoViewport.h"
-#  include "../../../OrthancStone/Sources/Scene2D/CairoCompositor.h"
-#  include "../../../OrthancStone/Sources/Scene2DViewport/ViewportController.h"
-#else
-// This is the case when using the WebAssembly side module, and this
-// source file must be compiled within the WebAssembly main module
-#  include <Viewport/WebAssemblyCairoViewport.h>
-#  include <Scene2D/CairoCompositor.h>
-#  include <Scene2DViewport/ViewportController.h>
-#endif
-
-
-#include <Images/Image.h>
-
-namespace OrthancStone
-{
-  void WebAssemblyCairoViewport::Paint(ICompositor& compositor,
-                                       ViewportController& controller)
-  {
-    compositor.Refresh(controller.GetScene());
-
-    // Create a temporary memory buffer for the canvas in JavaScript
-    Orthanc::ImageAccessor cairo;
-    dynamic_cast<CairoCompositor&>(compositor).GetCanvas().GetReadOnlyAccessor(cairo);
-
-    const unsigned int width = cairo.GetWidth();
-    const unsigned int height = cairo.GetHeight();
-
-    if (javascript_.get() == NULL ||
-        javascript_->GetWidth() != width ||
-        javascript_->GetHeight() != height)
-    {
-      javascript_.reset(new Orthanc::Image(Orthanc::PixelFormat_RGBA32, width, height,
-                                           true /* force minimal pitch */));
-    }
-      
-    // Convert from BGRA32 memory layout (only color mode supported
-    // by Cairo, which corresponds to CAIRO_FORMAT_ARGB32) to RGBA32
-    // (as expected by HTML5 canvas). This simply amounts to
-    // swapping the B and R channels. Alpha channel is also set to
-    // full opacity (255).
-    uint8_t* q = reinterpret_cast<uint8_t*>(javascript_->GetBuffer());
-    for (unsigned int y = 0; y < height; y++)
-    {
-      const uint8_t* p = reinterpret_cast<const uint8_t*>(cairo.GetConstRow(y));
-      for (unsigned int x = 0; x < width; x++)
-      {
-        q[0] = p[2];  // R
-        q[1] = p[1];  // G
-        q[2] = p[0];  // B
-        q[3] = 255;   // A
-
-        p += 4;
-        q += 4;
-      }
-    }
-
-    if (width != 0 &&
-        height != 0)
-    {
-      // Execute JavaScript commands to blit the image buffer onto the
-      // 2D drawing context of the HTML5 canvas
-      EM_ASM({
-          const data = new Uint8ClampedArray(Module.HEAP8.buffer, $1, 4 * $2 * $3);
-          const img = new ImageData(data, $2, $3);
-          const ctx = document.getElementById(UTF8ToString($0)).getContext('2d');
-          ctx.putImageData(img, 0, 0);
-        },
-        GetCanvasId().c_str(),      // $0
-        javascript_->GetBuffer(),   // $1
-        javascript_->GetWidth(),    // $2
-        javascript_->GetHeight());  // $3
-    }
-  }
-    
-
-  WebAssemblyCairoViewport::WebAssemblyCairoViewport(const std::string& canvasId,
-                                                     bool enableEmscriptenMouseEvents) :
-    WebAssemblyViewport(canvasId,enableEmscriptenMouseEvents)
-  {
-    RefreshCanvasSize();
-    AcquireCompositor(new CairoCompositor(GetCanvasWidth(), GetCanvasHeight()));
-  }
-  
-
-  boost::shared_ptr<WebAssemblyCairoViewport> WebAssemblyCairoViewport::Create(
-    const std::string& canvasId, bool enableEmscriptenMouseEvents)
-  {
-    boost::shared_ptr<WebAssemblyCairoViewport> that = boost::shared_ptr<WebAssemblyCairoViewport>(
-        new WebAssemblyCairoViewport(canvasId, enableEmscriptenMouseEvents));
-    
-    that->WebAssemblyViewport::PostConstructor();
-    return that;
-  }
-}
--- a/Applications/Platforms/WebAssembly/WebAssemblyCairoViewport.h	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
- **/
-
-
-#pragma once
-
-#include "WebAssemblyViewport.h"
-
-namespace OrthancStone
-{
-  class WebAssemblyCairoViewport : public WebAssemblyViewport
-  {
-  private:
-    std::unique_ptr<Orthanc::ImageAccessor>  javascript_;
-        
-    WebAssemblyCairoViewport(const std::string& canvasId,
-                             bool enableEmscriptenMouseEvents);
-
-  protected:
-    virtual void Paint(ICompositor& compositor,
-                       ViewportController& controller) ORTHANC_OVERRIDE;
-    
-  public:
-    static boost::shared_ptr<WebAssemblyCairoViewport> Create(const std::string& canvasId,
-                                                              bool enableEmscriptenMouseEvents = true);
-
-    virtual ~WebAssemblyCairoViewport()
-    {
-      ClearCompositor();
-    }
-  };
-}
--- a/Applications/Platforms/WebAssembly/WebAssemblyLoadersContext.cpp	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,98 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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 "WebAssemblyLoadersContext.h"
-
-namespace OrthancStone
-{
-  class WebAssemblyLoadersContext::Locker : public ILoadersContext::ILock
-  {
-  private:
-    WebAssemblyLoadersContext&  that_;
-
-  public:
-    explicit Locker(WebAssemblyLoadersContext& that) :
-      that_(that)
-    {
-    }      
-      
-    virtual ILoadersContext& GetContext() const ORTHANC_OVERRIDE
-    {
-      return that_;
-    }
-
-    virtual IObservable& GetOracleObservable() const ORTHANC_OVERRIDE
-    {
-      return that_.oracle_.GetOracleObservable();
-    }
-
-    virtual void Schedule(boost::shared_ptr<IObserver> receiver,
-                          int priority,
-                          IOracleCommand* command /* Takes ownership */) ORTHANC_OVERRIDE
-    {
-      that_.scheduler_->Schedule(receiver, priority, command);
-    }
-
-    virtual void CancelRequests(boost::shared_ptr<IObserver> receiver) ORTHANC_OVERRIDE
-    {
-      that_.scheduler_->CancelRequests(receiver);
-    }
-
-    virtual void CancelAllRequests() ORTHANC_OVERRIDE
-    {
-      that_.scheduler_->CancelAllRequests();
-    }
-
-    virtual void AddLoader(boost::shared_ptr<IObserver> loader) ORTHANC_OVERRIDE
-    {
-      that_.loaders_.push_back(loader);
-    }
-
-    virtual void GetStatistics(uint64_t& scheduledCommands,
-                               uint64_t& processedCommands) ORTHANC_OVERRIDE
-    {
-      scheduledCommands = that_.scheduler_->GetTotalScheduled();
-      processedCommands = that_.scheduler_->GetTotalProcessed();
-    }
-  };
-    
-
-  WebAssemblyLoadersContext::WebAssemblyLoadersContext(unsigned int maxHighPriority,
-                                                       unsigned int maxStandardPriority,
-                                                       unsigned int maxLowPriority)
-  {
-    oracle_.GetOracleObservable();
-    scheduler_ = OracleScheduler::Create(oracle_, oracle_.GetOracleObservable(), oracle_,
-                                         maxHighPriority, maxStandardPriority, maxLowPriority);
-
-    if (!scheduler_)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-    }
-  }
-
-
-  ILoadersContext::ILock* WebAssemblyLoadersContext::Lock()
-  {
-    return new Locker(*this);
-  }
-}
--- a/Applications/Platforms/WebAssembly/WebAssemblyLoadersContext.h	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,69 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
- **/
-
-
-#pragma once
-
-#include "../../../OrthancStone/Sources/Loaders/ILoadersContext.h"
-#include "../../../OrthancStone/Sources/Loaders/OracleScheduler.h"
-#include "WebAssemblyOracle.h"
-
-#include <list>
-
-namespace OrthancStone
-{
-  class WebAssemblyLoadersContext : public ILoadersContext
-  {
-  private:
-    class Locker;
-    
-    WebAssemblyOracle                          oracle_;
-    boost::shared_ptr<OracleScheduler>         scheduler_;
-    std::list< boost::shared_ptr<IObserver> >  loaders_;
-    
-  public:
-    WebAssemblyLoadersContext(unsigned int maxHighPriority,
-                              unsigned int maxStandardPriority,
-                              unsigned int maxLowPriority);
-
-    void SetLocalOrthanc(const std::string& root)
-    {
-      oracle_.SetLocalOrthanc(root);
-    }
-
-    void SetRemoteOrthanc(const Orthanc::WebServiceParameters& orthanc)
-    {
-      oracle_.SetRemoteOrthanc(orthanc);
-    }
-
-    void SetDicomCacheSize(size_t size)
-    {
-      oracle_.SetDicomCacheSize(size);
-    }
-
-    WebAssemblyOracle::CachedInstanceAccessor* AccessCachedInstance(const std::string& sopInstanceUid)
-    {
-      return new WebAssemblyOracle::CachedInstanceAccessor(oracle_, sopInstanceUid);
-    }
-
-    virtual ILock* Lock() ORTHANC_OVERRIDE;
-  };
-}
--- a/Applications/Platforms/WebAssembly/WebAssemblyOpenGLContext.cpp	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,201 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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 "WebAssemblyOpenGLContext.h"
-
-#include "../../../OrthancStone/Sources/StoneException.h"
-
-#include <Logging.h>
-#include <OrthancException.h>
-
-#include <emscripten/html5.h>
-#include <emscripten/em_asm.h>
-
-#include <boost/math/special_functions/round.hpp>
-
-namespace OrthancStone
-{
-  namespace OpenGL
-  {
-    class WebAssemblyOpenGLContext::PImpl
-    {
-    private:
-      std::string                     canvasSelector_;
-      EMSCRIPTEN_WEBGL_CONTEXT_HANDLE context_;
-      bool                            isContextLost_;
-
-    public:
-      explicit PImpl(const std::string& canvasSelector) :
-        canvasSelector_(canvasSelector),
-        isContextLost_(false)
-      {
-        // Context configuration
-        EmscriptenWebGLContextAttributes attr; 
-        emscripten_webgl_init_context_attributes(&attr);
-
-        // The next line might be necessary to print using
-        // WebGL. Sometimes, if set to "false" (the default value),
-        // the canvas was rendered as a fully white or black
-        // area. UNCONFIRMED.
-        attr.preserveDrawingBuffer = true;
-
-        context_ = emscripten_webgl_create_context(canvasSelector.c_str(), &attr);
-        if (context_ == 0)
-        {
-          std::string message("Cannot create an OpenGL context for the element with the following CSS selector: \"");
-          message += canvasSelector;
-          message += "\"  Please make sure the -s DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1 flag has been passed to Emscripten when building.";
-          LOG(ERROR) << message;
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, message);
-        }
-      }
-
-      void* DebugGetInternalContext() const
-      {
-        return reinterpret_cast<void*>(context_);
-      }
-
-      bool IsContextLost()
-      {
-        //LOG(TRACE) << "IsContextLost() for context " << std::hex << context_ << std::dec;
-        bool apiFlag = (emscripten_is_webgl_context_lost(context_) != 0);
-        isContextLost_ = apiFlag;
-        return isContextLost_;
-      }
-
-      void SetLostContext()
-      {
-        isContextLost_ = true;
-      }
-
-      ~PImpl()
-      {
-        try
-        {
-          EMSCRIPTEN_RESULT result = emscripten_webgl_destroy_context(context_);
-          if (result != EMSCRIPTEN_RESULT_SUCCESS)
-          {
-            LOG(ERROR) << "emscripten_webgl_destroy_context returned code " << result;
-          }
-        }
-        catch (const Orthanc::OrthancException& e)
-        {
-          if (e.HasDetails())
-          {
-            LOG(ERROR) << "OrthancException in WebAssemblyOpenGLContext::~PImpl: " << e.What() << " Details: " << e.GetDetails();
-          }
-          else
-          {
-            LOG(ERROR) << "OrthancException in WebAssemblyOpenGLContext::~PImpl: " << e.What();
-          }
-        }
-        catch (const std::exception& e)
-        {
-          LOG(ERROR) << "std::exception in WebAssemblyOpenGLContext::~PImpl: " << e.what();
-        }
-        catch (...)
-        {
-          LOG(ERROR) << "Unknown exception in WebAssemblyOpenGLContext::~PImpl";
-        }
-      }
-
-      const std::string& GetCanvasSelector() const
-      {
-        return canvasSelector_;
-      }
-
-      void MakeCurrent()
-      {
-        if (IsContextLost())
-        {
-          LOG(ERROR) << "MakeCurrent() called on lost context " << context_;
-          throw StoneException(ErrorCode_WebGLContextLost);
-        }
-
-        if (emscripten_is_webgl_context_lost(context_))
-        {
-          LOG(ERROR) << "OpenGL context has been lost for canvas selector: " << canvasSelector_;
-          SetLostContext();
-          throw StoneException(ErrorCode_WebGLContextLost);
-        }
-
-        if (emscripten_webgl_make_context_current(context_) != EMSCRIPTEN_RESULT_SUCCESS)
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
-                                          "Cannot set the OpenGL context");
-        }
-      }
-
-      void SwapBuffer() 
-      {
-        /**
-         * "Rendered WebGL content is implicitly presented (displayed to
-         * the user) on the canvas when the event handler that renders with
-         * WebGL returns back to the browser event loop."
-         * https://emscripten.org/docs/api_reference/html5.h.html#webgl-context
-         *
-         * Could call "emscripten_webgl_commit_frame()" if
-         * "explicitSwapControl" option were set to "true".
-         **/
-      }
-    };
-
-
-    WebAssemblyOpenGLContext::WebAssemblyOpenGLContext(const std::string& canvasSelector) :
-      pimpl_(new PImpl(canvasSelector))
-    {
-    }
-
-    bool WebAssemblyOpenGLContext::IsContextLost()
-    {
-      return pimpl_->IsContextLost();
-    }
-
-    void WebAssemblyOpenGLContext::SetLostContext()
-    {
-      pimpl_->SetLostContext();
-    }
-
-    void* WebAssemblyOpenGLContext::DebugGetInternalContext() const
-    {
-      return pimpl_->DebugGetInternalContext();
-    }
-    
-    void WebAssemblyOpenGLContext::MakeCurrent()
-    {
-      assert(pimpl_.get() != NULL);
-      pimpl_->MakeCurrent();
-    }
-
-    void WebAssemblyOpenGLContext::SwapBuffer() 
-    {
-      assert(pimpl_.get() != NULL);
-      pimpl_->SwapBuffer();
-    }
-
-    const std::string& WebAssemblyOpenGLContext::GetCanvasSelector() const
-    {
-      assert(pimpl_.get() != NULL);
-      return pimpl_->GetCanvasSelector();
-    }
-  }
-}
--- a/Applications/Platforms/WebAssembly/WebAssemblyOpenGLContext.h	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,81 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
- **/
-
-
-#pragma once
-
-#if !defined(ORTHANC_ENABLE_WASM)
-#  error Macro ORTHANC_ENABLE_WASM must be defined
-#endif
-
-#if ORTHANC_ENABLE_WASM != 1
-#  error This file can only be used if targeting WebAssembly
-#endif
-
-#if !defined(ORTHANC_ENABLE_OPENGL)
-#  error The macro ORTHANC_ENABLE_OPENGL must be defined
-#endif
-
-#if ORTHANC_ENABLE_OPENGL != 1
-#  error Support for OpenGL is disabled
-#endif
-
-#include "../../../OrthancStone/Sources/OpenGL/IOpenGLContext.h"
-
-#include <Compatibility.h>  // For ORTHANC_OVERRIDE
-
-#include <boost/shared_ptr.hpp>
-
-namespace OrthancStone
-{
-  namespace OpenGL
-  {
-    class WebAssemblyOpenGLContext : public OpenGL::IOpenGLContext
-    {
-    private:
-      class PImpl;
-      boost::shared_ptr<PImpl>  pimpl_;
-
-    public:
-      explicit WebAssemblyOpenGLContext(const std::string& canvasSelector);
-
-      virtual bool IsContextLost() ORTHANC_OVERRIDE;
-
-      virtual void MakeCurrent() ORTHANC_OVERRIDE;
-
-      virtual void SwapBuffer() ORTHANC_OVERRIDE;
-
-      /**
-      Returns true if the underlying context has been successfully recreated
-      */
-      //bool TryRecreate();
-
-      const std::string& GetCanvasSelector() const;
-
-
-      /**
-       * This is for manual context loss (debug purposes)
-       **/
-      void* DebugGetInternalContext() const;
-      void SetLostContext();
-    };
-  }
-}
--- a/Applications/Platforms/WebAssembly/WebAssemblyOracle.cpp	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,875 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
- **/
-
-
-#if defined(ORTHANC_BUILDING_STONE_LIBRARY) && ORTHANC_BUILDING_STONE_LIBRARY == 1
-#  include "WebAssemblyOracle_Includes.h"
-#else
-// This is the case when using the WebAssembly side module, and this
-// source file must be compiled within the WebAssembly main module
-#  include <Oracle/WebAssemblyOracle_Includes.h>
-#endif
-
-#include <OrthancException.h>
-#include <Toolbox.h>
-
-#include <emscripten.h>
-#include <emscripten/html5.h>
-#include <emscripten/fetch.h>
-
-
-#if ORTHANC_ENABLE_DCMTK == 1
-static unsigned int BUCKET_SOP = 1;
-#endif
-
-
-namespace OrthancStone
-{
-  class WebAssemblyOracle::TimeoutContext
-  {
-  private:
-    WebAssemblyOracle&                 oracle_;
-    boost::weak_ptr<IObserver>         receiver_;
-    std::unique_ptr<SleepOracleCommand>  command_;
-
-  public:
-    TimeoutContext(WebAssemblyOracle& oracle,
-                   boost::weak_ptr<IObserver> receiver,
-                   IOracleCommand* command) :
-      oracle_(oracle),
-      receiver_(receiver)
-    {
-      if (command == NULL)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-      }
-      else
-      {
-        command_.reset(dynamic_cast<SleepOracleCommand*>(command));
-      }
-    }
-
-    void EmitMessage()
-    {      
-      assert(command_.get() != NULL);
-
-      SleepOracleCommand::TimeoutMessage message(*command_);
-      oracle_.EmitMessage(receiver_, message);
-    }
-
-    static void Callback(void *userData)
-    {
-      std::unique_ptr<TimeoutContext> context(reinterpret_cast<TimeoutContext*>(userData));
-      context->EmitMessage();
-    }
-  };
-    
-
-  /**
-     This object is created on the heap for every http request.
-     It is deleted in the success (or error) callbacks.
-
-     This object references the receiver of the request. Since this is a raw
-     reference, we need additional checks to make sure we send the response to
-     the same object, for the object can be deleted and a new one recreated at the
-     same address (it often happens in the [single-threaded] browser context).
-  */
-  class WebAssemblyOracle::FetchContext : public boost::noncopyable
-  {
-  private:
-    WebAssemblyOracle&             oracle_;
-    boost::weak_ptr<IObserver>     receiver_;
-    std::unique_ptr<IOracleCommand>  command_;
-    std::string                    expectedContentType_;
-
-  public:
-    FetchContext(WebAssemblyOracle& oracle,
-                 boost::weak_ptr<IObserver> receiver,
-                 IOracleCommand* command,
-                 const std::string& expectedContentType) :
-      oracle_(oracle),
-      receiver_(receiver),
-      command_(command),
-      expectedContentType_(expectedContentType)
-    {
-      if (Orthanc::Logging::IsTraceLevelEnabled())
-      {
-        // Calling "receiver.lock()" is expensive, hence the quick check if TRACE is enabled
-        LOG(TRACE) << "WebAssemblyOracle::FetchContext::FetchContext() | "
-                   << "receiver address = " << std::hex << receiver.lock().get();
-      }
-
-      if (command == NULL)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-      }
-    }
-
-    const std::string& GetExpectedContentType() const
-    {
-      return expectedContentType_;
-    }
-
-    void EmitException(const Orthanc::OrthancException& exception)
-    {
-      assert(command_.get() != NULL);
-      OracleCommandExceptionMessage message(*command_, exception);
-      oracle_.EmitMessage(receiver_, message);
-    }
-
-    void ProcessFetchResult(const std::string& answer,
-                            const HttpHeaders& headers)
-    {
-      assert(command_.get() != NULL);
-      oracle_.ProcessFetchResult(receiver_, answer, headers, *command_);
-    }
-
-    static void SuccessCallback(emscripten_fetch_t *fetch)
-    {
-      /**
-       * Firstly, make a local copy of the fetched information, and
-       * free data associated with the fetch.
-       **/
-      
-      if (fetch->userData == NULL)
-      {
-        LOG(ERROR) << "WebAssemblyOracle::FetchContext::SuccessCallback fetch->userData is NULL!";
-        return;
-      }
-
-      std::unique_ptr<FetchContext> context(reinterpret_cast<FetchContext*>(fetch->userData));
-
-      std::string answer;
-      if (fetch->numBytes > 0)
-      {
-        answer.assign(fetch->data, fetch->numBytes);
-      }
-
-
-      /**
-       * Retrieving the headers of the HTTP answer.
-       **/
-      HttpHeaders headers;
-
-#if (__EMSCRIPTEN_major__ < 1 ||                                        \
-     (__EMSCRIPTEN_major__ == 1 && __EMSCRIPTEN_minor__ < 38) ||        \
-     (__EMSCRIPTEN_major__ == 1 && __EMSCRIPTEN_minor__ == 38 && __EMSCRIPTEN_tiny__ < 37))
-#  warning Consider upgrading Emscripten to a version above 1.38.37, incomplete support of Fetch API
-
-      /**
-       * HACK - If emscripten < 1.38.37, the fetch API does not
-       * contain a way to retrieve the HTTP headers of the answer. We
-       * make the assumption that the "Content-Type" header of the
-       * response is the same as the "Accept" header of the
-       * query. This is fixed thanks to the
-       * "emscripten_fetch_get_response_headers()" function that was
-       * added to "fetch.h" at emscripten-1.38.37 on 2019-06-26.
-       *
-       * https://github.com/emscripten-core/emscripten/blob/1.38.37/system/include/emscripten/fetch.h
-       * https://github.com/emscripten-core/emscripten/pull/8486
-       **/
-      if (fetch->userData != NULL)
-      {
-        if (!context->GetExpectedContentType().empty())
-        {
-          headers["Content-Type"] = context->GetExpectedContentType();
-        }
-      }
-#else
-      {
-        size_t size = emscripten_fetch_get_response_headers_length(fetch);
-
-        std::string plainHeaders(size + 1, '\0');
-        emscripten_fetch_get_response_headers(fetch, &plainHeaders[0], size + 1);
-
-        std::vector<std::string> tokens;
-        Orthanc::Toolbox::TokenizeString(tokens, plainHeaders, '\n');
-
-        for (size_t i = 0; i < tokens.size(); i++)
-        {
-          size_t p = tokens[i].find(':');
-          if (p != std::string::npos)
-          {
-            std::string key = Orthanc::Toolbox::StripSpaces(tokens[i].substr(0, p));
-            std::string value = Orthanc::Toolbox::StripSpaces(tokens[i].substr(p + 1));
-            headers[key] = value;
-          }
-        }
-      }
-#endif
-      
-      LOG(TRACE) << "About to call emscripten_fetch_close";
-      emscripten_fetch_close(fetch);
-      LOG(TRACE) << "Successfully called emscripten_fetch_close";
-
-      /**
-       * Secondly, use the retrieved data.
-       * IMPORTANT NOTE: the receiver might be dead. This is prevented 
-       * by the object responsible for zombie check, later on.
-       **/
-      try
-      {
-        if (context.get() == NULL)
-        {
-          LOG(ERROR) << "WebAssemblyOracle::FetchContext::SuccessCallback: (context.get() == NULL)";
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-        }
-        else
-        {
-          context->ProcessFetchResult(answer, headers);
-        }
-      }
-      catch (Orthanc::OrthancException& e)
-      {
-        LOG(INFO) << "Error while processing a fetch answer in the oracle: " << e.What();
-        context->EmitException(e);
-      }
-    }
-
-    static void FailureCallback(emscripten_fetch_t *fetch)
-    {
-      std::unique_ptr<FetchContext> context(reinterpret_cast<FetchContext*>(fetch->userData));
-
-#if 0
-      {
-        const size_t kEmscriptenStatusTextSize = sizeof(emscripten_fetch_t::statusText);
-        char message[kEmscriptenStatusTextSize + 1];
-        memcpy(message, fetch->statusText, kEmscriptenStatusTextSize);
-        message[kEmscriptenStatusTextSize] = 0;
-
-        LOG(ERROR) << "Fetching " << fetch->url
-                   << " failed, HTTP failure status code: " << fetch->status
-                   << " | statusText = " << message
-                   << " | numBytes = " << fetch->numBytes
-                   << " | totalBytes = " << fetch->totalBytes
-                   << " | readyState = " << fetch->readyState;
-      }
-#endif
-
-      context->EmitException(Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol));
-      
-      /**
-       * TODO - The following code leads to an infinite recursion, at
-       * least with Firefox running on incognito mode => WHY?
-       **/      
-      emscripten_fetch_close(fetch); // Also free data on failure.
-    }
-  };
-    
-
-
-  class WebAssemblyOracle::FetchCommand : public boost::noncopyable
-  {
-  private:
-    WebAssemblyOracle&             oracle_;
-    boost::weak_ptr<IObserver>     receiver_;
-    std::unique_ptr<IOracleCommand>  command_;
-    Orthanc::HttpMethod            method_;
-    std::string                    url_;
-    std::string                    body_;
-    HttpHeaders                    headers_;
-    unsigned int                   timeout_;
-    std::string                    expectedContentType_;
-    bool                           hasCredentials_;
-    std::string                    username_;
-    std::string                    password_;
-
-  public:
-    FetchCommand(WebAssemblyOracle& oracle,
-                 boost::weak_ptr<IObserver> receiver,
-                 IOracleCommand* command) :
-      oracle_(oracle),
-      receiver_(receiver),
-      command_(command),
-      method_(Orthanc::HttpMethod_Get),
-      timeout_(0),
-      hasCredentials_(false)
-    {
-      if (command == NULL)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-      }
-    }
-
-    void SetMethod(Orthanc::HttpMethod method)
-    {
-      method_ = method;
-    }
-
-    Orthanc::HttpMethod GetMethod() const
-    {
-      return method_;
-    }
-
-    void SetUrl(const std::string& url)
-    {
-      url_ = url;
-    }
-
-    const std::string& GetUrl() const
-    {
-      return url_;
-    }
-
-    void SetBody(std::string& body /* will be swapped */)
-    {
-      body_.swap(body);
-    }
-
-    void AddHttpHeaders(const HttpHeaders& headers)
-    {
-      for (HttpHeaders::const_iterator it = headers.begin(); it != headers.end(); ++it)
-      {
-        headers_[it->first] = it->second;
-      }
-    }
-
-    const HttpHeaders& GetHttpHeaders() const
-    {
-      return headers_;
-    }
-
-    void SetTimeout(unsigned int timeout)
-    {
-      timeout_ = timeout;
-    }
-
-    unsigned int GetTimeout() const
-    {
-      return timeout_;
-    }
-
-    void SetCredentials(const std::string& username,
-                        const std::string& password)
-    {
-      hasCredentials_ = true;
-      username_ = username;
-      password_ = password;
-    }
-
-    void Execute()
-    {
-      if (command_.get() == NULL)
-      {
-        // Cannot call Execute() twice
-        LOG(ERROR) << "WebAssemblyOracle::Execute(): (command_.get() == NULL)";
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-      }
-
-      emscripten_fetch_attr_t attr;
-      emscripten_fetch_attr_init(&attr);
-
-      const char* method;
-      
-      switch (method_)
-      {
-        case Orthanc::HttpMethod_Get:
-          method = "GET";
-          break;
-
-        case Orthanc::HttpMethod_Post:
-          method = "POST";
-          break;
-
-        case Orthanc::HttpMethod_Delete:
-          method = "DELETE";
-          break;
-
-        case Orthanc::HttpMethod_Put:
-          method = "PUT";
-          break;
-
-        default:
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-      }
-
-      strcpy(attr.requestMethod, method);
-
-      attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY | EMSCRIPTEN_FETCH_REPLACE;
-      attr.onsuccess = FetchContext::SuccessCallback;
-      attr.onerror = FetchContext::FailureCallback;
-      attr.timeoutMSecs = timeout_ * 1000;
-
-      if (hasCredentials_)
-      {
-        attr.withCredentials = EM_TRUE;
-        attr.userName = username_.c_str();
-        attr.password = password_.c_str();
-      }
-      
-      std::vector<const char*> headers;
-      headers.reserve(2 * headers_.size() + 1);
-
-      std::string expectedContentType;
-        
-      for (HttpHeaders::const_iterator it = headers_.begin(); it != headers_.end(); ++it)
-      {
-        std::string key;
-        Orthanc::Toolbox::ToLowerCase(key, it->first);
-          
-        if (key == "accept")
-        {
-          expectedContentType = it->second;
-        }
-
-        if (key != "accept-encoding")  // Web browsers forbid the modification of this HTTP header
-        {
-          headers.push_back(it->first.c_str());
-          headers.push_back(it->second.c_str());
-        }
-      }
-        
-      headers.push_back(NULL);  // Termination of the array of HTTP headers
-
-      attr.requestHeaders = &headers[0];
-
-      char* requestData = NULL;
-      if (!body_.empty())
-        requestData = reinterpret_cast<char*>(malloc(body_.size()));
-        
-      try 
-      {
-        if (!body_.empty())
-        {
-          memcpy(requestData, &(body_[0]), body_.size());
-          attr.requestDataSize = body_.size();
-          attr.requestData = requestData;
-        }
-        attr.userData = new FetchContext(oracle_, receiver_, command_.release(), expectedContentType);
-
-        // Must be the last call to prevent memory leak on error
-        emscripten_fetch(&attr, url_.c_str());
-      }        
-      catch(...)
-      {
-        if(requestData != NULL)
-          free(requestData);
-        throw;
-      }
-    }
-  };
-
-
-  void WebAssemblyOracle::ProcessFetchResult(boost::weak_ptr<IObserver>& receiver,
-                                             const std::string& answer,
-                                             const HttpHeaders& headers,
-                                             const IOracleCommand& command)
-  {
-    switch (command.GetType())
-    {
-      case IOracleCommand::Type_Http:
-      {
-        HttpCommand::SuccessMessage message(dynamic_cast<const HttpCommand&>(command), headers, answer);
-        EmitMessage(receiver, message);
-        break;
-      }
-
-      case IOracleCommand::Type_OrthancRestApi:
-      {
-        LOG(TRACE) << "WebAssemblyOracle::FetchContext::SuccessCallback. About to call EmitMessage(message);";
-        OrthancRestApiCommand::SuccessMessage message
-          (dynamic_cast<const OrthancRestApiCommand&>(command), headers, answer);
-        EmitMessage(receiver, message);
-        break;
-      }
-
-      case IOracleCommand::Type_GetOrthancImage:
-      {
-        dynamic_cast<const GetOrthancImageCommand&>(command).ProcessHttpAnswer(receiver, *this, answer, headers);
-        break;
-      }
-
-      case IOracleCommand::Type_GetOrthancWebViewerJpeg:
-      {
-        dynamic_cast<const GetOrthancWebViewerJpegCommand&>(command).ProcessHttpAnswer(receiver, *this, answer);
-        break;
-      }
-
-      case IOracleCommand::Type_ParseDicomFromWado:
-      {
-#if ORTHANC_ENABLE_DCMTK == 1
-        const ParseDicomFromWadoCommand& c = dynamic_cast<const ParseDicomFromWadoCommand&>(command);
-              
-        size_t fileSize;
-        std::unique_ptr<Orthanc::ParsedDicomFile> dicom
-          (ParseDicomSuccessMessage::ParseWadoAnswer(fileSize, answer, headers));
-
-        {
-          ParseDicomSuccessMessage message(c, c.GetSource(), *dicom, fileSize, true);
-          EmitMessage(receiver, message);
-        }
-
-        if (dicomCache_.get())
-        {
-          // Store it into the cache for future use
-          dicomCache_->Acquire(BUCKET_SOP, c.GetSopInstanceUid(), dicom.release(), fileSize, true);
-        }
-#else
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-#endif
-        break;
-      }
-
-      default:
-        LOG(ERROR) << "Command type not implemented by the WebAssembly Oracle (in SuccessCallback): "
-                   << command.GetType();
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-    }
-  }
-
-
-  void WebAssemblyOracle::SetOrthancUrl(FetchCommand& command,
-                                        const std::string& uri) const
-  {
-    if (isLocalOrthanc_)
-    {
-      command.SetUrl(localOrthancRoot_ + uri);
-    }
-    else
-    {
-      command.SetUrl(remoteOrthanc_.GetUrl() + uri);
-      command.AddHttpHeaders(remoteOrthanc_.GetHttpHeaders());
-      
-      if (!remoteOrthanc_.GetUsername().empty())
-      {
-        command.SetCredentials(remoteOrthanc_.GetUsername(), remoteOrthanc_.GetPassword());
-      }
-    }
-  }
-    
-
-  void WebAssemblyOracle::Execute(boost::weak_ptr<IObserver> receiver,
-                                  HttpCommand* command)
-  {
-    FetchCommand fetch(*this, receiver, command);
-    
-    fetch.SetMethod(command->GetMethod());
-    fetch.SetUrl(command->GetUrl());
-    fetch.AddHttpHeaders(command->GetHttpHeaders());
-    fetch.SetTimeout(command->GetTimeout());
-    
-    if (command->GetMethod() == Orthanc::HttpMethod_Post ||
-        command->GetMethod() == Orthanc::HttpMethod_Put)
-    {
-      std::string body;
-      command->SwapBody(body);
-      fetch.SetBody(body);
-    }
-    
-    fetch.Execute();
-  }
-  
-
-  void WebAssemblyOracle::Execute(boost::weak_ptr<IObserver> receiver,
-                                  OrthancRestApiCommand* command)
-  {
-    try
-    {
-      //LOG(TRACE) << "*********** WebAssemblyOracle::Execute.";
-      //LOG(TRACE) << "WebAssemblyOracle::Execute | command = " << command;
-      FetchCommand fetch(*this, receiver, command);
-
-      fetch.SetMethod(command->GetMethod());
-      SetOrthancUrl(fetch, command->GetUri());
-      fetch.AddHttpHeaders(command->GetHttpHeaders());
-      fetch.SetTimeout(command->GetTimeout());
-
-      if (command->GetMethod() == Orthanc::HttpMethod_Post ||
-          command->GetMethod() == Orthanc::HttpMethod_Put)
-      {
-        std::string body;
-        command->SwapBody(body);
-        fetch.SetBody(body);
-      }
-
-      fetch.Execute();
-      //LOG(TRACE) << "*********** successful end of WebAssemblyOracle::Execute.";
-    }
-    catch (const Orthanc::OrthancException& e)
-    {
-      if (e.HasDetails())
-      {
-        LOG(ERROR) << "OrthancException in WebAssemblyOracle::Execute: " << e.What() << " Details: " << e.GetDetails();
-      }
-      else
-      {
-        LOG(ERROR) << "OrthancException in WebAssemblyOracle::Execute: " << e.What();
-      }
-      //LOG(TRACE) << "*********** failing end of WebAssemblyOracle::Execute.";
-      throw;
-    }
-    catch (const std::exception& e)
-    {
-      LOG(ERROR) << "std::exception in WebAssemblyOracle::Execute: " << e.what();
-//       LOG(TRACE) << "*********** failing end of WebAssemblyOracle::Execute.";
-      throw;
-    }
-    catch (...)
-    {
-      LOG(ERROR) << "Unknown exception in WebAssemblyOracle::Execute";
-//       LOG(TRACE) << "*********** failing end of WebAssemblyOracle::Execute.";
-      throw;
-    }
-  }
-    
-    
-  void WebAssemblyOracle::Execute(boost::weak_ptr<IObserver> receiver,
-                                  GetOrthancImageCommand* command)
-  {
-    FetchCommand fetch(*this, receiver, command);
-
-    SetOrthancUrl(fetch, command->GetUri());
-    fetch.AddHttpHeaders(command->GetHttpHeaders());
-    fetch.SetTimeout(command->GetTimeout());
-      
-    fetch.Execute();
-  }
-    
-    
-  void WebAssemblyOracle::Execute(boost::weak_ptr<IObserver> receiver,
-                                  GetOrthancWebViewerJpegCommand* command)
-  {
-    FetchCommand fetch(*this, receiver, command);
-
-    SetOrthancUrl(fetch, command->GetUri());
-    fetch.AddHttpHeaders(command->GetHttpHeaders());
-    fetch.SetTimeout(command->GetTimeout());
-      
-    fetch.Execute();
-  }
-
-
-  void WebAssemblyOracle::Execute(boost::weak_ptr<IObserver> receiver,
-                                  ParseDicomFromWadoCommand* command)
-  {
-    std::unique_ptr<ParseDicomFromWadoCommand> protection(command);
-    
-#if ORTHANC_ENABLE_DCMTK == 1
-    if (dicomCache_.get())
-    {
-      ParsedDicomCache::Reader reader(*dicomCache_, BUCKET_SOP, protection->GetSopInstanceUid());
-      if (reader.IsValid() &&
-          reader.HasPixelData())
-      {
-        // Reuse the DICOM file from the cache
-        ParseDicomSuccessMessage message(*protection, protection->GetSource(), reader.GetDicom(),
-                                         reader.GetFileSize(), reader.HasPixelData());
-        EmitMessage(receiver, message);
-        return;
-      }
-    }
-#endif
-
-    switch (command->GetRestCommand().GetType())
-    {
-      case IOracleCommand::Type_Http:
-      {
-        const HttpCommand& rest =
-          dynamic_cast<const HttpCommand&>(protection->GetRestCommand());
-        
-        FetchCommand fetch(*this, receiver, protection.release());
-    
-        fetch.SetMethod(rest.GetMethod());
-        fetch.SetUrl(rest.GetUrl());
-        fetch.AddHttpHeaders(rest.GetHttpHeaders());
-        fetch.SetTimeout(rest.GetTimeout());
-    
-        if (rest.GetMethod() == Orthanc::HttpMethod_Post ||
-            rest.GetMethod() == Orthanc::HttpMethod_Put)
-        {
-          std::string body = rest.GetBody();
-          fetch.SetBody(body);
-        }
-    
-        fetch.Execute();
-        break;
-      }
-
-      case IOracleCommand::Type_OrthancRestApi:
-      {
-        const OrthancRestApiCommand& rest =
-          dynamic_cast<const OrthancRestApiCommand&>(protection->GetRestCommand());
-        
-        FetchCommand fetch(*this, receiver, protection.release());
-
-        fetch.SetMethod(rest.GetMethod());
-        SetOrthancUrl(fetch, rest.GetUri());
-        fetch.AddHttpHeaders(rest.GetHttpHeaders());
-        fetch.SetTimeout(rest.GetTimeout());
-
-        if (rest.GetMethod() == Orthanc::HttpMethod_Post ||
-            rest.GetMethod() == Orthanc::HttpMethod_Put)
-        {
-          std::string body = rest.GetBody();
-          fetch.SetBody(body);
-        }
-
-        fetch.Execute();
-        break;
-      }
-
-      default:
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-    }
-  }
-
-
-  bool WebAssemblyOracle::Schedule(boost::shared_ptr<IObserver> receiver,
-                                   IOracleCommand* command)
-  {
-    LOG(TRACE) << "WebAssemblyOracle::Schedule : receiver = "
-               << std::hex << receiver.get();
-    
-    std::unique_ptr<IOracleCommand> protection(command);
-
-    if (command == NULL)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-    }
-
-    switch (command->GetType())
-    {
-      case IOracleCommand::Type_Http:
-        Execute(receiver, dynamic_cast<HttpCommand*>(protection.release()));
-        break;
-        
-      case IOracleCommand::Type_OrthancRestApi:
-        Execute(receiver, dynamic_cast<OrthancRestApiCommand*>(protection.release()));
-        break;
-        
-      case IOracleCommand::Type_GetOrthancImage:
-        Execute(receiver, dynamic_cast<GetOrthancImageCommand*>(protection.release()));
-        break;
-
-      case IOracleCommand::Type_GetOrthancWebViewerJpeg:
-        Execute(receiver, dynamic_cast<GetOrthancWebViewerJpegCommand*>(protection.release()));
-        break;          
-            
-      case IOracleCommand::Type_Sleep:
-      {
-        unsigned int timeoutMS = dynamic_cast<SleepOracleCommand*>(command)->GetDelay();
-        emscripten_set_timeout(TimeoutContext::Callback, timeoutMS,
-                               new TimeoutContext(*this, receiver, protection.release()));
-        break;
-      }
-            
-      case IOracleCommand::Type_ParseDicomFromWado:
-#if ORTHANC_ENABLE_DCMTK == 1
-        Execute(receiver, dynamic_cast<ParseDicomFromWadoCommand*>(protection.release()));
-#else
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented,
-                                        "DCMTK must be enabled to parse DICOM files");
-#endif
-        break;
-            
-      default:
-        LOG(ERROR) << "Command type not implemented by the WebAssembly Oracle (in Schedule): "
-                   << command->GetType();
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-    }
-
-    return true;
-  }
-
-
-  void WebAssemblyOracle::SetDicomCacheSize(size_t size)
-  {
-#if ORTHANC_ENABLE_DCMTK == 1
-    if (size == 0)
-    {
-      dicomCache_.reset();
-    }
-    else
-    {
-      dicomCache_.reset(new ParsedDicomCache(size));
-    }
-#else
-    LOG(INFO) << "DCMTK support is disabled, the DICOM cache is disabled";
-#endif
-  }
-
-  
-  WebAssemblyOracle::CachedInstanceAccessor::CachedInstanceAccessor(WebAssemblyOracle& oracle,
-                                                                    const std::string& sopInstanceUid)
-  {
-#if ORTHANC_ENABLE_DCMTK == 1
-    if (oracle.dicomCache_.get() != NULL)
-    {
-      reader_.reset(new ParsedDicomCache::Reader(*oracle.dicomCache_, BUCKET_SOP, sopInstanceUid));
-    }
-#endif
-  }
-
-  bool WebAssemblyOracle::CachedInstanceAccessor::IsValid() const
-  {
-#if ORTHANC_ENABLE_DCMTK == 1
-    return (reader_.get() != NULL &&
-            reader_->IsValid());
-#else
-    return false;
-#endif
-  }
-
-#if ORTHANC_ENABLE_DCMTK == 1
-  const Orthanc::ParsedDicomFile& WebAssemblyOracle::CachedInstanceAccessor::GetDicom() const
-  {
-    if (IsValid())
-    {
-      assert(reader_.get() != NULL);
-      return reader_->GetDicom();
-    }
-    else
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-    }
-  }
-#endif
-
-  size_t WebAssemblyOracle::CachedInstanceAccessor::GetFileSize() const
-  {
-#if ORTHANC_ENABLE_DCMTK == 1
-    if (IsValid())
-    {
-      assert(reader_.get() != NULL);
-      return reader_->GetFileSize();
-    }
-    else
-#endif
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-    }
-  }
-
-  bool WebAssemblyOracle::CachedInstanceAccessor::HasPixelData() const
-  {
-#if ORTHANC_ENABLE_DCMTK == 1
-    if (IsValid())
-    {
-      assert(reader_.get() != NULL);
-      return reader_->HasPixelData();
-    }
-    else
-#endif
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-    }
-  }
-}
--- a/Applications/Platforms/WebAssembly/WebAssemblyOracle.h	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,155 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
- **/
-
-
-#pragma once
-
-#include "../../../OrthancStone/Sources/OrthancStone.h"
-
-#if !defined(ORTHANC_ENABLE_WASM)
-#  error The macro ORTHANC_ENABLE_WASM must be defined
-#endif
-
-#if ORTHANC_ENABLE_WASM != 1
-#  error This file can only compiled for WebAssembly
-#endif
-
-#include "../../../OrthancStone/Sources/Messages/IObservable.h"
-#include "../../../OrthancStone/Sources/Messages/IMessageEmitter.h"
-#include "../../../OrthancStone/Sources/Oracle/IOracle.h"
-
-#if ORTHANC_ENABLE_DCMTK == 1
-#  include "../../../OrthancStone/Sources/Toolbox/ParsedDicomCache.h"
-#endif
-
-#include <Compatibility.h>  // For ORTHANC_OVERRIDE
-#include <WebServiceParameters.h>
-
-#include <Enumerations.h>
-
-namespace OrthancStone
-{
-  class GetOrthancImageCommand;
-  class GetOrthancWebViewerJpegCommand;
-  class HttpCommand;
-  class OrthancRestApiCommand;
-  class ParseDicomFromWadoCommand;
-  
-  class WebAssemblyOracle :
-    public IOracle,
-    public IMessageEmitter
-  {
-  private:
-    typedef std::map<std::string, std::string>  HttpHeaders;
-    
-    class TimeoutContext;
-    class FetchContext;
-    class FetchCommand;
-
-    void SetOrthancUrl(FetchCommand& command,
-                       const std::string& uri) const;
-    
-    void Execute(boost::weak_ptr<IObserver> receiver,
-                 HttpCommand* command);    
-    
-    void Execute(boost::weak_ptr<IObserver> receiver,
-                 OrthancRestApiCommand* command);    
-    
-    void Execute(boost::weak_ptr<IObserver> receiver,
-                 GetOrthancImageCommand* command);    
-    
-    void Execute(boost::weak_ptr<IObserver> receiver,
-                 GetOrthancWebViewerJpegCommand* command);
-    
-    void Execute(boost::weak_ptr<IObserver> receiver,
-                 ParseDicomFromWadoCommand* command);
-
-    IObservable                    oracleObservable_;
-    bool                           isLocalOrthanc_;
-    std::string                    localOrthancRoot_;
-    Orthanc::WebServiceParameters  remoteOrthanc_;
-
-#if ORTHANC_ENABLE_DCMTK == 1
-    std::unique_ptr<ParsedDicomCache>  dicomCache_;
-#endif
-
-    void ProcessFetchResult(boost::weak_ptr<IObserver>& receiver,
-                            const std::string& answer,
-                            const HttpHeaders& headers,
-                            const IOracleCommand& command);
-
-  public:
-    WebAssemblyOracle() :
-      isLocalOrthanc_(false)
-    {
-    }
-    
-    virtual void EmitMessage(boost::weak_ptr<IObserver> observer,
-                             const IMessage& message) ORTHANC_OVERRIDE
-    {
-      oracleObservable_.EmitMessage(observer, message);
-    }
-    
-    virtual bool Schedule(boost::shared_ptr<IObserver> receiver,
-                          IOracleCommand* command) ORTHANC_OVERRIDE;
-
-    IObservable& GetOracleObservable()
-    {
-      return oracleObservable_;
-    }
-
-    void SetLocalOrthanc(const std::string& root)
-    {
-      isLocalOrthanc_ = true;
-      localOrthancRoot_ = root;
-    }
-
-    void SetRemoteOrthanc(const Orthanc::WebServiceParameters& orthanc)
-    {
-      isLocalOrthanc_ = false;
-      remoteOrthanc_ = orthanc;
-    }
-
-    void SetDicomCacheSize(size_t size);
-
-    class CachedInstanceAccessor : public boost::noncopyable
-    {
-    private:
-#if ORTHANC_ENABLE_DCMTK == 1
-      std::unique_ptr<ParsedDicomCache::Reader>  reader_;
-#endif
-
-    public:
-      CachedInstanceAccessor(WebAssemblyOracle& oracle,
-                             const std::string& sopInstanceUid);
-
-      bool IsValid() const;
-
-#if ORTHANC_ENABLE_DCMTK == 1
-      const Orthanc::ParsedDicomFile& GetDicom() const;
-#endif
-
-      size_t GetFileSize() const;
-
-      bool HasPixelData() const;
-    };    
-  };
-}
--- a/Applications/Platforms/WebAssembly/WebAssemblyOracle_Includes.h	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,43 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
- **/
-
-
-#pragma once
-
-/**
- * This file serves as an indirection to avoid large "#if
- * ORTHANC_BUILDING_STONE_LIBRARY == 1" in "WebAssemblyOracle.cpp"
- **/
-
-#include "WebAssemblyOracle.h"
-
-#include "../../../OrthancStone/Sources/Oracle/OracleCommandExceptionMessage.h"
-
-#if ORTHANC_ENABLE_DCMTK == 1
-#  include "../../../OrthancStone/Sources/Oracle/ParseDicomSuccessMessage.h"
-#endif
-
-#include "../../../OrthancStone/Sources/Oracle/GetOrthancImageCommand.h"
-#include "../../../OrthancStone/Sources/Oracle/GetOrthancWebViewerJpegCommand.h"
-#include "../../../OrthancStone/Sources/Oracle/HttpCommand.h"
-#include "../../../OrthancStone/Sources/Oracle/OrthancRestApiCommand.h"
-#include "../../../OrthancStone/Sources/Oracle/ParseDicomFromWadoCommand.h"
-#include "../../../OrthancStone/Sources/Oracle/SleepOracleCommand.h"
--- a/Applications/Platforms/WebAssembly/WebAssemblyViewport.cpp	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,473 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
- **/
-
-
-#if defined(ORTHANC_BUILDING_STONE_LIBRARY) && ORTHANC_BUILDING_STONE_LIBRARY == 1
-#  include "WebAssemblyViewport.h"
-#  include "../../../OrthancStone/Sources/Scene2DViewport/ViewportController.h"
-#  include "../../../OrthancStone/Sources/Toolbox/GenericToolbox.h"
-#  include "../../../OrthancStone/Sources/Viewport/DefaultViewportInteractor.h"
-#else
-// This is the case when using the WebAssembly side module, and this
-// source file must be compiled within the WebAssembly main module
-#  include <Viewport/WebAssemblyViewport.h>
-#  include <Toolbox/GenericToolbox.h>
-#  include <Scene2DViewport/ViewportController.h>
-#  include <Viewport/DefaultViewportInteractor.h>
-#endif
-
-
-#include <OrthancException.h>
-
-#include <boost/make_shared.hpp>
-#include <boost/enable_shared_from_this.hpp>
-#include <boost/math/special_functions/round.hpp>
-
-namespace OrthancStone
-{
-  static void ConvertMouseEvent(PointerEvent& target,
-                                const EmscriptenMouseEvent& source,
-                                const ICompositor& compositor)
-  {
-    int x = static_cast<int>(source.targetX);
-    int y = static_cast<int>(source.targetY);
-
-    switch (source.button)
-    {
-      case 0:
-        target.SetMouseButton(MouseButton_Left);
-        break;
-
-      case 1:
-        target.SetMouseButton(MouseButton_Middle);
-        break;
-
-      case 2:
-        target.SetMouseButton(MouseButton_Right);
-        break;
-
-      default:
-        target.SetMouseButton(MouseButton_None);
-        break;
-    }
-      
-    target.AddPosition(compositor.GetPixelCenterCoordinates(x, y));
-    target.SetAltModifier(source.altKey);
-    target.SetControlModifier(source.ctrlKey);
-    target.SetShiftModifier(source.shiftKey);
-  }
-
-
-  class WebAssemblyViewport::WasmLock : public ILock
-  {
-  private:
-    WebAssemblyViewport& that_;
-
-  public:
-    WasmLock(WebAssemblyViewport& that) :
-      that_(that)
-    {
-    }
-
-    virtual bool HasCompositor() const ORTHANC_OVERRIDE
-    {
-      return that_.compositor_.get() != NULL;
-    }
-
-    virtual ICompositor& GetCompositor() ORTHANC_OVERRIDE
-    {
-      if (that_.compositor_.get() == NULL)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-      }
-      else
-      {
-        return *that_.compositor_;
-      }
-    }
-
-    virtual ViewportController& GetController() ORTHANC_OVERRIDE
-    {
-      assert(that_.controller_);
-      return *that_.controller_;
-    }
-
-    virtual void Invalidate() ORTHANC_OVERRIDE
-    {
-      that_.Invalidate();
-    }
-
-    virtual void RefreshCanvasSize() ORTHANC_OVERRIDE
-    {
-      that_.RefreshCanvasSize();
-    }
-  };
-
-  EM_BOOL WebAssemblyViewport::OnRequestAnimationFrame(double time, void *userData)
-  {
-    LOG(TRACE) << __func__;
-
-    WebAssemblyViewport* that = 
-      WebAssemblyViewport::DereferenceObjectCookie(userData);
-    
-    if (that != NULL)
-    {
-      if (that->compositor_.get() != NULL &&
-          that->controller_ /* should always be true */)
-      {
-        that->Paint(*that->compositor_, *that->controller_);
-      }
-    } 
-    else
-    {
-      LOG(TRACE) << "WebAssemblyViewport::OnRequestAnimationFrame: the " << 
-        "WebAssemblyViewport is deleted and Paint will not be called.";
-    }
-    WebAssemblyViewport::ReleaseObjectCookie(userData);
-    LOG(TRACE) << "Exiting: " << __func__;
-    return true;
-  }
-
-  EM_BOOL WebAssemblyViewport::OnResize(int eventType, const EmscriptenUiEvent *uiEvent, void *userData)
-  {
-    LOG(TRACE) << __func__;
-    WebAssemblyViewport* that = reinterpret_cast<WebAssemblyViewport*>(userData);
-
-    if (that->compositor_.get() != NULL)
-    {
-      that->RefreshCanvasSize();
-      that->Invalidate();
-    }
-      
-    LOG(TRACE) << "Exiting: " << __func__;
-    return true;
-  }
-
-
-  EM_BOOL WebAssemblyViewport::OnMouseDown(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData)
-  {
-    WebAssemblyViewport* that = reinterpret_cast<WebAssemblyViewport*>(userData);
-
-    LOG(TRACE) << "mouse down: " << that->GetCanvasCssSelector();      
-
-    if (that->compositor_.get() != NULL &&
-        that->interactor_.get() != NULL)
-    {
-      PointerEvent pointer;
-      ConvertMouseEvent(pointer, *mouseEvent, *that->compositor_);
-
-      that->controller_->HandleMousePress(*that->interactor_, pointer,
-                                          that->compositor_->GetCanvasWidth(),
-                                          that->compositor_->GetCanvasHeight());        
-      that->Invalidate();
-    }
-
-    LOG(TRACE) << "Exiting: " << __func__;
-    return true;
-  }
-
-    
-  EM_BOOL WebAssemblyViewport::OnMouseMove(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData)
-  {
-    WebAssemblyViewport* that = reinterpret_cast<WebAssemblyViewport*>(userData);
-
-    if (that->compositor_.get() != NULL)
-    {
-      if (that->controller_->HasActiveTracker())
-      {
-        PointerEvent pointer;
-        ConvertMouseEvent(pointer, *mouseEvent, *that->compositor_);
-      
-        if (that->controller_->HandleMouseMove(pointer))
-        {
-          that->Invalidate();
-        }
-      }
-      else if (that->interactor_.get() != NULL &&
-               that->interactor_->HasMouseHover())
-      {
-        // New in Stone Web viewer 2.0
-        PointerEvent pointer;
-        ConvertMouseEvent(pointer, *mouseEvent, *that->compositor_);      
-        that->interactor_->HandleMouseHover(*that, pointer);
-      }
-    }
-
-    LOG(TRACE) << "Exiting: " << __func__;
-    return true;
-  }
-    
-  EM_BOOL WebAssemblyViewport::OnMouseUp(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData)
-  {
-    LOG(TRACE) << __func__;
-    WebAssemblyViewport* that = reinterpret_cast<WebAssemblyViewport*>(userData);
-
-    if (that->compositor_.get() != NULL)
-    {
-      PointerEvent pointer;
-      ConvertMouseEvent(pointer, *mouseEvent, *that->compositor_);
-      that->controller_->HandleMouseRelease(pointer);
-      that->Invalidate();
-    }
-
-    LOG(TRACE) << "Exiting: " << __func__;
-    return true;
-  }
-
-  void* WebAssemblyViewport::CreateObjectCookie()
-  {
-    boost::weak_ptr<WebAssemblyViewport>* weakThisPtr = 
-      new boost::weak_ptr<WebAssemblyViewport>();
-    
-    *weakThisPtr = shared_from_this();
-
-    void* cookie = reinterpret_cast<void*>(weakThisPtr);
-
-    LOG(TRACE) << "WebAssemblyViewport::CreateObjectCookie() => cookie = " 
-      << cookie << "\n";
-
-    return cookie;
-  }
-
-  WebAssemblyViewport* WebAssemblyViewport::DereferenceObjectCookie(void* cookie)
-  {
-    LOG(TRACE) << "WebAssemblyViewport::DereferenceObjectCookie(cookie = " 
-      << cookie << ")\n";
-
-    boost::weak_ptr<WebAssemblyViewport>* weakThisPtr = 
-      reinterpret_cast<boost::weak_ptr<WebAssemblyViewport>*>(cookie);
-
-    boost::shared_ptr<WebAssemblyViewport> sharedThis = weakThisPtr->lock();
-
-    return sharedThis.get();
-  }
-
-  void WebAssemblyViewport::ReleaseObjectCookie(void* cookie)
-  {
-    LOG(TRACE) << "WebAssemblyViewport::ReleaseObjectCookie(cookie = " 
-      << cookie << ")\n";
-
-    boost::weak_ptr<WebAssemblyViewport>* weakThisPtr = 
-      reinterpret_cast<boost::weak_ptr<WebAssemblyViewport>*>(cookie);
-    
-    delete weakThisPtr;
-  }
-
-  void WebAssemblyViewport::Invalidate()
-  {
-    LOG(TRACE) << "WebAssemblyViewport::Invalidate()\n";
-    long id = emscripten_request_animation_frame(OnRequestAnimationFrame, 
-                                                 CreateObjectCookie());
-    //animationFrameCallbackIds_.push_back(id);
-  }
-
-  void WebAssemblyViewport::FitForPrint()
-  {
-    if (compositor_.get() != NULL &&
-        controller_ /* should always be true */)
-    {
-      RefreshCanvasSize();
-      compositor_->FitContent(controller_->GetScene());
-
-      void* cookie = CreateObjectCookie();
-      OnRequestAnimationFrame(0, cookie);  // Mandatory to work with Firefox
-    }
-  }
-
-  void WebAssemblyViewport::AcquireCompositor(ICompositor* compositor /* takes ownership */)
-  {
-    if (compositor == NULL)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-    }
-    else
-    {
-      compositor_.reset(compositor);
-    }
-  }
-
-#if DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR == 1
-// everything OK..... we're using the new setting
-#else
-#pragma message("WARNING: DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR is not defined or equal to 0. Stone will use the OLD Emscripten rules for DOM element selection.")
-#endif
-
-  WebAssemblyViewport::WebAssemblyViewport(
-    const std::string& canvasId, bool enableEmscriptenMouseEvents) :
-    canvasId_(canvasId),
-#if DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR == 1
-    canvasCssSelector_("#" + canvasId),
-#else
-    canvasCssSelector_(canvasId),
-#endif
-    interactor_(new DefaultViewportInteractor),
-    enableEmscriptenMouseEvents_(enableEmscriptenMouseEvents),
-    canvasWidth_(0),
-    canvasHeight_(0)
-  {
-  }
-
-  void WebAssemblyViewport::PostConstructor()
-  {
-    boost::shared_ptr<IViewport> viewport = shared_from_this();
-    controller_.reset(new ViewportController(viewport));
-
-    LOG(INFO) << "Initializing Stone viewport on HTML canvas: " 
-              << canvasId_;
-
-    if (canvasId_.empty() ||
-        canvasId_[0] == '#')
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
-        "The canvas identifier must not start with '#'");
-    }
-
-    // Disable right-click on the canvas (i.e. context menu)
-    EM_ASM({
-        document.getElementById(UTF8ToString($0)).oncontextmenu = 
-        function(event)
-        {
-          event.preventDefault();
-        }
-      },
-      canvasId_.c_str()   // $0
-      );
-
-    // It is not possible to monitor the resizing of individual
-    // canvas, so we track the full window of the browser
-    emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW,
-                                   reinterpret_cast<void*>(this),
-                                   false,
-                                   OnResize);
-
-    if (enableEmscriptenMouseEvents_)
-    {
-
-      // if any of this function causes an error in the console, please
-      // make sure you are using the new (as of 1.39.x) version of 
-      // emscripten element lookup rules( pass 
-      // "-s DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1" to the linker.
-
-      emscripten_set_mousedown_callback(canvasCssSelector_.c_str(),
-                                        reinterpret_cast<void*>(this),
-                                        false,
-                                        OnMouseDown);
-
-      emscripten_set_mousemove_callback(canvasCssSelector_.c_str(),
-                                        reinterpret_cast<void*>(this),
-                                        false,
-                                        OnMouseMove);
-
-      emscripten_set_mouseup_callback(canvasCssSelector_.c_str(),
-                                      reinterpret_cast<void*>(this),
-                                      false,
-                                      OnMouseUp);
-    }
-  }
-
-  WebAssemblyViewport::~WebAssemblyViewport()
-  {
-    LOG(TRACE) << "WebAssemblyViewport::~WebAssemblyViewport()\n";
-    
-    emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW,
-                                   reinterpret_cast<void*>(this),
-                                   false,
-                                   NULL);
-
-    if (enableEmscriptenMouseEvents_)
-    {
-
-      emscripten_set_mousedown_callback(canvasCssSelector_.c_str(),
-                                        reinterpret_cast<void*>(this),
-                                        false,
-                                        NULL);
-
-      emscripten_set_mousemove_callback(canvasCssSelector_.c_str(),
-                                        reinterpret_cast<void*>(this),
-                                        false,
-                                        NULL);
-
-      emscripten_set_mouseup_callback(canvasCssSelector_.c_str(),
-                                      reinterpret_cast<void*>(this),
-                                      false,
-                                      NULL);
-    }
-  }
-  
-  IViewport::ILock* WebAssemblyViewport::Lock()
-  {
-    return new WasmLock(*this);
-  }
-  
-  void WebAssemblyViewport::AcquireInteractor(IViewportInteractor* interactor)
-  {
-    if (interactor == NULL)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-    }
-    else
-    {
-      interactor_.reset(interactor);
-    }
-  }
-
-
-  void WebAssemblyViewport::RefreshCanvasSize()
-  {
-    double w = -1, h = -1;
-    EMSCRIPTEN_RESULT result = 
-      emscripten_get_element_css_size(GetCanvasCssSelector().c_str(), &w, &h);
-
-    if (result != EMSCRIPTEN_RESULT_SUCCESS)
-    {
-      LOG(WARNING) << "WebAssemblyViewport::RefreshCanvasSize failed to "
-        << "retrieve CSS size for " << GetCanvasCssSelector();
-    }
-
-    /**
-     * Emscripten has the function emscripten_get_element_css_size()
-     * to query the width and height of a named HTML element. I'm
-     * calling this first to get the initial size of the canvas DOM
-     * element, and then call emscripten_set_canvas_size() to
-     * initialize the framebuffer size of the canvas to the same
-     * size as its DOM element.
-     * https://floooh.github.io/2017/02/22/emsc-html.html
-     **/
-    if (w > 0 &&
-        h > 0)
-    {
-      canvasWidth_ = static_cast<unsigned int>(boost::math::iround(w));
-      canvasHeight_ = static_cast<unsigned int>(boost::math::iround(h));
-    }
-    else
-    {
-      canvasWidth_ = 0;
-      canvasHeight_ = 0;
-    }
-
-    emscripten_set_canvas_element_size(GetCanvasCssSelector().c_str(), canvasWidth_, canvasHeight_);
-
-    if (compositor_.get() != NULL)
-    {
-      compositor_->SetCanvasSize(canvasWidth_, canvasHeight_);
-    }
-  }
-}
--- a/Applications/Platforms/WebAssembly/WebAssemblyViewport.h	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,180 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
- **/
-
-
-#pragma once
-
-#include "../../../OrthancStone/Sources/OrthancStone.h"
-
-#if !defined(ORTHANC_ENABLE_WASM)
-#  error Macro ORTHANC_ENABLE_WASM must be defined
-#endif
-
-#if ORTHANC_ENABLE_WASM != 1
-#  error This file can only be used if targeting WebAssembly
-#endif
-
-#include "../../../OrthancStone/Sources/Viewport/IViewport.h"
-#include "../../../OrthancStone/Sources/Viewport/IViewportInteractor.h"
-
-#include <Compatibility.h>
-
-#include <emscripten.h>
-#include <emscripten/html5.h>
-
-#include <memory>
-#include <string>
-#include <vector>
-#include <boost/enable_shared_from_this.hpp>
-
-namespace OrthancStone
-{
-  class WebAssemblyViewport : public IViewport,
-                              public boost::enable_shared_from_this<WebAssemblyViewport>
-
-  {
-  private:
-    class WasmLock;
-    
-    std::string                           canvasId_;
-    std::string                           canvasCssSelector_;
-    std::unique_ptr<ICompositor>          compositor_;
-    std::unique_ptr<ViewportController>   controller_;
-    std::unique_ptr<IViewportInteractor>  interactor_;
-    bool                                  enableEmscriptenMouseEvents_;
-    unsigned int                          canvasWidth_;
-    unsigned int                          canvasHeight_;
-
-    static EM_BOOL OnRequestAnimationFrame(double time, void *userData);
-    
-    static EM_BOOL OnResize(int eventType, const EmscriptenUiEvent *uiEvent, void *userData);
-
-    static EM_BOOL OnMouseDown(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData);
-    
-    static EM_BOOL OnMouseMove(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData);
-    
-    static EM_BOOL OnMouseUp(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData);
-
-  protected:
-    void Invalidate();
-    
-    void ClearCompositor()
-    {
-      compositor_.reset();
-    }
-
-    bool HasCompositor() const
-    {
-      return compositor_.get() != NULL;
-    }
-
-    void AcquireCompositor(ICompositor* compositor /* takes ownership */);
-
-    virtual void Paint(ICompositor& compositor,
-                       ViewportController& controller) = 0;
-
-    /**
-    The second argument is temporary and should be deleted once the migration 
-    to interactors is finished. It should be set to "true" for new applications.
-    */
-    WebAssemblyViewport(const std::string& canvasId, 
-                        bool enableEmscriptenMouseEvents);
-
-    void PostConstructor();
-
-
-    /**
-     * This method can be called to retrieve a cookie that can be passed to 
-     * C-style callbacks that expect the object to be passed as a void*
-     * 
-     * This cookie is a resource and must be freed when it is guaranteed 
-     * not to be used anymore, with ReleaseObjectCookie
-     */
-    void* CreateObjectCookie();
-
-    /**
-     * This static method can be used to dereference a cookie (i.e. retrieve 
-     * a pointer to the underlying object) that has been created with 
-     * WebAssemblyViewport::CreateObjectCookie()
-     * 
-     * If this method returns NULL, it basically means that the 
-     * WebAssemblyViewport has already been deleted and that you should NOT 
-     * attempt to use it!
-     * 
-     * This method never releases the cookie (for other in-flight callbacks
-     * could possibly require to cookie to be valid)
-     * 
-     * If this method is called AFTER ReleaseObjectCookie has been called on
-     * the same cookie, the behavior is undefined and things will CERTAINLY
-     * go wrong.
-     * 
-     * NEVER call this method on a void* that has not been generated by the
-     * CreateObjectCookie method of this class
-     */
-    static WebAssemblyViewport* DereferenceObjectCookie(void* cookie);
-
-    /**
-     * This method must be used when the object cookie will not be used anymore.
-     * 
-     * You must call it when you are certain that no entity will attempt to 
-     * dereference the cookie.
-     */
-    static void ReleaseObjectCookie(void* cookie);
-
-    void RefreshCanvasSize();
-
-    unsigned int GetCanvasWidth() const
-    {
-      return canvasWidth_;
-    }
-    
-    unsigned int GetCanvasHeight()
-    {
-      return canvasHeight_;
-    }
-
-  public:
-    virtual ILock* Lock() ORTHANC_OVERRIDE;
-
-    ~WebAssemblyViewport();
-
-    /**
-    This method takes ownership
-    */
-    void AcquireInteractor(IViewportInteractor* interactor);
-
-    const std::string& GetCanvasId() const
-    {
-      return canvasId_;
-    }
-
-    /**
-    emscripten functions requires the css selector for the canvas. This is 
-    different from the canvas id (the syntax is '#mycanvasid')
-    */
-    const std::string& GetCanvasCssSelector() const
-    {
-      return canvasCssSelector_;
-    }
-
-    void FitForPrint();
-  };
-}
--- a/Applications/Platforms/WebAssembly/WebGLViewport.cpp	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,87 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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 "WebGLViewport.h"
-
-#include "../../../OrthancStone/Sources/StoneException.h"
-#include "../../../OrthancStone/Sources/Scene2D/OpenGLCompositor.h"
-#include "../../../OrthancStone/Sources/Scene2DViewport/ViewportController.h"
-
-namespace OrthancStone
-{
-  void WebGLViewport::Paint(ICompositor& compositor,
-                            ViewportController& controller)
-  {
-    try
-    {
-      compositor.Refresh(controller.GetScene());
-
-      /**
-       * No need to manually swap the buffer: "Rendered WebGL content
-       * is implicitly presented (displayed to the user) on the canvas
-       * when the event handler that renders with WebGL returns back
-       * to the browser event loop."
-       * https://emscripten.org/docs/api_reference/html5.h.html#webgl-context
-       *
-       * Could call "emscripten_webgl_commit_frame()" if
-       * "explicitSwapControl" option were set to "true".
-       **/
-    }
-    catch (const StoneException& e)
-    {
-      // Ignore problems about the loss of the WebGL context (edge case)
-      if (e.GetErrorCode() == ErrorCode_WebGLContextLost)
-      {
-        return;
-      }
-      else
-      {
-        throw;
-      }
-    }
-  }
-    
-
-  WebGLViewport::WebGLViewport(const std::string& canvasId, bool enableEmscriptenMouseEvents) :
-    WebAssemblyViewport(canvasId,enableEmscriptenMouseEvents),
-    context_(GetCanvasCssSelector())
-  {
-    AcquireCompositor(new OpenGLCompositor(context_));
-  }
-
-  boost::shared_ptr<WebGLViewport> WebGLViewport::Create(
-    const std::string& canvasId, bool enableEmscriptenMouseEvents)
-  {
-    boost::shared_ptr<WebGLViewport> that = boost::shared_ptr<WebGLViewport>(
-        new WebGLViewport(canvasId, enableEmscriptenMouseEvents));
-    
-    that->WebAssemblyViewport::PostConstructor();
-    return that;
-  }
-
-  WebGLViewport::~WebGLViewport()
-  {
-    // Make sure to delete the compositor before its parent "context_" gets
-    // deleted
-    ClearCompositor();
-  }
-}
--- a/Applications/Platforms/WebAssembly/WebGLViewport.h	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
- **/
-
-
-#pragma once
-
-#include "WebAssemblyOpenGLContext.h"
-#include "WebAssemblyViewport.h"
-
-namespace OrthancStone
-{
-  class WebGLViewport : public WebAssemblyViewport
-  {
-  private:
-    OpenGL::WebAssemblyOpenGLContext  context_;
-    
-    WebGLViewport(const std::string& canvasId,
-                  bool enableEmscriptenMouseEvents);
-
-  protected:
-    virtual void Paint(ICompositor& compositor,
-                       ViewportController& controller) ORTHANC_OVERRIDE;
-    
-  public:
-    static boost::shared_ptr<WebGLViewport> Create(const std::string& canvasId,
-                                                   bool enableEmscriptenMouseEvents = true);
-
-    virtual ~WebGLViewport();
-
-    bool IsContextLost()
-    {
-      return context_.IsContextLost();
-    } 
-  };
-}
--- a/Applications/Platforms/WebAssembly/WebGLViewportsRegistry.cpp	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,222 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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 "WebGLViewportsRegistry.h"
-
-#include "../../../OrthancStone/Sources/Scene2DViewport/ViewportController.h"
-#include "../../../OrthancStone/Sources/Toolbox/GenericToolbox.h"
-
-#include <OrthancException.h>
-
-#include <boost/make_shared.hpp>
-
-
-static double viewportsTimeout_ = 1000;
-static std::unique_ptr<OrthancStone::WebGLViewportsRegistry>  globalRegistry_;
-
-
-namespace OrthancStone
-{
-  void WebGLViewportsRegistry::LaunchTimer()
-  {
-    timeOutID_ = emscripten_set_timeout(
-      OnTimeoutCallback, 
-      timeoutMS_, 
-      reinterpret_cast<void*>(this));
-  }
-  
-  void WebGLViewportsRegistry::OnTimeout()
-  {
-    for (Viewports::iterator it = viewports_.begin();
-         it != viewports_.end(); 
-         ++it)
-    {
-      if (it->second == NULL ||
-          it->second->IsContextLost())
-      {
-        LOG(INFO) << "WebGL context lost for canvas: " << it->first;
-
-        // Try and duplicate the HTML5 canvas in the DOM
-        EM_ASM({
-            var canvas = document.getElementById(UTF8ToString($0));
-            if (canvas) {
-              var parent = canvas.parentElement;
-              if (parent) {
-                var cloned = canvas.cloneNode(true /* deep copy */);
-                parent.insertBefore(cloned, canvas);
-                parent.removeChild(canvas);
-              }
-            }
-          },
-          it->first.c_str()  // $0 = ID of the canvas
-          );
-
-        // At this point, the old canvas is removed from the DOM and
-        // replaced by a fresh one with the same ID: Recreate the
-        // WebGL context on the new canvas
-        boost::shared_ptr<WebGLViewport> viewport;
-
-        // we need to steal the properties from the old viewport
-        // and set them to the new viewport
-        {
-          std::unique_ptr<IViewport::ILock> lock(it->second->Lock());
-
-          // TODO: remove ViewportController
-          Scene2D* scene = lock->GetController().ReleaseScene();
-          viewport = WebGLViewport::Create(it->first);
-
-          {
-            std::unique_ptr<IViewport::ILock> newLock(viewport->Lock());
-            newLock->GetController().AcquireScene(scene);
-          }
-        }
-
-        // Replace the old WebGL viewport by the new one
-        it->second = viewport;
-
-        // Tag the fresh canvas as needing a repaint
-        {
-          std::unique_ptr<IViewport::ILock> lock(it->second->Lock());
-          lock->Invalidate();
-        }
-      }
-    }
-      
-    LaunchTimer();
-  }
-
-  void WebGLViewportsRegistry::OnTimeoutCallback(void *userData)
-  {
-    // This object dies with the process or tab. 
-    WebGLViewportsRegistry* that = 
-      reinterpret_cast<WebGLViewportsRegistry*>(userData);
-    that->OnTimeout();
-  }
-
-  WebGLViewportsRegistry::WebGLViewportsRegistry(double timeoutMS) :
-    timeoutMS_(timeoutMS),
-    timeOutID_(0)
-  {
-    if (timeoutMS <= 0)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-    }          
-    
-    LaunchTimer();
-  }
-
-  WebGLViewportsRegistry::~WebGLViewportsRegistry()
-  {
-    emscripten_clear_timeout(timeOutID_);
-    Clear();
-  }
-
-  boost::shared_ptr<WebGLViewport> WebGLViewportsRegistry::Add(
-    const std::string& canvasId)
-  {
-    if (viewports_.find(canvasId) != viewports_.end())
-    {
-      LOG(ERROR) << "Canvas was already registered: " << canvasId;
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      boost::shared_ptr<WebGLViewport> viewport = 
-        WebGLViewport::Create(canvasId);
-      viewports_[canvasId] = viewport;
-      return viewport;
-    }
-  }
-      
-  void WebGLViewportsRegistry::Remove(const std::string& canvasId)
-  {
-    Viewports::iterator found = viewports_.find(canvasId);
-
-    if (found == viewports_.end())
-    {
-      LOG(ERROR) << "Cannot remove unregistered canvas: " << canvasId;
-    }
-    else
-    {
-      viewports_.erase(found);
-    }
-  }
-  
-  void WebGLViewportsRegistry::Clear()
-  {
-    viewports_.clear();
-  }
-
-  WebGLViewportsRegistry::Accessor::Accessor(WebGLViewportsRegistry& that,
-                                             const std::string& canvasId) :
-    that_(that)
-  {
-    Viewports::iterator viewport = that.viewports_.find(canvasId);
-    if (viewport != that.viewports_.end() &&
-        viewport->second != NULL)
-    {
-      lock_.reset(viewport->second->Lock());
-    }
-  }
-
-  IViewport::ILock& WebGLViewportsRegistry::Accessor::GetViewport() const
-  {
-    if (IsValid())
-    {
-      return *lock_;
-    }
-    else
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-    }
-  }
-
-
-  void WebGLViewportsRegistry::FinalizeGlobalRegistry()
-  {
-    globalRegistry_.reset();
-  }
-
-
-  void WebGLViewportsRegistry::SetGlobalRegistryTimeout(double timeout)
-  {
-    if (globalRegistry_.get())
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      viewportsTimeout_ = timeout;
-    }
-  }
-
-
-  WebGLViewportsRegistry& WebGLViewportsRegistry::GetGlobalRegistry()
-  {
-    if (globalRegistry_.get() == NULL)
-    {
-      globalRegistry_.reset(new WebGLViewportsRegistry(viewportsTimeout_));
-    }
-
-    return *globalRegistry_;
-  }
-}
--- a/Applications/Platforms/WebAssembly/WebGLViewportsRegistry.h	Wed Jan 26 19:42:04 2022 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,92 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2022 Osimis S.A., Belgium
- * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
- **/
-
-
-#pragma once
-
-#include "WebGLViewport.h"
-
-#include <boost/enable_shared_from_this.hpp>
-
-namespace OrthancStone
-{
-  /**
-   * This singleton class must be used if many WebGL viewports are
-   * created by the higher-level application, implying possible loss
-   * of WebGL contexts. The object will run an infinite update loop
-   * that checks whether all the WebGL context are still valid (not
-   * lost). If some WebGL context is lost, it is automatically
-   * reinitialized by created a fresh HTML5 canvas.
-   **/  
-  class WebGLViewportsRegistry : public boost::noncopyable,
-    public boost::enable_shared_from_this<WebGLViewportsRegistry>
-  {
-  private:
-    typedef std::map<std::string, boost::shared_ptr<WebGLViewport> >  Viewports;
-
-    double     timeoutMS_;
-    Viewports  viewports_;
-    long       timeOutID_;
-
-    void LaunchTimer();
-
-    void OnTimeout();
-
-    static void OnTimeoutCallback(void *userData);
-    
-  public:
-    explicit WebGLViewportsRegistry(double timeoutMS /* in milliseconds */);
-    
-    ~WebGLViewportsRegistry();
-
-    boost::shared_ptr<WebGLViewport> Add(const std::string& canvasId);
-
-    void Remove(const std::string& canvasId);
-
-    void Clear();
-
-    class Accessor : public boost::noncopyable
-    {
-    private:
-      WebGLViewportsRegistry&            that_;
-      std::unique_ptr<IViewport::ILock>  lock_;
-
-    public:
-      Accessor(WebGLViewportsRegistry& that,
-               const std::string& canvasId);
-
-      bool IsValid() const
-      {
-        return lock_.get() != NULL;
-      }
-
-      IViewport::ILock& GetViewport() const;
-    };
-
-
-
-    static void FinalizeGlobalRegistry();
-  
-    static void SetGlobalRegistryTimeout(double timeout);
-
-    static WebGLViewportsRegistry& GetGlobalRegistry();
-  };
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/Sdl/OrthancStoneSdlConfiguration.cmake	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,76 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+#####################################################################
+## Sanity check of the configuration
+#####################################################################
+
+include(${ORTHANC_STONE_ROOT}/../Resources/CMake/OrthancStoneConfiguration.cmake)
+
+if (ORTHANC_SANDBOXED)
+  message(FATAL_ERROR "Cannot enable SDL in sandboxed environments")
+endif()
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten" OR
+    CMAKE_SYSTEM_NAME STREQUAL "PNaCl" OR
+    CMAKE_SYSTEM_NAME STREQUAL "NaCl32" OR
+    CMAKE_SYSTEM_NAME STREQUAL "NaCl64")
+  message(FATAL_ERROR "Trying to use a Web compiler for a native build")
+endif()
+
+
+
+#####################################################################
+## Configure SDL
+#####################################################################
+
+message("SDL is enabled")
+include(${CMAKE_CURRENT_LIST_DIR}/SdlConfiguration.cmake)
+
+add_definitions(
+  -DORTHANC_ENABLE_SDL=1
+  -DORTHANC_ENABLE_WASM=0
+  )
+
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows" AND
+    NOT MSVC)
+  # This is necessary when compiling EXE for Windows using MinGW
+  link_libraries(mingw32)
+endif()
+
+
+
+#####################################################################
+## Additional source files for SDL
+#####################################################################
+
+list(APPEND ORTHANC_STONE_SOURCES
+  ${CMAKE_CURRENT_LIST_DIR}/SdlViewport.cpp
+  ${CMAKE_CURRENT_LIST_DIR}/SdlWindow.cpp
+  ${SDL_SOURCES}
+  )
+
+if (ENABLE_OPENGL)
+  list(APPEND ORTHANC_STONE_SOURCES
+    ${CMAKE_CURRENT_LIST_DIR}/SdlOpenGLContext.cpp
+    )
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/Sdl/OrthancStoneSdlParameters.cmake	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,44 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#####################################################################
+## Load generic parameters
+#####################################################################
+
+include(${CMAKE_CURRENT_LIST_DIR}/../../../OrthancStone/Resources/CMake/OrthancStoneParameters.cmake)
+
+
+#####################################################################
+## CMake parameters tunable by the user
+#####################################################################
+
+# Advanced parameters to fine-tune linking against system libraries
+set(USE_SYSTEM_SDL ON CACHE BOOL "Use the system version of SDL2")
+
+
+#####################################################################
+## Internal CMake parameters to enable the optional subcomponents of
+## the Stone of Orthanc
+#####################################################################
+
+set(ENABLE_THREADS ON CACHE INTERNAL "")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/Sdl/SdlConfiguration.cmake	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,227 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+if (STATIC_BUILD OR NOT USE_SYSTEM_SDL)
+  SET(SDL_SOURCES_DIR ${CMAKE_BINARY_DIR}/SDL2-2.0.4)
+  SET(SDL_URL "http://orthanc.osimis.io/ThirdPartyDownloads/SDL2-2.0.4.tar.gz")
+  SET(SDL_MD5 "44fc4a023349933e7f5d7a582f7b886e")
+  DownloadPackage(${SDL_MD5} ${SDL_URL} "${SDL_SOURCES_DIR}")
+
+  include_directories(${SDL_SOURCES_DIR}/include)
+
+  set(TMP "${SDL_SOURCES_DIR}/include/SDL_config_premake.h")
+  if (NOT EXISTS "${TMP}")
+    file(WRITE "${TMP}" "
+#include \"SDL_platform.h\"
+#define HAVE_STDARG_H 1
+#define HAVE_STDDEF_H 1
+#define HAVE_STDINT_H 1
+")
+  endif()
+
+  # General source files
+  file(GLOB SDL_SOURCES
+    ${SDL_SOURCES_DIR}/src/*.c
+    ${SDL_SOURCES_DIR}/src/atomic/*.c
+    ${SDL_SOURCES_DIR}/src/audio/*.c
+    ${SDL_SOURCES_DIR}/src/cpuinfo/*.c
+    ${SDL_SOURCES_DIR}/src/dynapi/*.c
+    ${SDL_SOURCES_DIR}/src/events/*.c
+    ${SDL_SOURCES_DIR}/src/file/*.c
+    ${SDL_SOURCES_DIR}/src/haptic/*.c
+    ${SDL_SOURCES_DIR}/src/joystick/*.c
+    ${SDL_SOURCES_DIR}/src/libm/*.c
+    ${SDL_SOURCES_DIR}/src/power/*.c
+    ${SDL_SOURCES_DIR}/src/render/*.c
+    ${SDL_SOURCES_DIR}/src/stdlib/*.c
+    ${SDL_SOURCES_DIR}/src/thread/*.c
+    ${SDL_SOURCES_DIR}/src/timer/*.c
+    ${SDL_SOURCES_DIR}/src/video/*.c
+
+    ${SDL_SOURCES_DIR}/src/loadso/dummy/*.c
+    #${SDL_SOURCES_DIR}/src/timer/dummy/*.c
+    ${SDL_SOURCES_DIR}/src/audio/dummy/*.c
+    ${SDL_SOURCES_DIR}/src/filesystem/dummy/*.c
+    ${SDL_SOURCES_DIR}/src/haptic/dummy/*.c
+    ${SDL_SOURCES_DIR}/src/joystick/dummy/*.c
+    #${SDL_SOURCES_DIR}/src/main/dummy/*.c
+    ${SDL_SOURCES_DIR}/src/video/dummy/*.c
+    )
+
+  add_definitions(
+    -DUSING_PREMAKE_CONFIG_H=1
+
+    -DSDL_AUDIO_DISABLED=1
+    -DSDL_AUDIO_DRIVER_DUMMY=1
+    -DSDL_FILESYSTEM_DISABLED=1
+    -DSDL_FILESYSTEM_DUMMY=1
+    -DSDL_FILE_DISABLED=1
+    -DSDL_HAPTIC_DISABLED=1
+    -DSDL_JOYSTICK_DISABLED=1
+
+    #-DSDL_THREADS_DISABLED=1
+    )
+
+  if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
+    file(GLOB TMP
+      ${SDL_SOURCES_DIR}/src/core/linux/*.c
+      ${SDL_SOURCES_DIR}/src/loadso/dlopen/*.c
+      ${SDL_SOURCES_DIR}/src/render/software/*.c
+      ${SDL_SOURCES_DIR}/src/thread/pthread/*.c
+      ${SDL_SOURCES_DIR}/src/timer/unix/*.c
+      ${SDL_SOURCES_DIR}/src/video/x11/*.c
+      )
+
+    list(APPEND SDL_SOURCES ${TMP})
+
+    add_definitions(
+      -DSDL_LOADSO_DLOPEN=1
+      -DSDL_THREAD_PTHREAD=1
+      -DSDL_TIMER_UNIX=1
+      -DSDL_POWER_DISABLED=1
+
+      -DSDL_VIDEO_DRIVER_X11=1
+
+      -DSDL_ASSEMBLY_ROUTINES=1
+      -DSDL_THREAD_PTHREAD_RECURSIVE_MUTEX=1
+      -DSDL_VIDEO_DRIVER_X11_SUPPORTS_GENERIC_EVENTS=1
+      -DHAVE_GCC_SYNC_LOCK_TEST_AND_SET=1
+      )
+
+    link_libraries(X11 Xext)
+
+    if (NOT CMAKE_SYSTEM_VERSION STREQUAL "Raspberry")
+      # Raspberry Pi has no support for OpenGL
+      file(GLOB TMP
+        ${SDL_SOURCES_DIR}/src/render/opengl/*.c
+        ${SDL_SOURCES_DIR}/src/render/opengles2/*.c
+        )
+
+      list(APPEND SDL_SOURCES ${TMP})
+
+      add_definitions(
+        -DSDL_VIDEO_OPENGL=1
+        -DSDL_VIDEO_OPENGL_ES2=1
+        -DSDL_VIDEO_RENDER_OGL=1
+        -DSDL_VIDEO_RENDER_OGL_ES2=1
+        -DSDL_VIDEO_OPENGL_GLX=1
+        -DSDL_VIDEO_OPENGL_EGL=1
+        )
+    endif()
+
+  elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows")
+    file(GLOB TMP
+      ${SDL_SOURCES_DIR}/src/audio/directsound/*.c
+      ${SDL_SOURCES_DIR}/src/audio/disk/*.c
+      ${SDL_SOURCES_DIR}/src/audio/winmm/*.c
+      ${SDL_SOURCES_DIR}/src/joystick/windows/*.c
+      ${SDL_SOURCES_DIR}/src/haptic/windows/*.c
+      ${SDL_SOURCES_DIR}/src/power/windows/*.c
+
+      ${SDL_SOURCES_DIR}/src/main/windows/*.c
+      ${SDL_SOURCES_DIR}/src/core/windows/*.c
+      ${SDL_SOURCES_DIR}/src/loadso/windows/*.c
+      ${SDL_SOURCES_DIR}/src/render/direct3d/*.c
+      ${SDL_SOURCES_DIR}/src/render/direct3d11/*.c
+      ${SDL_SOURCES_DIR}/src/render/opengl/*.c
+      ${SDL_SOURCES_DIR}/src/render/psp/*.c
+      ${SDL_SOURCES_DIR}/src/render/opengles/*.c
+      ${SDL_SOURCES_DIR}/src/render/opengles2/*.c
+      ${SDL_SOURCES_DIR}/src/render/software/*.c
+      ${SDL_SOURCES_DIR}/src/thread/generic/SDL_syscond.c   # Don't include more files from "thread/generic/*.c"!
+      ${SDL_SOURCES_DIR}/src/thread/windows/*.c
+      ${SDL_SOURCES_DIR}/src/timer/windows/*.c
+      ${SDL_SOURCES_DIR}/src/video/windows/*.c
+      ${SDL_SOURCES_DIR}/src/windows/dlopen/*.c
+      )
+
+    list(APPEND SDL_SOURCES ${TMP})
+
+    # NB: OpenGL ES headers are not available in MinGW-W64
+    add_definitions(
+      -DSDL_LOADSO_WINDOWS=1
+      -DSDL_THREAD_WINDOWS=1
+      -DSDL_TIMER_WINDOWS=1
+      -DSDL_POWER_WINDOWS=1
+
+      -DSDL_VIDEO_OPENGL=1
+      -DSDL_VIDEO_OPENGL_WGL=1
+      -DSDL_VIDEO_RENDER_D3D=1
+      -DSDL_VIDEO_RENDER_OGL=1
+      -DSDL_VIDEO_DRIVER_WINDOWS=1
+      )
+
+    if (MSVC)
+      add_definitions(
+        -D__FLTUSED__
+        -DHAVE_LIBC=1
+      )
+    else()
+      add_definitions(
+        -DHAVE_GCC_ATOMICS=1
+        -DSDL_ASSEMBLY_ROUTINES=1
+        )
+    endif()
+    
+    link_libraries(imm32 winmm version)
+
+  elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
+    file(GLOB TMP
+      ${SDL_SOURCES_DIR}/src/loadso/dlopen/*.c
+      ${SDL_SOURCES_DIR}/src/render/opengl/*.c
+      ${SDL_SOURCES_DIR}/src/render/opengles2/*.c
+      ${SDL_SOURCES_DIR}/src/render/software/*.c
+      ${SDL_SOURCES_DIR}/src/thread/pthread/*.c
+      ${SDL_SOURCES_DIR}/src/timer/unix/*.c
+      ${SDL_SOURCES_DIR}/src/video/cocoa/*.m
+      )
+
+    list(APPEND SDL_SOURCES ${TMP})
+
+    add_definitions(
+      -DSDL_LOADSO_DLOPEN=1
+      -DSDL_THREAD_PTHREAD=1
+      -DSDL_TIMER_UNIX=1
+      -DSDL_POWER_DISABLED=1
+
+      -DSDL_VIDEO_DRIVER_COCOA=1
+      -DSDL_VIDEO_OPENGL=1
+      -DSDL_VIDEO_OPENGL_CGL=1
+      -DSDL_VIDEO_RENDER_OGL=1
+      
+      -DSDL_ASSEMBLY_ROUTINES=1
+      -DSDL_THREAD_PTHREAD_RECURSIVE_MUTEX=1
+      )
+
+    find_library(CARBON_LIBRARY Carbon)
+    find_library(COCOA_LIBRARY Cocoa)
+    find_library(IOKIT_LIBRARY IOKit)
+    find_library(QUARTZ_LIBRARY QuartzCore)
+    link_libraries(${CARBON_LIBRARY} ${COCOA_LIBRARY} ${IOKIT_LIBRARY} ${QUARTZ_LIBRARY})
+
+  endif()
+
+else()
+  pkg_search_module(SDL2 REQUIRED sdl2)
+  include_directories(${SDL2_INCLUDE_DIRS})
+  link_libraries(${SDL2_LIBRARIES})
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/Sdl/SdlOpenGLContext.cpp	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,131 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+#include "SdlOpenGLContext.h"
+#include "../../../OrthancStone/Sources/StoneException.h"
+
+#if ORTHANC_ENABLE_SDL == 1
+
+#if !defined(ORTHANC_ENABLE_GLEW)
+#  error Macro ORTHANC_ENABLE_GLEW must be defined
+#endif
+
+#if ORTHANC_ENABLE_GLEW == 1
+#  include <GL/glew.h>
+#endif
+
+#include <Logging.h>
+#include <OrthancException.h>
+
+#include <boost/thread/mutex.hpp>
+
+namespace OrthancStone
+{
+  SdlOpenGLContext::SdlOpenGLContext(const char* title,
+                                     unsigned int width,
+                                     unsigned int height,
+                                     bool allowDpiScaling) 
+    : window_(title, width, height, true /* enable OpenGL */, allowDpiScaling)
+    , context_(NULL)
+  {
+    context_ = SDL_GL_CreateContext(window_.GetObject());
+    
+    if (context_ == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                      "Cannot initialize OpenGL");
+    }
+
+#if ORTHANC_ENABLE_GLEW == 1
+    // The initialization function of glew (i.e. "glewInit()") can
+    // only be called once an OpenGL is setup.
+    // https://stackoverflow.com/a/45033669/881731
+    {
+      static boost::mutex  mutex_;
+      static bool          isGlewInitialized_ = false;
+  
+      boost::mutex::scoped_lock lock(mutex_);
+
+      if (!isGlewInitialized_)
+      {
+        LOG(INFO) << "Initializing glew";
+        
+        GLenum err = glewInit();
+        if (GLEW_OK != err)
+        {
+          LOG(ERROR) << glewGetErrorString(err);
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                          "Cannot initialize glew");
+        }
+
+        isGlewInitialized_ = true;
+      }
+    }    
+#endif
+  }
+
+  
+  SdlOpenGLContext::~SdlOpenGLContext()
+  {
+    SDL_GL_DeleteContext(context_);
+  }
+
+  void SdlOpenGLContext::MakeCurrent()
+  {
+    if (SDL_GL_MakeCurrent(window_.GetObject(), context_) != 0)
+    {
+      const char* errText = SDL_GetError();
+      std::stringstream ss;
+      ss << "Cannot set current OpenGL context. SDL error text: " << errText;
+      std::string errStr = ss.str();
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, errStr.c_str());
+    }
+
+    // This makes our buffer swap synchronized with the monitor's vertical refresh
+    SDL_GL_SetSwapInterval(1);
+  }
+
+  
+  void SdlOpenGLContext::SwapBuffer()
+  {
+    // Swap our buffer to display the current contents of buffer on screen
+    SDL_GL_SwapWindow(window_.GetObject());
+  }
+
+  
+  unsigned int SdlOpenGLContext::GetCanvasWidth() const
+  {
+    int w = 0;
+    SDL_GL_GetDrawableSize(window_.GetObject(), &w, NULL);
+    return static_cast<unsigned int>(w);
+  }
+
+  
+  unsigned int SdlOpenGLContext::GetCanvasHeight() const
+  {
+    int h = 0;
+    SDL_GL_GetDrawableSize(window_.GetObject(), NULL, &h);
+    return static_cast<unsigned int>(h);
+  }
+}
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/Sdl/SdlOpenGLContext.h	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,78 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#if ORTHANC_ENABLE_SDL == 1
+
+#include "../../../OrthancStone/Sources/OpenGL/IOpenGLContext.h"
+#include "SdlWindow.h"
+
+#include <Compatibility.h>  // For ORTHANC_OVERRIDE
+
+#include <SDL_render.h>
+
+#include <Enumerations.h>
+
+namespace OrthancStone
+{
+  class SdlOpenGLContext : public OpenGL::IOpenGLContext
+  {
+  private:
+    SdlWindow      window_;
+    SDL_GLContext  context_;
+
+  public:
+    SdlOpenGLContext(const char* title,
+                     unsigned int width,
+                     unsigned int height,
+                     bool allowDpiScaling = true);
+
+    ~SdlOpenGLContext();
+
+    SdlWindow& GetWindow()
+    {
+      return window_;
+    }
+
+    virtual bool IsContextLost() ORTHANC_OVERRIDE
+    {
+      // On desktop applications, an OpenGL context should never be lost
+      return false;
+    }
+
+    virtual void MakeCurrent() ORTHANC_OVERRIDE;
+
+    virtual void SwapBuffer() ORTHANC_OVERRIDE;
+
+    unsigned int GetCanvasWidth() const;
+
+    unsigned int GetCanvasHeight() const;
+
+    void ToggleMaximize()
+    {
+      window_.ToggleMaximize();
+    }
+  };
+}
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/Sdl/SdlViewport.cpp	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,264 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+#include "SdlViewport.h"
+
+#include "../../../OrthancStone/Sources/Scene2DViewport/ViewportController.h"
+
+#include <OrthancException.h>
+
+#include <boost/make_shared.hpp>
+
+namespace OrthancStone
+{
+  ICompositor& SdlViewport::SdlLock::GetCompositor()
+  {
+    if (that_.compositor_.get() == NULL)
+    {
+      // The derived class should have called "AcquireCompositor()"
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+    else
+    {
+      return *that_.compositor_;
+    }
+  }
+
+
+  void SdlViewport::AcquireCompositor(ICompositor* compositor /* takes ownership */)
+  {
+    if (compositor == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+
+    compositor_.reset(compositor);
+  }
+
+
+  SdlViewport::SdlViewport()
+  {
+    refreshEvent_ = SDL_RegisterEvents(1);
+
+    if (refreshEvent_ == static_cast<uint32_t>(-1))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  }
+  
+  void SdlViewport::PostConstructor()
+  {
+    controller_ = boost::make_shared<ViewportController>(shared_from_this());
+  }
+    
+  void SdlViewport::SendRefreshEvent()
+  {
+    SDL_Event event;
+    SDL_memset(&event, 0, sizeof(event));
+    event.type = refreshEvent_;
+    SDL_PushEvent(&event);  // This function is thread-safe, and can be called from other threads safely.
+  }
+
+  
+  void SdlViewport::UpdateSize(unsigned int width, unsigned int height)
+  {
+    SdlLock lock(*this);
+    lock.GetCompositor().SetCanvasSize(width, height);
+    lock.Invalidate();
+  }
+
+
+#if ORTHANC_ENABLE_OPENGL == 1
+  SdlOpenGLViewport::SdlOpenGLViewport(const std::string& title,
+                                       unsigned int width,
+                                       unsigned int height,
+                                       bool allowDpiScaling) :
+    context_(title.c_str(), width, height, allowDpiScaling)
+  {
+    AcquireCompositor(new OpenGLCompositor(context_));  // (*)
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_OPENGL == 1
+  void SdlOpenGLViewport::RefreshCanvasSize()
+  {
+    UpdateSize(context_.GetCanvasWidth(), context_.GetCanvasHeight());
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_OPENGL == 1
+  boost::shared_ptr<SdlOpenGLViewport> SdlOpenGLViewport::Create(const std::string& title,
+                                                                 unsigned int width,
+                                                                 unsigned int height,
+                                                                 bool allowDpiScaling)
+  {
+    boost::shared_ptr<SdlOpenGLViewport> that =
+      boost::shared_ptr<SdlOpenGLViewport>(new SdlOpenGLViewport(title, width, height, allowDpiScaling));
+    that->SdlViewport::PostConstructor();
+    return that;
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_OPENGL == 1
+  uint32_t SdlOpenGLViewport::GetSdlWindowId()
+  {
+    const SdlWindow& sdlWindowWrapper = context_.GetWindow();
+    SDL_Window* sdlWindow = sdlWindowWrapper.GetObject();
+    Uint32 sdlWindowId = SDL_GetWindowID(sdlWindow);
+    return sdlWindowId;
+  }
+#endif
+  
+
+#if ORTHANC_ENABLE_OPENGL == 1
+  SdlOpenGLViewport::~SdlOpenGLViewport()
+  {
+    // Make sure that the "OpenGLCompositor" is destroyed BEFORE the
+    // "OpenGLContext" it references (*)
+    ClearCompositor();
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_OPENGL == 1
+  void SdlOpenGLViewport::Paint()
+  {
+    SdlLock lock(*this);
+    lock.GetCompositor().Refresh(lock.GetController().GetScene());
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_OPENGL == 1
+  void SdlOpenGLViewport::ToggleMaximize()
+  {
+    // No need to call "Invalidate()" here, as "UpdateSize()" will
+    // be invoked after event "SDL_WINDOWEVENT_SIZE_CHANGED"
+    SdlLock lock(*this);
+    context_.ToggleMaximize();
+  }
+#endif
+
+
+  void SdlCairoViewport::RefreshCanvasSize()
+  {
+    UpdateSize(window_.GetWidth(), window_.GetHeight());
+  }
+
+  SdlCairoViewport::SdlCairoViewport(const std::string& title,
+                                     unsigned int width,
+                                     unsigned int height,
+                                     bool allowDpiScaling) :
+    window_(title, width, height, false /* enable OpenGL */, allowDpiScaling),
+    sdlSurface_(NULL)
+  {
+    AcquireCompositor(new CairoCompositor(width, height));
+  }
+
+  SdlCairoViewport::~SdlCairoViewport()
+  {
+    if (sdlSurface_)
+    {
+      SDL_FreeSurface(sdlSurface_);
+    }
+  }
+  
+  uint32_t SdlCairoViewport::GetSdlWindowId()
+  {
+    SDL_Window* sdlWindow = window_.GetObject();
+    Uint32 sdlWindowId = SDL_GetWindowID(sdlWindow);
+    return sdlWindowId;
+  }
+
+  void SdlCairoViewport::Paint()
+  {
+    SdlLock lock(*this);
+
+    lock.GetCompositor().Refresh(lock.GetController().GetScene());
+    CreateSdlSurfaceFromCompositor(dynamic_cast<CairoCompositor&>(lock.GetCompositor()));
+    
+    if (sdlSurface_ != NULL)
+    {
+      window_.Render(sdlSurface_);
+    }
+  }
+
+
+  void SdlCairoViewport::ToggleMaximize()
+  {
+    // No need to call "Invalidate()" here, as "UpdateSize()" will
+    // be invoked after event "SDL_WINDOWEVENT_SIZE_CHANGED"
+    SdlLock lock(*this);
+    window_.ToggleMaximize();
+  }
+
+  
+  // Assumes that the mutex is locked
+  void SdlCairoViewport::CreateSdlSurfaceFromCompositor(const CairoCompositor& compositor)
+  {
+    static const uint32_t rmask = 0x00ff0000;
+    static const uint32_t gmask = 0x0000ff00;
+    static const uint32_t bmask = 0x000000ff;
+
+    const unsigned int width = compositor.GetCanvas().GetWidth();
+    const unsigned int height = compositor.GetCanvas().GetHeight();
+
+    if (sdlSurface_ != NULL)
+    {
+      if (sdlSurface_->pixels == compositor.GetCanvas().GetBuffer() &&
+          sdlSurface_->w == static_cast<int>(width) &&
+          sdlSurface_->h == static_cast<int>(height) &&
+          sdlSurface_->pitch == static_cast<int>(compositor.GetCanvas().GetPitch()))
+      {
+        // The image from the compositor has not changed, no need to update the surface
+        return;
+      }
+      else
+      {
+        SDL_FreeSurface(sdlSurface_);
+      }
+    }
+
+    sdlSurface_ = SDL_CreateRGBSurfaceFrom((void*)(compositor.GetCanvas().GetBuffer()), width, height, 32,
+                                           compositor.GetCanvas().GetPitch(), rmask, gmask, bmask, 0);
+    if (!sdlSurface_)
+    {
+      LOG(ERROR) << "Cannot create a SDL surface from a Cairo surface";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  }
+
+
+  boost::shared_ptr<SdlCairoViewport> SdlCairoViewport::Create(const std::string& title,
+                                                               unsigned int width,
+                                                               unsigned int height,
+                                                               bool allowDpiScaling)
+  {
+    boost::shared_ptr<SdlCairoViewport> that =
+      boost::shared_ptr<SdlCairoViewport>(new SdlCairoViewport(title, width, height, allowDpiScaling));
+    that->SdlViewport::PostConstructor();
+    return that;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/Sdl/SdlViewport.h	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,205 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#if !defined(ORTHANC_ENABLE_SDL)
+#  error Macro ORTHANC_ENABLE_SDL must be defined
+#endif
+
+#if ORTHANC_ENABLE_SDL != 1
+#  error SDL must be enabled to use this file
+#endif
+
+#if !defined(ORTHANC_ENABLE_OPENGL)
+#  error The macro ORTHANC_ENABLE_OPENGL must be defined
+#endif
+
+#include "SdlOpenGLContext.h"
+#include "../../../OrthancStone/Sources/Scene2D/CairoCompositor.h"
+#include "../../../OrthancStone/Sources/Viewport/IViewport.h"
+
+#if ORTHANC_ENABLE_OPENGL == 1
+#  include "../../../OrthancStone/Sources/Scene2D/OpenGLCompositor.h"
+#endif
+
+#include <SDL_events.h>
+
+// TODO: required for UndoStack injection
+// I don't like it either :)
+#include <boost/weak_ptr.hpp>
+
+#include <boost/thread/recursive_mutex.hpp>
+#include <boost/enable_shared_from_this.hpp>
+
+namespace OrthancStone
+{
+  class UndoStack;
+
+  class SdlViewport : public IViewport,
+                      public boost::enable_shared_from_this<SdlViewport>
+  {
+  private:
+    boost::recursive_mutex                 mutex_;
+    uint32_t                               refreshEvent_;
+    boost::shared_ptr<ViewportController>  controller_;
+    std::unique_ptr<ICompositor>           compositor_;
+
+    void SendRefreshEvent();
+
+  protected:
+    class SdlLock : public ILock
+    {
+    private:
+      SdlViewport&                        that_;
+      boost::recursive_mutex::scoped_lock lock_;
+
+    public:
+      explicit SdlLock(SdlViewport& that) :
+        that_(that),
+        lock_(that.mutex_)
+      {
+      }
+
+      virtual bool HasCompositor() const ORTHANC_OVERRIDE
+      {
+        return true;
+      }
+
+      virtual ICompositor& GetCompositor() ORTHANC_OVERRIDE;
+      
+      virtual ViewportController& GetController() ORTHANC_OVERRIDE
+      {
+        return *that_.controller_;
+      }
+
+      virtual void Invalidate() ORTHANC_OVERRIDE
+      {
+        that_.SendRefreshEvent();
+      }
+      
+      virtual void RefreshCanvasSize() ORTHANC_OVERRIDE
+      {
+        that_.RefreshCanvasSize();
+      }
+    };
+
+    void ClearCompositor()
+    {
+      compositor_.reset();
+    }
+
+    void AcquireCompositor(ICompositor* compositor /* takes ownership */);
+
+    virtual void RefreshCanvasSize() = 0;
+    
+  protected:
+    SdlViewport();
+
+    void PostConstructor();
+
+  public:
+    bool IsRefreshEvent(const SDL_Event& event) const
+    {
+      return (event.type == refreshEvent_);
+    }
+
+    virtual ILock* Lock() ORTHANC_OVERRIDE
+    {
+      return new SdlLock(*this);
+    }
+
+    virtual uint32_t GetSdlWindowId() = 0;
+
+    void UpdateSize(unsigned int width,
+                    unsigned int height);
+
+    virtual void ToggleMaximize() = 0;
+
+    // Must be invoked from the main SDL thread
+    virtual void Paint() = 0;
+  };
+
+
+#if ORTHANC_ENABLE_OPENGL == 1
+  class SdlOpenGLViewport : public SdlViewport
+  {
+  private:
+    SdlOpenGLContext  context_;
+
+    SdlOpenGLViewport(const std::string& title,
+                      unsigned int       width,
+                      unsigned int       height,
+                      bool               allowDpiScaling = true);
+
+  protected:
+    virtual void RefreshCanvasSize() ORTHANC_OVERRIDE;
+    
+  public:
+    static boost::shared_ptr<SdlOpenGLViewport> Create(const std::string& title,
+                                                       unsigned int width,
+                                                       unsigned int height,
+                                                       bool allowDpiScaling = true);
+
+
+    virtual ~SdlOpenGLViewport();
+
+    virtual uint32_t GetSdlWindowId() ORTHANC_OVERRIDE;
+
+    virtual void Paint() ORTHANC_OVERRIDE;
+
+    virtual void ToggleMaximize() ORTHANC_OVERRIDE;
+  };
+#endif
+
+
+  class SdlCairoViewport : public SdlViewport
+  {
+  private:
+    SdlWindow     window_;
+    SDL_Surface*  sdlSurface_;
+
+    void CreateSdlSurfaceFromCompositor(const CairoCompositor& compositor);
+
+    SdlCairoViewport(const std::string& title,
+                     unsigned int width,
+                     unsigned int height,
+                     bool allowDpiScaling);
+
+  protected:
+    virtual void RefreshCanvasSize() ORTHANC_OVERRIDE;
+    
+  public:
+    static boost::shared_ptr<SdlCairoViewport> Create(const std::string& title,
+                                                      unsigned int width,
+                                                      unsigned int height,
+                                                      bool allowDpiScaling = true);
+
+    virtual ~SdlCairoViewport();
+
+    virtual uint32_t GetSdlWindowId() ORTHANC_OVERRIDE;
+
+    virtual void Paint() ORTHANC_OVERRIDE;
+
+    virtual void ToggleMaximize() ORTHANC_OVERRIDE;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/Sdl/SdlWindow.cpp	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,212 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "SdlWindow.h"
+
+#if ORTHANC_ENABLE_SDL == 1
+
+#include <Logging.h>
+#include <OrthancException.h>
+
+#ifdef WIN32 
+#include <windows.h> // for SetProcessDpiAware
+#endif 
+// WIN32
+
+#include <SDL_render.h>
+#include <SDL_video.h>
+#include <SDL.h>
+
+namespace OrthancStone
+{
+  SdlWindow::SdlWindow(const std::string& title,
+                       unsigned int width,
+                       unsigned int height,
+                       bool enableOpenGl,
+                       bool allowDpiScaling) :
+    maximized_(false)
+  {
+    // TODO Understand why, with SDL_WINDOW_OPENGL + MinGW32 + Release
+    // build mode, the application crashes whenever the SDL window is
+    // resized or maximized
+
+    uint32_t windowFlags, rendererFlags;
+    if (enableOpenGl)
+    {
+      windowFlags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE;
+      rendererFlags = SDL_RENDERER_ACCELERATED;
+    }
+    else
+    {
+      windowFlags = SDL_WINDOW_RESIZABLE;
+      rendererFlags = SDL_RENDERER_SOFTWARE;
+    }
+
+// TODO: probably required on MacOS X, too
+#if defined(WIN32) && (_WIN32_WINNT >= 0x0600)
+    if (!allowDpiScaling)
+    {
+      // if we do NOT allow DPI scaling, it means an SDL pixel will be a real
+      // monitor pixel. This is needed for high-DPI applications
+
+      // Enable high-DPI support on Windows
+
+      // THE FOLLOWING HAS BEEN COMMENTED OUT BECAUSE IT WILL CRASH UNDER 
+      // OLD WINDOWS VERSIONS
+      // ADD THIS AT THE TOP TO ENABLE IT:
+      // 
+      //#pragma comment(lib, "Shcore.lib") THIS IS ONLY REQUIRED FOR SetProcessDpiAwareness
+      //#include <windows.h>
+      //#include <ShellScalingAPI.h> THIS IS ONLY REQUIRED FOR SetProcessDpiAwareness
+      //#include <comdef.h> THIS IS ONLY REQUIRED FOR SetProcessDpiAwareness
+      // SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE);
+      
+      // This is supported on Vista+
+      SetProcessDPIAware();
+
+      windowFlags |= SDL_WINDOW_ALLOW_HIGHDPI;
+    }
+#endif 
+// WIN32
+    
+    window_ = SDL_CreateWindow(title.c_str(),
+                               SDL_WINDOWPOS_UNDEFINED,
+                               SDL_WINDOWPOS_UNDEFINED,
+                               width, height, windowFlags);
+
+    if (window_ == NULL) 
+    {
+      LOG(ERROR) << "Cannot create the SDL window: " << SDL_GetError();
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    renderer_ = SDL_CreateRenderer(window_, -1, rendererFlags);
+    if (!renderer_)
+    {
+      LOG(ERROR) << "Cannot create the SDL renderer: " << SDL_GetError();
+      SDL_DestroyWindow(window_);
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  }
+
+
+  SdlWindow::~SdlWindow()
+  {
+    if (renderer_ != NULL)
+    { 
+      SDL_DestroyRenderer(renderer_);
+    }
+
+    if (window_ != NULL)
+    { 
+      SDL_DestroyWindow(window_);
+    }
+  }
+
+
+  unsigned int SdlWindow::GetWidth() const
+  {
+    int w = -1;
+    SDL_GetWindowSize(window_, &w, NULL);
+
+    if (w < 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+    else
+    {
+      return static_cast<unsigned int>(w);
+    }
+  }
+
+
+  unsigned int SdlWindow::GetHeight() const
+  {
+    int h = -1;
+    SDL_GetWindowSize(window_, NULL, &h);
+
+    if (h < 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+    else
+    {
+      return static_cast<unsigned int>(h);
+    }
+  }
+
+
+  void SdlWindow::Render(SDL_Surface* surface)
+  {
+    /**
+     * "You are strongly encouraged to call SDL_RenderClear() to
+     * initialize the backbuffer before starting each new frame's
+     * drawing, even if you plan to overwrite every pixel."
+     * https://wiki.libsdl.org/SDL_RenderPresent
+     **/
+    SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255);
+    SDL_RenderClear(renderer_);  // Clear the entire screen to our selected color
+
+    SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer_, surface);
+    if (texture != NULL)
+    {
+      SDL_RenderCopy(renderer_, texture, NULL, NULL);
+      SDL_DestroyTexture(texture);
+    }
+
+    SDL_RenderPresent(renderer_);
+  }
+
+
+  void SdlWindow::ToggleMaximize()
+  {
+    if (maximized_)
+    {
+      SDL_RestoreWindow(window_);
+      maximized_ = false;
+    }
+    else
+    {
+      SDL_MaximizeWindow(window_);
+      maximized_ = true;
+    }
+  }
+
+
+  void SdlWindow::GlobalInitialize()
+  {
+    if (SDL_Init(SDL_INIT_VIDEO) != 0)
+    {
+      LOG(ERROR) << "Cannot initialize SDL";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  }
+
+
+  void SdlWindow::GlobalFinalize()
+  {
+    SDL_Quit();
+  }
+}
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/Sdl/SdlWindow.h	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,79 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if ORTHANC_ENABLE_SDL == 1
+
+#include <boost/noncopyable.hpp>
+#include <string>
+
+// Forward declaration of SDL type to avoid clashes with DCMTK headers
+// on "typedef Sint8", in "StoneInitialization.cpp"
+struct SDL_Window;
+struct SDL_Renderer;
+struct SDL_Surface;
+
+namespace OrthancStone
+{
+  class SdlWindow : public boost::noncopyable
+  {
+  private:
+    struct SDL_Window   *window_;
+    struct SDL_Renderer *renderer_;
+    bool                 maximized_;
+
+  public:
+    SdlWindow(const std::string& title,
+              unsigned int width,
+              unsigned int height,
+              bool enableOpenGl,
+              bool allowDpiScaling = true);
+
+    ~SdlWindow();
+
+    SDL_Window *GetObject() const
+    {
+      return window_;
+    }
+
+    unsigned int GetWidth() const;
+
+    unsigned int GetHeight() const;
+
+    /**
+     * WARNING: "Refresh()" cannot only be called from the main SDL
+     * thread, in which the window was created. Otherwise, the
+     * renderer displays nothing!
+     **/
+    void Render(struct SDL_Surface* surface);
+
+    void ToggleMaximize();
+
+    static void GlobalInitialize();
+
+    static void GlobalFinalize();
+  };
+}
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/WebAssembly/OrthancStoneWebAssemblyConfiguration.cmake	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,63 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+
+#####################################################################
+## Sanity check of the configuration
+#####################################################################
+
+set(ENABLE_WEB_CLIENT OFF)
+include(${ORTHANC_STONE_ROOT}/../Resources/CMake/OrthancStoneConfiguration.cmake)
+
+if (NOT ORTHANC_SANDBOXED)
+  message(FATAL_ERROR "WebAssembly target must me configured as sandboxed")
+endif()
+
+if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+  message(FATAL_ERROR "WebAssembly target requires the emscripten compiler")    
+endif()
+
+add_definitions(
+  -DORTHANC_ENABLE_SDL=0
+  -DORTHANC_ENABLE_WASM=1
+  )
+
+
+#####################################################################
+## Additional source files for WebAssembly
+#####################################################################
+
+list(APPEND ORTHANC_STONE_SOURCES
+  ${CMAKE_CURRENT_LIST_DIR}/WebAssemblyCairoViewport.cpp
+  ${CMAKE_CURRENT_LIST_DIR}/WebAssemblyLoadersContext.cpp
+  ${CMAKE_CURRENT_LIST_DIR}/WebAssemblyOracle.cpp
+  ${CMAKE_CURRENT_LIST_DIR}/WebAssemblyViewport.cpp
+  )
+
+if (ENABLE_OPENGL)
+  list(APPEND ORTHANC_STONE_SOURCES
+    ${CMAKE_CURRENT_LIST_DIR}/WebAssemblyOpenGLContext.cpp
+    ${CMAKE_CURRENT_LIST_DIR}/WebGLViewport.cpp
+    ${CMAKE_CURRENT_LIST_DIR}/WebGLViewportsRegistry.cpp
+    )
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/WebAssembly/OrthancStoneWebAssemblyParameters.cmake	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,44 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+
+#####################################################################
+## Load generic parameters
+#####################################################################
+
+include(${CMAKE_CURRENT_LIST_DIR}/../../../OrthancStone/Resources/CMake/OrthancStoneParameters.cmake)
+
+
+#####################################################################
+## CMake parameters tunable by the user
+#####################################################################
+
+# None for now, but might contain stuff like memory settings
+
+
+#####################################################################
+## Internal CMake parameters to enable the optional subcomponents of
+## the Stone of Orthanc
+#####################################################################
+
+set(ENABLE_THREADS OFF CACHE INTERNAL "")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/WebAssembly/SharedLibrary/CMakeLists.txt	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,214 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+cmake_minimum_required(VERSION 2.8.3)
+cmake_policy(SET CMP0058 NEW)
+
+project(OrthancStoneModule)
+
+
+# Warning message related to WebAssembly modules: We know that 1.38.41
+# DOES NOT work, but that 1.39.17 works.
+message("")
+message("=== IMPORTANT: Make sure to use a recent version of Emscripten (preferably >= 2.0.0) ===")
+message("")
+
+
+set(ORTHANC_STONE_INSTALL_PREFIX "${CMAKE_SOURCE_DIR}/../../../../wasm-binaries/OrthancStoneModule" CACHE PATH "Where to put the WebAssembly binaries")
+
+
+# Ask for the generation of a side module
+set(WASM_FLAGS "-s SIDE_MODULE=1 -s EXPORT_ALL=1")  # Must be before "Compiler.cmake"
+
+
+# Configuration of the Emscripten compiler for WebAssembly target
+# ---------------------------------------------------------------
+
+set(USE_WASM ON CACHE BOOL "")
+
+set(WASM_FLAGS "${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(WASM_LINKER_FLAGS "${WASM_LINKER_FLAGS} -s ASSERTIONS=1 -s DISABLE_EXCEPTION_CATCHING=0")
+set(WASM_LINKER_FLAGS "${WASM_LINKER_FLAGS} -s DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1")
+add_definitions(
+  -DDISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1
+)
+
+
+# Stone of Orthanc configuration
+# ---------------------------------------------------------------
+
+include(${CMAKE_SOURCE_DIR}/../OrthancStoneWebAssemblyParameters.cmake)
+
+SET(ENABLE_DCMTK ON)
+SET(ENABLE_DCMTK_NETWORKING OFF)
+SET(ENABLE_DCMTK_TRANSCODING OFF)
+SET(ENABLE_GOOGLE_TEST OFF)
+SET(ENABLE_LOCALE ON)  # Necessary for text rendering
+SET(ORTHANC_SANDBOXED ON)
+
+include(${CMAKE_SOURCE_DIR}/../OrthancStoneWebAssemblyConfiguration.cmake)
+
+
+
+
+################################################################################
+
+# The source files that register a callback cannot be part of a side
+# module, and must be compiled in the main module. The following
+# command can be used to identify such files:
+#  $ grep -lrE 'emscripten_' ../../Sources/
+
+set(SOURCES_WITH_EMSCRIPTEN_CALLBACKS
+  ${CMAKE_SOURCE_DIR}/../WebAssemblyOracle.cpp
+  ${CMAKE_SOURCE_DIR}/../WebAssemblyViewport.cpp
+  ${CMAKE_SOURCE_DIR}/../WebAssemblyCairoViewport.cpp
+  )
+
+list(REMOVE_ITEM ORTHANC_STONE_SOURCES
+  ${SOURCES_WITH_EMSCRIPTEN_CALLBACKS}
+  )
+
+configure_file(
+  ${ORTHANC_STONE_ROOT}/../SharedLibrary/OrthancStone.h.in
+  ${CMAKE_CURRENT_BINARY_DIR}/Include/orthanc-stone/OrthancStone.h
+  )
+
+configure_file(
+  ${ORTHANC_FRAMEWORK_ROOT}/../SharedLibrary/OrthancFramework.h.in
+  ${CMAKE_CURRENT_BINARY_DIR}/Include/orthanc-framework/OrthancFramework.h
+  )
+
+file(
+  COPY ${CMAKE_SOURCE_DIR}/../../../../OrthancStone/Sources/
+  DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/orthanc-stone
+  NO_SOURCE_PERMISSIONS
+  FILES_MATCHING
+  PATTERN "*.h"
+  PATTERN OrthancStone.h EXCLUDE
+  PATTERN "Deprecated*" EXCLUDE
+  )
+
+file(
+  COPY ${ORTHANC_FRAMEWORK_ROOT}/
+  DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/orthanc-framework
+  NO_SOURCE_PERMISSIONS
+  FILES_MATCHING
+  PATTERN "*.h"
+  PATTERN OrthancFramework.h EXCLUDE
+  )
+
+add_executable(OrthancStoneModule
+  ${ORTHANC_STONE_SOURCES}
+  ${AUTOGENERATED_SOURCES}  
+  ${CAIRO_SOURCES}
+  ${PIXMAN_SOURCES}
+  ${FREETYPE_SOURCES}
+  )
+
+set_target_properties(OrthancStoneModule
+  PROPERTIES
+  COMPILE_FLAGS "${WASM_FLAGS}"
+  LINK_FLAGS "${WASM_LINKER_FLAGS}"
+  )
+
+# CMake does not natively handle SIDE_MODULE, and believes that
+# Emscripten produces a ".js" file (whereas it creates only the
+# ".wasm"). Create a dummy ".js" for target to work.
+add_custom_command(
+  TARGET OrthancStoneModule POST_BUILD
+  COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_CURRENT_BINARY_DIR}/OrthancStoneModule.js
+  )
+
+file(
+  COPY ${BOOST_SOURCES_DIR}/boost/
+  DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/boost/
+  NO_SOURCE_PERMISSIONS
+  FILES_MATCHING
+  PATTERN "*.h"
+  PATTERN "*.hpp"
+  PATTERN "*.ipp"
+  )
+
+file(
+  COPY ${JSONCPP_SOURCES_DIR}/include/json/
+  DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/json/
+  NO_SOURCE_PERMISSIONS
+  FILES_MATCHING
+  PATTERN "*.h"
+  )
+
+file(
+  COPY ${CAIRO_SOURCES_DIR}/src/
+  DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/cairo/
+  NO_SOURCE_PERMISSIONS
+  FILES_MATCHING
+  PATTERN "*.h"
+  )
+
+set(DCMTK_MODULES
+  dcmdata
+  config
+  ofstd
+  oflog
+  )
+
+foreach (module IN LISTS DCMTK_MODULES)
+  file(
+    COPY ${DCMTK_SOURCES_DIR}/ofstd/include/dcmtk/${module}/
+    DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/dcmtk/${module}/
+    NO_SOURCE_PERMISSIONS
+    FILES_MATCHING
+    PATTERN "*.h"
+    )
+endforeach()
+
+    
+install(
+  TARGETS OrthancStoneModule
+  DESTINATION ${ORTHANC_STONE_INSTALL_PREFIX}/lib
+  )
+
+install(FILES
+  ${CMAKE_CURRENT_BINARY_DIR}/OrthancStoneModule.wasm
+  DESTINATION ${ORTHANC_STONE_INSTALL_PREFIX}/lib
+  )
+
+install(
+  DIRECTORY
+  ${CMAKE_CURRENT_BINARY_DIR}/Include/boost
+  ${CMAKE_CURRENT_BINARY_DIR}/Include/cairo
+  ${CMAKE_CURRENT_BINARY_DIR}/Include/dcmtk
+  ${CMAKE_CURRENT_BINARY_DIR}/Include/json
+  ${CMAKE_CURRENT_BINARY_DIR}/Include/orthanc-framework
+  ${CMAKE_CURRENT_BINARY_DIR}/Include/orthanc-stone
+  DESTINATION ${ORTHANC_STONE_INSTALL_PREFIX}/include/
+  )
+
+install(FILES
+  ${SOURCES_WITH_EMSCRIPTEN_CALLBACKS}
+  DESTINATION ${ORTHANC_STONE_INSTALL_PREFIX}/src/orthanc-stone
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/WebAssembly/SharedLibrary/NOTES.txt	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,18 @@
+
+Install Emscripten:
+https://emscripten.org/docs/getting_started/downloads.html
+
+# cd ~/Downloads
+# git clone https://github.com/emscripten-core/emsdk.git
+# cd emsdk
+# ./emsdk install 2.0.0
+# ./emsdk activate 2.0.0
+
+
+Then, if the installation path was "~/Downloads/emsdk/":
+
+# source ~/Downloads/emsdk/emsdk_env.sh
+# cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=${EMSDK}/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake -DALLOW_DOWNLOADS=ON -G Ninja
+# ninja install
+
+=> The binaries will be put in "../../../../wasm-binaries/"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/WebAssembly/UnitTests/CMakeLists.txt	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,140 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+cmake_minimum_required(VERSION 2.8.3)
+cmake_policy(SET CMP0058 NEW)
+
+project(OrthancStone)
+
+set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "mainline")
+set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
+set(ORTHANC_STONE_INSTALL_PREFIX "${CMAKE_SOURCE_DIR}/../../../../wasm-binaries/" CACHE PATH "Where to put the WebAssembly binaries")
+
+
+# Configuration of the Emscripten compiler for WebAssembly target
+# ---------------------------------------------------------------
+set(USE_WASM ON CACHE BOOL "")
+
+set(WASM_FLAGS "-s WASM=1 -s FETCH=1 -s ASSERTIONS=1 -s DISABLE_EXCEPTION_CATCHING=0")
+if (CMAKE_BUILD_TYPE STREQUAL "Debug")
+  set(WASM_FLAGS "${WASM_FLAGS} -s SAFE_HEAP=1")
+endif()
+
+set(WASM_LINKER_FLAGS "${WASM_LINKER_FLAGS} -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]'")
+set(WASM_LINKER_FLAGS "${WASM_LINKER_FLAGS} -s ERROR_ON_UNDEFINED_SYMBOLS=1")
+set(WASM_LINKER_FLAGS "${WASM_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1 -s TOTAL_MEMORY=268435456")  # 256MB + resize
+set(WASM_LINKER_FLAGS "${WASM_LINKER_FLAGS} -s DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1")
+add_definitions(
+  -DDISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1
+)
+
+
+# Stone of Orthanc configuration
+# ---------------------------------------------------------------
+set(ALLOW_DOWNLOADS ON)
+
+include(${CMAKE_SOURCE_DIR}/../OrthancStoneWebAssemblyParameters.cmake)
+
+SET(ENABLE_DCMTK ON)  # Necessary for the tests of the Orthanc framework
+SET(ENABLE_PUGIXML ON)  # Necessary for the tests of the Orthanc framework
+SET(ENABLE_GOOGLE_TEST OFF)
+SET(ENABLE_LOCALE ON)  # Necessary for text rendering
+SET(ENABLE_LUA ON)  # Necessary for the tests of the Orthanc framework
+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(${CMAKE_SOURCE_DIR}/../OrthancStoneWebAssemblyConfiguration.cmake)
+
+
+################################################################################
+
+# Define the WASM module
+# ---------------------------------------------------------------
+
+set(USE_SYSTEM_GOOGLE_TEST OFF CACHE BOOL "Use the system version of Google Test")
+set(USE_GOOGLE_TEST_DEBIAN_PACKAGE OFF CACHE BOOL "Use the sources of Google Test shipped with libgtest-dev (Debian only)")
+mark_as_advanced(USE_GOOGLE_TEST_DEBIAN_PACKAGE)
+include(${ORTHANC_STONE_ROOT}/../Resources/Orthanc/CMake/DownloadPackage.cmake)
+include(${ORTHANC_STONE_ROOT}/../Resources/Orthanc/CMake/GoogleTestConfiguration.cmake)
+
+
+include(${CMAKE_SOURCE_DIR}/../../../../OrthancStone/UnitTestsSources/UnitTestsSources.cmake)
+
+EmbedResources(
+  RT_STRUCT_00 ${CMAKE_SOURCE_DIR}/../../../../OrthancStone/UnitTestsSources/TestStructureSet.json
+  )
+
+add_executable(UnitTests
+  # Testing Stone
+  ${UNIT_TESTS_SOURCES}
+
+  # Testing the Orthanc Framework
+  ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/DicomMapTests.cpp
+  ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/FrameworkTests.cpp
+  ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/FromDcmtkTests.cpp
+  ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/ImageProcessingTests.cpp
+  ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/ImageTests.cpp
+  ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/JpegLosslessTests.cpp
+  ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/LoggingTests.cpp
+  ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/LuaTests.cpp
+  ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/RestApiTests.cpp
+  ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/StreamTests.cpp
+  ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/ToolboxTests.cpp
+
+  # Tests that are not compatible with WebAssembly
+
+  # ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/FileStorageTests.cpp
+  # ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/JobsTests.cpp
+  # ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/MemoryCacheTests.cpp
+  # ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/SQLiteChromiumTests.cpp
+  # ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/SQLiteTests.cpp
+  # ${ORTHANC_FRAMEWORK_ROOT}/../UnitTestsSources/ZipTests.cpp
+
+  # Shared files
+  ${AUTOGENERATED_SOURCES}
+  ${BOOST_EXTENDED_SOURCES}
+  ${GOOGLE_TEST_SOURCES}
+  ${ORTHANC_STONE_SOURCES}
+  )
+
+  
+# Declare installation files for the module
+# ---------------------------------------------------------------
+install(
+  TARGETS UnitTests
+  RUNTIME DESTINATION ${ORTHANC_STONE_INSTALL_PREFIX}/UnitTests/
+  )
+
+# Declare installation files for the companion files (web scaffolding)
+# please note that ${CMAKE_CURRENT_BINARY_DIR}/RtViewerWasm.js
+# (the generated JS loader for the WASM module) is handled by the `install1`
+# section above: it is considered to be the binary output of 
+# the linker.
+# ---------------------------------------------------------------
+install(
+  FILES
+  ${CMAKE_SOURCE_DIR}/index.html
+  ${CMAKE_CURRENT_BINARY_DIR}/UnitTests.wasm
+  DESTINATION ${ORTHANC_STONE_INSTALL_PREFIX}/UnitTests
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/WebAssembly/UnitTests/NOTES.txt	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,18 @@
+Native compilation (without Docker)
+===================================
+
+Install Emscripten:
+https://emscripten.org/docs/getting_started/downloads.html
+
+Then, if the installation path is "~/Downloads/emsdk/":
+
+# source ~/Downloads/emsdk/emsdk_env.sh
+# mkdir Build && cd Build
+# cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=${EMSDK}/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake -DALLOW_DOWNLOADS=ON -G Ninja
+# ninja install
+
+=> The binaries will be put in "../../../../../wasm-binaries/UnitTests/"
+
+# cd `pwd`/../../../../../wasm-binaries/UnitTests
+# python3 -m http.server 8000
+# firefox http://localhost:8000/index.html
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/WebAssembly/UnitTests/index.html	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>Stone of Orthanc</title>
+  </head>
+  <body>
+    <h1>Stone of Orthanc - Unit tests</h1>
+    <pre id="output">
+      Running the tests...
+    </pre>
+    <script src="UnitTests.js" async></script>
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyCairoViewport.cpp	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,119 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#if defined(ORTHANC_BUILDING_STONE_LIBRARY) && ORTHANC_BUILDING_STONE_LIBRARY == 1
+#  include "WebAssemblyCairoViewport.h"
+#  include "../../../OrthancStone/Sources/Scene2D/CairoCompositor.h"
+#  include "../../../OrthancStone/Sources/Scene2DViewport/ViewportController.h"
+#else
+// This is the case when using the WebAssembly side module, and this
+// source file must be compiled within the WebAssembly main module
+#  include <Viewport/WebAssemblyCairoViewport.h>
+#  include <Scene2D/CairoCompositor.h>
+#  include <Scene2DViewport/ViewportController.h>
+#endif
+
+
+#include <Images/Image.h>
+
+namespace OrthancStone
+{
+  void WebAssemblyCairoViewport::Paint(ICompositor& compositor,
+                                       ViewportController& controller)
+  {
+    compositor.Refresh(controller.GetScene());
+
+    // Create a temporary memory buffer for the canvas in JavaScript
+    Orthanc::ImageAccessor cairo;
+    dynamic_cast<CairoCompositor&>(compositor).GetCanvas().GetReadOnlyAccessor(cairo);
+
+    const unsigned int width = cairo.GetWidth();
+    const unsigned int height = cairo.GetHeight();
+
+    if (javascript_.get() == NULL ||
+        javascript_->GetWidth() != width ||
+        javascript_->GetHeight() != height)
+    {
+      javascript_.reset(new Orthanc::Image(Orthanc::PixelFormat_RGBA32, width, height,
+                                           true /* force minimal pitch */));
+    }
+      
+    // Convert from BGRA32 memory layout (only color mode supported
+    // by Cairo, which corresponds to CAIRO_FORMAT_ARGB32) to RGBA32
+    // (as expected by HTML5 canvas). This simply amounts to
+    // swapping the B and R channels. Alpha channel is also set to
+    // full opacity (255).
+    uint8_t* q = reinterpret_cast<uint8_t*>(javascript_->GetBuffer());
+    for (unsigned int y = 0; y < height; y++)
+    {
+      const uint8_t* p = reinterpret_cast<const uint8_t*>(cairo.GetConstRow(y));
+      for (unsigned int x = 0; x < width; x++)
+      {
+        q[0] = p[2];  // R
+        q[1] = p[1];  // G
+        q[2] = p[0];  // B
+        q[3] = 255;   // A
+
+        p += 4;
+        q += 4;
+      }
+    }
+
+    if (width != 0 &&
+        height != 0)
+    {
+      // Execute JavaScript commands to blit the image buffer onto the
+      // 2D drawing context of the HTML5 canvas
+      EM_ASM({
+          const data = new Uint8ClampedArray(Module.HEAP8.buffer, $1, 4 * $2 * $3);
+          const img = new ImageData(data, $2, $3);
+          const ctx = document.getElementById(UTF8ToString($0)).getContext('2d');
+          ctx.putImageData(img, 0, 0);
+        },
+        GetCanvasId().c_str(),      // $0
+        javascript_->GetBuffer(),   // $1
+        javascript_->GetWidth(),    // $2
+        javascript_->GetHeight());  // $3
+    }
+  }
+    
+
+  WebAssemblyCairoViewport::WebAssemblyCairoViewport(const std::string& canvasId,
+                                                     bool enableEmscriptenMouseEvents) :
+    WebAssemblyViewport(canvasId,enableEmscriptenMouseEvents)
+  {
+    RefreshCanvasSize();
+    AcquireCompositor(new CairoCompositor(GetCanvasWidth(), GetCanvasHeight()));
+  }
+  
+
+  boost::shared_ptr<WebAssemblyCairoViewport> WebAssemblyCairoViewport::Create(
+    const std::string& canvasId, bool enableEmscriptenMouseEvents)
+  {
+    boost::shared_ptr<WebAssemblyCairoViewport> that = boost::shared_ptr<WebAssemblyCairoViewport>(
+        new WebAssemblyCairoViewport(canvasId, enableEmscriptenMouseEvents));
+    
+    that->WebAssemblyViewport::PostConstructor();
+    return that;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyCairoViewport.h	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,50 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
+ **/
+
+
+#pragma once
+
+#include "WebAssemblyViewport.h"
+
+namespace OrthancStone
+{
+  class WebAssemblyCairoViewport : public WebAssemblyViewport
+  {
+  private:
+    std::unique_ptr<Orthanc::ImageAccessor>  javascript_;
+        
+    WebAssemblyCairoViewport(const std::string& canvasId,
+                             bool enableEmscriptenMouseEvents);
+
+  protected:
+    virtual void Paint(ICompositor& compositor,
+                       ViewportController& controller) ORTHANC_OVERRIDE;
+    
+  public:
+    static boost::shared_ptr<WebAssemblyCairoViewport> Create(const std::string& canvasId,
+                                                              bool enableEmscriptenMouseEvents = true);
+
+    virtual ~WebAssemblyCairoViewport()
+    {
+      ClearCompositor();
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyLoadersContext.cpp	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,99 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "WebAssemblyLoadersContext.h"
+
+namespace OrthancStone
+{
+  class WebAssemblyLoadersContext::Locker : public ILoadersContext::ILock
+  {
+  private:
+    WebAssemblyLoadersContext&  that_;
+
+  public:
+    explicit Locker(WebAssemblyLoadersContext& that) :
+      that_(that)
+    {
+    }      
+      
+    virtual ILoadersContext& GetContext() const ORTHANC_OVERRIDE
+    {
+      return that_;
+    }
+
+    virtual IObservable& GetOracleObservable() const ORTHANC_OVERRIDE
+    {
+      return that_.oracle_.GetOracleObservable();
+    }
+
+    virtual void Schedule(boost::shared_ptr<IObserver> receiver,
+                          int priority,
+                          IOracleCommand* command /* Takes ownership */) ORTHANC_OVERRIDE
+    {
+      that_.scheduler_->Schedule(receiver, priority, command);
+    }
+
+    virtual void CancelRequests(boost::shared_ptr<IObserver> receiver) ORTHANC_OVERRIDE
+    {
+      that_.scheduler_->CancelRequests(receiver);
+    }
+
+    virtual void CancelAllRequests() ORTHANC_OVERRIDE
+    {
+      that_.scheduler_->CancelAllRequests();
+    }
+
+    virtual void AddLoader(boost::shared_ptr<IObserver> loader) ORTHANC_OVERRIDE
+    {
+      that_.loaders_.push_back(loader);
+    }
+
+    virtual void GetStatistics(uint64_t& scheduledCommands,
+                               uint64_t& processedCommands) ORTHANC_OVERRIDE
+    {
+      scheduledCommands = that_.scheduler_->GetTotalScheduled();
+      processedCommands = that_.scheduler_->GetTotalProcessed();
+    }
+  };
+    
+
+  WebAssemblyLoadersContext::WebAssemblyLoadersContext(unsigned int maxHighPriority,
+                                                       unsigned int maxStandardPriority,
+                                                       unsigned int maxLowPriority)
+  {
+    oracle_.GetOracleObservable();
+    scheduler_ = OracleScheduler::Create(oracle_, oracle_.GetOracleObservable(), oracle_,
+                                         maxHighPriority, maxStandardPriority, maxLowPriority);
+
+    if (!scheduler_)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  }
+
+
+  ILoadersContext::ILock* WebAssemblyLoadersContext::Lock()
+  {
+    return new Locker(*this);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyLoadersContext.h	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,70 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../../OrthancStone/Sources/Loaders/ILoadersContext.h"
+#include "../../../OrthancStone/Sources/Loaders/OracleScheduler.h"
+#include "WebAssemblyOracle.h"
+
+#include <list>
+
+namespace OrthancStone
+{
+  class WebAssemblyLoadersContext : public ILoadersContext
+  {
+  private:
+    class Locker;
+    
+    WebAssemblyOracle                          oracle_;
+    boost::shared_ptr<OracleScheduler>         scheduler_;
+    std::list< boost::shared_ptr<IObserver> >  loaders_;
+    
+  public:
+    WebAssemblyLoadersContext(unsigned int maxHighPriority,
+                              unsigned int maxStandardPriority,
+                              unsigned int maxLowPriority);
+
+    void SetLocalOrthanc(const std::string& root)
+    {
+      oracle_.SetLocalOrthanc(root);
+    }
+
+    void SetRemoteOrthanc(const Orthanc::WebServiceParameters& orthanc)
+    {
+      oracle_.SetRemoteOrthanc(orthanc);
+    }
+
+    void SetDicomCacheSize(size_t size)
+    {
+      oracle_.SetDicomCacheSize(size);
+    }
+
+    WebAssemblyOracle::CachedInstanceAccessor* AccessCachedInstance(const std::string& sopInstanceUid)
+    {
+      return new WebAssemblyOracle::CachedInstanceAccessor(oracle_, sopInstanceUid);
+    }
+
+    virtual ILock* Lock() ORTHANC_OVERRIDE;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyOpenGLContext.cpp	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,202 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "WebAssemblyOpenGLContext.h"
+
+#include "../../../OrthancStone/Sources/StoneException.h"
+
+#include <Logging.h>
+#include <OrthancException.h>
+
+#include <emscripten/html5.h>
+#include <emscripten/em_asm.h>
+
+#include <boost/math/special_functions/round.hpp>
+
+namespace OrthancStone
+{
+  namespace OpenGL
+  {
+    class WebAssemblyOpenGLContext::PImpl
+    {
+    private:
+      std::string                     canvasSelector_;
+      EMSCRIPTEN_WEBGL_CONTEXT_HANDLE context_;
+      bool                            isContextLost_;
+
+    public:
+      explicit PImpl(const std::string& canvasSelector) :
+        canvasSelector_(canvasSelector),
+        isContextLost_(false)
+      {
+        // Context configuration
+        EmscriptenWebGLContextAttributes attr; 
+        emscripten_webgl_init_context_attributes(&attr);
+
+        // The next line might be necessary to print using
+        // WebGL. Sometimes, if set to "false" (the default value),
+        // the canvas was rendered as a fully white or black
+        // area. UNCONFIRMED.
+        attr.preserveDrawingBuffer = true;
+
+        context_ = emscripten_webgl_create_context(canvasSelector.c_str(), &attr);
+        if (context_ == 0)
+        {
+          std::string message("Cannot create an OpenGL context for the element with the following CSS selector: \"");
+          message += canvasSelector;
+          message += "\"  Please make sure the -s DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1 flag has been passed to Emscripten when building.";
+          LOG(ERROR) << message;
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, message);
+        }
+      }
+
+      void* DebugGetInternalContext() const
+      {
+        return reinterpret_cast<void*>(context_);
+      }
+
+      bool IsContextLost()
+      {
+        //LOG(TRACE) << "IsContextLost() for context " << std::hex << context_ << std::dec;
+        bool apiFlag = (emscripten_is_webgl_context_lost(context_) != 0);
+        isContextLost_ = apiFlag;
+        return isContextLost_;
+      }
+
+      void SetLostContext()
+      {
+        isContextLost_ = true;
+      }
+
+      ~PImpl()
+      {
+        try
+        {
+          EMSCRIPTEN_RESULT result = emscripten_webgl_destroy_context(context_);
+          if (result != EMSCRIPTEN_RESULT_SUCCESS)
+          {
+            LOG(ERROR) << "emscripten_webgl_destroy_context returned code " << result;
+          }
+        }
+        catch (const Orthanc::OrthancException& e)
+        {
+          if (e.HasDetails())
+          {
+            LOG(ERROR) << "OrthancException in WebAssemblyOpenGLContext::~PImpl: " << e.What() << " Details: " << e.GetDetails();
+          }
+          else
+          {
+            LOG(ERROR) << "OrthancException in WebAssemblyOpenGLContext::~PImpl: " << e.What();
+          }
+        }
+        catch (const std::exception& e)
+        {
+          LOG(ERROR) << "std::exception in WebAssemblyOpenGLContext::~PImpl: " << e.what();
+        }
+        catch (...)
+        {
+          LOG(ERROR) << "Unknown exception in WebAssemblyOpenGLContext::~PImpl";
+        }
+      }
+
+      const std::string& GetCanvasSelector() const
+      {
+        return canvasSelector_;
+      }
+
+      void MakeCurrent()
+      {
+        if (IsContextLost())
+        {
+          LOG(ERROR) << "MakeCurrent() called on lost context " << context_;
+          throw StoneException(ErrorCode_WebGLContextLost);
+        }
+
+        if (emscripten_is_webgl_context_lost(context_))
+        {
+          LOG(ERROR) << "OpenGL context has been lost for canvas selector: " << canvasSelector_;
+          SetLostContext();
+          throw StoneException(ErrorCode_WebGLContextLost);
+        }
+
+        if (emscripten_webgl_make_context_current(context_) != EMSCRIPTEN_RESULT_SUCCESS)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                          "Cannot set the OpenGL context");
+        }
+      }
+
+      void SwapBuffer() 
+      {
+        /**
+         * "Rendered WebGL content is implicitly presented (displayed to
+         * the user) on the canvas when the event handler that renders with
+         * WebGL returns back to the browser event loop."
+         * https://emscripten.org/docs/api_reference/html5.h.html#webgl-context
+         *
+         * Could call "emscripten_webgl_commit_frame()" if
+         * "explicitSwapControl" option were set to "true".
+         **/
+      }
+    };
+
+
+    WebAssemblyOpenGLContext::WebAssemblyOpenGLContext(const std::string& canvasSelector) :
+      pimpl_(new PImpl(canvasSelector))
+    {
+    }
+
+    bool WebAssemblyOpenGLContext::IsContextLost()
+    {
+      return pimpl_->IsContextLost();
+    }
+
+    void WebAssemblyOpenGLContext::SetLostContext()
+    {
+      pimpl_->SetLostContext();
+    }
+
+    void* WebAssemblyOpenGLContext::DebugGetInternalContext() const
+    {
+      return pimpl_->DebugGetInternalContext();
+    }
+    
+    void WebAssemblyOpenGLContext::MakeCurrent()
+    {
+      assert(pimpl_.get() != NULL);
+      pimpl_->MakeCurrent();
+    }
+
+    void WebAssemblyOpenGLContext::SwapBuffer() 
+    {
+      assert(pimpl_.get() != NULL);
+      pimpl_->SwapBuffer();
+    }
+
+    const std::string& WebAssemblyOpenGLContext::GetCanvasSelector() const
+    {
+      assert(pimpl_.get() != NULL);
+      return pimpl_->GetCanvasSelector();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyOpenGLContext.h	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,82 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if !defined(ORTHANC_ENABLE_WASM)
+#  error Macro ORTHANC_ENABLE_WASM must be defined
+#endif
+
+#if ORTHANC_ENABLE_WASM != 1
+#  error This file can only be used if targeting WebAssembly
+#endif
+
+#if !defined(ORTHANC_ENABLE_OPENGL)
+#  error The macro ORTHANC_ENABLE_OPENGL must be defined
+#endif
+
+#if ORTHANC_ENABLE_OPENGL != 1
+#  error Support for OpenGL is disabled
+#endif
+
+#include "../../../OrthancStone/Sources/OpenGL/IOpenGLContext.h"
+
+#include <Compatibility.h>  // For ORTHANC_OVERRIDE
+
+#include <boost/shared_ptr.hpp>
+
+namespace OrthancStone
+{
+  namespace OpenGL
+  {
+    class WebAssemblyOpenGLContext : public OpenGL::IOpenGLContext
+    {
+    private:
+      class PImpl;
+      boost::shared_ptr<PImpl>  pimpl_;
+
+    public:
+      explicit WebAssemblyOpenGLContext(const std::string& canvasSelector);
+
+      virtual bool IsContextLost() ORTHANC_OVERRIDE;
+
+      virtual void MakeCurrent() ORTHANC_OVERRIDE;
+
+      virtual void SwapBuffer() ORTHANC_OVERRIDE;
+
+      /**
+      Returns true if the underlying context has been successfully recreated
+      */
+      //bool TryRecreate();
+
+      const std::string& GetCanvasSelector() const;
+
+
+      /**
+       * This is for manual context loss (debug purposes)
+       **/
+      void* DebugGetInternalContext() const;
+      void SetLostContext();
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyOracle.cpp	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,876 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#if defined(ORTHANC_BUILDING_STONE_LIBRARY) && ORTHANC_BUILDING_STONE_LIBRARY == 1
+#  include "WebAssemblyOracle_Includes.h"
+#else
+// This is the case when using the WebAssembly side module, and this
+// source file must be compiled within the WebAssembly main module
+#  include <Oracle/WebAssemblyOracle_Includes.h>
+#endif
+
+#include <OrthancException.h>
+#include <Toolbox.h>
+
+#include <emscripten.h>
+#include <emscripten/html5.h>
+#include <emscripten/fetch.h>
+
+
+#if ORTHANC_ENABLE_DCMTK == 1
+static unsigned int BUCKET_SOP = 1;
+#endif
+
+
+namespace OrthancStone
+{
+  class WebAssemblyOracle::TimeoutContext
+  {
+  private:
+    WebAssemblyOracle&                 oracle_;
+    boost::weak_ptr<IObserver>         receiver_;
+    std::unique_ptr<SleepOracleCommand>  command_;
+
+  public:
+    TimeoutContext(WebAssemblyOracle& oracle,
+                   boost::weak_ptr<IObserver> receiver,
+                   IOracleCommand* command) :
+      oracle_(oracle),
+      receiver_(receiver)
+    {
+      if (command == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+      else
+      {
+        command_.reset(dynamic_cast<SleepOracleCommand*>(command));
+      }
+    }
+
+    void EmitMessage()
+    {      
+      assert(command_.get() != NULL);
+
+      SleepOracleCommand::TimeoutMessage message(*command_);
+      oracle_.EmitMessage(receiver_, message);
+    }
+
+    static void Callback(void *userData)
+    {
+      std::unique_ptr<TimeoutContext> context(reinterpret_cast<TimeoutContext*>(userData));
+      context->EmitMessage();
+    }
+  };
+    
+
+  /**
+     This object is created on the heap for every http request.
+     It is deleted in the success (or error) callbacks.
+
+     This object references the receiver of the request. Since this is a raw
+     reference, we need additional checks to make sure we send the response to
+     the same object, for the object can be deleted and a new one recreated at the
+     same address (it often happens in the [single-threaded] browser context).
+  */
+  class WebAssemblyOracle::FetchContext : public boost::noncopyable
+  {
+  private:
+    WebAssemblyOracle&             oracle_;
+    boost::weak_ptr<IObserver>     receiver_;
+    std::unique_ptr<IOracleCommand>  command_;
+    std::string                    expectedContentType_;
+
+  public:
+    FetchContext(WebAssemblyOracle& oracle,
+                 boost::weak_ptr<IObserver> receiver,
+                 IOracleCommand* command,
+                 const std::string& expectedContentType) :
+      oracle_(oracle),
+      receiver_(receiver),
+      command_(command),
+      expectedContentType_(expectedContentType)
+    {
+      if (Orthanc::Logging::IsTraceLevelEnabled())
+      {
+        // Calling "receiver.lock()" is expensive, hence the quick check if TRACE is enabled
+        LOG(TRACE) << "WebAssemblyOracle::FetchContext::FetchContext() | "
+                   << "receiver address = " << std::hex << receiver.lock().get();
+      }
+
+      if (command == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+    }
+
+    const std::string& GetExpectedContentType() const
+    {
+      return expectedContentType_;
+    }
+
+    void EmitException(const Orthanc::OrthancException& exception)
+    {
+      assert(command_.get() != NULL);
+      OracleCommandExceptionMessage message(*command_, exception);
+      oracle_.EmitMessage(receiver_, message);
+    }
+
+    void ProcessFetchResult(const std::string& answer,
+                            const HttpHeaders& headers)
+    {
+      assert(command_.get() != NULL);
+      oracle_.ProcessFetchResult(receiver_, answer, headers, *command_);
+    }
+
+    static void SuccessCallback(emscripten_fetch_t *fetch)
+    {
+      /**
+       * Firstly, make a local copy of the fetched information, and
+       * free data associated with the fetch.
+       **/
+      
+      if (fetch->userData == NULL)
+      {
+        LOG(ERROR) << "WebAssemblyOracle::FetchContext::SuccessCallback fetch->userData is NULL!";
+        return;
+      }
+
+      std::unique_ptr<FetchContext> context(reinterpret_cast<FetchContext*>(fetch->userData));
+
+      std::string answer;
+      if (fetch->numBytes > 0)
+      {
+        answer.assign(fetch->data, fetch->numBytes);
+      }
+
+
+      /**
+       * Retrieving the headers of the HTTP answer.
+       **/
+      HttpHeaders headers;
+
+#if (__EMSCRIPTEN_major__ < 1 ||                                        \
+     (__EMSCRIPTEN_major__ == 1 && __EMSCRIPTEN_minor__ < 38) ||        \
+     (__EMSCRIPTEN_major__ == 1 && __EMSCRIPTEN_minor__ == 38 && __EMSCRIPTEN_tiny__ < 37))
+#  warning Consider upgrading Emscripten to a version above 1.38.37, incomplete support of Fetch API
+
+      /**
+       * HACK - If emscripten < 1.38.37, the fetch API does not
+       * contain a way to retrieve the HTTP headers of the answer. We
+       * make the assumption that the "Content-Type" header of the
+       * response is the same as the "Accept" header of the
+       * query. This is fixed thanks to the
+       * "emscripten_fetch_get_response_headers()" function that was
+       * added to "fetch.h" at emscripten-1.38.37 on 2019-06-26.
+       *
+       * https://github.com/emscripten-core/emscripten/blob/1.38.37/system/include/emscripten/fetch.h
+       * https://github.com/emscripten-core/emscripten/pull/8486
+       **/
+      if (fetch->userData != NULL)
+      {
+        if (!context->GetExpectedContentType().empty())
+        {
+          headers["Content-Type"] = context->GetExpectedContentType();
+        }
+      }
+#else
+      {
+        size_t size = emscripten_fetch_get_response_headers_length(fetch);
+
+        std::string plainHeaders(size + 1, '\0');
+        emscripten_fetch_get_response_headers(fetch, &plainHeaders[0], size + 1);
+
+        std::vector<std::string> tokens;
+        Orthanc::Toolbox::TokenizeString(tokens, plainHeaders, '\n');
+
+        for (size_t i = 0; i < tokens.size(); i++)
+        {
+          size_t p = tokens[i].find(':');
+          if (p != std::string::npos)
+          {
+            std::string key = Orthanc::Toolbox::StripSpaces(tokens[i].substr(0, p));
+            std::string value = Orthanc::Toolbox::StripSpaces(tokens[i].substr(p + 1));
+            headers[key] = value;
+          }
+        }
+      }
+#endif
+      
+      LOG(TRACE) << "About to call emscripten_fetch_close";
+      emscripten_fetch_close(fetch);
+      LOG(TRACE) << "Successfully called emscripten_fetch_close";
+
+      /**
+       * Secondly, use the retrieved data.
+       * IMPORTANT NOTE: the receiver might be dead. This is prevented 
+       * by the object responsible for zombie check, later on.
+       **/
+      try
+      {
+        if (context.get() == NULL)
+        {
+          LOG(ERROR) << "WebAssemblyOracle::FetchContext::SuccessCallback: (context.get() == NULL)";
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+        }
+        else
+        {
+          context->ProcessFetchResult(answer, headers);
+        }
+      }
+      catch (Orthanc::OrthancException& e)
+      {
+        LOG(INFO) << "Error while processing a fetch answer in the oracle: " << e.What();
+        context->EmitException(e);
+      }
+    }
+
+    static void FailureCallback(emscripten_fetch_t *fetch)
+    {
+      std::unique_ptr<FetchContext> context(reinterpret_cast<FetchContext*>(fetch->userData));
+
+#if 0
+      {
+        const size_t kEmscriptenStatusTextSize = sizeof(emscripten_fetch_t::statusText);
+        char message[kEmscriptenStatusTextSize + 1];
+        memcpy(message, fetch->statusText, kEmscriptenStatusTextSize);
+        message[kEmscriptenStatusTextSize] = 0;
+
+        LOG(ERROR) << "Fetching " << fetch->url
+                   << " failed, HTTP failure status code: " << fetch->status
+                   << " | statusText = " << message
+                   << " | numBytes = " << fetch->numBytes
+                   << " | totalBytes = " << fetch->totalBytes
+                   << " | readyState = " << fetch->readyState;
+      }
+#endif
+
+      context->EmitException(Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol));
+      
+      /**
+       * TODO - The following code leads to an infinite recursion, at
+       * least with Firefox running on incognito mode => WHY?
+       **/      
+      emscripten_fetch_close(fetch); // Also free data on failure.
+    }
+  };
+    
+
+
+  class WebAssemblyOracle::FetchCommand : public boost::noncopyable
+  {
+  private:
+    WebAssemblyOracle&             oracle_;
+    boost::weak_ptr<IObserver>     receiver_;
+    std::unique_ptr<IOracleCommand>  command_;
+    Orthanc::HttpMethod            method_;
+    std::string                    url_;
+    std::string                    body_;
+    HttpHeaders                    headers_;
+    unsigned int                   timeout_;
+    std::string                    expectedContentType_;
+    bool                           hasCredentials_;
+    std::string                    username_;
+    std::string                    password_;
+
+  public:
+    FetchCommand(WebAssemblyOracle& oracle,
+                 boost::weak_ptr<IObserver> receiver,
+                 IOracleCommand* command) :
+      oracle_(oracle),
+      receiver_(receiver),
+      command_(command),
+      method_(Orthanc::HttpMethod_Get),
+      timeout_(0),
+      hasCredentials_(false)
+    {
+      if (command == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+    }
+
+    void SetMethod(Orthanc::HttpMethod method)
+    {
+      method_ = method;
+    }
+
+    Orthanc::HttpMethod GetMethod() const
+    {
+      return method_;
+    }
+
+    void SetUrl(const std::string& url)
+    {
+      url_ = url;
+    }
+
+    const std::string& GetUrl() const
+    {
+      return url_;
+    }
+
+    void SetBody(std::string& body /* will be swapped */)
+    {
+      body_.swap(body);
+    }
+
+    void AddHttpHeaders(const HttpHeaders& headers)
+    {
+      for (HttpHeaders::const_iterator it = headers.begin(); it != headers.end(); ++it)
+      {
+        headers_[it->first] = it->second;
+      }
+    }
+
+    const HttpHeaders& GetHttpHeaders() const
+    {
+      return headers_;
+    }
+
+    void SetTimeout(unsigned int timeout)
+    {
+      timeout_ = timeout;
+    }
+
+    unsigned int GetTimeout() const
+    {
+      return timeout_;
+    }
+
+    void SetCredentials(const std::string& username,
+                        const std::string& password)
+    {
+      hasCredentials_ = true;
+      username_ = username;
+      password_ = password;
+    }
+
+    void Execute()
+    {
+      if (command_.get() == NULL)
+      {
+        // Cannot call Execute() twice
+        LOG(ERROR) << "WebAssemblyOracle::Execute(): (command_.get() == NULL)";
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+
+      emscripten_fetch_attr_t attr;
+      emscripten_fetch_attr_init(&attr);
+
+      const char* method;
+      
+      switch (method_)
+      {
+        case Orthanc::HttpMethod_Get:
+          method = "GET";
+          break;
+
+        case Orthanc::HttpMethod_Post:
+          method = "POST";
+          break;
+
+        case Orthanc::HttpMethod_Delete:
+          method = "DELETE";
+          break;
+
+        case Orthanc::HttpMethod_Put:
+          method = "PUT";
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+
+      strcpy(attr.requestMethod, method);
+
+      attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY | EMSCRIPTEN_FETCH_REPLACE;
+      attr.onsuccess = FetchContext::SuccessCallback;
+      attr.onerror = FetchContext::FailureCallback;
+      attr.timeoutMSecs = timeout_ * 1000;
+
+      if (hasCredentials_)
+      {
+        attr.withCredentials = EM_TRUE;
+        attr.userName = username_.c_str();
+        attr.password = password_.c_str();
+      }
+      
+      std::vector<const char*> headers;
+      headers.reserve(2 * headers_.size() + 1);
+
+      std::string expectedContentType;
+        
+      for (HttpHeaders::const_iterator it = headers_.begin(); it != headers_.end(); ++it)
+      {
+        std::string key;
+        Orthanc::Toolbox::ToLowerCase(key, it->first);
+          
+        if (key == "accept")
+        {
+          expectedContentType = it->second;
+        }
+
+        if (key != "accept-encoding")  // Web browsers forbid the modification of this HTTP header
+        {
+          headers.push_back(it->first.c_str());
+          headers.push_back(it->second.c_str());
+        }
+      }
+        
+      headers.push_back(NULL);  // Termination of the array of HTTP headers
+
+      attr.requestHeaders = &headers[0];
+
+      char* requestData = NULL;
+      if (!body_.empty())
+        requestData = reinterpret_cast<char*>(malloc(body_.size()));
+        
+      try 
+      {
+        if (!body_.empty())
+        {
+          memcpy(requestData, &(body_[0]), body_.size());
+          attr.requestDataSize = body_.size();
+          attr.requestData = requestData;
+        }
+        attr.userData = new FetchContext(oracle_, receiver_, command_.release(), expectedContentType);
+
+        // Must be the last call to prevent memory leak on error
+        emscripten_fetch(&attr, url_.c_str());
+      }        
+      catch(...)
+      {
+        if(requestData != NULL)
+          free(requestData);
+        throw;
+      }
+    }
+  };
+
+
+  void WebAssemblyOracle::ProcessFetchResult(boost::weak_ptr<IObserver>& receiver,
+                                             const std::string& answer,
+                                             const HttpHeaders& headers,
+                                             const IOracleCommand& command)
+  {
+    switch (command.GetType())
+    {
+      case IOracleCommand::Type_Http:
+      {
+        HttpCommand::SuccessMessage message(dynamic_cast<const HttpCommand&>(command), headers, answer);
+        EmitMessage(receiver, message);
+        break;
+      }
+
+      case IOracleCommand::Type_OrthancRestApi:
+      {
+        LOG(TRACE) << "WebAssemblyOracle::FetchContext::SuccessCallback. About to call EmitMessage(message);";
+        OrthancRestApiCommand::SuccessMessage message
+          (dynamic_cast<const OrthancRestApiCommand&>(command), headers, answer);
+        EmitMessage(receiver, message);
+        break;
+      }
+
+      case IOracleCommand::Type_GetOrthancImage:
+      {
+        dynamic_cast<const GetOrthancImageCommand&>(command).ProcessHttpAnswer(receiver, *this, answer, headers);
+        break;
+      }
+
+      case IOracleCommand::Type_GetOrthancWebViewerJpeg:
+      {
+        dynamic_cast<const GetOrthancWebViewerJpegCommand&>(command).ProcessHttpAnswer(receiver, *this, answer);
+        break;
+      }
+
+      case IOracleCommand::Type_ParseDicomFromWado:
+      {
+#if ORTHANC_ENABLE_DCMTK == 1
+        const ParseDicomFromWadoCommand& c = dynamic_cast<const ParseDicomFromWadoCommand&>(command);
+              
+        size_t fileSize;
+        std::unique_ptr<Orthanc::ParsedDicomFile> dicom
+          (ParseDicomSuccessMessage::ParseWadoAnswer(fileSize, answer, headers));
+
+        {
+          ParseDicomSuccessMessage message(c, c.GetSource(), *dicom, fileSize, true);
+          EmitMessage(receiver, message);
+        }
+
+        if (dicomCache_.get())
+        {
+          // Store it into the cache for future use
+          dicomCache_->Acquire(BUCKET_SOP, c.GetSopInstanceUid(), dicom.release(), fileSize, true);
+        }
+#else
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+#endif
+        break;
+      }
+
+      default:
+        LOG(ERROR) << "Command type not implemented by the WebAssembly Oracle (in SuccessCallback): "
+                   << command.GetType();
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+  }
+
+
+  void WebAssemblyOracle::SetOrthancUrl(FetchCommand& command,
+                                        const std::string& uri) const
+  {
+    if (isLocalOrthanc_)
+    {
+      command.SetUrl(localOrthancRoot_ + uri);
+    }
+    else
+    {
+      command.SetUrl(remoteOrthanc_.GetUrl() + uri);
+      command.AddHttpHeaders(remoteOrthanc_.GetHttpHeaders());
+      
+      if (!remoteOrthanc_.GetUsername().empty())
+      {
+        command.SetCredentials(remoteOrthanc_.GetUsername(), remoteOrthanc_.GetPassword());
+      }
+    }
+  }
+    
+
+  void WebAssemblyOracle::Execute(boost::weak_ptr<IObserver> receiver,
+                                  HttpCommand* command)
+  {
+    FetchCommand fetch(*this, receiver, command);
+    
+    fetch.SetMethod(command->GetMethod());
+    fetch.SetUrl(command->GetUrl());
+    fetch.AddHttpHeaders(command->GetHttpHeaders());
+    fetch.SetTimeout(command->GetTimeout());
+    
+    if (command->GetMethod() == Orthanc::HttpMethod_Post ||
+        command->GetMethod() == Orthanc::HttpMethod_Put)
+    {
+      std::string body;
+      command->SwapBody(body);
+      fetch.SetBody(body);
+    }
+    
+    fetch.Execute();
+  }
+  
+
+  void WebAssemblyOracle::Execute(boost::weak_ptr<IObserver> receiver,
+                                  OrthancRestApiCommand* command)
+  {
+    try
+    {
+      //LOG(TRACE) << "*********** WebAssemblyOracle::Execute.";
+      //LOG(TRACE) << "WebAssemblyOracle::Execute | command = " << command;
+      FetchCommand fetch(*this, receiver, command);
+
+      fetch.SetMethod(command->GetMethod());
+      SetOrthancUrl(fetch, command->GetUri());
+      fetch.AddHttpHeaders(command->GetHttpHeaders());
+      fetch.SetTimeout(command->GetTimeout());
+
+      if (command->GetMethod() == Orthanc::HttpMethod_Post ||
+          command->GetMethod() == Orthanc::HttpMethod_Put)
+      {
+        std::string body;
+        command->SwapBody(body);
+        fetch.SetBody(body);
+      }
+
+      fetch.Execute();
+      //LOG(TRACE) << "*********** successful end of WebAssemblyOracle::Execute.";
+    }
+    catch (const Orthanc::OrthancException& e)
+    {
+      if (e.HasDetails())
+      {
+        LOG(ERROR) << "OrthancException in WebAssemblyOracle::Execute: " << e.What() << " Details: " << e.GetDetails();
+      }
+      else
+      {
+        LOG(ERROR) << "OrthancException in WebAssemblyOracle::Execute: " << e.What();
+      }
+      //LOG(TRACE) << "*********** failing end of WebAssemblyOracle::Execute.";
+      throw;
+    }
+    catch (const std::exception& e)
+    {
+      LOG(ERROR) << "std::exception in WebAssemblyOracle::Execute: " << e.what();
+//       LOG(TRACE) << "*********** failing end of WebAssemblyOracle::Execute.";
+      throw;
+    }
+    catch (...)
+    {
+      LOG(ERROR) << "Unknown exception in WebAssemblyOracle::Execute";
+//       LOG(TRACE) << "*********** failing end of WebAssemblyOracle::Execute.";
+      throw;
+    }
+  }
+    
+    
+  void WebAssemblyOracle::Execute(boost::weak_ptr<IObserver> receiver,
+                                  GetOrthancImageCommand* command)
+  {
+    FetchCommand fetch(*this, receiver, command);
+
+    SetOrthancUrl(fetch, command->GetUri());
+    fetch.AddHttpHeaders(command->GetHttpHeaders());
+    fetch.SetTimeout(command->GetTimeout());
+      
+    fetch.Execute();
+  }
+    
+    
+  void WebAssemblyOracle::Execute(boost::weak_ptr<IObserver> receiver,
+                                  GetOrthancWebViewerJpegCommand* command)
+  {
+    FetchCommand fetch(*this, receiver, command);
+
+    SetOrthancUrl(fetch, command->GetUri());
+    fetch.AddHttpHeaders(command->GetHttpHeaders());
+    fetch.SetTimeout(command->GetTimeout());
+      
+    fetch.Execute();
+  }
+
+
+  void WebAssemblyOracle::Execute(boost::weak_ptr<IObserver> receiver,
+                                  ParseDicomFromWadoCommand* command)
+  {
+    std::unique_ptr<ParseDicomFromWadoCommand> protection(command);
+    
+#if ORTHANC_ENABLE_DCMTK == 1
+    if (dicomCache_.get())
+    {
+      ParsedDicomCache::Reader reader(*dicomCache_, BUCKET_SOP, protection->GetSopInstanceUid());
+      if (reader.IsValid() &&
+          reader.HasPixelData())
+      {
+        // Reuse the DICOM file from the cache
+        ParseDicomSuccessMessage message(*protection, protection->GetSource(), reader.GetDicom(),
+                                         reader.GetFileSize(), reader.HasPixelData());
+        EmitMessage(receiver, message);
+        return;
+      }
+    }
+#endif
+
+    switch (command->GetRestCommand().GetType())
+    {
+      case IOracleCommand::Type_Http:
+      {
+        const HttpCommand& rest =
+          dynamic_cast<const HttpCommand&>(protection->GetRestCommand());
+        
+        FetchCommand fetch(*this, receiver, protection.release());
+    
+        fetch.SetMethod(rest.GetMethod());
+        fetch.SetUrl(rest.GetUrl());
+        fetch.AddHttpHeaders(rest.GetHttpHeaders());
+        fetch.SetTimeout(rest.GetTimeout());
+    
+        if (rest.GetMethod() == Orthanc::HttpMethod_Post ||
+            rest.GetMethod() == Orthanc::HttpMethod_Put)
+        {
+          std::string body = rest.GetBody();
+          fetch.SetBody(body);
+        }
+    
+        fetch.Execute();
+        break;
+      }
+
+      case IOracleCommand::Type_OrthancRestApi:
+      {
+        const OrthancRestApiCommand& rest =
+          dynamic_cast<const OrthancRestApiCommand&>(protection->GetRestCommand());
+        
+        FetchCommand fetch(*this, receiver, protection.release());
+
+        fetch.SetMethod(rest.GetMethod());
+        SetOrthancUrl(fetch, rest.GetUri());
+        fetch.AddHttpHeaders(rest.GetHttpHeaders());
+        fetch.SetTimeout(rest.GetTimeout());
+
+        if (rest.GetMethod() == Orthanc::HttpMethod_Post ||
+            rest.GetMethod() == Orthanc::HttpMethod_Put)
+        {
+          std::string body = rest.GetBody();
+          fetch.SetBody(body);
+        }
+
+        fetch.Execute();
+        break;
+      }
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+  }
+
+
+  bool WebAssemblyOracle::Schedule(boost::shared_ptr<IObserver> receiver,
+                                   IOracleCommand* command)
+  {
+    LOG(TRACE) << "WebAssemblyOracle::Schedule : receiver = "
+               << std::hex << receiver.get();
+    
+    std::unique_ptr<IOracleCommand> protection(command);
+
+    if (command == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+
+    switch (command->GetType())
+    {
+      case IOracleCommand::Type_Http:
+        Execute(receiver, dynamic_cast<HttpCommand*>(protection.release()));
+        break;
+        
+      case IOracleCommand::Type_OrthancRestApi:
+        Execute(receiver, dynamic_cast<OrthancRestApiCommand*>(protection.release()));
+        break;
+        
+      case IOracleCommand::Type_GetOrthancImage:
+        Execute(receiver, dynamic_cast<GetOrthancImageCommand*>(protection.release()));
+        break;
+
+      case IOracleCommand::Type_GetOrthancWebViewerJpeg:
+        Execute(receiver, dynamic_cast<GetOrthancWebViewerJpegCommand*>(protection.release()));
+        break;          
+            
+      case IOracleCommand::Type_Sleep:
+      {
+        unsigned int timeoutMS = dynamic_cast<SleepOracleCommand*>(command)->GetDelay();
+        emscripten_set_timeout(TimeoutContext::Callback, timeoutMS,
+                               new TimeoutContext(*this, receiver, protection.release()));
+        break;
+      }
+            
+      case IOracleCommand::Type_ParseDicomFromWado:
+#if ORTHANC_ENABLE_DCMTK == 1
+        Execute(receiver, dynamic_cast<ParseDicomFromWadoCommand*>(protection.release()));
+#else
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented,
+                                        "DCMTK must be enabled to parse DICOM files");
+#endif
+        break;
+            
+      default:
+        LOG(ERROR) << "Command type not implemented by the WebAssembly Oracle (in Schedule): "
+                   << command->GetType();
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+
+    return true;
+  }
+
+
+  void WebAssemblyOracle::SetDicomCacheSize(size_t size)
+  {
+#if ORTHANC_ENABLE_DCMTK == 1
+    if (size == 0)
+    {
+      dicomCache_.reset();
+    }
+    else
+    {
+      dicomCache_.reset(new ParsedDicomCache(size));
+    }
+#else
+    LOG(INFO) << "DCMTK support is disabled, the DICOM cache is disabled";
+#endif
+  }
+
+  
+  WebAssemblyOracle::CachedInstanceAccessor::CachedInstanceAccessor(WebAssemblyOracle& oracle,
+                                                                    const std::string& sopInstanceUid)
+  {
+#if ORTHANC_ENABLE_DCMTK == 1
+    if (oracle.dicomCache_.get() != NULL)
+    {
+      reader_.reset(new ParsedDicomCache::Reader(*oracle.dicomCache_, BUCKET_SOP, sopInstanceUid));
+    }
+#endif
+  }
+
+  bool WebAssemblyOracle::CachedInstanceAccessor::IsValid() const
+  {
+#if ORTHANC_ENABLE_DCMTK == 1
+    return (reader_.get() != NULL &&
+            reader_->IsValid());
+#else
+    return false;
+#endif
+  }
+
+#if ORTHANC_ENABLE_DCMTK == 1
+  const Orthanc::ParsedDicomFile& WebAssemblyOracle::CachedInstanceAccessor::GetDicom() const
+  {
+    if (IsValid())
+    {
+      assert(reader_.get() != NULL);
+      return reader_->GetDicom();
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+#endif
+
+  size_t WebAssemblyOracle::CachedInstanceAccessor::GetFileSize() const
+  {
+#if ORTHANC_ENABLE_DCMTK == 1
+    if (IsValid())
+    {
+      assert(reader_.get() != NULL);
+      return reader_->GetFileSize();
+    }
+    else
+#endif
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+  bool WebAssemblyOracle::CachedInstanceAccessor::HasPixelData() const
+  {
+#if ORTHANC_ENABLE_DCMTK == 1
+    if (IsValid())
+    {
+      assert(reader_.get() != NULL);
+      return reader_->HasPixelData();
+    }
+    else
+#endif
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyOracle.h	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,156 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../../OrthancStone/Sources/OrthancStone.h"
+
+#if !defined(ORTHANC_ENABLE_WASM)
+#  error The macro ORTHANC_ENABLE_WASM must be defined
+#endif
+
+#if ORTHANC_ENABLE_WASM != 1
+#  error This file can only compiled for WebAssembly
+#endif
+
+#include "../../../OrthancStone/Sources/Messages/IObservable.h"
+#include "../../../OrthancStone/Sources/Messages/IMessageEmitter.h"
+#include "../../../OrthancStone/Sources/Oracle/IOracle.h"
+
+#if ORTHANC_ENABLE_DCMTK == 1
+#  include "../../../OrthancStone/Sources/Toolbox/ParsedDicomCache.h"
+#endif
+
+#include <Compatibility.h>  // For ORTHANC_OVERRIDE
+#include <WebServiceParameters.h>
+
+#include <Enumerations.h>
+
+namespace OrthancStone
+{
+  class GetOrthancImageCommand;
+  class GetOrthancWebViewerJpegCommand;
+  class HttpCommand;
+  class OrthancRestApiCommand;
+  class ParseDicomFromWadoCommand;
+  
+  class WebAssemblyOracle :
+    public IOracle,
+    public IMessageEmitter
+  {
+  private:
+    typedef std::map<std::string, std::string>  HttpHeaders;
+    
+    class TimeoutContext;
+    class FetchContext;
+    class FetchCommand;
+
+    void SetOrthancUrl(FetchCommand& command,
+                       const std::string& uri) const;
+    
+    void Execute(boost::weak_ptr<IObserver> receiver,
+                 HttpCommand* command);    
+    
+    void Execute(boost::weak_ptr<IObserver> receiver,
+                 OrthancRestApiCommand* command);    
+    
+    void Execute(boost::weak_ptr<IObserver> receiver,
+                 GetOrthancImageCommand* command);    
+    
+    void Execute(boost::weak_ptr<IObserver> receiver,
+                 GetOrthancWebViewerJpegCommand* command);
+    
+    void Execute(boost::weak_ptr<IObserver> receiver,
+                 ParseDicomFromWadoCommand* command);
+
+    IObservable                    oracleObservable_;
+    bool                           isLocalOrthanc_;
+    std::string                    localOrthancRoot_;
+    Orthanc::WebServiceParameters  remoteOrthanc_;
+
+#if ORTHANC_ENABLE_DCMTK == 1
+    std::unique_ptr<ParsedDicomCache>  dicomCache_;
+#endif
+
+    void ProcessFetchResult(boost::weak_ptr<IObserver>& receiver,
+                            const std::string& answer,
+                            const HttpHeaders& headers,
+                            const IOracleCommand& command);
+
+  public:
+    WebAssemblyOracle() :
+      isLocalOrthanc_(false)
+    {
+    }
+    
+    virtual void EmitMessage(boost::weak_ptr<IObserver> observer,
+                             const IMessage& message) ORTHANC_OVERRIDE
+    {
+      oracleObservable_.EmitMessage(observer, message);
+    }
+    
+    virtual bool Schedule(boost::shared_ptr<IObserver> receiver,
+                          IOracleCommand* command) ORTHANC_OVERRIDE;
+
+    IObservable& GetOracleObservable()
+    {
+      return oracleObservable_;
+    }
+
+    void SetLocalOrthanc(const std::string& root)
+    {
+      isLocalOrthanc_ = true;
+      localOrthancRoot_ = root;
+    }
+
+    void SetRemoteOrthanc(const Orthanc::WebServiceParameters& orthanc)
+    {
+      isLocalOrthanc_ = false;
+      remoteOrthanc_ = orthanc;
+    }
+
+    void SetDicomCacheSize(size_t size);
+
+    class CachedInstanceAccessor : public boost::noncopyable
+    {
+    private:
+#if ORTHANC_ENABLE_DCMTK == 1
+      std::unique_ptr<ParsedDicomCache::Reader>  reader_;
+#endif
+
+    public:
+      CachedInstanceAccessor(WebAssemblyOracle& oracle,
+                             const std::string& sopInstanceUid);
+
+      bool IsValid() const;
+
+#if ORTHANC_ENABLE_DCMTK == 1
+      const Orthanc::ParsedDicomFile& GetDicom() const;
+#endif
+
+      size_t GetFileSize() const;
+
+      bool HasPixelData() const;
+    };    
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyOracle_Includes.h	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,44 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+/**
+ * This file serves as an indirection to avoid large "#if
+ * ORTHANC_BUILDING_STONE_LIBRARY == 1" in "WebAssemblyOracle.cpp"
+ **/
+
+#include "WebAssemblyOracle.h"
+
+#include "../../../OrthancStone/Sources/Oracle/OracleCommandExceptionMessage.h"
+
+#if ORTHANC_ENABLE_DCMTK == 1
+#  include "../../../OrthancStone/Sources/Oracle/ParseDicomSuccessMessage.h"
+#endif
+
+#include "../../../OrthancStone/Sources/Oracle/GetOrthancImageCommand.h"
+#include "../../../OrthancStone/Sources/Oracle/GetOrthancWebViewerJpegCommand.h"
+#include "../../../OrthancStone/Sources/Oracle/HttpCommand.h"
+#include "../../../OrthancStone/Sources/Oracle/OrthancRestApiCommand.h"
+#include "../../../OrthancStone/Sources/Oracle/ParseDicomFromWadoCommand.h"
+#include "../../../OrthancStone/Sources/Oracle/SleepOracleCommand.h"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyViewport.cpp	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,474 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#if defined(ORTHANC_BUILDING_STONE_LIBRARY) && ORTHANC_BUILDING_STONE_LIBRARY == 1
+#  include "WebAssemblyViewport.h"
+#  include "../../../OrthancStone/Sources/Scene2DViewport/ViewportController.h"
+#  include "../../../OrthancStone/Sources/Toolbox/GenericToolbox.h"
+#  include "../../../OrthancStone/Sources/Viewport/DefaultViewportInteractor.h"
+#else
+// This is the case when using the WebAssembly side module, and this
+// source file must be compiled within the WebAssembly main module
+#  include <Viewport/WebAssemblyViewport.h>
+#  include <Toolbox/GenericToolbox.h>
+#  include <Scene2DViewport/ViewportController.h>
+#  include <Viewport/DefaultViewportInteractor.h>
+#endif
+
+
+#include <OrthancException.h>
+
+#include <boost/make_shared.hpp>
+#include <boost/enable_shared_from_this.hpp>
+#include <boost/math/special_functions/round.hpp>
+
+namespace OrthancStone
+{
+  static void ConvertMouseEvent(PointerEvent& target,
+                                const EmscriptenMouseEvent& source,
+                                const ICompositor& compositor)
+  {
+    int x = static_cast<int>(source.targetX);
+    int y = static_cast<int>(source.targetY);
+
+    switch (source.button)
+    {
+      case 0:
+        target.SetMouseButton(MouseButton_Left);
+        break;
+
+      case 1:
+        target.SetMouseButton(MouseButton_Middle);
+        break;
+
+      case 2:
+        target.SetMouseButton(MouseButton_Right);
+        break;
+
+      default:
+        target.SetMouseButton(MouseButton_None);
+        break;
+    }
+      
+    target.AddPosition(compositor.GetPixelCenterCoordinates(x, y));
+    target.SetAltModifier(source.altKey);
+    target.SetControlModifier(source.ctrlKey);
+    target.SetShiftModifier(source.shiftKey);
+  }
+
+
+  class WebAssemblyViewport::WasmLock : public ILock
+  {
+  private:
+    WebAssemblyViewport& that_;
+
+  public:
+    WasmLock(WebAssemblyViewport& that) :
+      that_(that)
+    {
+    }
+
+    virtual bool HasCompositor() const ORTHANC_OVERRIDE
+    {
+      return that_.compositor_.get() != NULL;
+    }
+
+    virtual ICompositor& GetCompositor() ORTHANC_OVERRIDE
+    {
+      if (that_.compositor_.get() == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return *that_.compositor_;
+      }
+    }
+
+    virtual ViewportController& GetController() ORTHANC_OVERRIDE
+    {
+      assert(that_.controller_);
+      return *that_.controller_;
+    }
+
+    virtual void Invalidate() ORTHANC_OVERRIDE
+    {
+      that_.Invalidate();
+    }
+
+    virtual void RefreshCanvasSize() ORTHANC_OVERRIDE
+    {
+      that_.RefreshCanvasSize();
+    }
+  };
+
+  EM_BOOL WebAssemblyViewport::OnRequestAnimationFrame(double time, void *userData)
+  {
+    LOG(TRACE) << __func__;
+
+    WebAssemblyViewport* that = 
+      WebAssemblyViewport::DereferenceObjectCookie(userData);
+    
+    if (that != NULL)
+    {
+      if (that->compositor_.get() != NULL &&
+          that->controller_ /* should always be true */)
+      {
+        that->Paint(*that->compositor_, *that->controller_);
+      }
+    } 
+    else
+    {
+      LOG(TRACE) << "WebAssemblyViewport::OnRequestAnimationFrame: the " << 
+        "WebAssemblyViewport is deleted and Paint will not be called.";
+    }
+    WebAssemblyViewport::ReleaseObjectCookie(userData);
+    LOG(TRACE) << "Exiting: " << __func__;
+    return true;
+  }
+
+  EM_BOOL WebAssemblyViewport::OnResize(int eventType, const EmscriptenUiEvent *uiEvent, void *userData)
+  {
+    LOG(TRACE) << __func__;
+    WebAssemblyViewport* that = reinterpret_cast<WebAssemblyViewport*>(userData);
+
+    if (that->compositor_.get() != NULL)
+    {
+      that->RefreshCanvasSize();
+      that->Invalidate();
+    }
+      
+    LOG(TRACE) << "Exiting: " << __func__;
+    return true;
+  }
+
+
+  EM_BOOL WebAssemblyViewport::OnMouseDown(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData)
+  {
+    WebAssemblyViewport* that = reinterpret_cast<WebAssemblyViewport*>(userData);
+
+    LOG(TRACE) << "mouse down: " << that->GetCanvasCssSelector();      
+
+    if (that->compositor_.get() != NULL &&
+        that->interactor_.get() != NULL)
+    {
+      PointerEvent pointer;
+      ConvertMouseEvent(pointer, *mouseEvent, *that->compositor_);
+
+      that->controller_->HandleMousePress(*that->interactor_, pointer,
+                                          that->compositor_->GetCanvasWidth(),
+                                          that->compositor_->GetCanvasHeight());        
+      that->Invalidate();
+    }
+
+    LOG(TRACE) << "Exiting: " << __func__;
+    return true;
+  }
+
+    
+  EM_BOOL WebAssemblyViewport::OnMouseMove(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData)
+  {
+    WebAssemblyViewport* that = reinterpret_cast<WebAssemblyViewport*>(userData);
+
+    if (that->compositor_.get() != NULL)
+    {
+      if (that->controller_->HasActiveTracker())
+      {
+        PointerEvent pointer;
+        ConvertMouseEvent(pointer, *mouseEvent, *that->compositor_);
+      
+        if (that->controller_->HandleMouseMove(pointer))
+        {
+          that->Invalidate();
+        }
+      }
+      else if (that->interactor_.get() != NULL &&
+               that->interactor_->HasMouseHover())
+      {
+        // New in Stone Web viewer 2.0
+        PointerEvent pointer;
+        ConvertMouseEvent(pointer, *mouseEvent, *that->compositor_);      
+        that->interactor_->HandleMouseHover(*that, pointer);
+      }
+    }
+
+    LOG(TRACE) << "Exiting: " << __func__;
+    return true;
+  }
+    
+  EM_BOOL WebAssemblyViewport::OnMouseUp(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData)
+  {
+    LOG(TRACE) << __func__;
+    WebAssemblyViewport* that = reinterpret_cast<WebAssemblyViewport*>(userData);
+
+    if (that->compositor_.get() != NULL)
+    {
+      PointerEvent pointer;
+      ConvertMouseEvent(pointer, *mouseEvent, *that->compositor_);
+      that->controller_->HandleMouseRelease(pointer);
+      that->Invalidate();
+    }
+
+    LOG(TRACE) << "Exiting: " << __func__;
+    return true;
+  }
+
+  void* WebAssemblyViewport::CreateObjectCookie()
+  {
+    boost::weak_ptr<WebAssemblyViewport>* weakThisPtr = 
+      new boost::weak_ptr<WebAssemblyViewport>();
+    
+    *weakThisPtr = shared_from_this();
+
+    void* cookie = reinterpret_cast<void*>(weakThisPtr);
+
+    LOG(TRACE) << "WebAssemblyViewport::CreateObjectCookie() => cookie = " 
+      << cookie << "\n";
+
+    return cookie;
+  }
+
+  WebAssemblyViewport* WebAssemblyViewport::DereferenceObjectCookie(void* cookie)
+  {
+    LOG(TRACE) << "WebAssemblyViewport::DereferenceObjectCookie(cookie = " 
+      << cookie << ")\n";
+
+    boost::weak_ptr<WebAssemblyViewport>* weakThisPtr = 
+      reinterpret_cast<boost::weak_ptr<WebAssemblyViewport>*>(cookie);
+
+    boost::shared_ptr<WebAssemblyViewport> sharedThis = weakThisPtr->lock();
+
+    return sharedThis.get();
+  }
+
+  void WebAssemblyViewport::ReleaseObjectCookie(void* cookie)
+  {
+    LOG(TRACE) << "WebAssemblyViewport::ReleaseObjectCookie(cookie = " 
+      << cookie << ")\n";
+
+    boost::weak_ptr<WebAssemblyViewport>* weakThisPtr = 
+      reinterpret_cast<boost::weak_ptr<WebAssemblyViewport>*>(cookie);
+    
+    delete weakThisPtr;
+  }
+
+  void WebAssemblyViewport::Invalidate()
+  {
+    LOG(TRACE) << "WebAssemblyViewport::Invalidate()\n";
+    long id = emscripten_request_animation_frame(OnRequestAnimationFrame, 
+                                                 CreateObjectCookie());
+    //animationFrameCallbackIds_.push_back(id);
+  }
+
+  void WebAssemblyViewport::FitForPrint()
+  {
+    if (compositor_.get() != NULL &&
+        controller_ /* should always be true */)
+    {
+      RefreshCanvasSize();
+      compositor_->FitContent(controller_->GetScene());
+
+      void* cookie = CreateObjectCookie();
+      OnRequestAnimationFrame(0, cookie);  // Mandatory to work with Firefox
+    }
+  }
+
+  void WebAssemblyViewport::AcquireCompositor(ICompositor* compositor /* takes ownership */)
+  {
+    if (compositor == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+    else
+    {
+      compositor_.reset(compositor);
+    }
+  }
+
+#if DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR == 1
+// everything OK..... we're using the new setting
+#else
+#pragma message("WARNING: DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR is not defined or equal to 0. Stone will use the OLD Emscripten rules for DOM element selection.")
+#endif
+
+  WebAssemblyViewport::WebAssemblyViewport(
+    const std::string& canvasId, bool enableEmscriptenMouseEvents) :
+    canvasId_(canvasId),
+#if DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR == 1
+    canvasCssSelector_("#" + canvasId),
+#else
+    canvasCssSelector_(canvasId),
+#endif
+    interactor_(new DefaultViewportInteractor),
+    enableEmscriptenMouseEvents_(enableEmscriptenMouseEvents),
+    canvasWidth_(0),
+    canvasHeight_(0)
+  {
+  }
+
+  void WebAssemblyViewport::PostConstructor()
+  {
+    boost::shared_ptr<IViewport> viewport = shared_from_this();
+    controller_.reset(new ViewportController(viewport));
+
+    LOG(INFO) << "Initializing Stone viewport on HTML canvas: " 
+              << canvasId_;
+
+    if (canvasId_.empty() ||
+        canvasId_[0] == '#')
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
+        "The canvas identifier must not start with '#'");
+    }
+
+    // Disable right-click on the canvas (i.e. context menu)
+    EM_ASM({
+        document.getElementById(UTF8ToString($0)).oncontextmenu = 
+        function(event)
+        {
+          event.preventDefault();
+        }
+      },
+      canvasId_.c_str()   // $0
+      );
+
+    // It is not possible to monitor the resizing of individual
+    // canvas, so we track the full window of the browser
+    emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW,
+                                   reinterpret_cast<void*>(this),
+                                   false,
+                                   OnResize);
+
+    if (enableEmscriptenMouseEvents_)
+    {
+
+      // if any of this function causes an error in the console, please
+      // make sure you are using the new (as of 1.39.x) version of 
+      // emscripten element lookup rules( pass 
+      // "-s DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1" to the linker.
+
+      emscripten_set_mousedown_callback(canvasCssSelector_.c_str(),
+                                        reinterpret_cast<void*>(this),
+                                        false,
+                                        OnMouseDown);
+
+      emscripten_set_mousemove_callback(canvasCssSelector_.c_str(),
+                                        reinterpret_cast<void*>(this),
+                                        false,
+                                        OnMouseMove);
+
+      emscripten_set_mouseup_callback(canvasCssSelector_.c_str(),
+                                      reinterpret_cast<void*>(this),
+                                      false,
+                                      OnMouseUp);
+    }
+  }
+
+  WebAssemblyViewport::~WebAssemblyViewport()
+  {
+    LOG(TRACE) << "WebAssemblyViewport::~WebAssemblyViewport()\n";
+    
+    emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW,
+                                   reinterpret_cast<void*>(this),
+                                   false,
+                                   NULL);
+
+    if (enableEmscriptenMouseEvents_)
+    {
+
+      emscripten_set_mousedown_callback(canvasCssSelector_.c_str(),
+                                        reinterpret_cast<void*>(this),
+                                        false,
+                                        NULL);
+
+      emscripten_set_mousemove_callback(canvasCssSelector_.c_str(),
+                                        reinterpret_cast<void*>(this),
+                                        false,
+                                        NULL);
+
+      emscripten_set_mouseup_callback(canvasCssSelector_.c_str(),
+                                      reinterpret_cast<void*>(this),
+                                      false,
+                                      NULL);
+    }
+  }
+  
+  IViewport::ILock* WebAssemblyViewport::Lock()
+  {
+    return new WasmLock(*this);
+  }
+  
+  void WebAssemblyViewport::AcquireInteractor(IViewportInteractor* interactor)
+  {
+    if (interactor == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+    else
+    {
+      interactor_.reset(interactor);
+    }
+  }
+
+
+  void WebAssemblyViewport::RefreshCanvasSize()
+  {
+    double w = -1, h = -1;
+    EMSCRIPTEN_RESULT result = 
+      emscripten_get_element_css_size(GetCanvasCssSelector().c_str(), &w, &h);
+
+    if (result != EMSCRIPTEN_RESULT_SUCCESS)
+    {
+      LOG(WARNING) << "WebAssemblyViewport::RefreshCanvasSize failed to "
+        << "retrieve CSS size for " << GetCanvasCssSelector();
+    }
+
+    /**
+     * Emscripten has the function emscripten_get_element_css_size()
+     * to query the width and height of a named HTML element. I'm
+     * calling this first to get the initial size of the canvas DOM
+     * element, and then call emscripten_set_canvas_size() to
+     * initialize the framebuffer size of the canvas to the same
+     * size as its DOM element.
+     * https://floooh.github.io/2017/02/22/emsc-html.html
+     **/
+    if (w > 0 &&
+        h > 0)
+    {
+      canvasWidth_ = static_cast<unsigned int>(boost::math::iround(w));
+      canvasHeight_ = static_cast<unsigned int>(boost::math::iround(h));
+    }
+    else
+    {
+      canvasWidth_ = 0;
+      canvasHeight_ = 0;
+    }
+
+    emscripten_set_canvas_element_size(GetCanvasCssSelector().c_str(), canvasWidth_, canvasHeight_);
+
+    if (compositor_.get() != NULL)
+    {
+      compositor_->SetCanvasSize(canvasWidth_, canvasHeight_);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyViewport.h	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,181 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../../OrthancStone/Sources/OrthancStone.h"
+
+#if !defined(ORTHANC_ENABLE_WASM)
+#  error Macro ORTHANC_ENABLE_WASM must be defined
+#endif
+
+#if ORTHANC_ENABLE_WASM != 1
+#  error This file can only be used if targeting WebAssembly
+#endif
+
+#include "../../../OrthancStone/Sources/Viewport/IViewport.h"
+#include "../../../OrthancStone/Sources/Viewport/IViewportInteractor.h"
+
+#include <Compatibility.h>
+
+#include <emscripten.h>
+#include <emscripten/html5.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+#include <boost/enable_shared_from_this.hpp>
+
+namespace OrthancStone
+{
+  class WebAssemblyViewport : public IViewport,
+                              public boost::enable_shared_from_this<WebAssemblyViewport>
+
+  {
+  private:
+    class WasmLock;
+    
+    std::string                           canvasId_;
+    std::string                           canvasCssSelector_;
+    std::unique_ptr<ICompositor>          compositor_;
+    std::unique_ptr<ViewportController>   controller_;
+    std::unique_ptr<IViewportInteractor>  interactor_;
+    bool                                  enableEmscriptenMouseEvents_;
+    unsigned int                          canvasWidth_;
+    unsigned int                          canvasHeight_;
+
+    static EM_BOOL OnRequestAnimationFrame(double time, void *userData);
+    
+    static EM_BOOL OnResize(int eventType, const EmscriptenUiEvent *uiEvent, void *userData);
+
+    static EM_BOOL OnMouseDown(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData);
+    
+    static EM_BOOL OnMouseMove(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData);
+    
+    static EM_BOOL OnMouseUp(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData);
+
+  protected:
+    void Invalidate();
+    
+    void ClearCompositor()
+    {
+      compositor_.reset();
+    }
+
+    bool HasCompositor() const
+    {
+      return compositor_.get() != NULL;
+    }
+
+    void AcquireCompositor(ICompositor* compositor /* takes ownership */);
+
+    virtual void Paint(ICompositor& compositor,
+                       ViewportController& controller) = 0;
+
+    /**
+    The second argument is temporary and should be deleted once the migration 
+    to interactors is finished. It should be set to "true" for new applications.
+    */
+    WebAssemblyViewport(const std::string& canvasId, 
+                        bool enableEmscriptenMouseEvents);
+
+    void PostConstructor();
+
+
+    /**
+     * This method can be called to retrieve a cookie that can be passed to 
+     * C-style callbacks that expect the object to be passed as a void*
+     * 
+     * This cookie is a resource and must be freed when it is guaranteed 
+     * not to be used anymore, with ReleaseObjectCookie
+     */
+    void* CreateObjectCookie();
+
+    /**
+     * This static method can be used to dereference a cookie (i.e. retrieve 
+     * a pointer to the underlying object) that has been created with 
+     * WebAssemblyViewport::CreateObjectCookie()
+     * 
+     * If this method returns NULL, it basically means that the 
+     * WebAssemblyViewport has already been deleted and that you should NOT 
+     * attempt to use it!
+     * 
+     * This method never releases the cookie (for other in-flight callbacks
+     * could possibly require to cookie to be valid)
+     * 
+     * If this method is called AFTER ReleaseObjectCookie has been called on
+     * the same cookie, the behavior is undefined and things will CERTAINLY
+     * go wrong.
+     * 
+     * NEVER call this method on a void* that has not been generated by the
+     * CreateObjectCookie method of this class
+     */
+    static WebAssemblyViewport* DereferenceObjectCookie(void* cookie);
+
+    /**
+     * This method must be used when the object cookie will not be used anymore.
+     * 
+     * You must call it when you are certain that no entity will attempt to 
+     * dereference the cookie.
+     */
+    static void ReleaseObjectCookie(void* cookie);
+
+    void RefreshCanvasSize();
+
+    unsigned int GetCanvasWidth() const
+    {
+      return canvasWidth_;
+    }
+    
+    unsigned int GetCanvasHeight()
+    {
+      return canvasHeight_;
+    }
+
+  public:
+    virtual ILock* Lock() ORTHANC_OVERRIDE;
+
+    ~WebAssemblyViewport();
+
+    /**
+    This method takes ownership
+    */
+    void AcquireInteractor(IViewportInteractor* interactor);
+
+    const std::string& GetCanvasId() const
+    {
+      return canvasId_;
+    }
+
+    /**
+    emscripten functions requires the css selector for the canvas. This is 
+    different from the canvas id (the syntax is '#mycanvasid')
+    */
+    const std::string& GetCanvasCssSelector() const
+    {
+      return canvasCssSelector_;
+    }
+
+    void FitForPrint();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/WebAssembly/WebGLViewport.cpp	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,88 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "WebGLViewport.h"
+
+#include "../../../OrthancStone/Sources/StoneException.h"
+#include "../../../OrthancStone/Sources/Scene2D/OpenGLCompositor.h"
+#include "../../../OrthancStone/Sources/Scene2DViewport/ViewportController.h"
+
+namespace OrthancStone
+{
+  void WebGLViewport::Paint(ICompositor& compositor,
+                            ViewportController& controller)
+  {
+    try
+    {
+      compositor.Refresh(controller.GetScene());
+
+      /**
+       * No need to manually swap the buffer: "Rendered WebGL content
+       * is implicitly presented (displayed to the user) on the canvas
+       * when the event handler that renders with WebGL returns back
+       * to the browser event loop."
+       * https://emscripten.org/docs/api_reference/html5.h.html#webgl-context
+       *
+       * Could call "emscripten_webgl_commit_frame()" if
+       * "explicitSwapControl" option were set to "true".
+       **/
+    }
+    catch (const StoneException& e)
+    {
+      // Ignore problems about the loss of the WebGL context (edge case)
+      if (e.GetErrorCode() == ErrorCode_WebGLContextLost)
+      {
+        return;
+      }
+      else
+      {
+        throw;
+      }
+    }
+  }
+    
+
+  WebGLViewport::WebGLViewport(const std::string& canvasId, bool enableEmscriptenMouseEvents) :
+    WebAssemblyViewport(canvasId,enableEmscriptenMouseEvents),
+    context_(GetCanvasCssSelector())
+  {
+    AcquireCompositor(new OpenGLCompositor(context_));
+  }
+
+  boost::shared_ptr<WebGLViewport> WebGLViewport::Create(
+    const std::string& canvasId, bool enableEmscriptenMouseEvents)
+  {
+    boost::shared_ptr<WebGLViewport> that = boost::shared_ptr<WebGLViewport>(
+        new WebGLViewport(canvasId, enableEmscriptenMouseEvents));
+    
+    that->WebAssemblyViewport::PostConstructor();
+    return that;
+  }
+
+  WebGLViewport::~WebGLViewport()
+  {
+    // Make sure to delete the compositor before its parent "context_" gets
+    // deleted
+    ClearCompositor();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/WebAssembly/WebGLViewport.h	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,54 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "WebAssemblyOpenGLContext.h"
+#include "WebAssemblyViewport.h"
+
+namespace OrthancStone
+{
+  class WebGLViewport : public WebAssemblyViewport
+  {
+  private:
+    OpenGL::WebAssemblyOpenGLContext  context_;
+    
+    WebGLViewport(const std::string& canvasId,
+                  bool enableEmscriptenMouseEvents);
+
+  protected:
+    virtual void Paint(ICompositor& compositor,
+                       ViewportController& controller) ORTHANC_OVERRIDE;
+    
+  public:
+    static boost::shared_ptr<WebGLViewport> Create(const std::string& canvasId,
+                                                   bool enableEmscriptenMouseEvents = true);
+
+    virtual ~WebGLViewport();
+
+    bool IsContextLost()
+    {
+      return context_.IsContextLost();
+    } 
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/WebAssembly/WebGLViewportsRegistry.cpp	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,223 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "WebGLViewportsRegistry.h"
+
+#include "../../../OrthancStone/Sources/Scene2DViewport/ViewportController.h"
+#include "../../../OrthancStone/Sources/Toolbox/GenericToolbox.h"
+
+#include <OrthancException.h>
+
+#include <boost/make_shared.hpp>
+
+
+static double viewportsTimeout_ = 1000;
+static std::unique_ptr<OrthancStone::WebGLViewportsRegistry>  globalRegistry_;
+
+
+namespace OrthancStone
+{
+  void WebGLViewportsRegistry::LaunchTimer()
+  {
+    timeOutID_ = emscripten_set_timeout(
+      OnTimeoutCallback, 
+      timeoutMS_, 
+      reinterpret_cast<void*>(this));
+  }
+  
+  void WebGLViewportsRegistry::OnTimeout()
+  {
+    for (Viewports::iterator it = viewports_.begin();
+         it != viewports_.end(); 
+         ++it)
+    {
+      if (it->second == NULL ||
+          it->second->IsContextLost())
+      {
+        LOG(INFO) << "WebGL context lost for canvas: " << it->first;
+
+        // Try and duplicate the HTML5 canvas in the DOM
+        EM_ASM({
+            var canvas = document.getElementById(UTF8ToString($0));
+            if (canvas) {
+              var parent = canvas.parentElement;
+              if (parent) {
+                var cloned = canvas.cloneNode(true /* deep copy */);
+                parent.insertBefore(cloned, canvas);
+                parent.removeChild(canvas);
+              }
+            }
+          },
+          it->first.c_str()  // $0 = ID of the canvas
+          );
+
+        // At this point, the old canvas is removed from the DOM and
+        // replaced by a fresh one with the same ID: Recreate the
+        // WebGL context on the new canvas
+        boost::shared_ptr<WebGLViewport> viewport;
+
+        // we need to steal the properties from the old viewport
+        // and set them to the new viewport
+        {
+          std::unique_ptr<IViewport::ILock> lock(it->second->Lock());
+
+          // TODO: remove ViewportController
+          Scene2D* scene = lock->GetController().ReleaseScene();
+          viewport = WebGLViewport::Create(it->first);
+
+          {
+            std::unique_ptr<IViewport::ILock> newLock(viewport->Lock());
+            newLock->GetController().AcquireScene(scene);
+          }
+        }
+
+        // Replace the old WebGL viewport by the new one
+        it->second = viewport;
+
+        // Tag the fresh canvas as needing a repaint
+        {
+          std::unique_ptr<IViewport::ILock> lock(it->second->Lock());
+          lock->Invalidate();
+        }
+      }
+    }
+      
+    LaunchTimer();
+  }
+
+  void WebGLViewportsRegistry::OnTimeoutCallback(void *userData)
+  {
+    // This object dies with the process or tab. 
+    WebGLViewportsRegistry* that = 
+      reinterpret_cast<WebGLViewportsRegistry*>(userData);
+    that->OnTimeout();
+  }
+
+  WebGLViewportsRegistry::WebGLViewportsRegistry(double timeoutMS) :
+    timeoutMS_(timeoutMS),
+    timeOutID_(0)
+  {
+    if (timeoutMS <= 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }          
+    
+    LaunchTimer();
+  }
+
+  WebGLViewportsRegistry::~WebGLViewportsRegistry()
+  {
+    emscripten_clear_timeout(timeOutID_);
+    Clear();
+  }
+
+  boost::shared_ptr<WebGLViewport> WebGLViewportsRegistry::Add(
+    const std::string& canvasId)
+  {
+    if (viewports_.find(canvasId) != viewports_.end())
+    {
+      LOG(ERROR) << "Canvas was already registered: " << canvasId;
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      boost::shared_ptr<WebGLViewport> viewport = 
+        WebGLViewport::Create(canvasId);
+      viewports_[canvasId] = viewport;
+      return viewport;
+    }
+  }
+      
+  void WebGLViewportsRegistry::Remove(const std::string& canvasId)
+  {
+    Viewports::iterator found = viewports_.find(canvasId);
+
+    if (found == viewports_.end())
+    {
+      LOG(ERROR) << "Cannot remove unregistered canvas: " << canvasId;
+    }
+    else
+    {
+      viewports_.erase(found);
+    }
+  }
+  
+  void WebGLViewportsRegistry::Clear()
+  {
+    viewports_.clear();
+  }
+
+  WebGLViewportsRegistry::Accessor::Accessor(WebGLViewportsRegistry& that,
+                                             const std::string& canvasId) :
+    that_(that)
+  {
+    Viewports::iterator viewport = that.viewports_.find(canvasId);
+    if (viewport != that.viewports_.end() &&
+        viewport->second != NULL)
+    {
+      lock_.reset(viewport->second->Lock());
+    }
+  }
+
+  IViewport::ILock& WebGLViewportsRegistry::Accessor::GetViewport() const
+  {
+    if (IsValid())
+    {
+      return *lock_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void WebGLViewportsRegistry::FinalizeGlobalRegistry()
+  {
+    globalRegistry_.reset();
+  }
+
+
+  void WebGLViewportsRegistry::SetGlobalRegistryTimeout(double timeout)
+  {
+    if (globalRegistry_.get())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      viewportsTimeout_ = timeout;
+    }
+  }
+
+
+  WebGLViewportsRegistry& WebGLViewportsRegistry::GetGlobalRegistry()
+  {
+    if (globalRegistry_.get() == NULL)
+    {
+      globalRegistry_.reset(new WebGLViewportsRegistry(viewportsTimeout_));
+    }
+
+    return *globalRegistry_;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/Platforms/WebAssembly/WebGLViewportsRegistry.h	Sat Jan 29 12:47:32 2022 +0100
@@ -0,0 +1,93 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "WebGLViewport.h"
+
+#include <boost/enable_shared_from_this.hpp>
+
+namespace OrthancStone
+{
+  /**
+   * This singleton class must be used if many WebGL viewports are
+   * created by the higher-level application, implying possible loss
+   * of WebGL contexts. The object will run an infinite update loop
+   * that checks whether all the WebGL context are still valid (not
+   * lost). If some WebGL context is lost, it is automatically
+   * reinitialized by created a fresh HTML5 canvas.
+   **/  
+  class WebGLViewportsRegistry : public boost::noncopyable,
+    public boost::enable_shared_from_this<WebGLViewportsRegistry>
+  {
+  private:
+    typedef std::map<std::string, boost::shared_ptr<WebGLViewport> >  Viewports;
+
+    double     timeoutMS_;
+    Viewports  viewports_;
+    long       timeOutID_;
+
+    void LaunchTimer();
+
+    void OnTimeout();
+
+    static void OnTimeoutCallback(void *userData);
+    
+  public:
+    explicit WebGLViewportsRegistry(double timeoutMS /* in milliseconds */);
+    
+    ~WebGLViewportsRegistry();
+
+    boost::shared_ptr<WebGLViewport> Add(const std::string& canvasId);
+
+    void Remove(const std::string& canvasId);
+
+    void Clear();
+
+    class Accessor : public boost::noncopyable
+    {
+    private:
+      WebGLViewportsRegistry&            that_;
+      std::unique_ptr<IViewport::ILock>  lock_;
+
+    public:
+      Accessor(WebGLViewportsRegistry& that,
+               const std::string& canvasId);
+
+      bool IsValid() const
+      {
+        return lock_.get() != NULL;
+      }
+
+      IViewport::ILock& GetViewport() const;
+    };
+
+
+
+    static void FinalizeGlobalRegistry();
+  
+    static void SetGlobalRegistryTimeout(double timeout);
+
+    static WebGLViewportsRegistry& GetGlobalRegistry();
+  };
+}