changeset 673:3f13f7f1b55d am-dev

merge default -> am-dev
author Alain Mazy <alain@mazy.be>
date Thu, 16 May 2019 09:11:14 +0200
parents 9e3bb8b4f726 (current diff) 86930bc676c6 (diff)
children 163ac23b8bff
files
diffstat 222 files changed, 17782 insertions(+), 1949 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Tue May 14 18:24:12 2019 +0200
+++ b/.hgignore	Thu May 16 09:11:14 2019 +0200
@@ -1,21 +1,22 @@
-CMakeLists.txt.user
-Platforms/Generic/ThirdPartyDownloads/
-Applications/build-*
+syntax: glob
+*~
+*.cpp.orig
+*.h.orig
+.vs/
+.vscode/
 Applications/Qt/archive/
 Applications/Samples/ThirdPartyDownloads/
+Applications/Samples/build-wasm/
+Applications/Samples/build-web/
+Applications/Samples/node_modules/
+Applications/Samples/rt-viewer-demo/ThirdPartyDownloads/
 Applications/Samples/rt-viewer-demo/build-sdl-msvc15/
 Applications/Samples/rt-viewer-demo/build-tsc-output/
 Applications/Samples/rt-viewer-demo/build-wasm/
 Applications/Samples/rt-viewer-demo/build-web/
-Applications/Samples/rt-viewer-demo/ThirdPartyDownloads/
-Applications/Samples/build-wasm/
-Applications/Samples/build-web/
-Applications/Samples/node_modules/
-Resources/CommandTool/protoc-tests/node_modules/
-Resources/CommandTool/protoc-tests/generated_js/
-Resources/CommandTool/protoc-tests/generated_ts/
-Resources/CommandTool/flatc-tests/basic/build/
-.vscode/
+Applications/build-*
+CMakeLists.txt.user
+Platforms/Generic/ThirdPartyDownloads/
 Resources/CodeGeneration/.env
 Resources/CodeGeneration/.idea
 Resources/CodeGeneration/__pycache__
@@ -23,8 +24,13 @@
 Resources/CodeGeneration/build_browser/
 Resources/CodeGeneration/testCppHandler/build/
 Resources/CodeGeneration/testCppHandler/build_msbuild/
-syntax: glob
-Resources/CodeGeneration/testWasmIntegrated/build-wasm/
+Resources/CodeGeneration/testWasmIntegrated/build-final/
 Resources/CodeGeneration/testWasmIntegrated/build-tsc/
-Resources/CodeGeneration/testWasmIntegrated/build-final/
+Resources/CodeGeneration/testWasmIntegrated/build-wasm/
+Resources/CommandTool/flatc-tests/basic/build/
+Resources/CommandTool/protoc-tests/generated_js/
+Resources/CommandTool/protoc-tests/generated_ts/
+Resources/CommandTool/protoc-tests/node_modules/
+Samples/Sdl/ThirdPartyDownloads/
+Samples/Sdl/CMakeLists.txt.orig
 
--- a/Applications/Generic/NativeStoneApplicationRunner.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Applications/Generic/NativeStoneApplicationRunner.cpp	Thu May 16 09:11:14 2019 +0200
@@ -81,11 +81,13 @@
       generic.add_options()
           ("help", "Display this help and exit")
           ("verbose", "Be verbose in logs")
-          ("orthanc", boost::program_options::value<std::string>()->default_value("http://localhost:8042/"),
+          ("orthanc", boost::program_options::value<std::string>()->
+            default_value("http://localhost:8042/"),
            "URL to the Orthanc server")
           ("username", "Username for the Orthanc server")
           ("password", "Password for the Orthanc server")
-          ("https-verify", boost::program_options::value<bool>()->default_value(true), "Check HTTPS certificates")
+          ("https-verify", boost::program_options::value<bool>()->
+            default_value(true), "Check HTTPS certificates")
           ;
 
       options.add(generic);
@@ -102,13 +104,15 @@
 
     try
     {
-      boost::program_options::store(boost::program_options::command_line_parser(argc, argv).
-                                    options(options).allow_unregistered().run(), parameters);
+      boost::program_options::store(
+        boost::program_options::command_line_parser(argc, argv).
+          options(options).allow_unregistered().run(), parameters);
       boost::program_options::notify(parameters);
     }
     catch (boost::program_options::error& e)
     {
-      LOG(ERROR) << "Error while parsing the command-line arguments: " << e.what();
+      LOG(ERROR) << 
+        "Error while parsing the command-line arguments: " << e.what();
       error = true;
     }
 
@@ -119,13 +123,7 @@
 
     if (error || parameters.count("help"))
     {
-      std::cout << std::endl
-                << "Usage: " << argv[0] << " [OPTION]..."
-                << std::endl
-                << "Orthanc, lightweight, RESTful DICOM server for healthcare and medical research."
-                << std::endl << std::endl
-                << "Demonstration application of Orthanc Stone in native environment."
-                << std::endl;
+      std::cout << std::endl;
 
       std::cout << options << "\n";
       return error ? -1 : 0;
@@ -138,21 +136,24 @@
       Orthanc::HttpClient::ConfigureSsl(false, "");
     }
 
+    LOG(ERROR) << "???????? if (parameters.count(\"verbose\"))";
     if (parameters.count("verbose"))
     {
+      LOG(ERROR) << "parameters.count(\"verbose\") != 0";
       Orthanc::Logging::EnableInfoLevel(true);
       LOG(INFO) << "Verbose logs are enabled";
     }
 
+    LOG(ERROR) << "???????? if (parameters.count(\"trace\"))";
     if (parameters.count("trace"))
     {
+      LOG(ERROR) << "parameters.count(\"trace\") != 0";
       Orthanc::Logging::EnableTraceLevel(true);
       VLOG(1) << "Trace logs are enabled";
     }
 
     ParseCommandLineOptions(parameters);
 
-
     bool success = true;
     try
     {
@@ -169,17 +170,20 @@
 
       if (parameters.count("username") && parameters.count("password"))
       {
-        webServiceParameters.SetCredentials(parameters["username"].as<std::string>(),
-            parameters["password"].as<std::string>());
+        webServiceParameters.SetCredentials(parameters["username"].
+          as<std::string>(),
+          parameters["password"].as<std::string>());
       }
 
-      LOG(WARNING) << "URL to the Orthanc REST API: " << webServiceParameters.GetUrl();
+      LOG(WARNING) << "URL to the Orthanc REST API: " << 
+        webServiceParameters.GetUrl();
 
       {
         OrthancPlugins::OrthancHttpConnection orthanc(webServiceParameters);
         if (!MessagingToolbox::CheckOrthancVersion(orthanc))
         {
-          LOG(ERROR) << "Your version of Orthanc is incompatible with Stone of Orthanc, please upgrade";
+          LOG(ERROR) << "Your version of Orthanc is incompatible with Stone of "
+            << "Orthanc, please upgrade";
           throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
         }
       }
@@ -196,11 +200,15 @@
       NativeStoneApplicationContext context(broker_);
 
       {
-        Oracle oracle(6); // use multiple threads to execute asynchronous tasks like download content
+        // use multiple threads to execute asynchronous tasks like 
+        // download content
+        Oracle oracle(6); 
         oracle.Start();
 
         {
-          OracleWebService webService(broker_, oracle, webServiceParameters, context);
+          OracleWebService webService(
+            broker_, oracle, webServiceParameters, context);
+          
           context.SetWebService(webService);
           context.SetOrthancBaseUrl(webServiceParameters.GetUrl());
 
@@ -255,5 +263,4 @@
 
     return (success ? 0 : -1);
   }
-
 }
--- a/Applications/Samples/CMakeLists.txt	Tue May 14 18:24:12 2019 +0200
+++ b/Applications/Samples/CMakeLists.txt	Thu May 16 09:11:14 2019 +0200
@@ -216,7 +216,6 @@
       ${SIMPLE_VIEWER_APPLICATION_SOURCES}
       )
     target_link_libraries(OrthancStoneSimpleViewer OrthancStone)
-
 endif()
 
 #####################################################################
--- a/Applications/Samples/SampleMainNative.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Applications/Samples/SampleMainNative.cpp	Thu May 16 09:11:14 2019 +0200
@@ -42,3 +42,4 @@
   return qtAppRunner.Execute(argc, argv);
 #endif
 }
+
--- a/Applications/Samples/SingleFrameApplication.h	Tue May 14 18:24:12 2019 +0200
+++ b/Applications/Samples/SingleFrameApplication.h	Thu May 16 09:11:14 2019 +0200
@@ -136,9 +136,9 @@
             slice = 0;
           }
 
-          if (slice >= static_cast<int>(source_->GetSliceCount()))
+          if (slice >= static_cast<int>(source_->GetSlicesCount()))
           {
-            slice = static_cast<int>(source_->GetSliceCount()) - 1;
+            slice = static_cast<int>(source_->GetSlicesCount()) - 1;
           }
 
           if (slice != static_cast<int>(slice_)) 
@@ -158,7 +158,7 @@
       void SetSlice(size_t index)
       {
         if (source_ != NULL &&
-            index < source_->GetSliceCount())
+            index < source_->GetSlicesCount())
         {
           slice_ = static_cast<unsigned int>(index);
           
@@ -191,7 +191,7 @@
         // slice
         if (source_ == &message.GetOrigin())
         {
-          SetSlice(source_->GetSliceCount() / 2);
+          SetSlice(source_->GetSlicesCount() / 2);
         }
 
         GetMainWidget().FitContent();
--- a/Applications/Samples/StoneSampleCommands_generated.hpp	Tue May 14 18:24:12 2019 +0200
+++ b/Applications/Samples/StoneSampleCommands_generated.hpp	Thu May 16 09:11:14 2019 +0200
@@ -1,703 +1,703 @@
-/*
-         1         2         3         4         5         6         7
-12345678901234567890123456789012345678901234567890123456789012345678901234567890
-
-Generated on 2019-03-18 12:07:42.696093 by stonegentool
-
-*/
-#pragma once
-
-#include <exception>
-#include <iostream>
-#include <string>
-#include <sstream>
-#include <assert.h>
-#include <memory>
-#include <json/json.h>
-
-//#define STONEGEN_NO_CPP11 1
-
-#ifdef STONEGEN_NO_CPP11
-#define StoneSmartPtr std::auto_ptr
-#else 
-#define StoneSmartPtr std::unique_ptr
-#endif 
-
-namespace StoneSampleCommands
-{
-  /** Throws in case of problem */
-  inline void _StoneDeserializeValue(int32_t& destValue, const Json::Value& jsonValue)
-  {
-    destValue = jsonValue.asInt();
-  }
-
-  inline Json::Value _StoneSerializeValue(int32_t value)
-  {
-    Json::Value result(value);
-    return result;
-  }
-
-  inline void _StoneDeserializeValue(Json::Value& destValue, const Json::Value& jsonValue)
-  {
-    destValue = jsonValue;
-  }
-
-  inline Json::Value _StoneSerializeValue(Json::Value value)
-  {
-    return value;
-  }
-
-  /** Throws in case of problem */
-  inline void _StoneDeserializeValue(double& destValue, const Json::Value& jsonValue)
-  {
-    destValue = jsonValue.asDouble();
-  }
-
-  inline Json::Value _StoneSerializeValue(double value)
-  {
-    Json::Value result(value);
-    return result;
-  }
-
-  /** Throws in case of problem */
-  inline void _StoneDeserializeValue(bool& destValue, const Json::Value& jsonValue)
-  {
-    destValue = jsonValue.asBool();
-  }
-
-  inline Json::Value _StoneSerializeValue(bool value)
-  {
-    Json::Value result(value);
-    return result;
-  }
-
-  /** Throws in case of problem */
-  inline void _StoneDeserializeValue(
-       std::string& destValue
-     , const Json::Value& jsonValue)
-  {
-    destValue = jsonValue.asString();
-  }
-
-  inline Json::Value _StoneSerializeValue(const std::string& value)
-  {
-    // the following is better than 
-    Json::Value result(value.data(),value.data()+value.size());
-    return result;
-  }
-
-  inline std::string MakeIndent(size_t indent)
-  {
-    char* txt = reinterpret_cast<char*>(malloc(indent+1)); // NO EXCEPTION BELOW!!!!!!!!!!!!
-    for(size_t i = 0; i < indent; ++i)
-      txt[i] = ' ';
-    txt[indent] = 0;
-    std::string retVal(txt);
-    free(txt); // NO EXCEPTION ABOVE !!!!!!!!!!
-    return retVal;
-  }
-
-  // generic dumper
-  template<typename T>
-  std::ostream& StoneDumpValue(std::ostream& out, const T& value, size_t indent)
-  {
-    out << MakeIndent(indent) << value;
-    return out;
-  }
-
-  // string dumper
-  inline std::ostream& StoneDumpValue(std::ostream& out, const std::string& value, size_t indent)
-  {
-    out << MakeIndent(indent) << "\"" << value  << "\"";
-    return out;
-  }
-
-  /** Throws in case of problem */
-  template<typename T>
-  void _StoneDeserializeValue(
-    std::map<std::string, T>& destValue, const Json::Value& jsonValue)
-  {
-    destValue.clear();
-    for (
-      Json::Value::const_iterator itr = jsonValue.begin();
-      itr != jsonValue.end();
-      itr++)
-    {
-      std::string key;
-      _StoneDeserializeValue(key, itr.key());
-
-      T innerDestValue;
-      _StoneDeserializeValue(innerDestValue, *itr);
-
-      destValue[key] = innerDestValue;
-    }
-  }
-
-  template<typename T>
-  Json::Value _StoneSerializeValue(const std::map<std::string,T>& value)
-  {
-    Json::Value result(Json::objectValue);
-
-    for (typename std::map<std::string, T>::const_iterator it = value.cbegin();
-      it != value.cend(); ++it)
-    {
-      // it->first it->second
-      result[it->first] = _StoneSerializeValue(it->second);
-    }
-    return result;
-  }
-
-  template<typename T>
-  std::ostream& StoneDumpValue(std::ostream& out, const std::map<std::string,T>& value, size_t indent)
-  {
-    out << MakeIndent(indent) << "{\n";
-    for (typename std::map<std::string, T>::const_iterator it = value.cbegin();
-      it != value.cend(); ++it)
-    {
-      out << MakeIndent(indent+2) << "\"" << it->first << "\" : ";
-      StoneDumpValue(out, it->second, indent+2);
-    }
-    out << MakeIndent(indent) << "}\n";
-    return out;
-  }
-
-  /** Throws in case of problem */
-  template<typename T>
-  void _StoneDeserializeValue(
-    std::vector<T>& destValue, const Json::Value& jsonValue)
-  {
-    destValue.clear();
-    destValue.reserve(jsonValue.size());
-    for (Json::Value::ArrayIndex i = 0; i != jsonValue.size(); i++)
-    {
-      T innerDestValue;
-      _StoneDeserializeValue(innerDestValue, jsonValue[i]);
-      destValue.push_back(innerDestValue);
-    }
-  }
-
-  template<typename T>
-  Json::Value _StoneSerializeValue(const std::vector<T>& value)
-  {
-    Json::Value result(Json::arrayValue);
-    for (size_t i = 0; i < value.size(); ++i)
-    {
-      result.append(_StoneSerializeValue(value[i]));
-    }
-    return result;
-  }
-
-  template<typename T>
-  std::ostream& StoneDumpValue(std::ostream& out, const std::vector<T>& value, size_t indent)
-  {
-    out << MakeIndent(indent) << "[\n";
-    for (size_t i = 0; i < value.size(); ++i)
-    {
-      StoneDumpValue(out, value[i], indent+2);
-    }
-    out << MakeIndent(indent) << "]\n";
-    return out;
-  }
-
-  inline void StoneCheckSerializedValueTypeGeneric(const Json::Value& value)
-  {
-    if ((!value.isMember("type")) || (!value["type"].isString()))
-    {
-      std::stringstream ss;
-      ss << "Cannot deserialize value ('type' key invalid)";
-      throw std::runtime_error(ss.str());
-    }
-  }
-
-  inline void StoneCheckSerializedValueType(
-    const Json::Value& value, std::string typeStr)
-  {
-    StoneCheckSerializedValueTypeGeneric(value);
-
-    std::string actTypeStr = value["type"].asString();
-    if (actTypeStr != typeStr)
-    {
-      std::stringstream ss;
-      ss << "Cannot deserialize type" << actTypeStr
-        << "into " << typeStr;
-      throw std::runtime_error(ss.str());
-    }
-  }
-
-  // end of generic methods
-
-// end of generic methods
-
-  enum Tool {
-    Tool_LineMeasure,
-    Tool_CircleMeasure,
-    Tool_Crop,
-    Tool_Windowing,
-    Tool_Zoom,
-    Tool_Pan,
-    Tool_Move,
-    Tool_Rotate,
-    Tool_Resize,
-    Tool_Mask,
-  };
-
-  inline std::string ToString(const Tool& value)
-  {
-    if( value == Tool_LineMeasure)
-    {
-      return std::string("LineMeasure");
-    }
-    if( value == Tool_CircleMeasure)
-    {
-      return std::string("CircleMeasure");
-    }
-    if( value == Tool_Crop)
-    {
-      return std::string("Crop");
-    }
-    if( value == Tool_Windowing)
-    {
-      return std::string("Windowing");
-    }
-    if( value == Tool_Zoom)
-    {
-      return std::string("Zoom");
-    }
-    if( value == Tool_Pan)
-    {
-      return std::string("Pan");
-    }
-    if( value == Tool_Move)
-    {
-      return std::string("Move");
-    }
-    if( value == Tool_Rotate)
-    {
-      return std::string("Rotate");
-    }
-    if( value == Tool_Resize)
-    {
-      return std::string("Resize");
-    }
-    if( value == Tool_Mask)
-    {
-      return std::string("Mask");
-    }
-    std::stringstream ss;
-    ss << "Value \"" << value << "\" cannot be converted to Tool. Possible values are: "
-        << " LineMeasure = " << static_cast<int64_t>(Tool_LineMeasure)  << ", " 
-        << " CircleMeasure = " << static_cast<int64_t>(Tool_CircleMeasure)  << ", " 
-        << " Crop = " << static_cast<int64_t>(Tool_Crop)  << ", " 
-        << " Windowing = " << static_cast<int64_t>(Tool_Windowing)  << ", " 
-        << " Zoom = " << static_cast<int64_t>(Tool_Zoom)  << ", " 
-        << " Pan = " << static_cast<int64_t>(Tool_Pan)  << ", " 
-        << " Move = " << static_cast<int64_t>(Tool_Move)  << ", " 
-        << " Rotate = " << static_cast<int64_t>(Tool_Rotate)  << ", " 
-        << " Resize = " << static_cast<int64_t>(Tool_Resize)  << ", " 
-        << " Mask = " << static_cast<int64_t>(Tool_Mask)  << ", " 
-        << std::endl;
-    std::string msg = ss.str();
-    throw std::runtime_error(msg);
-  }
-
-  inline void FromString(Tool& value, std::string strValue)
-  {
-    if( strValue == std::string("LineMeasure") )
-    {
-      value = Tool_LineMeasure;
-      return;
-    }
-    if( strValue == std::string("CircleMeasure") )
-    {
-      value = Tool_CircleMeasure;
-      return;
-    }
-    if( strValue == std::string("Crop") )
-    {
-      value = Tool_Crop;
-      return;
-    }
-    if( strValue == std::string("Windowing") )
-    {
-      value = Tool_Windowing;
-      return;
-    }
-    if( strValue == std::string("Zoom") )
-    {
-      value = Tool_Zoom;
-      return;
-    }
-    if( strValue == std::string("Pan") )
-    {
-      value = Tool_Pan;
-      return;
-    }
-    if( strValue == std::string("Move") )
-    {
-      value = Tool_Move;
-      return;
-    }
-    if( strValue == std::string("Rotate") )
-    {
-      value = Tool_Rotate;
-      return;
-    }
-    if( strValue == std::string("Resize") )
-    {
-      value = Tool_Resize;
-      return;
-    }
-    if( strValue == std::string("Mask") )
-    {
-      value = Tool_Mask;
-      return;
-    }
-
-    std::stringstream ss;
-    ss << "String \"" << strValue << "\" cannot be converted to Tool. Possible values are: LineMeasure CircleMeasure Crop Windowing Zoom Pan Move Rotate Resize Mask ";
-    std::string msg = ss.str();
-    throw std::runtime_error(msg);
-  }
-
-
-  inline void _StoneDeserializeValue(
-    Tool& destValue, const Json::Value& jsonValue)
-  {
-    FromString(destValue, jsonValue.asString());
-  }
-
-  inline Json::Value _StoneSerializeValue(const Tool& value)
-  {
-    std::string strValue = ToString(value);
-    return Json::Value(strValue);
-  }
-
-  inline std::ostream& StoneDumpValue(std::ostream& out, const Tool& value, size_t indent = 0)
-  {
-    if( value == Tool_LineMeasure)
-    {
-      out << MakeIndent(indent) << "LineMeasure" << std::endl;
-    }
-    if( value == Tool_CircleMeasure)
-    {
-      out << MakeIndent(indent) << "CircleMeasure" << std::endl;
-    }
-    if( value == Tool_Crop)
-    {
-      out << MakeIndent(indent) << "Crop" << std::endl;
-    }
-    if( value == Tool_Windowing)
-    {
-      out << MakeIndent(indent) << "Windowing" << std::endl;
-    }
-    if( value == Tool_Zoom)
-    {
-      out << MakeIndent(indent) << "Zoom" << std::endl;
-    }
-    if( value == Tool_Pan)
-    {
-      out << MakeIndent(indent) << "Pan" << std::endl;
-    }
-    if( value == Tool_Move)
-    {
-      out << MakeIndent(indent) << "Move" << std::endl;
-    }
-    if( value == Tool_Rotate)
-    {
-      out << MakeIndent(indent) << "Rotate" << std::endl;
-    }
-    if( value == Tool_Resize)
-    {
-      out << MakeIndent(indent) << "Resize" << std::endl;
-    }
-    if( value == Tool_Mask)
-    {
-      out << MakeIndent(indent) << "Mask" << std::endl;
-    }
-    return out;
-  }
-
-
-  enum ActionType {
-    ActionType_UndoCrop,
-    ActionType_Rotate,
-    ActionType_Invert,
-  };
-
-  inline std::string ToString(const ActionType& value)
-  {
-    if( value == ActionType_UndoCrop)
-    {
-      return std::string("UndoCrop");
-    }
-    if( value == ActionType_Rotate)
-    {
-      return std::string("Rotate");
-    }
-    if( value == ActionType_Invert)
-    {
-      return std::string("Invert");
-    }
-    std::stringstream ss;
-    ss << "Value \"" << value << "\" cannot be converted to ActionType. Possible values are: "
-        << " UndoCrop = " << static_cast<int64_t>(ActionType_UndoCrop)  << ", " 
-        << " Rotate = " << static_cast<int64_t>(ActionType_Rotate)  << ", " 
-        << " Invert = " << static_cast<int64_t>(ActionType_Invert)  << ", " 
-        << std::endl;
-    std::string msg = ss.str();
-    throw std::runtime_error(msg);
-  }
-
-  inline void FromString(ActionType& value, std::string strValue)
-  {
-    if( strValue == std::string("UndoCrop") )
-    {
-      value = ActionType_UndoCrop;
-      return;
-    }
-    if( strValue == std::string("Rotate") )
-    {
-      value = ActionType_Rotate;
-      return;
-    }
-    if( strValue == std::string("Invert") )
-    {
-      value = ActionType_Invert;
-      return;
-    }
-
-    std::stringstream ss;
-    ss << "String \"" << strValue << "\" cannot be converted to ActionType. Possible values are: UndoCrop Rotate Invert ";
-    std::string msg = ss.str();
-    throw std::runtime_error(msg);
-  }
-
-
-  inline void _StoneDeserializeValue(
-    ActionType& destValue, const Json::Value& jsonValue)
-  {
-    FromString(destValue, jsonValue.asString());
-  }
-
-  inline Json::Value _StoneSerializeValue(const ActionType& value)
-  {
-    std::string strValue = ToString(value);
-    return Json::Value(strValue);
-  }
-
-  inline std::ostream& StoneDumpValue(std::ostream& out, const ActionType& value, size_t indent = 0)
-  {
-    if( value == ActionType_UndoCrop)
-    {
-      out << MakeIndent(indent) << "UndoCrop" << std::endl;
-    }
-    if( value == ActionType_Rotate)
-    {
-      out << MakeIndent(indent) << "Rotate" << std::endl;
-    }
-    if( value == ActionType_Invert)
-    {
-      out << MakeIndent(indent) << "Invert" << std::endl;
-    }
-    return out;
-  }
-
-
-
-#ifdef _MSC_VER
-#pragma region SelectTool
-#endif //_MSC_VER
-
-  struct SelectTool
-  {
-    Tool tool;
-
-    SelectTool(Tool tool = Tool())
-    {
-      this->tool = tool;
-    }
-  };
-
-  inline void _StoneDeserializeValue(SelectTool& destValue, const Json::Value& value)
-  {
-    _StoneDeserializeValue(destValue.tool, value["tool"]);
-    }
-
-  inline Json::Value _StoneSerializeValue(const SelectTool& value)
-  {
-    Json::Value result(Json::objectValue);
-    result["tool"] = _StoneSerializeValue(value.tool);
-
-    return result;
-  }
-
-  inline std::ostream& StoneDumpValue(std::ostream& out, const SelectTool& value, size_t indent = 0)
-  {
-    out << MakeIndent(indent) << "{\n";
-    out << MakeIndent(indent) << "tool:\n";
-    StoneDumpValue(out, value.tool,indent+2);
-    out << "\n";
-
-    out << MakeIndent(indent) << "}\n";
-    return out;
-  }
-
-  inline void StoneDeserialize(SelectTool& destValue, const Json::Value& value)
-  {
-    StoneCheckSerializedValueType(value, "StoneSampleCommands.SelectTool");
-    _StoneDeserializeValue(destValue, value["value"]);
-  }
-
-  inline Json::Value StoneSerializeToJson(const SelectTool& value)
-  {
-    Json::Value result(Json::objectValue);
-    result["type"] = "StoneSampleCommands.SelectTool";
-    result["value"] = _StoneSerializeValue(value);
-    return result;
-  }
-
-  inline std::string StoneSerialize(const SelectTool& value)
-  {
-    Json::Value resultJson = StoneSerializeToJson(value);
-    std::string resultStr = resultJson.toStyledString();
-    return resultStr;
-  }
-
-#ifdef _MSC_VER
-#pragma endregion SelectTool
-#endif //_MSC_VER
-
-#ifdef _MSC_VER
-#pragma region Action
-#endif //_MSC_VER
-
-  struct Action
-  {
-    ActionType type;
-
-    Action(ActionType type = ActionType())
-    {
-      this->type = type;
-    }
-  };
-
-  inline void _StoneDeserializeValue(Action& destValue, const Json::Value& value)
-  {
-    _StoneDeserializeValue(destValue.type, value["type"]);
-    }
-
-  inline Json::Value _StoneSerializeValue(const Action& value)
-  {
-    Json::Value result(Json::objectValue);
-    result["type"] = _StoneSerializeValue(value.type);
-
-    return result;
-  }
-
-  inline std::ostream& StoneDumpValue(std::ostream& out, const Action& value, size_t indent = 0)
-  {
-    out << MakeIndent(indent) << "{\n";
-    out << MakeIndent(indent) << "type:\n";
-    StoneDumpValue(out, value.type,indent+2);
-    out << "\n";
-
-    out << MakeIndent(indent) << "}\n";
-    return out;
-  }
-
-  inline void StoneDeserialize(Action& destValue, const Json::Value& value)
-  {
-    StoneCheckSerializedValueType(value, "StoneSampleCommands.Action");
-    _StoneDeserializeValue(destValue, value["value"]);
-  }
-
-  inline Json::Value StoneSerializeToJson(const Action& value)
-  {
-    Json::Value result(Json::objectValue);
-    result["type"] = "StoneSampleCommands.Action";
-    result["value"] = _StoneSerializeValue(value);
-    return result;
-  }
-
-  inline std::string StoneSerialize(const Action& value)
-  {
-    Json::Value resultJson = StoneSerializeToJson(value);
-    std::string resultStr = resultJson.toStyledString();
-    return resultStr;
-  }
-
-#ifdef _MSC_VER
-#pragma endregion Action
-#endif //_MSC_VER
-
-#ifdef _MSC_VER
-#pragma region Dispatching code
-#endif //_MSC_VER
-
-  class IHandler
-  {
-  public:
-    virtual bool Handle(const SelectTool& value) = 0;
-    virtual bool Handle(const Action& value) = 0;
-  };
-
-  /** Service function for StoneDispatchToHandler */
-  inline bool StoneDispatchJsonToHandler(
-    const Json::Value& jsonValue, IHandler* handler)
-  {
-    StoneCheckSerializedValueTypeGeneric(jsonValue);
-    std::string type = jsonValue["type"].asString();
-    if (type == "")
-    {
-      // this should never ever happen
-      throw std::runtime_error("Caught empty type while dispatching");
-    }
-    else if (type == "StoneSampleCommands.SelectTool")
-    {
-      SelectTool value;
-      _StoneDeserializeValue(value, jsonValue["value"]);
-      return handler->Handle(value);
-    }
-    else if (type == "StoneSampleCommands.Action")
-    {
-      Action value;
-      _StoneDeserializeValue(value, jsonValue["value"]);
-      return handler->Handle(value);
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-  /** Takes a serialized type and passes this to the handler */
-  inline bool StoneDispatchToHandler(std::string strValue, IHandler* handler)
-  {
-    Json::Value readValue;
-
-    Json::CharReaderBuilder builder;
-    Json::CharReader* reader = builder.newCharReader();
-
-    StoneSmartPtr<Json::CharReader> ptr(reader);
-
-    std::string errors;
-
-    bool ok = reader->parse(
-      strValue.c_str(),
-      strValue.c_str() + strValue.size(),
-      &readValue,
-      &errors
-    );
-    if (!ok)
-    {
-      std::stringstream ss;
-      ss << "Jsoncpp parsing error: " << errors;
-      throw std::runtime_error(ss.str());
-    }
-    return StoneDispatchJsonToHandler(readValue, handler);
-  }
-
-#ifdef _MSC_VER
-#pragma endregion Dispatching code
-#endif //_MSC_VER
+/*
+         1         2         3         4         5         6         7
+12345678901234567890123456789012345678901234567890123456789012345678901234567890
+
+Generated on 2019-03-18 12:07:42.696093 by stonegentool
+
+*/
+#pragma once
+
+#include <exception>
+#include <iostream>
+#include <string>
+#include <sstream>
+#include <assert.h>
+#include <memory>
+#include <json/json.h>
+
+//#define STONEGEN_NO_CPP11 1
+
+#ifdef STONEGEN_NO_CPP11
+#define StoneSmartPtr std::auto_ptr
+#else 
+#define StoneSmartPtr std::unique_ptr
+#endif 
+
+namespace StoneSampleCommands
+{
+  /** Throws in case of problem */
+  inline void _StoneDeserializeValue(int32_t& destValue, const Json::Value& jsonValue)
+  {
+    destValue = jsonValue.asInt();
+  }
+
+  inline Json::Value _StoneSerializeValue(int32_t value)
+  {
+    Json::Value result(value);
+    return result;
+  }
+
+  inline void _StoneDeserializeValue(Json::Value& destValue, const Json::Value& jsonValue)
+  {
+    destValue = jsonValue;
+  }
+
+  inline Json::Value _StoneSerializeValue(Json::Value value)
+  {
+    return value;
+  }
+
+  /** Throws in case of problem */
+  inline void _StoneDeserializeValue(double& destValue, const Json::Value& jsonValue)
+  {
+    destValue = jsonValue.asDouble();
+  }
+
+  inline Json::Value _StoneSerializeValue(double value)
+  {
+    Json::Value result(value);
+    return result;
+  }
+
+  /** Throws in case of problem */
+  inline void _StoneDeserializeValue(bool& destValue, const Json::Value& jsonValue)
+  {
+    destValue = jsonValue.asBool();
+  }
+
+  inline Json::Value _StoneSerializeValue(bool value)
+  {
+    Json::Value result(value);
+    return result;
+  }
+
+  /** Throws in case of problem */
+  inline void _StoneDeserializeValue(
+       std::string& destValue
+     , const Json::Value& jsonValue)
+  {
+    destValue = jsonValue.asString();
+  }
+
+  inline Json::Value _StoneSerializeValue(const std::string& value)
+  {
+    // the following is better than 
+    Json::Value result(value.data(),value.data()+value.size());
+    return result;
+  }
+
+  inline std::string MakeIndent(size_t indent)
+  {
+    char* txt = reinterpret_cast<char*>(malloc(indent+1)); // NO EXCEPTION BELOW!!!!!!!!!!!!
+    for(size_t i = 0; i < indent; ++i)
+      txt[i] = ' ';
+    txt[indent] = 0;
+    std::string retVal(txt);
+    free(txt); // NO EXCEPTION ABOVE !!!!!!!!!!
+    return retVal;
+  }
+
+  // generic dumper
+  template<typename T>
+  std::ostream& StoneDumpValue(std::ostream& out, const T& value, size_t indent)
+  {
+    out << MakeIndent(indent) << value;
+    return out;
+  }
+
+  // string dumper
+  inline std::ostream& StoneDumpValue(std::ostream& out, const std::string& value, size_t indent)
+  {
+    out << MakeIndent(indent) << "\"" << value  << "\"";
+    return out;
+  }
+
+  /** Throws in case of problem */
+  template<typename T>
+  void _StoneDeserializeValue(
+    std::map<std::string, T>& destValue, const Json::Value& jsonValue)
+  {
+    destValue.clear();
+    for (
+      Json::Value::const_iterator itr = jsonValue.begin();
+      itr != jsonValue.end();
+      itr++)
+    {
+      std::string key;
+      _StoneDeserializeValue(key, itr.key());
+
+      T innerDestValue;
+      _StoneDeserializeValue(innerDestValue, *itr);
+
+      destValue[key] = innerDestValue;
+    }
+  }
+
+  template<typename T>
+  Json::Value _StoneSerializeValue(const std::map<std::string,T>& value)
+  {
+    Json::Value result(Json::objectValue);
+
+    for (typename std::map<std::string, T>::const_iterator it = value.cbegin();
+      it != value.cend(); ++it)
+    {
+      // it->first it->second
+      result[it->first] = _StoneSerializeValue(it->second);
+    }
+    return result;
+  }
+
+  template<typename T>
+  std::ostream& StoneDumpValue(std::ostream& out, const std::map<std::string,T>& value, size_t indent)
+  {
+    out << MakeIndent(indent) << "{\n";
+    for (typename std::map<std::string, T>::const_iterator it = value.cbegin();
+      it != value.cend(); ++it)
+    {
+      out << MakeIndent(indent+2) << "\"" << it->first << "\" : ";
+      StoneDumpValue(out, it->second, indent+2);
+    }
+    out << MakeIndent(indent) << "}\n";
+    return out;
+  }
+
+  /** Throws in case of problem */
+  template<typename T>
+  void _StoneDeserializeValue(
+    std::vector<T>& destValue, const Json::Value& jsonValue)
+  {
+    destValue.clear();
+    destValue.reserve(jsonValue.size());
+    for (Json::Value::ArrayIndex i = 0; i != jsonValue.size(); i++)
+    {
+      T innerDestValue;
+      _StoneDeserializeValue(innerDestValue, jsonValue[i]);
+      destValue.push_back(innerDestValue);
+    }
+  }
+
+  template<typename T>
+  Json::Value _StoneSerializeValue(const std::vector<T>& value)
+  {
+    Json::Value result(Json::arrayValue);
+    for (size_t i = 0; i < value.size(); ++i)
+    {
+      result.append(_StoneSerializeValue(value[i]));
+    }
+    return result;
+  }
+
+  template<typename T>
+  std::ostream& StoneDumpValue(std::ostream& out, const std::vector<T>& value, size_t indent)
+  {
+    out << MakeIndent(indent) << "[\n";
+    for (size_t i = 0; i < value.size(); ++i)
+    {
+      StoneDumpValue(out, value[i], indent+2);
+    }
+    out << MakeIndent(indent) << "]\n";
+    return out;
+  }
+
+  inline void StoneCheckSerializedValueTypeGeneric(const Json::Value& value)
+  {
+    if ((!value.isMember("type")) || (!value["type"].isString()))
+    {
+      std::stringstream ss;
+      ss << "Cannot deserialize value ('type' key invalid)";
+      throw std::runtime_error(ss.str());
+    }
+  }
+
+  inline void StoneCheckSerializedValueType(
+    const Json::Value& value, std::string typeStr)
+  {
+    StoneCheckSerializedValueTypeGeneric(value);
+
+    std::string actTypeStr = value["type"].asString();
+    if (actTypeStr != typeStr)
+    {
+      std::stringstream ss;
+      ss << "Cannot deserialize type" << actTypeStr
+        << "into " << typeStr;
+      throw std::runtime_error(ss.str());
+    }
+  }
+
+  // end of generic methods
+
+// end of generic methods
+
+  enum Tool {
+    Tool_LineMeasure,
+    Tool_CircleMeasure,
+    Tool_Crop,
+    Tool_Windowing,
+    Tool_Zoom,
+    Tool_Pan,
+    Tool_Move,
+    Tool_Rotate,
+    Tool_Resize,
+    Tool_Mask,
+  };
+
+  inline std::string ToString(const Tool& value)
+  {
+    if( value == Tool_LineMeasure)
+    {
+      return std::string("LineMeasure");
+    }
+    if( value == Tool_CircleMeasure)
+    {
+      return std::string("CircleMeasure");
+    }
+    if( value == Tool_Crop)
+    {
+      return std::string("Crop");
+    }
+    if( value == Tool_Windowing)
+    {
+      return std::string("Windowing");
+    }
+    if( value == Tool_Zoom)
+    {
+      return std::string("Zoom");
+    }
+    if( value == Tool_Pan)
+    {
+      return std::string("Pan");
+    }
+    if( value == Tool_Move)
+    {
+      return std::string("Move");
+    }
+    if( value == Tool_Rotate)
+    {
+      return std::string("Rotate");
+    }
+    if( value == Tool_Resize)
+    {
+      return std::string("Resize");
+    }
+    if( value == Tool_Mask)
+    {
+      return std::string("Mask");
+    }
+    std::stringstream ss;
+    ss << "Value \"" << value << "\" cannot be converted to Tool. Possible values are: "
+        << " LineMeasure = " << static_cast<int64_t>(Tool_LineMeasure)  << ", " 
+        << " CircleMeasure = " << static_cast<int64_t>(Tool_CircleMeasure)  << ", " 
+        << " Crop = " << static_cast<int64_t>(Tool_Crop)  << ", " 
+        << " Windowing = " << static_cast<int64_t>(Tool_Windowing)  << ", " 
+        << " Zoom = " << static_cast<int64_t>(Tool_Zoom)  << ", " 
+        << " Pan = " << static_cast<int64_t>(Tool_Pan)  << ", " 
+        << " Move = " << static_cast<int64_t>(Tool_Move)  << ", " 
+        << " Rotate = " << static_cast<int64_t>(Tool_Rotate)  << ", " 
+        << " Resize = " << static_cast<int64_t>(Tool_Resize)  << ", " 
+        << " Mask = " << static_cast<int64_t>(Tool_Mask)  << ", " 
+        << std::endl;
+    std::string msg = ss.str();
+    throw std::runtime_error(msg);
+  }
+
+  inline void FromString(Tool& value, std::string strValue)
+  {
+    if( strValue == std::string("LineMeasure") )
+    {
+      value = Tool_LineMeasure;
+      return;
+    }
+    if( strValue == std::string("CircleMeasure") )
+    {
+      value = Tool_CircleMeasure;
+      return;
+    }
+    if( strValue == std::string("Crop") )
+    {
+      value = Tool_Crop;
+      return;
+    }
+    if( strValue == std::string("Windowing") )
+    {
+      value = Tool_Windowing;
+      return;
+    }
+    if( strValue == std::string("Zoom") )
+    {
+      value = Tool_Zoom;
+      return;
+    }
+    if( strValue == std::string("Pan") )
+    {
+      value = Tool_Pan;
+      return;
+    }
+    if( strValue == std::string("Move") )
+    {
+      value = Tool_Move;
+      return;
+    }
+    if( strValue == std::string("Rotate") )
+    {
+      value = Tool_Rotate;
+      return;
+    }
+    if( strValue == std::string("Resize") )
+    {
+      value = Tool_Resize;
+      return;
+    }
+    if( strValue == std::string("Mask") )
+    {
+      value = Tool_Mask;
+      return;
+    }
+
+    std::stringstream ss;
+    ss << "String \"" << strValue << "\" cannot be converted to Tool. Possible values are: LineMeasure CircleMeasure Crop Windowing Zoom Pan Move Rotate Resize Mask ";
+    std::string msg = ss.str();
+    throw std::runtime_error(msg);
+  }
+
+
+  inline void _StoneDeserializeValue(
+    Tool& destValue, const Json::Value& jsonValue)
+  {
+    FromString(destValue, jsonValue.asString());
+  }
+
+  inline Json::Value _StoneSerializeValue(const Tool& value)
+  {
+    std::string strValue = ToString(value);
+    return Json::Value(strValue);
+  }
+
+  inline std::ostream& StoneDumpValue(std::ostream& out, const Tool& value, size_t indent = 0)
+  {
+    if( value == Tool_LineMeasure)
+    {
+      out << MakeIndent(indent) << "LineMeasure" << std::endl;
+    }
+    if( value == Tool_CircleMeasure)
+    {
+      out << MakeIndent(indent) << "CircleMeasure" << std::endl;
+    }
+    if( value == Tool_Crop)
+    {
+      out << MakeIndent(indent) << "Crop" << std::endl;
+    }
+    if( value == Tool_Windowing)
+    {
+      out << MakeIndent(indent) << "Windowing" << std::endl;
+    }
+    if( value == Tool_Zoom)
+    {
+      out << MakeIndent(indent) << "Zoom" << std::endl;
+    }
+    if( value == Tool_Pan)
+    {
+      out << MakeIndent(indent) << "Pan" << std::endl;
+    }
+    if( value == Tool_Move)
+    {
+      out << MakeIndent(indent) << "Move" << std::endl;
+    }
+    if( value == Tool_Rotate)
+    {
+      out << MakeIndent(indent) << "Rotate" << std::endl;
+    }
+    if( value == Tool_Resize)
+    {
+      out << MakeIndent(indent) << "Resize" << std::endl;
+    }
+    if( value == Tool_Mask)
+    {
+      out << MakeIndent(indent) << "Mask" << std::endl;
+    }
+    return out;
+  }
+
+
+  enum ActionType {
+    ActionType_UndoCrop,
+    ActionType_Rotate,
+    ActionType_Invert,
+  };
+
+  inline std::string ToString(const ActionType& value)
+  {
+    if( value == ActionType_UndoCrop)
+    {
+      return std::string("UndoCrop");
+    }
+    if( value == ActionType_Rotate)
+    {
+      return std::string("Rotate");
+    }
+    if( value == ActionType_Invert)
+    {
+      return std::string("Invert");
+    }
+    std::stringstream ss;
+    ss << "Value \"" << value << "\" cannot be converted to ActionType. Possible values are: "
+        << " UndoCrop = " << static_cast<int64_t>(ActionType_UndoCrop)  << ", " 
+        << " Rotate = " << static_cast<int64_t>(ActionType_Rotate)  << ", " 
+        << " Invert = " << static_cast<int64_t>(ActionType_Invert)  << ", " 
+        << std::endl;
+    std::string msg = ss.str();
+    throw std::runtime_error(msg);
+  }
+
+  inline void FromString(ActionType& value, std::string strValue)
+  {
+    if( strValue == std::string("UndoCrop") )
+    {
+      value = ActionType_UndoCrop;
+      return;
+    }
+    if( strValue == std::string("Rotate") )
+    {
+      value = ActionType_Rotate;
+      return;
+    }
+    if( strValue == std::string("Invert") )
+    {
+      value = ActionType_Invert;
+      return;
+    }
+
+    std::stringstream ss;
+    ss << "String \"" << strValue << "\" cannot be converted to ActionType. Possible values are: UndoCrop Rotate Invert ";
+    std::string msg = ss.str();
+    throw std::runtime_error(msg);
+  }
+
+
+  inline void _StoneDeserializeValue(
+    ActionType& destValue, const Json::Value& jsonValue)
+  {
+    FromString(destValue, jsonValue.asString());
+  }
+
+  inline Json::Value _StoneSerializeValue(const ActionType& value)
+  {
+    std::string strValue = ToString(value);
+    return Json::Value(strValue);
+  }
+
+  inline std::ostream& StoneDumpValue(std::ostream& out, const ActionType& value, size_t indent = 0)
+  {
+    if( value == ActionType_UndoCrop)
+    {
+      out << MakeIndent(indent) << "UndoCrop" << std::endl;
+    }
+    if( value == ActionType_Rotate)
+    {
+      out << MakeIndent(indent) << "Rotate" << std::endl;
+    }
+    if( value == ActionType_Invert)
+    {
+      out << MakeIndent(indent) << "Invert" << std::endl;
+    }
+    return out;
+  }
+
+
+
+#ifdef _MSC_VER
+#pragma region SelectTool
+#endif //_MSC_VER
+
+  struct SelectTool
+  {
+    Tool tool;
+
+    SelectTool(Tool tool = Tool())
+    {
+      this->tool = tool;
+    }
+  };
+
+  inline void _StoneDeserializeValue(SelectTool& destValue, const Json::Value& value)
+  {
+    _StoneDeserializeValue(destValue.tool, value["tool"]);
+    }
+
+  inline Json::Value _StoneSerializeValue(const SelectTool& value)
+  {
+    Json::Value result(Json::objectValue);
+    result["tool"] = _StoneSerializeValue(value.tool);
+
+    return result;
+  }
+
+  inline std::ostream& StoneDumpValue(std::ostream& out, const SelectTool& value, size_t indent = 0)
+  {
+    out << MakeIndent(indent) << "{\n";
+    out << MakeIndent(indent) << "tool:\n";
+    StoneDumpValue(out, value.tool,indent+2);
+    out << "\n";
+
+    out << MakeIndent(indent) << "}\n";
+    return out;
+  }
+
+  inline void StoneDeserialize(SelectTool& destValue, const Json::Value& value)
+  {
+    StoneCheckSerializedValueType(value, "StoneSampleCommands.SelectTool");
+    _StoneDeserializeValue(destValue, value["value"]);
+  }
+
+  inline Json::Value StoneSerializeToJson(const SelectTool& value)
+  {
+    Json::Value result(Json::objectValue);
+    result["type"] = "StoneSampleCommands.SelectTool";
+    result["value"] = _StoneSerializeValue(value);
+    return result;
+  }
+
+  inline std::string StoneSerialize(const SelectTool& value)
+  {
+    Json::Value resultJson = StoneSerializeToJson(value);
+    std::string resultStr = resultJson.toStyledString();
+    return resultStr;
+  }
+
+#ifdef _MSC_VER
+#pragma endregion SelectTool
+#endif //_MSC_VER
+
+#ifdef _MSC_VER
+#pragma region Action
+#endif //_MSC_VER
+
+  struct Action
+  {
+    ActionType type;
+
+    Action(ActionType type = ActionType())
+    {
+      this->type = type;
+    }
+  };
+
+  inline void _StoneDeserializeValue(Action& destValue, const Json::Value& value)
+  {
+    _StoneDeserializeValue(destValue.type, value["type"]);
+    }
+
+  inline Json::Value _StoneSerializeValue(const Action& value)
+  {
+    Json::Value result(Json::objectValue);
+    result["type"] = _StoneSerializeValue(value.type);
+
+    return result;
+  }
+
+  inline std::ostream& StoneDumpValue(std::ostream& out, const Action& value, size_t indent = 0)
+  {
+    out << MakeIndent(indent) << "{\n";
+    out << MakeIndent(indent) << "type:\n";
+    StoneDumpValue(out, value.type,indent+2);
+    out << "\n";
+
+    out << MakeIndent(indent) << "}\n";
+    return out;
+  }
+
+  inline void StoneDeserialize(Action& destValue, const Json::Value& value)
+  {
+    StoneCheckSerializedValueType(value, "StoneSampleCommands.Action");
+    _StoneDeserializeValue(destValue, value["value"]);
+  }
+
+  inline Json::Value StoneSerializeToJson(const Action& value)
+  {
+    Json::Value result(Json::objectValue);
+    result["type"] = "StoneSampleCommands.Action";
+    result["value"] = _StoneSerializeValue(value);
+    return result;
+  }
+
+  inline std::string StoneSerialize(const Action& value)
+  {
+    Json::Value resultJson = StoneSerializeToJson(value);
+    std::string resultStr = resultJson.toStyledString();
+    return resultStr;
+  }
+
+#ifdef _MSC_VER
+#pragma endregion Action
+#endif //_MSC_VER
+
+#ifdef _MSC_VER
+#pragma region Dispatching code
+#endif //_MSC_VER
+
+  class IHandler
+  {
+  public:
+    virtual bool Handle(const SelectTool& value) = 0;
+    virtual bool Handle(const Action& value) = 0;
+  };
+
+  /** Service function for StoneDispatchToHandler */
+  inline bool StoneDispatchJsonToHandler(
+    const Json::Value& jsonValue, IHandler* handler)
+  {
+    StoneCheckSerializedValueTypeGeneric(jsonValue);
+    std::string type = jsonValue["type"].asString();
+    if (type == "")
+    {
+      // this should never ever happen
+      throw std::runtime_error("Caught empty type while dispatching");
+    }
+    else if (type == "StoneSampleCommands.SelectTool")
+    {
+      SelectTool value;
+      _StoneDeserializeValue(value, jsonValue["value"]);
+      return handler->Handle(value);
+    }
+    else if (type == "StoneSampleCommands.Action")
+    {
+      Action value;
+      _StoneDeserializeValue(value, jsonValue["value"]);
+      return handler->Handle(value);
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  /** Takes a serialized type and passes this to the handler */
+  inline bool StoneDispatchToHandler(std::string strValue, IHandler* handler)
+  {
+    Json::Value readValue;
+
+    Json::CharReaderBuilder builder;
+    Json::CharReader* reader = builder.newCharReader();
+
+    StoneSmartPtr<Json::CharReader> ptr(reader);
+
+    std::string errors;
+
+    bool ok = reader->parse(
+      strValue.c_str(),
+      strValue.c_str() + strValue.size(),
+      &readValue,
+      &errors
+    );
+    if (!ok)
+    {
+      std::stringstream ss;
+      ss << "Jsoncpp parsing error: " << errors;
+      throw std::runtime_error(ss.str());
+    }
+    return StoneDispatchJsonToHandler(readValue, handler);
+  }
+
+#ifdef _MSC_VER
+#pragma endregion Dispatching code
+#endif //_MSC_VER
 }
\ No newline at end of file
--- a/Applications/Samples/StoneSampleCommands_generated.ts	Tue May 14 18:24:12 2019 +0200
+++ b/Applications/Samples/StoneSampleCommands_generated.ts	Thu May 16 09:11:14 2019 +0200
@@ -1,333 +1,333 @@
-/*
-         1         2         3         4         5         6         7
-12345678901234567890123456789012345678901234567890123456789012345678901234567890
-
-Generated on 2019-03-18 12:07:42.696093 by stonegentool
-
-*/
-
-function StoneCheckSerializedValueType(value: any, typeStr: string)
-{
-  StoneCheckSerializedValueTypeGeneric(value);
-
-  if (value['type'] != typeStr)
-  {
-    throw new Error(
-      `Cannot deserialize type ${value['type']} into ${typeStr}`);
-  }
-}
-
-function isString(val: any) :boolean
-{
-  return ((typeof val === 'string') || (val instanceof String));
-}
-
-function StoneCheckSerializedValueTypeGeneric(value: any)
-{
-  // console.//log("+-------------------------------------------------+");
-  // console.//log("|            StoneCheckSerializedValueTypeGeneric |");
-  // console.//log("+-------------------------------------------------+");
-  // console.//log("value = ");
-  // console.//log(value);
-  if ( (!('type' in value)) || (!isString(value.type)) )
-  {
-    throw new Error(
-      "Cannot deserialize value ('type' key invalid)");
-  }
-}
-
-// end of generic methods
-
-export enum Tool {
-  LineMeasure = "LineMeasure",
-  CircleMeasure = "CircleMeasure",
-  Crop = "Crop",
-  Windowing = "Windowing",
-  Zoom = "Zoom",
-  Pan = "Pan",
-  Move = "Move",
-  Rotate = "Rotate",
-  Resize = "Resize",
-  Mask = "Mask"
-};
-
-export function Tool_FromString(strValue:string) : Tool
-{
-  if( strValue == "LineMeasure" )
-  {
-    return Tool.LineMeasure;
-  }
-  if( strValue == "CircleMeasure" )
-  {
-    return Tool.CircleMeasure;
-  }
-  if( strValue == "Crop" )
-  {
-    return Tool.Crop;
-  }
-  if( strValue == "Windowing" )
-  {
-    return Tool.Windowing;
-  }
-  if( strValue == "Zoom" )
-  {
-    return Tool.Zoom;
-  }
-  if( strValue == "Pan" )
-  {
-    return Tool.Pan;
-  }
-  if( strValue == "Move" )
-  {
-    return Tool.Move;
-  }
-  if( strValue == "Rotate" )
-  {
-    return Tool.Rotate;
-  }
-  if( strValue == "Resize" )
-  {
-    return Tool.Resize;
-  }
-  if( strValue == "Mask" )
-  {
-    return Tool.Mask;
-  }
-
-  let msg : string =  `String ${strValue} cannot be converted to Tool. Possible values are: LineMeasure, CircleMeasure, Crop, Windowing, Zoom, Pan, Move, Rotate, Resize, Mask`;
-  throw new Error(msg);
-}
-
-export function Tool_ToString(value:Tool) : string
-{
-  if( value == Tool.LineMeasure )
-  {
-    return "LineMeasure";
-  }
-  if( value == Tool.CircleMeasure )
-  {
-    return "CircleMeasure";
-  }
-  if( value == Tool.Crop )
-  {
-    return "Crop";
-  }
-  if( value == Tool.Windowing )
-  {
-    return "Windowing";
-  }
-  if( value == Tool.Zoom )
-  {
-    return "Zoom";
-  }
-  if( value == Tool.Pan )
-  {
-    return "Pan";
-  }
-  if( value == Tool.Move )
-  {
-    return "Move";
-  }
-  if( value == Tool.Rotate )
-  {
-    return "Rotate";
-  }
-  if( value == Tool.Resize )
-  {
-    return "Resize";
-  }
-  if( value == Tool.Mask )
-  {
-    return "Mask";
-  }
-
-  let msg : string = `Value ${value} cannot be converted to Tool. Possible values are: `;
-  {
-    let _LineMeasure_enumValue : string = Tool.LineMeasure; // enums are strings in stonecodegen, so this will work.
-    let msg_LineMeasure : string = `LineMeasure (${_LineMeasure_enumValue}), `;
-    msg = msg + msg_LineMeasure;
-  }
-  {
-    let _CircleMeasure_enumValue : string = Tool.CircleMeasure; // enums are strings in stonecodegen, so this will work.
-    let msg_CircleMeasure : string = `CircleMeasure (${_CircleMeasure_enumValue}), `;
-    msg = msg + msg_CircleMeasure;
-  }
-  {
-    let _Crop_enumValue : string = Tool.Crop; // enums are strings in stonecodegen, so this will work.
-    let msg_Crop : string = `Crop (${_Crop_enumValue}), `;
-    msg = msg + msg_Crop;
-  }
-  {
-    let _Windowing_enumValue : string = Tool.Windowing; // enums are strings in stonecodegen, so this will work.
-    let msg_Windowing : string = `Windowing (${_Windowing_enumValue}), `;
-    msg = msg + msg_Windowing;
-  }
-  {
-    let _Zoom_enumValue : string = Tool.Zoom; // enums are strings in stonecodegen, so this will work.
-    let msg_Zoom : string = `Zoom (${_Zoom_enumValue}), `;
-    msg = msg + msg_Zoom;
-  }
-  {
-    let _Pan_enumValue : string = Tool.Pan; // enums are strings in stonecodegen, so this will work.
-    let msg_Pan : string = `Pan (${_Pan_enumValue}), `;
-    msg = msg + msg_Pan;
-  }
-  {
-    let _Move_enumValue : string = Tool.Move; // enums are strings in stonecodegen, so this will work.
-    let msg_Move : string = `Move (${_Move_enumValue}), `;
-    msg = msg + msg_Move;
-  }
-  {
-    let _Rotate_enumValue : string = Tool.Rotate; // enums are strings in stonecodegen, so this will work.
-    let msg_Rotate : string = `Rotate (${_Rotate_enumValue}), `;
-    msg = msg + msg_Rotate;
-  }
-  {
-    let _Resize_enumValue : string = Tool.Resize; // enums are strings in stonecodegen, so this will work.
-    let msg_Resize : string = `Resize (${_Resize_enumValue}), `;
-    msg = msg + msg_Resize;
-  }
-  {
-    let _Mask_enumValue : string = Tool.Mask; // enums are strings in stonecodegen, so this will work.
-    let msg_Mask : string = `Mask (${_Mask_enumValue})`;
-    msg = msg + msg_Mask;
-  }
-  throw new Error(msg);
-}
-
-export enum ActionType {
-  UndoCrop = "UndoCrop",
-  Rotate = "Rotate",
-  Invert = "Invert"
-};
-
-export function ActionType_FromString(strValue:string) : ActionType
-{
-  if( strValue == "UndoCrop" )
-  {
-    return ActionType.UndoCrop;
-  }
-  if( strValue == "Rotate" )
-  {
-    return ActionType.Rotate;
-  }
-  if( strValue == "Invert" )
-  {
-    return ActionType.Invert;
-  }
-
-  let msg : string =  `String ${strValue} cannot be converted to ActionType. Possible values are: UndoCrop, Rotate, Invert`;
-  throw new Error(msg);
-}
-
-export function ActionType_ToString(value:ActionType) : string
-{
-  if( value == ActionType.UndoCrop )
-  {
-    return "UndoCrop";
-  }
-  if( value == ActionType.Rotate )
-  {
-    return "Rotate";
-  }
-  if( value == ActionType.Invert )
-  {
-    return "Invert";
-  }
-
-  let msg : string = `Value ${value} cannot be converted to ActionType. Possible values are: `;
-  {
-    let _UndoCrop_enumValue : string = ActionType.UndoCrop; // enums are strings in stonecodegen, so this will work.
-    let msg_UndoCrop : string = `UndoCrop (${_UndoCrop_enumValue}), `;
-    msg = msg + msg_UndoCrop;
-  }
-  {
-    let _Rotate_enumValue : string = ActionType.Rotate; // enums are strings in stonecodegen, so this will work.
-    let msg_Rotate : string = `Rotate (${_Rotate_enumValue}), `;
-    msg = msg + msg_Rotate;
-  }
-  {
-    let _Invert_enumValue : string = ActionType.Invert; // enums are strings in stonecodegen, so this will work.
-    let msg_Invert : string = `Invert (${_Invert_enumValue})`;
-    msg = msg + msg_Invert;
-  }
-  throw new Error(msg);
-}
-
-
-
-export class SelectTool {
-  tool:Tool;
-
-  constructor() {
-  }
-
-  public StoneSerialize(): string {
-    let container: object = {};
-    container['type'] = 'StoneSampleCommands.SelectTool';
-    container['value'] = this;
-    return JSON.stringify(container);
-  }
-
-  public static StoneDeserialize(valueStr: string) : SelectTool
-  {
-    let value: any = JSON.parse(valueStr);
-    StoneCheckSerializedValueType(value, 'StoneSampleCommands.SelectTool');
-    let result: SelectTool = value['value'] as SelectTool;
-    return result;
-  }
-}
-export class Action {
-  type:ActionType;
-
-  constructor() {
-  }
-
-  public StoneSerialize(): string {
-    let container: object = {};
-    container['type'] = 'StoneSampleCommands.Action';
-    container['value'] = this;
-    return JSON.stringify(container);
-  }
-
-  public static StoneDeserialize(valueStr: string) : Action
-  {
-    let value: any = JSON.parse(valueStr);
-    StoneCheckSerializedValueType(value, 'StoneSampleCommands.Action');
-    let result: Action = value['value'] as Action;
-    return result;
-  }
-}
-
-export interface IHandler {
-};
-
-/** Service function for StoneDispatchToHandler */
-export function StoneDispatchJsonToHandler(
-  jsonValue: any, handler: IHandler): boolean
-{
-  StoneCheckSerializedValueTypeGeneric(jsonValue);
-  let type: string = jsonValue["type"];
-  if (type == "")
-  {
-    // this should never ever happen
-    throw new Error("Caught empty type while dispatching");
-  }
-  else
-  {
-    return false;
-  }
-}
-
-/** Takes a serialized type and passes this to the handler */
-export function StoneDispatchToHandler(
-  strValue: string, handler: IHandler): boolean
-{
-  // console.//log("+------------------------------------------------+");
-  // console.//log("|            StoneDispatchToHandler              |");
-  // console.//log("+------------------------------------------------+");
-  // console.//log("strValue = ");
-  // console.//log(strValue);
-  let jsonValue: any = JSON.parse(strValue)
-  return StoneDispatchJsonToHandler(jsonValue, handler);
+/*
+         1         2         3         4         5         6         7
+12345678901234567890123456789012345678901234567890123456789012345678901234567890
+
+Generated on 2019-03-18 12:07:42.696093 by stonegentool
+
+*/
+
+function StoneCheckSerializedValueType(value: any, typeStr: string)
+{
+  StoneCheckSerializedValueTypeGeneric(value);
+
+  if (value['type'] != typeStr)
+  {
+    throw new Error(
+      `Cannot deserialize type ${value['type']} into ${typeStr}`);
+  }
+}
+
+function isString(val: any) :boolean
+{
+  return ((typeof val === 'string') || (val instanceof String));
+}
+
+function StoneCheckSerializedValueTypeGeneric(value: any)
+{
+  // console.//log("+-------------------------------------------------+");
+  // console.//log("|            StoneCheckSerializedValueTypeGeneric |");
+  // console.//log("+-------------------------------------------------+");
+  // console.//log("value = ");
+  // console.//log(value);
+  if ( (!('type' in value)) || (!isString(value.type)) )
+  {
+    throw new Error(
+      "Cannot deserialize value ('type' key invalid)");
+  }
+}
+
+// end of generic methods
+
+export enum Tool {
+  LineMeasure = "LineMeasure",
+  CircleMeasure = "CircleMeasure",
+  Crop = "Crop",
+  Windowing = "Windowing",
+  Zoom = "Zoom",
+  Pan = "Pan",
+  Move = "Move",
+  Rotate = "Rotate",
+  Resize = "Resize",
+  Mask = "Mask"
+};
+
+export function Tool_FromString(strValue:string) : Tool
+{
+  if( strValue == "LineMeasure" )
+  {
+    return Tool.LineMeasure;
+  }
+  if( strValue == "CircleMeasure" )
+  {
+    return Tool.CircleMeasure;
+  }
+  if( strValue == "Crop" )
+  {
+    return Tool.Crop;
+  }
+  if( strValue == "Windowing" )
+  {
+    return Tool.Windowing;
+  }
+  if( strValue == "Zoom" )
+  {
+    return Tool.Zoom;
+  }
+  if( strValue == "Pan" )
+  {
+    return Tool.Pan;
+  }
+  if( strValue == "Move" )
+  {
+    return Tool.Move;
+  }
+  if( strValue == "Rotate" )
+  {
+    return Tool.Rotate;
+  }
+  if( strValue == "Resize" )
+  {
+    return Tool.Resize;
+  }
+  if( strValue == "Mask" )
+  {
+    return Tool.Mask;
+  }
+
+  let msg : string =  `String ${strValue} cannot be converted to Tool. Possible values are: LineMeasure, CircleMeasure, Crop, Windowing, Zoom, Pan, Move, Rotate, Resize, Mask`;
+  throw new Error(msg);
+}
+
+export function Tool_ToString(value:Tool) : string
+{
+  if( value == Tool.LineMeasure )
+  {
+    return "LineMeasure";
+  }
+  if( value == Tool.CircleMeasure )
+  {
+    return "CircleMeasure";
+  }
+  if( value == Tool.Crop )
+  {
+    return "Crop";
+  }
+  if( value == Tool.Windowing )
+  {
+    return "Windowing";
+  }
+  if( value == Tool.Zoom )
+  {
+    return "Zoom";
+  }
+  if( value == Tool.Pan )
+  {
+    return "Pan";
+  }
+  if( value == Tool.Move )
+  {
+    return "Move";
+  }
+  if( value == Tool.Rotate )
+  {
+    return "Rotate";
+  }
+  if( value == Tool.Resize )
+  {
+    return "Resize";
+  }
+  if( value == Tool.Mask )
+  {
+    return "Mask";
+  }
+
+  let msg : string = `Value ${value} cannot be converted to Tool. Possible values are: `;
+  {
+    let _LineMeasure_enumValue : string = Tool.LineMeasure; // enums are strings in stonecodegen, so this will work.
+    let msg_LineMeasure : string = `LineMeasure (${_LineMeasure_enumValue}), `;
+    msg = msg + msg_LineMeasure;
+  }
+  {
+    let _CircleMeasure_enumValue : string = Tool.CircleMeasure; // enums are strings in stonecodegen, so this will work.
+    let msg_CircleMeasure : string = `CircleMeasure (${_CircleMeasure_enumValue}), `;
+    msg = msg + msg_CircleMeasure;
+  }
+  {
+    let _Crop_enumValue : string = Tool.Crop; // enums are strings in stonecodegen, so this will work.
+    let msg_Crop : string = `Crop (${_Crop_enumValue}), `;
+    msg = msg + msg_Crop;
+  }
+  {
+    let _Windowing_enumValue : string = Tool.Windowing; // enums are strings in stonecodegen, so this will work.
+    let msg_Windowing : string = `Windowing (${_Windowing_enumValue}), `;
+    msg = msg + msg_Windowing;
+  }
+  {
+    let _Zoom_enumValue : string = Tool.Zoom; // enums are strings in stonecodegen, so this will work.
+    let msg_Zoom : string = `Zoom (${_Zoom_enumValue}), `;
+    msg = msg + msg_Zoom;
+  }
+  {
+    let _Pan_enumValue : string = Tool.Pan; // enums are strings in stonecodegen, so this will work.
+    let msg_Pan : string = `Pan (${_Pan_enumValue}), `;
+    msg = msg + msg_Pan;
+  }
+  {
+    let _Move_enumValue : string = Tool.Move; // enums are strings in stonecodegen, so this will work.
+    let msg_Move : string = `Move (${_Move_enumValue}), `;
+    msg = msg + msg_Move;
+  }
+  {
+    let _Rotate_enumValue : string = Tool.Rotate; // enums are strings in stonecodegen, so this will work.
+    let msg_Rotate : string = `Rotate (${_Rotate_enumValue}), `;
+    msg = msg + msg_Rotate;
+  }
+  {
+    let _Resize_enumValue : string = Tool.Resize; // enums are strings in stonecodegen, so this will work.
+    let msg_Resize : string = `Resize (${_Resize_enumValue}), `;
+    msg = msg + msg_Resize;
+  }
+  {
+    let _Mask_enumValue : string = Tool.Mask; // enums are strings in stonecodegen, so this will work.
+    let msg_Mask : string = `Mask (${_Mask_enumValue})`;
+    msg = msg + msg_Mask;
+  }
+  throw new Error(msg);
+}
+
+export enum ActionType {
+  UndoCrop = "UndoCrop",
+  Rotate = "Rotate",
+  Invert = "Invert"
+};
+
+export function ActionType_FromString(strValue:string) : ActionType
+{
+  if( strValue == "UndoCrop" )
+  {
+    return ActionType.UndoCrop;
+  }
+  if( strValue == "Rotate" )
+  {
+    return ActionType.Rotate;
+  }
+  if( strValue == "Invert" )
+  {
+    return ActionType.Invert;
+  }
+
+  let msg : string =  `String ${strValue} cannot be converted to ActionType. Possible values are: UndoCrop, Rotate, Invert`;
+  throw new Error(msg);
+}
+
+export function ActionType_ToString(value:ActionType) : string
+{
+  if( value == ActionType.UndoCrop )
+  {
+    return "UndoCrop";
+  }
+  if( value == ActionType.Rotate )
+  {
+    return "Rotate";
+  }
+  if( value == ActionType.Invert )
+  {
+    return "Invert";
+  }
+
+  let msg : string = `Value ${value} cannot be converted to ActionType. Possible values are: `;
+  {
+    let _UndoCrop_enumValue : string = ActionType.UndoCrop; // enums are strings in stonecodegen, so this will work.
+    let msg_UndoCrop : string = `UndoCrop (${_UndoCrop_enumValue}), `;
+    msg = msg + msg_UndoCrop;
+  }
+  {
+    let _Rotate_enumValue : string = ActionType.Rotate; // enums are strings in stonecodegen, so this will work.
+    let msg_Rotate : string = `Rotate (${_Rotate_enumValue}), `;
+    msg = msg + msg_Rotate;
+  }
+  {
+    let _Invert_enumValue : string = ActionType.Invert; // enums are strings in stonecodegen, so this will work.
+    let msg_Invert : string = `Invert (${_Invert_enumValue})`;
+    msg = msg + msg_Invert;
+  }
+  throw new Error(msg);
+}
+
+
+
+export class SelectTool {
+  tool:Tool;
+
+  constructor() {
+  }
+
+  public StoneSerialize(): string {
+    let container: object = {};
+    container['type'] = 'StoneSampleCommands.SelectTool';
+    container['value'] = this;
+    return JSON.stringify(container);
+  }
+
+  public static StoneDeserialize(valueStr: string) : SelectTool
+  {
+    let value: any = JSON.parse(valueStr);
+    StoneCheckSerializedValueType(value, 'StoneSampleCommands.SelectTool');
+    let result: SelectTool = value['value'] as SelectTool;
+    return result;
+  }
+}
+export class Action {
+  type:ActionType;
+
+  constructor() {
+  }
+
+  public StoneSerialize(): string {
+    let container: object = {};
+    container['type'] = 'StoneSampleCommands.Action';
+    container['value'] = this;
+    return JSON.stringify(container);
+  }
+
+  public static StoneDeserialize(valueStr: string) : Action
+  {
+    let value: any = JSON.parse(valueStr);
+    StoneCheckSerializedValueType(value, 'StoneSampleCommands.Action');
+    let result: Action = value['value'] as Action;
+    return result;
+  }
+}
+
+export interface IHandler {
+};
+
+/** Service function for StoneDispatchToHandler */
+export function StoneDispatchJsonToHandler(
+  jsonValue: any, handler: IHandler): boolean
+{
+  StoneCheckSerializedValueTypeGeneric(jsonValue);
+  let type: string = jsonValue["type"];
+  if (type == "")
+  {
+    // this should never ever happen
+    throw new Error("Caught empty type while dispatching");
+  }
+  else
+  {
+    return false;
+  }
+}
+
+/** Takes a serialized type and passes this to the handler */
+export function StoneDispatchToHandler(
+  strValue: string, handler: IHandler): boolean
+{
+  // console.//log("+------------------------------------------------+");
+  // console.//log("|            StoneDispatchToHandler              |");
+  // console.//log("+------------------------------------------------+");
+  // console.//log("strValue = ");
+  // console.//log(strValue);
+  let jsonValue: any = JSON.parse(strValue)
+  return StoneDispatchJsonToHandler(jsonValue, handler);
 }
\ No newline at end of file
--- a/Applications/Samples/build-web-ext.sh	Tue May 14 18:24:12 2019 +0200
+++ b/Applications/Samples/build-web-ext.sh	Thu May 16 09:11:14 2019 +0200
@@ -1,58 +1,58 @@
-#!/bin/bash
-
-set -e
-
-target=${1:-all}
-# this script currently assumes that the wasm code has been built on its side and is availabie in build-wasm/
-
-currentDir=$(pwd)
-
-scriptDirRel=$(dirname $0)
-#echo $scriptDirRel
-scriptDirAbs=$(realpath $scriptDirRel)
-echo $scriptDirAbs
-
-samplesRootDir=scriptDirAbs
-
-outputDir=$samplesRootDir/build-web/
-mkdir -p $outputDir
-
-# files used by all single files samples
-cp $samplesRootDir/Web/index.html $outputDir
-cp $samplesRootDir/Web/samples-styles.css $outputDir
-
-# build simple-viewer-single-file (obsolete project)
-if [[ $target == "all" || $target == "OrthancStoneSimpleViewerSingleFile" ]]; then
-  cp $samplesRootDir/Web/simple-viewer-single-file.html $outputDir
-  tsc --allowJs --project $samplesRootDir/Web/simple-viewer-single-file.tsconfig.json
-  cp $currentDir/build-wasm/OrthancStoneSimpleViewerSingleFile.js  $outputDir
-  cp $currentDir/build-wasm/OrthancStoneSimpleViewerSingleFile.wasm  $outputDir
-fi
-
-# build single-frame
-if [[ $target == "all" || $target == "OrthancStoneSingleFrame" ]]; then
-  cp $samplesRootDir/Web/single-frame.html $outputDir
-  tsc --allowJs --project $samplesRootDir/Web/single-frame.tsconfig.json
-  cp $currentDir/build-wasm/OrthancStoneSingleFrame.js  $outputDir
-  cp $currentDir/build-wasm/OrthancStoneSingleFrame.wasm  $outputDir
-fi
-
-# build single-frame-editor
-if [[ $target == "all" || $target == "OrthancStoneSingleFrameEditor" ]]; then
-  cp $samplesRootDir/Web/single-frame-editor.html $outputDir
-  tsc --allowJs --project $samplesRootDir/Web/single-frame-editor.tsconfig.json
-  cp $currentDir/build-wasm/OrthancStoneSingleFrameEditor.js  $outputDir
-  cp $currentDir/build-wasm/OrthancStoneSingleFrameEditor.wasm  $outputDir
-fi
-
-# build simple-viewer project
-if [[ $target == "all" || $target == "OrthancStoneSimpleViewer" ]]; then
-  mkdir -p $outputDir/simple-viewer/
-  cp $samplesRootDir/SimpleViewer/Wasm/simple-viewer.html $outputDir/simple-viewer/
-  cp $samplesRootDir/SimpleViewer/Wasm/styles.css $outputDir/simple-viewer/
-  tsc --allowJs --project $samplesRootDir/SimpleViewer/Wasm/tsconfig-simple-viewer.json
-  cp $currentDir/build-wasm/OrthancStoneSimpleViewer.js  $outputDir/simple-viewer/
-  cp $currentDir/build-wasm/OrthancStoneSimpleViewer.wasm  $outputDir/simple-viewer/
-fi
-
-cd $currentDir
+#!/bin/bash
+
+set -e
+
+target=${1:-all}
+# this script currently assumes that the wasm code has been built on its side and is availabie in build-wasm/
+
+currentDir=$(pwd)
+
+scriptDirRel=$(dirname $0)
+#echo $scriptDirRel
+scriptDirAbs=$(realpath $scriptDirRel)
+echo $scriptDirAbs
+
+samplesRootDir=scriptDirAbs
+
+outputDir=$samplesRootDir/build-web/
+mkdir -p $outputDir
+
+# files used by all single files samples
+cp $samplesRootDir/Web/index.html $outputDir
+cp $samplesRootDir/Web/samples-styles.css $outputDir
+
+# build simple-viewer-single-file (obsolete project)
+if [[ $target == "all" || $target == "OrthancStoneSimpleViewerSingleFile" ]]; then
+  cp $samplesRootDir/Web/simple-viewer-single-file.html $outputDir
+  tsc --allowJs --project $samplesRootDir/Web/simple-viewer-single-file.tsconfig.json
+  cp $currentDir/build-wasm/OrthancStoneSimpleViewerSingleFile.js  $outputDir
+  cp $currentDir/build-wasm/OrthancStoneSimpleViewerSingleFile.wasm  $outputDir
+fi
+
+# build single-frame
+if [[ $target == "all" || $target == "OrthancStoneSingleFrame" ]]; then
+  cp $samplesRootDir/Web/single-frame.html $outputDir
+  tsc --allowJs --project $samplesRootDir/Web/single-frame.tsconfig.json
+  cp $currentDir/build-wasm/OrthancStoneSingleFrame.js  $outputDir
+  cp $currentDir/build-wasm/OrthancStoneSingleFrame.wasm  $outputDir
+fi
+
+# build single-frame-editor
+if [[ $target == "all" || $target == "OrthancStoneSingleFrameEditor" ]]; then
+  cp $samplesRootDir/Web/single-frame-editor.html $outputDir
+  tsc --allowJs --project $samplesRootDir/Web/single-frame-editor.tsconfig.json
+  cp $currentDir/build-wasm/OrthancStoneSingleFrameEditor.js  $outputDir
+  cp $currentDir/build-wasm/OrthancStoneSingleFrameEditor.wasm  $outputDir
+fi
+
+# build simple-viewer project
+if [[ $target == "all" || $target == "OrthancStoneSimpleViewer" ]]; then
+  mkdir -p $outputDir/simple-viewer/
+  cp $samplesRootDir/SimpleViewer/Wasm/simple-viewer.html $outputDir/simple-viewer/
+  cp $samplesRootDir/SimpleViewer/Wasm/styles.css $outputDir/simple-viewer/
+  tsc --allowJs --project $samplesRootDir/SimpleViewer/Wasm/tsconfig-simple-viewer.json
+  cp $currentDir/build-wasm/OrthancStoneSimpleViewer.js  $outputDir/simple-viewer/
+  cp $currentDir/build-wasm/OrthancStoneSimpleViewer.wasm  $outputDir/simple-viewer/
+fi
+
+cd $currentDir
--- a/Applications/Samples/get-requirements-windows.ps1	Tue May 14 18:24:12 2019 +0200
+++ b/Applications/Samples/get-requirements-windows.ps1	Thu May 16 09:11:14 2019 +0200
@@ -1,50 +1,50 @@
-
-if ($true) {
-
-    Write-Error "This script is obsolete. Please work under WSL and run build-wasm.sh"
-
-} else {
-
-    param(
-        [IO.DirectoryInfo] $EmsdkRootDir = "C:\Emscripten",
-        [bool] $Overwrite = $false
-      )
-    
-    if (Test-Path -Path $EmsdkRootDir) {
-        if( $Override) {
-            Remove-Item -Path $EmsdkRootDir -Force -Recurse
-        } else {
-            throw "The `"$EmsdkRootDir`" folder may not exist! Use the Overwrite flag to bypass this check."
-        }
-    }
-    
-    # TODO: detect whether git is installed
-    # choco install -y git
-    
-    Write-Host "Will retrieve the Emscripten SDK to the `"$EmsdkRootDir`" folder"
-    
-    $EmsdkParentDir = split-path -Parent $EmsdkRootDir
-    $EmsdkRootName = split-path -Leaf $EmsdkRootDir
-    
-    Push-Location $EmsdkParentDir
-    
-    git clone https://github.com/juj/emsdk.git $EmsdkRootName
-    cd $EmsdkRootName
-    
-    git pull
-    
-    ./emsdk install latest
-    
-    ./emsdk activate latest
-    
-    echo "INFO: the ~/.emscripten file has been configured for this installation of Emscripten."
-    
-    Write-Host "emsdk is now installed in $EmsdkRootDir"
-    
-    Pop-Location
-
-}
-
-
-
-
+
+if ($true) {
+
+    Write-Error "This script is obsolete. Please work under WSL and run build-wasm.sh"
+
+} else {
+
+    param(
+        [IO.DirectoryInfo] $EmsdkRootDir = "C:\Emscripten",
+        [bool] $Overwrite = $false
+      )
+    
+    if (Test-Path -Path $EmsdkRootDir) {
+        if( $Override) {
+            Remove-Item -Path $EmsdkRootDir -Force -Recurse
+        } else {
+            throw "The `"$EmsdkRootDir`" folder may not exist! Use the Overwrite flag to bypass this check."
+        }
+    }
+    
+    # TODO: detect whether git is installed
+    # choco install -y git
+    
+    Write-Host "Will retrieve the Emscripten SDK to the `"$EmsdkRootDir`" folder"
+    
+    $EmsdkParentDir = split-path -Parent $EmsdkRootDir
+    $EmsdkRootName = split-path -Leaf $EmsdkRootDir
+    
+    Push-Location $EmsdkParentDir
+    
+    git clone https://github.com/juj/emsdk.git $EmsdkRootName
+    cd $EmsdkRootName
+    
+    git pull
+    
+    ./emsdk install latest
+    
+    ./emsdk activate latest
+    
+    echo "INFO: the ~/.emscripten file has been configured for this installation of Emscripten."
+    
+    Write-Host "emsdk is now installed in $EmsdkRootDir"
+    
+    Pop-Location
+
+}
+
+
+
+
--- a/Applications/Samples/rt-viewer-demo/main.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Applications/Samples/rt-viewer-demo/main.cpp	Thu May 16 09:11:14 2019 +0200
@@ -546,6 +546,24 @@
 
       void ParseParameters(const boost::program_options::variables_map&  parameters)
       {
+        // Generic
+        {
+          if (parameters.count("verbose"))
+          {
+            Orthanc::Logging::EnableInfoLevel(true);
+            LOG(INFO) << "Verbose logs (info) are enabled";
+          }
+        }
+
+        {
+          if (parameters.count("trace"))
+          {
+            LOG(INFO) << "parameters.count(\"trace\") != 0";
+            Orthanc::Logging::EnableTraceLevel(true);
+            VLOG(1) << "Trace logs (debug) are enabled";
+          }
+        }
+
         // CT series
         {
 
@@ -570,12 +588,14 @@
             if (parameters.count("dose-series") != 1)
             {
               LOG(ERROR) << "the RTDOSE series is missing";
-              throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+              throw Orthanc::OrthancException(
+                Orthanc::ErrorCode_ParameterOutOfRange);
             }
             doseSeries_ = parameters["ct"].as<std::string>();
 #endif
             LOG(ERROR) << "the RTSTRUCT instance is missing";
-            throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+            throw Orthanc::OrthancException(
+              Orthanc::ErrorCode_ParameterOutOfRange);
           }
         }
         
@@ -592,26 +612,40 @@
             if (parameters.count("struct-series") != 1)
             {
               LOG(ERROR) << "the RTSTRUCT series is missing";
-              throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+              throw Orthanc::OrthancException(
+                Orthanc::ErrorCode_ParameterOutOfRange);
             }
-            structSeries_ = parametersstruct - series"].as<std::string>();
+            structSeries_ = parameters["struct-series"].as<std::string>();
 #endif
             LOG(ERROR) << "the RTSTRUCT instance is missing";
-            throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+            throw Orthanc::OrthancException(
+              Orthanc::ErrorCode_ParameterOutOfRange);
           }
         }
       }
 
-      virtual void DeclareStartupOptions(boost::program_options::options_description& options)
+      virtual void DeclareStartupOptions(
+        boost::program_options::options_description& options)
       {
-        boost::program_options::options_description generic("RtViewerDemo options. Please note that some of these options are mutually exclusive");
+        boost::program_options::options_description generic(
+          "RtViewerDemo options. Please note that some of these options "
+          "are mutually exclusive");
         generic.add_options()
-          ("ct-series", boost::program_options::value<std::string>(),"Orthanc ID of the CT series")
-          ("dose-instance", boost::program_options::value<std::string>(), "Orthanc ID of the RTDOSE instance (incompatible with dose-series)")
-          ("dose-series", boost::program_options::value<std::string>(), "NOT IMPLEMENTED YET. Orthanc ID of the RTDOSE series (incompatible with dose-instance)")
-          ("struct-instance", boost::program_options::value<std::string>(), "Orthanc ID of the RTSTRUCT instance (incompatible with struct-series)")
-          ("struct-series", boost::program_options::value<std::string>(), "NOT IMPLEMENTED YET. Orthanc ID of the RTSTRUCT (incompatible with struct-instance)")
-          ("smooth", boost::program_options::value<bool>()->default_value(true),"Enable bilinear image smoothing")
+          ("ct-series", boost::program_options::value<std::string>(),
+            "Orthanc ID of the CT series")
+          ("dose-instance", boost::program_options::value<std::string>(), 
+            "Orthanc ID of the RTDOSE instance (incompatible with dose-series)")
+          ("dose-series", boost::program_options::value<std::string>(), 
+            "NOT IMPLEMENTED YET. Orthanc ID of the RTDOSE series (incompatible"
+            " with dose-instance)")
+          ("struct-instance", boost::program_options::value<std::string>(), 
+            "Orthanc ID of the RTSTRUCT instance (incompatible with struct-"
+            "series)")
+          ("struct-series", boost::program_options::value<std::string>(), 
+            "NOT IMPLEMENTED YET. Orthanc ID of the RTSTRUCT (incompatible with"
+            " struct-instance)")
+          ("smooth", boost::program_options::value<bool>()->default_value(true),
+            "Enable bilinear image smoothing")
           ;
 
         options.add(generic);
@@ -637,8 +671,10 @@
           ct_.reset(new OrthancStone::OrthancVolumeImage(
             IObserver::GetBroker(), context->GetOrthancApiClient(), false));
           ct_->ScheduleLoadSeries(ctSeries_);
-          //ct_->ScheduleLoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");  // IBA
-          //ct_->ScheduleLoadSeries("03677739-1d8bca40-db1daf59-d74ff548-7f6fc9c0");  // 0522c0001 TCIA
+          //ct_->ScheduleLoadSeries(
+          //  "a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa"); // IBA
+          //ct_->ScheduleLoadSeries(
+          //  "03677739-1d8bca40-db1daf59-d74ff548-7f6fc9c0"); // 0522c0001 TCIA
         }
 
         if (!doseSeries_.empty() ||
@@ -665,8 +701,10 @@
             dose_->ScheduleLoadInstance(doseInstance_);
           }
 
-          //dose_->ScheduleLoadInstance("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb");  // IBA 1
-          //dose_->ScheduleLoadInstance("269f26f4-0c83eeeb-2e67abbd-5467a40f-f1bec90c");  // 0522c0001 TCIA
+          //dose_->ScheduleLoadInstance(
+            //"830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb");  // IBA 1
+          //dose_->ScheduleLoadInstance(
+            //"269f26f4-0c83eeeb-2e67abbd-5467a40f-f1bec90c"); //0522c0001 TCIA
         }
 
         if (!structInstance_.empty())
@@ -676,8 +714,10 @@
 
           struct_->ScheduleLoadInstance(structInstance_);
 
-          //struct_->ScheduleLoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // IBA
-          //struct_->ScheduleLoadInstance("17cd032b-ad92a438-ca05f06a-f9e96668-7e3e9e20");  // 0522c0001 TCIA
+          //struct_->ScheduleLoadInstance(
+            //"54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9"); // IBA
+          //struct_->ScheduleLoadInstance(
+            //"17cd032b-ad92a438-ca05f06a-f9e96668-7e3e9e20"); // 0522c0001 TCIA
         }
 
         mainWidget_ = new LayoutWidget("main-layout");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Sdl/SdlOpenGLWindow.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,122 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "SdlOpenGLWindow.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 <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  SdlOpenGLWindow::SdlOpenGLWindow(const char* title,
+                                   unsigned int width,
+                                   unsigned int height,
+                                   bool allowDpiScaling) :
+    window_(title, width, height, true /* enable OpenGL */, allowDpiScaling)
+  {
+    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)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                          "Cannot initialize glew");
+        }
+
+        isGlewInitialized_ = true;
+      }
+    }    
+#endif
+  }
+
+  
+  SdlOpenGLWindow::~SdlOpenGLWindow()
+  {
+    SDL_GL_DeleteContext(context_);
+  }
+
+
+  void SdlOpenGLWindow::MakeCurrent()
+  {
+    if (SDL_GL_MakeCurrent(window_.GetObject(), context_) != 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                      "Cannot set current OpenGL context");
+    }
+
+    // This makes our buffer swap syncronized with the monitor's vertical refresh
+    SDL_GL_SetSwapInterval(1);
+  }
+
+  
+  void SdlOpenGLWindow::SwapBuffer()
+  {
+    // Swap our buffer to display the current contents of buffer on screen
+    SDL_GL_SwapWindow(window_.GetObject());
+  }
+
+  
+  unsigned int SdlOpenGLWindow::GetCanvasWidth() const
+  {
+    int w = 0;
+    SDL_GL_GetDrawableSize(window_.GetObject(), &w, NULL);
+    return static_cast<unsigned int>(w);
+  }
+
+  
+  unsigned int SdlOpenGLWindow::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/Applications/Sdl/SdlOpenGLWindow.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,60 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if ORTHANC_ENABLE_SDL == 1
+
+#include "../../Framework/OpenGL/IOpenGLContext.h"
+#include "SdlWindow.h"
+
+namespace OrthancStone
+{
+  class SdlOpenGLWindow : public OpenGL::IOpenGLContext
+  {
+  private:
+    SdlWindow      window_;
+    SDL_GLContext  context_;
+
+  public:
+    SdlOpenGLWindow(const char* title,
+                    unsigned int width,
+                    unsigned int height,
+                    bool allowDpiScaling = true);
+
+    ~SdlOpenGLWindow();
+
+    SdlWindow& GetWindow()
+    {
+      return window_;
+    }
+
+    virtual void MakeCurrent();
+
+    virtual void SwapBuffer();
+
+    virtual unsigned int GetCanvasWidth() const;
+
+    virtual unsigned int GetCanvasHeight() const;
+  };
+}
+
+#endif
--- a/Applications/Sdl/SdlWindow.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Applications/Sdl/SdlWindow.cpp	Thu May 16 09:11:14 2019 +0200
@@ -26,6 +26,11 @@
 #include <Core/Logging.h>
 #include <Core/OrthancException.h>
 
+#ifdef WIN32 
+#include <windows.h> // for SetProcessDpiAware
+#endif 
+// WIN32
+
 #include <SDL.h>
 
 namespace OrthancStone
@@ -33,7 +38,8 @@
   SdlWindow::SdlWindow(const char* title,
                        unsigned int width,
                        unsigned int height,
-                       bool enableOpenGl) :
+                       bool enableOpenGl,
+                       bool allowDpiScaling) :
     maximized_(false)
   {
     // TODO Understand why, with SDL_WINDOW_OPENGL + MinGW32 + Release
@@ -51,6 +57,33 @@
       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,
                                SDL_WINDOWPOS_UNDEFINED,
--- a/Applications/Sdl/SdlWindow.h	Tue May 14 18:24:12 2019 +0200
+++ b/Applications/Sdl/SdlWindow.h	Thu May 16 09:11:14 2019 +0200
@@ -40,7 +40,8 @@
     SdlWindow(const char* title,
               unsigned int width,
               unsigned int height,
-              bool enableOpenGl);
+              bool enableOpenGl,
+              bool allowDpiScaling = true);
 
     ~SdlWindow();
 
--- a/Applications/Wasm/StartupParametersBuilder.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Applications/Wasm/StartupParametersBuilder.cpp	Thu May 16 09:11:14 2019 +0200
@@ -1,43 +1,71 @@
 #include "StartupParametersBuilder.h"
+#include <iostream>
+#include <cstdio>
+#include "emscripten/html5.h"
 
 namespace OrthancStone
 {
-    void StartupParametersBuilder::Clear() {
-        startupParameters_.clear();
-    }
+  void StartupParametersBuilder::Clear()
+  {
+    startupParameters_.clear();
+  }
+
+  void StartupParametersBuilder::SetStartupParameter(
+    const char* name,
+    const char* value)
+  {
+    startupParameters_.push_back(std::make_tuple(name, value));
+  }
 
-    void StartupParametersBuilder::SetStartupParameter(const char* name, const char* value) {
-        startupParameters_.push_back(std::make_tuple(name, value));
+  void StartupParametersBuilder::GetStartupParameters(
+    boost::program_options::variables_map& parameters,
+    const boost::program_options::options_description& options) 
+  {
+    std::vector<std::string> argvStrings(startupParameters_.size() + 1);
+    // argv mirrors pointers to the internal argvStrings buffers.
+    // ******************************************************
+    // THIS IS HIGHLY DANGEROUS SO BEWARE!!!!!!!!!!!!!!
+    // ******************************************************
+    std::vector<const char*> argv(startupParameters_.size() + 1);
+    
+    int argCounter = 0;
+    argvStrings[argCounter] = "dummy.exe";
+    argv[argCounter] = argvStrings[argCounter].c_str();
+    
+    argCounter++;
+    
+    std::string cmdLine = "";
+    for ( StartupParameters::const_iterator it = startupParameters_.begin(); 
+          it != startupParameters_.end(); 
+          it++)
+    {
+      std::stringstream argSs;
+
+      argSs << "--" << std::get<0>(*it);
+      if(std::get<1>(*it).length() > 0)
+        argSs << "=" << std::get<1>(*it);
+      
+      argvStrings[argCounter] = argSs.str();
+      cmdLine = cmdLine + " " + argvStrings[argCounter];
+      std::cout << cmdLine << std::endl;
+      argv[argCounter] = argvStrings[argCounter].c_str();
+      argCounter++;
     }
 
-    void StartupParametersBuilder::GetStartupParameters(boost::program_options::variables_map& parameters, const boost::program_options::options_description& options) {
-        
-        const char* argv[startupParameters_.size() + 1];
-        int argCounter = 0;
-        argv[0] = "Toto.exe";
-        argCounter++;
 
-        std::string cmdLine = "";
-        for (StartupParameters::const_iterator it = startupParameters_.begin(); it != startupParameters_.end(); it++) {
-            char* arg = new char[128];
-            snprintf(arg, 128, "--%s=%s", std::get<0>(*it).c_str(), std::get<1>(*it).c_str());
-            argv[argCounter] = arg;
-            cmdLine = cmdLine + " --" + std::get<0>(*it) + "=" + std::get<1>(*it);
-            argCounter++;
-        }
+    std::cout << "simulated cmdLine = \"" << cmdLine.c_str() << "\"\n";
 
-        printf("simulated cmdLine = %s\n", cmdLine.c_str());
-
-        try
-        {
-            boost::program_options::store(boost::program_options::command_line_parser(argCounter, argv).
-                                            options(options).allow_unregistered().run(), parameters);
-            boost::program_options::notify(parameters);
-        }
-        catch (boost::program_options::error& e)
-        {
-            printf("Error while parsing the command-line arguments: %s\n", e.what());
-        }
-
+    try
+    {
+      boost::program_options::store(
+        boost::program_options::command_line_parser(argCounter, argv.data()).
+          options(options).allow_unregistered().run(), parameters);
+      boost::program_options::notify(parameters);
     }
-}
\ No newline at end of file
+    catch (boost::program_options::error& e)
+    {
+      std::cerr << "Error while parsing the command-line arguments: " <<
+        e.what() << std::endl;    
+    }
+  }
+}
--- a/Applications/Wasm/StartupParametersBuilder.h	Tue May 14 18:24:12 2019 +0200
+++ b/Applications/Wasm/StartupParametersBuilder.h	Thu May 16 09:11:14 2019 +0200
@@ -43,8 +43,12 @@
   public:
 
     void Clear();
+    // Please note that if a parameter is a flag-style one, the value that 
+    // is passed should be an empty string
     void SetStartupParameter(const char* name, const char* value);
-    void GetStartupParameters(boost::program_options::variables_map& parameters_, const boost::program_options::options_description& options);
+    void GetStartupParameters(
+      boost::program_options::variables_map& parameters_, 
+      const boost::program_options::options_description& options);
   };
 
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Fonts/FontRenderer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,185 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "FontRenderer.h"
+
+#include "../Toolbox/DynamicBitmap.h"
+
+#include <Core/OrthancException.h>
+
+
+#include <ft2build.h>
+#include FT_FREETYPE_H
+#include FT_GLYPH_H
+
+
+// https://stackoverflow.com/questions/31161284/how-can-i-get-the-corresponding-error-string-from-an-ft-error-code
+static std::string GetErrorMessage(FT_Error err)
+{
+#undef __FTERRORS_H__
+#define FT_ERRORDEF( e, v, s )  case e: return s;
+#define FT_ERROR_START_LIST     switch (err) {
+#define FT_ERROR_END_LIST       }
+#include FT_ERRORS_H
+  return "(Unknown error)";
+}
+
+
+static void CheckError(FT_Error err)
+{
+  if (err != 0)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                    "Error in FreeType: " + GetErrorMessage(err));
+  }
+}
+
+
+namespace OrthancStone
+{
+  class FontRenderer::PImpl : public boost::noncopyable
+  {
+  private:
+    std::string  fontContent_;
+    FT_Library   library_;
+    FT_Face      face_;
+
+    void Clear()
+    {
+      if (face_ != NULL)
+      {
+        FT_Done_Face(face_);        
+        face_ = NULL;
+      }
+
+      fontContent_.clear();
+    }
+
+  public:
+    PImpl() :
+      library_(NULL),
+      face_(NULL)
+    {
+      CheckError(FT_Init_FreeType(&library_));
+    }
+
+    
+    ~PImpl()
+    {
+      Clear();
+      FT_Done_FreeType(library_);
+    }
+
+    
+    void LoadFont(const std::string& fontContent,
+                  unsigned int fontSize)
+    {
+      Clear();
+
+      // It is necessary to make a private copy of the font, as
+      // Freetype makes the assumption that the buffer containing the
+      // font is never deleted
+      fontContent_.assign(fontContent);
+      
+      const FT_Byte* data = reinterpret_cast<const FT_Byte*>(fontContent_.c_str());
+
+      CheckError(FT_New_Memory_Face(library_, data, fontContent_.size(), 0, &face_));
+      CheckError(FT_Set_Char_Size(face_,         // handle to face object  
+                                  0,             // char_width in 1/64th of points  
+                                  fontSize * 64, // char_height in 1/64th of points 
+                                  72,            // horizontal device resolution 
+                                  72));          // vertical device resolution
+
+      CheckError(FT_Select_Charmap(face_, FT_ENCODING_UNICODE));
+    }
+    
+
+    Glyph* Render(uint32_t unicode)
+    {
+      if (face_ == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls,
+                                        "First call LoadFont()");
+      }
+      else if (FT_Load_Char(face_, unicode, FT_LOAD_RENDER) != 0)
+      {
+        // This character is not available
+        return NULL;
+      }
+      else
+      {
+        if (face_->glyph->format != FT_GLYPH_FORMAT_BITMAP)                 
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+          //CheckError(FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, 0, 1));
+        }
+
+        Orthanc::ImageAccessor bitmap;
+        bitmap.AssignReadOnly(Orthanc::PixelFormat_Grayscale8,
+                              face_->glyph->bitmap.width,
+                              face_->glyph->bitmap.rows,
+                              face_->glyph->bitmap.pitch,
+                              face_->glyph->bitmap.buffer);
+
+        std::auto_ptr<Glyph> glyph(
+          new Glyph(bitmap.GetWidth(),
+                    bitmap.GetHeight(),
+                    face_->glyph->bitmap_left,
+                    -face_->glyph->bitmap_top,  // Positive for an upwards vertical distance
+                    face_->glyph->advance.x >> 6,
+                    face_->glyph->metrics.vertAdvance >> 6));
+
+        glyph->SetPayload(new DynamicBitmap(bitmap));
+        
+        return glyph.release();
+      }
+    }
+  };
+
+
+
+  FontRenderer::FontRenderer() :
+    pimpl_(new PImpl)
+  {
+  }
+
+  
+  void FontRenderer::LoadFont(const std::string& fontContent,
+                              unsigned int fontSize)
+  {
+    pimpl_->LoadFont(fontContent, fontSize);
+  }
+
+  
+  void FontRenderer::LoadFont(Orthanc::EmbeddedResources::FileResourceId resource,
+                              unsigned int fontSize)
+  {
+    std::string content;
+    Orthanc::EmbeddedResources::GetFileResource(content, resource);
+    LoadFont(content, fontSize);
+  }
+
+  
+  Glyph* FontRenderer::Render(uint32_t unicode)
+  {
+    return pimpl_->Render(unicode);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Fonts/FontRenderer.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,51 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "Glyph.h"
+
+#include <EmbeddedResources.h>
+
+#include <stdint.h>
+#include <boost/shared_ptr.hpp>
+
+
+namespace OrthancStone
+{
+  class FontRenderer : public boost::noncopyable
+  {
+  private:
+    class PImpl;
+    boost::shared_ptr<PImpl>  pimpl_;
+    
+  public:
+    FontRenderer();
+
+    void LoadFont(const std::string& fontContent,
+                  unsigned int fontSize);
+    
+    void LoadFont(Orthanc::EmbeddedResources::FileResourceId resource,
+                  unsigned int fontSize);
+
+    Glyph* Render(uint32_t unicode);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Fonts/Glyph.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,93 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "Glyph.h"
+
+#include <Core/OrthancException.h>
+
+
+namespace OrthancStone
+{
+  Glyph::Glyph(const Glyph& other) : 
+    width_(other.width_),
+    height_(other.height_),
+    offsetLeft_(other.offsetLeft_),
+    offsetTop_(other.offsetTop_),
+    advanceX_(other.advanceX_),
+    lineHeight_(other.lineHeight_)
+  {
+  }
+  
+    
+  Glyph::Glyph(unsigned int width,
+               unsigned int height,
+               int offsetLeft,
+               int offsetTop,
+               int advanceX,
+               unsigned int lineHeight) :
+    width_(width),
+    height_(height),
+    offsetLeft_(offsetLeft),
+    offsetTop_(offsetTop),
+    advanceX_(advanceX),
+    lineHeight_(lineHeight)
+  {
+  }
+
+  
+  void Glyph::SetPayload(Orthanc::IDynamicObject* payload)  // Takes ownership
+  {
+    if (payload == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+    else
+    {
+      payload_.reset(payload);
+    }
+  }
+
+      
+  const Orthanc::IDynamicObject& Glyph::GetPayload() const
+  {
+    if (payload_.get() == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *payload_;
+    }
+  }
+
+  
+  Orthanc::IDynamicObject* Glyph::ReleasePayload()
+  {
+    if (payload_.get() == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return payload_.release();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Fonts/Glyph.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,95 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <Core/IDynamicObject.h>
+
+#include <memory>
+
+
+namespace OrthancStone
+{
+  class Glyph : public boost::noncopyable
+  {
+  private:
+    unsigned int   width_;
+    unsigned int   height_;
+    int            offsetLeft_;
+    int            offsetTop_;
+    int            advanceX_;
+    unsigned int   lineHeight_;
+      
+    std::auto_ptr<Orthanc::IDynamicObject>  payload_;
+
+  public:
+    // WARNING: This does not copy the payload
+    Glyph(const Glyph& other);
+    
+    Glyph(unsigned int width,
+          unsigned int height,
+          int offsetLeft,
+          int offsetTop,
+          int advanceX,
+          unsigned int lineHeight);
+
+    void SetPayload(Orthanc::IDynamicObject* payload);
+
+    int GetOffsetLeft() const
+    {
+      return offsetLeft_;
+    }
+
+    int GetOffsetTop() const
+    {
+      return offsetTop_;
+    }
+
+    unsigned int GetWidth() const
+    {
+      return width_;
+    }
+
+    unsigned int GetHeight() const
+    {
+      return height_;
+    }
+
+    unsigned int GetAdvanceX() const
+    {
+      return advanceX_;
+    }
+
+    unsigned int GetLineHeight() const
+    {
+      return lineHeight_;
+    }
+
+    bool HasPayload() const
+    {
+      return payload_.get() != NULL;
+    }
+      
+    const Orthanc::IDynamicObject& GetPayload() const;
+      
+    Orthanc::IDynamicObject* ReleasePayload();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Fonts/GlyphAlphabet.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,170 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "GlyphAlphabet.h"
+
+#include <Core/OrthancException.h>
+#include <Core/Toolbox.h>
+
+
+namespace OrthancStone
+{
+  void GlyphAlphabet::Clear()
+  {
+    for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+      
+    content_.clear();
+    lineHeight_ = 0;
+  }
+    
+    
+  void GlyphAlphabet::Register(uint32_t unicode,
+                               const Glyph& glyph,
+                               Orthanc::IDynamicObject* payload)
+  {
+    std::auto_ptr<Orthanc::IDynamicObject> protection(payload);
+      
+    // Don't add twice the same character
+    if (content_.find(unicode) == content_.end())
+    {
+      std::auto_ptr<Glyph> raii(new Glyph(glyph));
+        
+      if (payload != NULL)
+      {
+        raii->SetPayload(protection.release());
+      }
+
+      content_[unicode] = raii.release();
+
+      lineHeight_ = std::max(lineHeight_, glyph.GetLineHeight());
+    }
+  }
+
+
+  void GlyphAlphabet::Register(FontRenderer& renderer,
+                               uint32_t unicode)
+  {
+    std::auto_ptr<Glyph>  glyph(renderer.Render(unicode));
+      
+    if (glyph.get() != NULL)
+    {
+      Register(unicode, *glyph, glyph->ReleasePayload());
+    }
+  }
+    
+
+#if ORTHANC_ENABLE_LOCALE == 1
+  bool GlyphAlphabet::GetUnicodeFromCodepage(uint32_t& unicode,
+                                             unsigned int index,
+                                             Orthanc::Encoding encoding)
+  {
+    if (index > 255)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+      
+    std::string character;
+    character.resize(1);
+    character[0] = static_cast<unsigned char>(index);
+    
+    std::string utf8 = Orthanc::Toolbox::ConvertToUtf8(character, encoding, false /* no code extensions */);
+      
+    if (utf8.empty())
+    {
+      // This character is not available in this codepage
+      return false;
+    }
+    else
+    {
+      size_t length;
+      Orthanc::Toolbox::Utf8ToUnicodeCharacter(unicode, length, utf8, 0);
+      assert(length != 0);
+      return true;
+    }
+  }
+#endif
+
+
+  void GlyphAlphabet::Apply(IGlyphVisitor& visitor) const
+  {
+    for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      visitor.Visit(it->first, *it->second);
+    }
+  }
+
+
+  void GlyphAlphabet::Apply(ITextVisitor& visitor,
+                            const std::string& utf8) const
+  {
+    size_t pos = 0;
+    int x = 0;
+    int y = 0;
+
+    while (pos < utf8.size())
+    {
+      if (utf8[pos] == '\r')
+      {
+        // Ignore carriage return
+        pos++;
+      }
+      else if (utf8[pos] == '\n')
+      {
+        // This is a newline character
+        x = 0;
+        y += static_cast<int>(lineHeight_);
+
+        pos++;
+      }
+      else
+      {         
+        uint32_t unicode;
+        size_t length;
+        Orthanc::Toolbox::Utf8ToUnicodeCharacter(unicode, length, utf8, pos);
+
+        Content::const_iterator glyph = content_.find(unicode);
+
+        if (glyph != content_.end())
+        {
+          assert(glyph->second != NULL);
+          const Orthanc::IDynamicObject* payload =
+            (glyph->second->HasPayload() ? &glyph->second->GetPayload() : NULL);
+            
+          visitor.Visit(unicode,
+                        x + glyph->second->GetOffsetLeft(),
+                        y + glyph->second->GetOffsetTop(),
+                        glyph->second->GetWidth(),
+                        glyph->second->GetHeight(),
+                        payload);
+          x += glyph->second->GetAdvanceX();
+        }
+        
+        assert(length != 0);
+        pos += length;
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Fonts/GlyphAlphabet.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,105 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "FontRenderer.h"
+
+#include <Core/Enumerations.h>
+
+#include <map>
+
+namespace OrthancStone
+{
+  class GlyphAlphabet : public boost::noncopyable
+  {
+  public:
+    class ITextVisitor : public boost::noncopyable
+    {
+    public:
+      virtual ~ITextVisitor()
+      {
+      }
+
+      virtual void Visit(uint32_t unicode,
+                         int x,
+                         int y,
+                         unsigned int width,
+                         unsigned int height,
+                         const Orthanc::IDynamicObject* payload /* can be NULL */) = 0;
+    };
+
+
+    class IGlyphVisitor : public boost::noncopyable
+    {
+    public:
+      virtual ~IGlyphVisitor()
+      {
+      }
+
+      virtual void Visit(uint32_t unicode,
+                         const Glyph& glyph) = 0;
+    };
+
+    
+  private:
+    typedef std::map<uint32_t, Glyph*>  Content;
+
+    Content        content_;
+    unsigned int   lineHeight_;
+
+  public:
+    GlyphAlphabet() :
+      lineHeight_(0)
+    {
+    }
+
+    ~GlyphAlphabet()
+    {
+      Clear();
+    }
+    
+    void Clear();
+    
+    void Register(uint32_t unicode,
+                  const Glyph& glyph,
+                  Orthanc::IDynamicObject* payload);
+
+    void Register(FontRenderer& renderer,
+                  uint32_t unicode);
+
+#if ORTHANC_ENABLE_LOCALE == 1
+    static bool GetUnicodeFromCodepage(uint32_t& unicode,
+                                       unsigned int index,
+                                       Orthanc::Encoding encoding);
+#endif
+
+    size_t GetSize() const
+    {
+      return content_.size();
+    }
+
+    void Apply(IGlyphVisitor& visitor) const;
+
+    void Apply(ITextVisitor& visitor,
+               const std::string& utf8) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Fonts/GlyphBitmapAlphabet.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,113 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "GlyphBitmapAlphabet.h"
+
+#include "TextBoundingBox.h"
+#include "../Toolbox/DynamicBitmap.h"
+
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+
+namespace OrthancStone
+{
+  class GlyphBitmapAlphabet::RenderTextVisitor : public GlyphAlphabet::ITextVisitor
+  {
+  private:
+    Orthanc::ImageAccessor&     target_;
+    const GlyphBitmapAlphabet&  that_;
+    int                         offsetX_;
+    int                         offsetY_;
+      
+  public:
+    RenderTextVisitor(Orthanc::ImageAccessor&  target,
+                      const GlyphBitmapAlphabet&  that,
+                      int  offsetX,
+                      int  offsetY) :
+      target_(target),
+      that_(that),
+      offsetX_(offsetX),
+      offsetY_(offsetY)
+    {
+    }
+
+    virtual void Visit(uint32_t unicode,
+                       int x,
+                       int y,
+                       unsigned int width,
+                       unsigned int height,
+                       const Orthanc::IDynamicObject* payload)
+    {
+      int left = x + offsetX_;
+      int top = y + offsetY_;
+
+      assert(payload != NULL);
+      const DynamicBitmap& glyph = *dynamic_cast<const DynamicBitmap*>(payload);
+        
+      assert(left >= 0 &&
+             top >= 0 &&
+             static_cast<unsigned int>(left) + width <= target_.GetWidth() &&
+             static_cast<unsigned int>(top) + height <= target_.GetHeight() &&
+             width == glyph.GetBitmap().GetWidth() &&
+             height == glyph.GetBitmap().GetHeight());
+        
+      {
+        Orthanc::ImageAccessor region;
+        target_.GetRegion(region, left, top, width, height);
+        Orthanc::ImageProcessing::Copy(region, glyph.GetBitmap());
+      }
+    }
+  };
+  
+    
+#if ORTHANC_ENABLE_LOCALE == 1
+  void GlyphBitmapAlphabet::LoadCodepage(FontRenderer& renderer,
+                                         Orthanc::Encoding codepage)
+  {
+    for (unsigned int i = 0; i < 256; i++)
+    {
+      uint32_t unicode;
+      if (GlyphAlphabet::GetUnicodeFromCodepage(unicode, i, codepage))
+      {
+        AddUnicodeCharacter(renderer, unicode);
+      }
+    }
+  }
+#endif
+
+    
+  Orthanc::ImageAccessor* GlyphBitmapAlphabet::RenderText(const std::string& utf8) const
+  {
+    TextBoundingBox box(alphabet_, utf8);
+
+    std::auto_ptr<Orthanc::ImageAccessor> bitmap(
+      new Orthanc::Image(Orthanc::PixelFormat_Grayscale8,
+                         box.GetWidth(), box.GetHeight(),
+                         true /* force minimal pitch */));
+
+    Orthanc::ImageProcessing::Set(*bitmap, 0);
+
+    RenderTextVisitor visitor(*bitmap, *this, -box.GetLeft(), -box.GetTop());
+    alphabet_.Apply(visitor, utf8);
+
+    return bitmap.release();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Fonts/GlyphBitmapAlphabet.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,58 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "GlyphAlphabet.h"
+
+#include <Core/Images/ImageAccessor.h>
+
+namespace OrthancStone
+{
+  class GlyphBitmapAlphabet : public boost::noncopyable
+  {
+  private:
+    class RenderTextVisitor;
+
+    GlyphAlphabet  alphabet_;
+
+  public:
+    const GlyphAlphabet& GetAlphabet() const
+    {
+      return alphabet_;
+    }    
+    
+    void AddUnicodeCharacter(FontRenderer& renderer,
+                             uint32_t unicode)
+    {
+      alphabet_.Register(renderer, unicode);
+    }
+
+
+#if ORTHANC_ENABLE_LOCALE == 1
+    void LoadCodepage(FontRenderer& renderer,
+                      Orthanc::Encoding codepage);
+#endif
+    
+    
+    Orthanc::ImageAccessor* RenderText(const std::string& utf8) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Fonts/GlyphTextureAlphabet.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,295 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "GlyphTextureAlphabet.h"
+
+#include "TextBoundingBox.h"
+#include "../Toolbox/DynamicBitmap.h"
+
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Core/OrthancException.h>
+
+#include <boost/math/special_functions/round.hpp>
+
+
+namespace OrthancStone
+{
+  class GlyphTextureAlphabet::GlyphSizeVisitor : public GlyphAlphabet::IGlyphVisitor
+  {
+  private:
+    unsigned int maxWidth_;
+    unsigned int maxHeight_;
+
+  public:
+    GlyphSizeVisitor() :
+      maxWidth_(0),
+      maxHeight_(0)
+    {
+    }
+      
+    virtual void Visit(uint32_t unicode,
+                       const Glyph& glyph)
+    {
+      maxWidth_ = std::max(maxWidth_, glyph.GetWidth());
+      maxHeight_ = std::max(maxHeight_, glyph.GetHeight());
+    }
+
+    unsigned int GetMaxWidth() const
+    {
+      return maxWidth_;
+    }
+
+    unsigned int GetMaxHeight() const
+    {
+      return maxHeight_;
+    }
+  };
+
+    
+  class GlyphTextureAlphabet::TextureGenerator : public GlyphAlphabet::IGlyphVisitor
+  {
+  private:
+    std::auto_ptr<Orthanc::ImageAccessor>  texture_;
+
+    unsigned int    countColumns_;
+    unsigned int    countRows_;
+    GlyphAlphabet&  targetAlphabet_;
+    unsigned int    glyphMaxWidth_;
+    unsigned int    glyphMaxHeight_;
+    unsigned int    column_;
+    unsigned int    row_;
+
+  public:
+    TextureGenerator(GlyphAlphabet& targetAlphabet,
+                     unsigned int countGlyphs,
+                     unsigned int glyphMaxWidth,
+                     unsigned int glyphMaxHeight) :
+      targetAlphabet_(targetAlphabet),
+      glyphMaxWidth_(glyphMaxWidth),
+      glyphMaxHeight_(glyphMaxHeight),
+      column_(0),
+      row_(0)
+    {
+      int c = boost::math::iround<int>(sqrt(static_cast<float>(countGlyphs)));
+
+      if (c <= 0)
+      {
+        countColumns_ = 1;
+      }
+      else
+      {
+        countColumns_ = static_cast<unsigned int>(c);
+      }
+
+      countRows_ = countGlyphs / countColumns_;
+      if (countGlyphs % countColumns_ != 0)
+      {
+        countRows_++;
+      }
+
+      texture_.reset(new Orthanc::Image(Orthanc::PixelFormat_RGBA32,
+                                        countColumns_ * glyphMaxWidth_,
+                                        countRows_ * glyphMaxHeight_,
+                                        true /* force minimal pitch */));
+
+      Orthanc::ImageProcessing::Set(*texture_, 0, 0, 0, 0);
+    }
+      
+      
+    virtual void Visit(uint32_t unicode,
+                       const Glyph& glyph)
+    {
+      if (!glyph.HasPayload())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+
+      if (column_ >= countColumns_ ||
+          row_ >= countRows_)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      unsigned int x = column_ * glyphMaxWidth_;
+      unsigned int y = row_ * glyphMaxHeight_;
+
+      const Orthanc::ImageAccessor& source = dynamic_cast<const DynamicBitmap&>(glyph.GetPayload()).GetBitmap();
+
+      if (source.GetFormat() != Orthanc::PixelFormat_Grayscale8)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+        
+      targetAlphabet_.Register(unicode, glyph, new TextureLocation(x, y));
+
+      Orthanc::ImageAccessor target;
+      texture_->GetRegion(target, x, y, source.GetWidth(), source.GetHeight());
+
+      //Orthanc::ImageProcessing::Copy(target, bitmap->GetBitmap());
+
+      for (unsigned int y = 0; y < source.GetHeight(); y++)
+      {
+        const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
+        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
+
+        for (unsigned int x = 0; x < source.GetWidth(); x++)
+        {
+          // Premultiplied alpha
+          q[0] = 0;
+          q[1] = 0;
+          q[2] = 0;
+          q[3] = *p;
+            
+          p++;
+          q += 4;
+        }
+      }
+        
+      column_++;
+      if (column_ == countColumns_)
+      {
+        column_ = 0;
+        row_++;
+      }
+    }
+
+
+    Orthanc::ImageAccessor* ReleaseTexture()
+    {
+      return texture_.release();
+    }
+  };
+
+
+  class GlyphTextureAlphabet::RenderTextVisitor : public GlyphAlphabet::ITextVisitor
+  {
+  private:
+    Orthanc::ImageAccessor&        target_;
+    const Orthanc::ImageAccessor&  texture_;
+    int                            offsetX_;
+    int                            offsetY_;
+      
+  public:
+    RenderTextVisitor(Orthanc::ImageAccessor&  target,
+                      const GlyphTextureAlphabet&  that,
+                      int  offsetX,
+                      int  offsetY) :
+      target_(target),
+      texture_(that.GetTexture()),
+      offsetX_(offsetX),
+      offsetY_(offsetY)
+    {
+    }
+
+    virtual void Visit(uint32_t unicode,
+                       int x,
+                       int y,
+                       unsigned int width,
+                       unsigned int height,
+                       const Orthanc::IDynamicObject* payload)
+    {
+      int left = x + offsetX_;
+      int top = y + offsetY_;
+
+      assert(payload != NULL);
+      const TextureLocation& location = *dynamic_cast<const TextureLocation*>(payload);
+        
+      assert(left >= 0 &&
+             top >= 0 &&
+             static_cast<unsigned int>(left) + width <= target_.GetWidth() &&
+             static_cast<unsigned int>(top) + height <= target_.GetHeight());
+        
+      {
+        Orthanc::ImageAccessor to;
+        target_.GetRegion(to, left, top, width, height);
+
+        Orthanc::ImageAccessor from;
+        texture_.GetRegion(from, location.GetX(), location.GetY(), width, height);
+                                       
+        Orthanc::ImageProcessing::Copy(to, from);
+      }
+    }
+  };
+
+    
+  GlyphTextureAlphabet::GlyphTextureAlphabet(const GlyphBitmapAlphabet& sourceAlphabet) :
+    textureWidth_(0),
+    textureHeight_(0)
+  {
+    GlyphSizeVisitor size;
+    sourceAlphabet.GetAlphabet().Apply(size);
+
+    TextureGenerator generator(alphabet_,
+                               sourceAlphabet.GetAlphabet().GetSize(),
+                               size.GetMaxWidth(),
+                               size.GetMaxHeight());
+    sourceAlphabet.GetAlphabet().Apply(generator);
+
+    texture_.reset(generator.ReleaseTexture());
+    textureWidth_ = texture_->GetWidth();
+    textureHeight_ = texture_->GetHeight();
+  }
+
+    
+  const Orthanc::ImageAccessor& GlyphTextureAlphabet::GetTexture() const
+  {
+    if (texture_.get() == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *texture_;
+    }
+  }
+    
+    
+  Orthanc::ImageAccessor* GlyphTextureAlphabet::ReleaseTexture()
+  {
+    if (texture_.get() == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return texture_.release();
+    }
+  }
+    
+
+  Orthanc::ImageAccessor* GlyphTextureAlphabet::RenderText(const std::string& utf8)
+  {
+    TextBoundingBox box(alphabet_, utf8);
+
+    std::auto_ptr<Orthanc::ImageAccessor> bitmap(
+      new Orthanc::Image(Orthanc::PixelFormat_RGBA32,
+                         box.GetWidth(), box.GetHeight(),
+                         true /* force minimal pitch */));
+
+    Orthanc::ImageProcessing::Set(*bitmap, 0, 0, 0, 0);
+
+    RenderTextVisitor visitor(*bitmap, *this, -box.GetLeft(), -box.GetTop());
+    alphabet_.Apply(visitor, utf8);
+
+    return bitmap.release();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Fonts/GlyphTextureAlphabet.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,92 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "GlyphBitmapAlphabet.h"
+
+#include <Core/Images/ImageAccessor.h>
+
+namespace OrthancStone
+{
+  class GlyphTextureAlphabet : public boost::noncopyable
+  {
+  public:
+    class TextureLocation : public Orthanc::IDynamicObject
+    {
+    private:
+      unsigned int  x_;
+      unsigned int  y_;
+
+    public:
+      TextureLocation(unsigned int x,
+                      unsigned int y) :
+        x_(x),
+        y_(y)
+      {
+      }
+
+      unsigned int GetX() const
+      {
+        return x_;
+      }
+
+      unsigned int GetY() const
+      {
+        return y_;
+      }
+    };
+
+  private:
+    class GlyphSizeVisitor;
+    class TextureGenerator;
+    class RenderTextVisitor;
+    
+    GlyphAlphabet                          alphabet_;
+    std::auto_ptr<Orthanc::ImageAccessor>  texture_;
+    unsigned int                           textureWidth_;
+    unsigned int                           textureHeight_;
+    
+  public:
+    GlyphTextureAlphabet(const GlyphBitmapAlphabet& sourceAlphabet);
+    
+    const Orthanc::ImageAccessor& GetTexture() const;
+    
+    Orthanc::ImageAccessor* ReleaseTexture();
+
+    Orthanc::ImageAccessor* RenderText(const std::string& utf8);
+
+    const GlyphAlphabet& GetAlphabet() const
+    {
+      return alphabet_;
+    }
+
+    unsigned int GetTextureWidth() const
+    {
+      return textureWidth_;
+    }
+
+    unsigned int GetTextureHeight() const
+    {
+      return textureHeight_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Fonts/OpenGLTextCoordinates.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,117 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OpenGLTextCoordinates.h"
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  namespace OpenGL
+  {
+    void OpenGLTextCoordinates::Visit(uint32_t unicode,
+                                      int x,
+                                      int y,
+                                      unsigned int width,
+                                      unsigned int height,
+                                      const Orthanc::IDynamicObject* payload)
+    {
+      // Rendering coordinates
+      float rx1 = x - box_.GetLeft();
+      float ry1 = y - box_.GetTop();
+      float rx2 = rx1 + static_cast<float>(width);
+      float ry2 = ry1 + static_cast<float>(height);
+
+      // Texture coordinates
+      assert(payload != NULL);
+      const GlyphTextureAlphabet::TextureLocation& location =
+        *dynamic_cast<const GlyphTextureAlphabet::TextureLocation*>(payload);
+
+      float tx1 = location.GetX() / textureWidth_;
+      float ty1 = location.GetY() / textureHeight_;
+      float tx2 = tx1 + (static_cast<float>(width) / textureWidth_);
+      float ty2 = ty1 + (static_cast<float>(height) / textureHeight_);
+
+      const float rpos[6][2] = {
+        { rx1, ry1 },
+        { rx1, ry2 },
+        { rx2, ry1 },
+        { rx2, ry1 },
+        { rx1, ry2 },
+        { rx2, ry2 }
+      };
+
+      const float tpos[6][2] = {
+        { tx1, ty1 },
+        { tx1, ty2 },
+        { tx2, ty1 },
+        { tx2, ty1 },
+        { tx1, ty2 },
+        { tx2, ty2 }
+      };
+
+      for (unsigned int i = 0; i < 6; i++)
+      {
+        renderingCoords_.push_back(rpos[i][0]);
+        renderingCoords_.push_back(rpos[i][1]);
+        textureCoords_.push_back(tpos[i][0]);
+        textureCoords_.push_back(tpos[i][1]);
+      }
+    }
+
+
+    OpenGLTextCoordinates::OpenGLTextCoordinates(const GlyphTextureAlphabet& alphabet,
+                                                 const std::string& utf8) :
+      box_(alphabet.GetAlphabet(), utf8),
+      textureWidth_(alphabet.GetTextureWidth()),
+      textureHeight_(alphabet.GetTextureHeight())
+    {
+      if (textureWidth_ <= 0 ||
+          textureHeight_ <= 0)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+      
+      width_ = static_cast<float>(box_.GetWidth());
+      height_ = static_cast<float>(box_.GetHeight());
+
+      // Each character is made of two 2D triangles (= 2 * 3 * 2 = 12)
+      renderingCoords_.reserve(box_.GetCharactersCount() * 12);
+      textureCoords_.reserve(box_.GetCharactersCount() * 12);
+
+      alphabet.GetAlphabet().Apply(*this, utf8);
+    }
+
+
+    const std::vector<float>& OpenGLTextCoordinates::GetRenderingCoords() const
+    {
+      assert(renderingCoords_.size() == textureCoords_.size());
+      return renderingCoords_;
+    }
+
+    
+    const std::vector<float>& OpenGLTextCoordinates::GetTextureCoords() const
+    {
+      assert(renderingCoords_.size() == textureCoords_.size());
+      return textureCoords_;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Fonts/OpenGLTextCoordinates.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,76 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "GlyphTextureAlphabet.h"
+#include "TextBoundingBox.h"
+
+#include <vector>
+
+namespace OrthancStone
+{
+  namespace OpenGL
+  {
+    class OpenGLTextCoordinates : protected GlyphAlphabet::ITextVisitor
+    {
+    private:
+      TextBoundingBox     box_;
+      float               width_;
+      float               height_;
+      std::vector<float>  renderingCoords_;
+      std::vector<float>  textureCoords_;
+      float               textureWidth_;
+      float               textureHeight_;
+    
+    protected:
+      virtual void Visit(uint32_t unicode,
+                         int x,
+                         int y,
+                         unsigned int width,
+                         unsigned int height,
+                         const Orthanc::IDynamicObject* payload);
+
+    public:
+      OpenGLTextCoordinates(const GlyphTextureAlphabet& alphabet,
+                            const std::string& utf8);
+
+      unsigned int GetTextWidth() const
+      {
+        return box_.GetWidth();
+      }
+
+      unsigned int GetTextHeight() const
+      {
+        return box_.GetHeight();
+      }
+
+      bool IsEmpty() const
+      {
+        return renderingCoords_.empty();
+      }
+
+      const std::vector<float>& GetRenderingCoords() const;
+
+      const std::vector<float>& GetTextureCoords() const;
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Fonts/TextBoundingBox.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,80 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "TextBoundingBox.h"
+
+namespace OrthancStone
+{
+  void TextBoundingBox::AddPoint(int x,
+                                 int y)
+  {
+    left_ = std::min(left_, x);
+    right_ = std::max(right_, x);
+    top_ = std::min(top_, y);
+    bottom_ = std::max(bottom_, y);
+  }
+
+
+  void TextBoundingBox::Clear()
+  {
+    left_ = 0;
+    top_ = 0;
+    right_ = 0;
+    bottom_ = 0;
+    countCharacters_ = 0;
+  }
+
+
+  void TextBoundingBox::Visit(uint32_t unicode,
+                              int x,
+                              int y,
+                              unsigned int width,
+                              unsigned int height,
+                              const Orthanc::IDynamicObject* payload /* ignored */)
+  {
+    AddPoint(x, y);
+    AddPoint(x + static_cast<int>(width),
+             y + static_cast<int>(height));
+    countCharacters_++;
+  }
+
+
+  TextBoundingBox::TextBoundingBox(const GlyphAlphabet& alphabet,
+                                   const std::string& utf8)
+  {
+    Clear();
+    alphabet.Apply(*this, utf8);
+  }
+
+
+  unsigned int TextBoundingBox::GetWidth() const
+  {
+    assert(left_ <= right_);
+    return static_cast<unsigned int>(right_ - left_ + 1);
+  }
+
+  
+  unsigned int TextBoundingBox::GetHeight() const
+  {
+    assert(top_ <= bottom_);
+    return static_cast<unsigned int>(bottom_ - top_ + 1);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Fonts/TextBoundingBox.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,73 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "GlyphAlphabet.h"
+
+namespace OrthancStone
+{
+  class TextBoundingBox : protected GlyphAlphabet::ITextVisitor
+  {
+  private:
+    int          left_;
+    int          top_;
+    int          right_;
+    int          bottom_;
+    unsigned int countCharacters_;
+
+    void AddPoint(int x,
+                  int y);
+
+    void Clear();
+
+  protected:
+    virtual void Visit(uint32_t unicode,
+                       int x,
+                       int y,
+                       unsigned int width,
+                       unsigned int height,
+                       const Orthanc::IDynamicObject* payload /* ignored */);
+
+  public:
+    TextBoundingBox(const GlyphAlphabet& alphabet,
+                    const std::string& utf8);
+
+    int GetLeft() const
+    {
+      return left_;
+    }
+
+    int GetTop() const
+    {
+      return top_;
+    }
+
+    unsigned int GetWidth() const;
+
+    unsigned int GetHeight() const;
+
+    unsigned int GetCharactersCount() const
+    {
+      return countCharacters_;
+    }
+  };
+}
--- a/Framework/Layers/DicomSeriesVolumeSlicer.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Layers/DicomSeriesVolumeSlicer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -34,19 +34,19 @@
 
   void DicomSeriesVolumeSlicer::OnSliceGeometryReady(const OrthancSlicesLoader::SliceGeometryReadyMessage& message)
   {
-    if (message.GetOrigin().GetSliceCount() > 0)
+    if (message.GetOrigin().GetSlicesCount() > 0)
     {
-      EmitMessage(IVolumeSlicer::GeometryReadyMessage(*this));
+      BroadcastMessage(IVolumeSlicer::GeometryReadyMessage(*this));
     }
     else
     {
-      EmitMessage(IVolumeSlicer::GeometryErrorMessage(*this));
+      BroadcastMessage(IVolumeSlicer::GeometryErrorMessage(*this));
     }
   }
 
   void DicomSeriesVolumeSlicer::OnSliceGeometryError(const OrthancSlicesLoader::SliceGeometryErrorMessage& message)
   {
-    EmitMessage(IVolumeSlicer::GeometryErrorMessage(*this));
+    BroadcastMessage(IVolumeSlicer::GeometryErrorMessage(*this));
   }
 
 
@@ -73,17 +73,17 @@
   void DicomSeriesVolumeSlicer::OnSliceImageReady(const OrthancSlicesLoader::SliceImageReadyMessage& message)
   {
     // first notify that the pixel data of the frame is ready (targeted to, i.e: an image cache)
-    EmitMessage(FrameReadyMessage(*this, message.GetImage(), 
+    BroadcastMessage(FrameReadyMessage(*this, message.GetImage(), 
                                   message.GetEffectiveQuality(), message.GetSlice()));
 
     // then notify that the layer is ready for rendering
     RendererFactory factory(message);
-    EmitMessage(IVolumeSlicer::LayerReadyMessage(*this, factory, message.GetSlice().GetGeometry()));
+    BroadcastMessage(IVolumeSlicer::LayerReadyMessage(*this, factory, message.GetSlice().GetGeometry()));
   }
 
   void DicomSeriesVolumeSlicer::OnSliceImageError(const OrthancSlicesLoader::SliceImageErrorMessage& message)
   {
-    EmitMessage(IVolumeSlicer::LayerErrorMessage(*this, message.GetSlice().GetGeometry()));
+    BroadcastMessage(IVolumeSlicer::LayerErrorMessage(*this, message.GetSlice().GetGeometry()));
   }
 
 
--- a/Framework/Layers/DicomSeriesVolumeSlicer.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Layers/DicomSeriesVolumeSlicer.h	Thu May 16 09:11:14 2019 +0200
@@ -38,8 +38,10 @@
   {
   public:
     // TODO: Add "frame" and "instanceId"
-    class FrameReadyMessage : public OriginMessage<MessageType_DicomSeriesVolumeSlicer_FrameReady, DicomSeriesVolumeSlicer>
+    class FrameReadyMessage : public OriginMessage<DicomSeriesVolumeSlicer>
     {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+      
     private:
       const Orthanc::ImageAccessor&  frame_;
       SliceImageQuality              imageQuality_;
@@ -100,9 +102,9 @@
       return quality_;
     }
 
-    size_t GetSliceCount() const
+    size_t GetSlicesCount() const
     {
-      return loader_.GetSliceCount();
+      return loader_.GetSlicesCount();
     }
 
     const Slice& GetSlice(size_t slice) const 
--- a/Framework/Layers/DicomStructureSetSlicer.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Layers/DicomStructureSetSlicer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -164,7 +164,7 @@
     if (loader_.HasStructureSet())
     {
       RendererFactory factory(loader_.GetStructureSet(), viewportPlane);
-      EmitMessage(IVolumeSlicer::LayerReadyMessage(*this, factory, viewportPlane));
+      BroadcastMessage(IVolumeSlicer::LayerReadyMessage(*this, factory, viewportPlane));
     }
   }
 }
--- a/Framework/Layers/DicomStructureSetSlicer.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Layers/DicomStructureSetSlicer.h	Thu May 16 09:11:14 2019 +0200
@@ -38,7 +38,7 @@
 
     void OnStructureSetLoaded(const IVolumeLoader::ContentChangedMessage& message)
     {
-      EmitMessage(IVolumeSlicer::ContentChangedMessage(*this));
+      BroadcastMessage(IVolumeSlicer::ContentChangedMessage(*this));
     }
 
   public:
--- a/Framework/Layers/IVolumeSlicer.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Layers/IVolumeSlicer.h	Thu May 16 09:11:14 2019 +0200
@@ -33,18 +33,20 @@
   class IVolumeSlicer : public IObservable
   {
   public:
-    typedef OriginMessage<MessageType_VolumeSlicer_GeometryReady, IVolumeSlicer>  GeometryReadyMessage;
-    typedef OriginMessage<MessageType_VolumeSlicer_GeometryError, IVolumeSlicer>  GeometryErrorMessage;
-    typedef OriginMessage<MessageType_VolumeSlicer_ContentChanged, IVolumeSlicer> ContentChangedMessage;
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, GeometryReadyMessage, IVolumeSlicer);
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, GeometryErrorMessage, IVolumeSlicer);
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, ContentChangedMessage, IVolumeSlicer);
 
-    class SliceContentChangedMessage : public OriginMessage<MessageType_VolumeSlicer_SliceChanged, IVolumeSlicer>
+    class SliceContentChangedMessage : public OriginMessage<IVolumeSlicer>
     {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+      
     private:
       const Slice& slice_;
 
     public:
       SliceContentChangedMessage(IVolumeSlicer& origin,
-                          const Slice& slice) :
+                                 const Slice& slice) :
         OriginMessage(origin),
         slice_(slice)
       {
@@ -57,8 +59,10 @@
     };
     
 
-    class LayerReadyMessage : public OriginMessage<MessageType_VolumeSlicer_LayerReady, IVolumeSlicer>
+    class LayerReadyMessage : public OriginMessage<IVolumeSlicer>
     {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+      
     public:
       class IRendererFactory : public boost::noncopyable
       {
@@ -96,8 +100,10 @@
     };
 
 
-    class LayerErrorMessage : public OriginMessage<MessageType_VolumeSlicer_LayerError, IVolumeSlicer>
+    class LayerErrorMessage : public OriginMessage<IVolumeSlicer>
     {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+      
     private:
       const CoordinateSystem3D&  slice_;
 
--- a/Framework/Layers/RenderStyle.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Layers/RenderStyle.cpp	Thu May 16 09:11:14 2019 +0200
@@ -29,7 +29,7 @@
   {
     visible_ = true;
     reverse_ = false;
-    windowing_ = ImageWindowing_Default;
+    windowing_ = ImageWindowing_Custom;
     alpha_ = 1;
     applyLut_ = false;
     lut_ = Orthanc::EmbeddedResources::COLORMAP_HOT;
--- a/Framework/Messages/ICallable.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Messages/ICallable.h	Thu May 16 09:11:14 2019 +0200
@@ -41,7 +41,8 @@
 
     virtual void Apply(const IMessage& message) = 0;
 
-    virtual MessageType GetMessageType() const = 0;
+    virtual const MessageIdentifier& GetMessageIdentifier() = 0;
+
     virtual IObserver* GetObserver() const = 0;
   };
 
@@ -58,8 +59,8 @@
   private:
     typedef void (TObserver::* MemberFunction) (const TMessage&);
 
-    TObserver&      observer_;
-    MemberFunction  function_;
+    TObserver&         observer_;
+    MemberFunction     function_;
 
   public:
     Callable(TObserver& observer,
@@ -79,9 +80,9 @@
       ApplyInternal(dynamic_cast<const TMessage&>(message));
     }
 
-    virtual MessageType GetMessageType() const
+    virtual const MessageIdentifier& GetMessageIdentifier()
     {
-      return static_cast<MessageType>(TMessage::Type);
+      return TMessage::GetStaticIdentifier();
     }
 
     virtual IObserver* GetObserver() const
@@ -115,11 +116,6 @@
       lambda_(dynamic_cast<const TMessage&>(message));
     }
 
-    virtual MessageType GetMessageType() const
-    {
-      return static_cast<MessageType>(TMessage::Type);
-    }
-
     virtual IObserver* GetObserver() const
     {
       return &observer_;
--- a/Framework/Messages/IMessage.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Messages/IMessage.h	Thu May 16 09:11:14 2019 +0200
@@ -21,79 +21,79 @@
 
 #pragma once
 
-#include "../StoneEnumerations.h"
+#include <boost/noncopyable.hpp>
 
-#include <boost/noncopyable.hpp>
+#include <string.h>
 
 namespace OrthancStone 
 {
-  // base message that are exchanged between IObservable and IObserver
-  class IMessage : public boost::noncopyable
+  class MessageIdentifier
   {
   private:
-    int messageType_;
-    
-  protected:
-    IMessage(const int& messageType) :
-      messageType_(messageType)
+    const char*  file_;
+    int          line_;
+
+  public:
+    MessageIdentifier(const char* file,
+                      int line) :
+      file_(file),
+      line_(line)
+    {
+    }
+
+    MessageIdentifier() :
+      file_(NULL),
+      line_(0)
     {
     }
+
+    bool operator< (const MessageIdentifier& other) const
+    {
+      if (file_ == NULL)
+      {
+        return false;
+      }
+      else if (line_ != other.line_)
+      {
+        return line_ < other.line_;
+      }
+      else
+      {
+        return strcmp(file_, other.file_) < 0;
+      }
+    }
+  };
+
     
+  /**
+   * Base messages that are exchanged between IObservable and
+   * IObserver. Messages are distinguished by the "__FILE__" and
+   * "__LINE__" macro, as in "Orthanc::SQLite::StatementId".
+   **/
+  class IMessage : public boost::noncopyable
+  {
   public:
     virtual ~IMessage()
     {
     }
 
-    virtual int GetType() const
-    {
-      return messageType_;
-    }
+    virtual const MessageIdentifier& GetIdentifier() const = 0;
   };
 
 
-  // base class to derive from to implement your own messages
-  // it handles the message type for you
-  template <int type>
-  class BaseMessage : public IMessage
-  {
-  public:
-    enum
-    {
-      Type = type
-    };
-
-    BaseMessage() :
-      IMessage(static_cast<int>(Type))
-    {
-    }
-  };
-  
-
-  // simple message implementation when no payload is needed
-  // sample usage:
-  // typedef NoPayloadMessage<MessageType_VolumeSlicer_GeometryReady> GeometryReadyMessage;
-  template <int type>
-  class NoPayloadMessage : public BaseMessage<type>
-  {
-  public:
-    NoPayloadMessage() :
-      BaseMessage<type>()
-    {
-    }
-  };
-
-  // simple message implementation when no payload is needed but the origin is required
-  // sample usage:
-  // typedef OriginMessage<MessageType_SliceLoader_GeometryError, OrthancSlicesLoader> SliceGeometryErrorMessage;
-  template <int type, typename TOrigin>
-  class OriginMessage : public BaseMessage<type>
+  /**
+   * Simple message implementation when no payload is needed but the
+   * origin is required. Sample usage:
+   * typedef OriginMessage<OrthancSlicesLoader> SliceGeometryErrorMessage;
+   **/
+  template <typename TOrigin>
+  class OriginMessage : public IMessage
   {
   private:
-    const TOrigin& origin_;
+    const TOrigin&  origin_;
 
   public:
     OriginMessage(const TOrigin& origin) :
-      BaseMessage<type>(),
       origin_(origin)
     {
     }
@@ -104,3 +104,36 @@
     }
   };
 }
+
+
+#define ORTHANC_STONE_MESSAGE(FILE, LINE)                               \
+  public:                                                               \
+  static const ::OrthancStone::MessageIdentifier& GetStaticIdentifier() \
+  {                                                                     \
+    static const ::OrthancStone::MessageIdentifier id(FILE, LINE);      \
+    return id;                                                          \
+  }                                                                     \
+                                                                        \
+  virtual const ::OrthancStone::MessageIdentifier& GetIdentifier() const \
+  {                                                                     \
+    return GetStaticIdentifier();                                       \
+  }
+
+
+#define ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(FILE, LINE, NAME, ORIGIN)   \
+  class NAME : public ::OrthancStone::OriginMessage<ORIGIN>             \
+  {                                                                     \
+    ORTHANC_STONE_MESSAGE(FILE, LINE);                                  \
+                                                                        \
+    NAME(const ORIGIN& origin) :                                        \
+      OriginMessage(origin)                                             \
+    {                                                                   \
+    }                                                                   \
+  };
+
+
+#define ORTHANC_STONE_DEFINE_EMPTY_MESSAGE(FILE, LINE, NAME)            \
+  class NAME : public ::OrthancStone::IMessage                          \
+  {                                                                     \
+    ORTHANC_STONE_MESSAGE(FILE, LINE);                                  \
+  };
--- a/Framework/Messages/IObservable.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Messages/IObservable.cpp	Thu May 16 09:11:14 2019 +0200
@@ -59,9 +59,8 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
     }
     
-    MessageType messageType = callable->GetMessageType();
-
-    callables_[messageType].insert(callable);
+    const MessageIdentifier& id = callable->GetMessageIdentifier();
+    callables_[id].insert(callable);
   }
 
   void IObservable::Unregister(IObserver *observer)
@@ -84,9 +83,10 @@
     }
   }
   
-  void IObservable::EmitMessage(const IMessage& message)
+  void IObservable::EmitMessageInternal(const IObserver* receiver,
+                                        const IMessage& message)
   {
-    Callables::const_iterator found = callables_.find(message.GetType());
+    Callables::const_iterator found = callables_.find(message.GetIdentifier());
 
     if (found != callables_.end())
     {
@@ -94,14 +94,33 @@
              it = found->second.begin(); it != found->second.end(); ++it)
       {
         assert(*it != NULL);
-        if (broker_.IsActive(*(*it)->GetObserver()))
+
+        const IObserver* observer = (*it)->GetObserver();
+        if (broker_.IsActive(*observer))
         {
-          (*it)->Apply(message);
+          if (receiver == NULL ||    // Are we broadcasting?
+              observer == receiver)  // Not broadcasting, but this is the receiver
+          {
+            (*it)->Apply(message);
+          }
         }
       }
     }
   }
 
+
+  void IObservable::BroadcastMessage(const IMessage& message)
+  {
+    EmitMessageInternal(NULL, message);
+  }
+
+  
+  void IObservable::EmitMessage(const IObserver& observer,
+                                const IMessage& message)
+  {
+    EmitMessageInternal(&observer, message);
+  }
+
   
   void IObservable::RegisterForwarder(IMessageForwarder* forwarder)
   {
--- a/Framework/Messages/IObservable.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Messages/IObservable.h	Thu May 16 09:11:14 2019 +0200
@@ -35,13 +35,17 @@
   class IObservable : public boost::noncopyable
   {
   private:
-    typedef std::map<int, std::set<ICallable*> >  Callables;
-    typedef std::set<IMessageForwarder*>          Forwarders;
+    typedef std::map<MessageIdentifier, std::set<ICallable*> >  Callables;
+
+    typedef std::set<IMessageForwarder*>     Forwarders;
 
     MessageBroker&  broker_;
     Callables       callables_;
     Forwarders      forwarders_;
 
+    void EmitMessageInternal(const IObserver* receiver,
+                             const IMessage& message);
+
   public:
     IObservable(MessageBroker& broker) :
       broker_(broker)
@@ -60,7 +64,10 @@
 
     void Unregister(IObserver* observer);
 
-    void EmitMessage(const IMessage& message);
+    void BroadcastMessage(const IMessage& message);
+
+    void EmitMessage(const IObserver& observer,
+                     const IMessage& message);
 
     // Takes ownsership
     void RegisterForwarder(IMessageForwarder* forwarder);
--- a/Framework/Messages/MessageForwarder.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Messages/MessageForwarder.cpp	Thu May 16 09:11:14 2019 +0200
@@ -28,7 +28,7 @@
 
   void IMessageForwarder::ForwardMessageInternal(const IMessage& message)
   {
-    emitter_.EmitMessage(message);
+    emitter_.BroadcastMessage(message);
   }
 
   void IMessageForwarder::RegisterForwarderInEmitter()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/OpenGL/IOpenGLContext.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,47 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <boost/noncopyable.hpp>
+
+namespace OrthancStone
+{
+  namespace OpenGL
+  {
+    class IOpenGLContext : public boost::noncopyable
+    {
+    public:
+      virtual ~IOpenGLContext()
+      {
+      }
+
+      virtual void MakeCurrent() = 0;
+
+      virtual void SwapBuffer() = 0;
+
+      virtual unsigned int GetCanvasWidth() const = 0;
+
+      virtual unsigned int GetCanvasHeight() const = 0;
+    };
+  }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/OpenGL/OpenGLIncludes.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,41 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#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
+
+#if defined(__APPLE__)
+#  include <OpenGL/gl.h>
+#  include <OpenGL/glext.h>
+#elif defined(_WIN32)
+// On Windows, use the compatibility headers provided by glew
+#  include <GL/glew.h>
+#else
+#  include <GL/gl.h>
+#  include <GL/glext.h>
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/OpenGL/OpenGLProgram.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,103 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OpenGLProgram.h"
+
+#include "OpenGLShader.h"
+
+#include <Core/OrthancException.h>
+
+
+namespace OrthancStone
+{
+  namespace OpenGL
+  {
+    OpenGLProgram::OpenGLProgram()
+    {
+      program_ = glCreateProgram();
+      if (program_ == 0)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                        "Cannot create an OpenGL program");
+      }
+    }
+
+
+    OpenGLProgram::~OpenGLProgram()
+    {
+      assert(program_ != 0);
+      glDeleteProgram(program_);
+    }
+
+
+    void OpenGLProgram::Use()
+    {
+      glUseProgram(program_);
+    }
+
+    
+    void OpenGLProgram::CompileShaders(const std::string& vertexCode,
+                                       const std::string& fragmentCode)
+    {
+      assert(program_ != 0);
+
+      OpenGLShader vertexShader(GL_VERTEX_SHADER, vertexCode);
+      OpenGLShader fragmentShader(GL_FRAGMENT_SHADER, fragmentCode);
+
+      glAttachShader(program_, vertexShader.Release());
+      glAttachShader(program_, fragmentShader.Release());
+      glLinkProgram(program_);
+      glValidateProgram(program_);
+    }
+
+
+    GLint OpenGLProgram::GetUniformLocation(const std::string& name)
+    { 
+      GLint location = glGetUniformLocation(program_, name.c_str());
+
+      if (location == -1)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem,
+                                        "Inexistent uniform variable in shader: " + name);
+      }
+      else
+      {
+        return location;
+      }
+    }
+
+    
+    GLint OpenGLProgram::GetAttributeLocation(const std::string& name)
+    { 
+      GLint location = glGetAttribLocation(program_, name.c_str());
+
+      if (location == -1)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem,
+                                        "Inexistent attribute in shader: " + name);
+      }
+      else
+      {
+        return location;
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/OpenGL/OpenGLProgram.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,55 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "OpenGLIncludes.h"
+
+#include <string>
+#include <boost/noncopyable.hpp>
+
+namespace OrthancStone
+{
+  namespace OpenGL
+  {
+    class OpenGLProgram : public boost::noncopyable
+    {
+    private:
+      GLuint  program_;
+
+    public:
+      // WARNING: A global OpenGL context must be active to create this object!
+      OpenGLProgram();
+
+      ~OpenGLProgram();
+
+      void Use();
+
+      // WARNING: A global OpenGL context must be active to run this method!
+      void CompileShaders(const std::string& vertexCode,
+                          const std::string& fragmentCode);
+
+      GLint GetUniformLocation(const std::string& name);
+
+      GLint GetAttributeLocation(const std::string& name);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/OpenGL/OpenGLShader.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,104 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OpenGLShader.h"
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  namespace OpenGL
+  {
+    static GLuint CompileShader(GLenum type,
+                                const std::string& source) 
+    {
+      // Create shader object
+      const GLchar* sourceString[1];
+      GLint sourceStringLengths[1];
+
+      sourceString[0] = source.c_str();
+      sourceStringLengths[0] = source.length();
+      GLuint shader = glCreateShader(type);
+
+      if (shader == 0)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                        "Cannot create an OpenGL shader");
+      }
+      else
+      {
+        // Assign and compile the source to the shader object
+        glShaderSource(shader, 1, sourceString, sourceStringLengths);
+        glCompileShader(shader);
+
+        // Check if there were errors
+        int infoLen = 0;
+        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);
+
+        if (infoLen > 1)  // Might be equal to 1, which amounts to no error
+        {
+          std::string infoLog;
+          infoLog.resize(infoLen + 1);
+          glGetShaderInfoLog(shader, infoLen, NULL, &infoLog[0]);
+          glDeleteShader(shader);
+
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                          "Error while creating an OpenGL shader: " + infoLog);
+        }
+        else
+        {
+          return shader;
+        }
+      }
+    }
+
+
+    OpenGLShader::OpenGLShader(GLenum type,
+                               const std::string& source)
+    {
+      shader_ = CompileShader(type, source);
+      isValid_ = true;
+    }
+
+    
+    OpenGLShader::~OpenGLShader()
+    {
+      if (isValid_)
+      {
+        glDeleteShader(shader_);
+      }
+    }
+
+
+    GLuint OpenGLShader::Release()
+    {
+      if (isValid_)
+      {
+        isValid_ = false;
+        return shader_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/OpenGL/OpenGLShader.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,53 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "OpenGLIncludes.h"
+
+#include <string>
+#include <boost/noncopyable.hpp>
+
+namespace OrthancStone
+{
+  namespace OpenGL
+  {
+    class OpenGLShader : public boost::noncopyable
+    {
+    private:
+      bool     isValid_;
+      GLuint   shader_;
+
+    public:
+      OpenGLShader(GLenum type,
+                   const std::string& source);
+
+      ~OpenGLShader();
+
+      bool IsValid() const
+      {
+        return isValid_;
+      }
+
+      GLuint Release();
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/OpenGL/OpenGLTexture.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,113 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OpenGLTexture.h"
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  namespace OpenGL
+  {
+    OpenGLTexture::OpenGLTexture() :
+      width_(0),
+      height_(0)
+    {
+      // Generate a texture object
+      glGenTextures(1, &texture_);
+      if (texture_ == 0)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                        "Cannot create an OpenGL program");
+      }
+    }
+
+    
+    OpenGLTexture::~OpenGLTexture()
+    {
+      assert(texture_ != 0);
+      glDeleteTextures(1, &texture_);
+    }
+
+
+    void OpenGLTexture::Load(const Orthanc::ImageAccessor& image,
+                             bool isLinearInterpolation)
+    {
+      glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // Disable byte-alignment restriction
+    
+      if (image.GetPitch() != image.GetBytesPerPixel() * image.GetWidth())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented,
+                                        "Unsupported non-zero padding");
+      }
+
+      // Bind it
+      glActiveTexture(GL_TEXTURE0);
+      glBindTexture(GL_TEXTURE_2D, texture_);
+
+      GLenum sourceFormat, internalFormat;
+
+      switch (image.GetFormat())
+      {
+        case Orthanc::PixelFormat_Grayscale8:
+          sourceFormat = GL_RED;
+          internalFormat = GL_RED;
+          break;
+
+        case Orthanc::PixelFormat_RGB24:
+          sourceFormat = GL_RGB;
+          internalFormat = GL_RGB;
+          break;
+
+        case Orthanc::PixelFormat_RGBA32:
+          sourceFormat = GL_RGBA;
+          internalFormat = GL_RGBA;
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented,
+                                          "No support for this format in OpenGL textures: " + 
+                                          std::string(EnumerationToString(image.GetFormat())));
+      }
+
+      width_ = image.GetWidth();
+      height_ = image.GetHeight();
+    
+      GLint interpolation = (isLinearInterpolation ? GL_LINEAR : GL_NEAREST);
+
+      // Load the texture from the image buffer
+      glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, image.GetWidth(), image.GetHeight(), 
+                   0, sourceFormat, GL_UNSIGNED_BYTE, image.GetBuffer());
+      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, interpolation);
+      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, interpolation);
+      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+    }
+
+
+    void OpenGLTexture::Bind(GLint location)
+    {
+      glActiveTexture(GL_TEXTURE0);
+      glBindTexture(GL_TEXTURE_2D, texture_);
+      glUniform1i(location, 0 /* texture unit */);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/OpenGL/OpenGLTexture.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,63 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "OpenGLIncludes.h"
+
+#include <Core/Images/ImageAccessor.h>
+
+#include <boost/noncopyable.hpp>
+
+
+namespace OrthancStone
+{
+  namespace OpenGL
+  {
+    class OpenGLTexture : public boost::noncopyable
+    {
+    private:
+      GLuint        texture_;
+      unsigned int  width_;
+      unsigned int  height_;
+
+    public:
+      OpenGLTexture();
+
+      ~OpenGLTexture();
+
+      unsigned int GetWidth() const
+      {
+        return width_;
+      }
+
+      unsigned int GetHeight() const
+      {
+        return height_;
+      }
+
+      void Load(const Orthanc::ImageAccessor& image,
+                bool isLinearInterpolation);
+
+      void Bind(GLint location);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/OpenGL/WebAssemblyOpenGLContext.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,179 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "WebAssemblyOpenGLContext.h"
+
+#if ORTHANC_ENABLE_WASM == 1
+
+#include <Core/OrthancException.h>
+
+#include <emscripten/html5.h>
+#include <boost/math/special_functions/round.hpp>
+
+namespace OrthancStone
+{
+  namespace OpenGL
+  {
+    class WebAssemblyOpenGLContext::PImpl
+    {
+    private:
+      std::string                      canvas_;
+      EMSCRIPTEN_WEBGL_CONTEXT_HANDLE  context_;
+      unsigned int                     canvasWidth_;
+      unsigned int                     canvasHeight_;
+
+    public:
+      PImpl(const std::string& canvas) :
+        canvas_(canvas)
+      {
+        // Context configuration
+        EmscriptenWebGLContextAttributes attr; 
+        emscripten_webgl_init_context_attributes(&attr);
+
+        context_ = emscripten_webgl_create_context(canvas.c_str(), &attr);
+        if (context_ < 0)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                          "Cannot create an OpenGL context for canvas: " + canvas);
+        }
+
+        UpdateSize();
+      }
+
+      ~PImpl()
+      {
+        emscripten_webgl_destroy_context(context_);
+      }
+
+      const std::string& GetCanvasIdentifier() const
+      {
+        return canvas_;
+      }
+
+      void MakeCurrent()
+      {
+        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".
+         **/
+      }
+
+      unsigned int GetCanvasWidth() const
+      {
+        return canvasWidth_;
+      }
+
+      unsigned int GetCanvasHeight() const
+      {
+        return canvasHeight_;
+      }
+
+      void UpdateSize()
+      {
+        double w, h;
+        emscripten_get_element_css_size(canvas_.c_str(), &w, &h);
+
+        /**
+         * 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_ = 0;
+          canvasHeight_ = 0;
+        }
+        else
+        {
+          canvasWidth_ = static_cast<unsigned int>(boost::math::iround(w));
+          canvasHeight_ = static_cast<unsigned int>(boost::math::iround(h));
+        }
+    
+        emscripten_set_canvas_element_size(canvas_.c_str(), canvasWidth_, canvasHeight_);
+      }
+    };
+
+
+    WebAssemblyOpenGLContext::WebAssemblyOpenGLContext(const std::string& canvas) :
+      pimpl_(new PImpl(canvas))
+    {
+    }
+
+    void WebAssemblyOpenGLContext::MakeCurrent()
+    {
+      assert(pimpl_.get() != NULL);
+      pimpl_->MakeCurrent();
+    }
+
+
+    void WebAssemblyOpenGLContext::SwapBuffer() 
+    {
+      assert(pimpl_.get() != NULL);
+      pimpl_->SwapBuffer();
+    }
+
+    unsigned int WebAssemblyOpenGLContext::GetCanvasWidth() const
+    {
+      assert(pimpl_.get() != NULL);
+      return pimpl_->GetCanvasWidth();
+    }
+
+    unsigned int WebAssemblyOpenGLContext::GetCanvasHeight() const
+    {
+      assert(pimpl_.get() != NULL);
+      return pimpl_->GetCanvasHeight();
+    }
+
+    void WebAssemblyOpenGLContext::UpdateSize()
+    {
+      assert(pimpl_.get() != NULL);
+      pimpl_->UpdateSize();
+    }
+
+    const std::string& WebAssemblyOpenGLContext::GetCanvasIdentifier() const
+    {
+      assert(pimpl_.get() != NULL);
+      return pimpl_->GetCanvasIdentifier();
+    }
+  }
+}
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/OpenGL/WebAssemblyOpenGLContext.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,62 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if !defined(ORTHANC_ENABLE_WASM)
+#  error Macro ORTHANC_ENABLE_WASM must be defined
+#endif
+
+#if ORTHANC_ENABLE_WASM == 1
+
+#include "IOpenGLContext.h"
+
+#include <boost/shared_ptr.hpp>
+
+namespace OrthancStone
+{
+  namespace OpenGL
+  {
+    class WebAssemblyOpenGLContext : public OpenGL::IOpenGLContext
+    {
+    private:
+      class PImpl;
+      boost::shared_ptr<PImpl>  pimpl_;
+
+    public:
+      WebAssemblyOpenGLContext(const std::string& canvas);
+
+      virtual void MakeCurrent();
+
+      virtual void SwapBuffer();
+
+      virtual unsigned int GetCanvasWidth() const;
+
+      virtual unsigned int GetCanvasHeight() const;
+
+      void UpdateSize();
+
+      const std::string& GetCanvasIdentifier() const;
+    };
+  }
+}
+
+#endif
--- a/Framework/Radiography/RadiographyAlphaLayer.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Radiography/RadiographyAlphaLayer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -46,7 +46,7 @@
     SetSize(image->GetWidth(), image->GetHeight());
     alpha_ = raii;
 
-    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
   void RadiographyAlphaLayer::Render(Orthanc::ImageAccessor& buffer,
--- a/Framework/Radiography/RadiographyDicomLayer.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Radiography/RadiographyDicomLayer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -47,6 +47,11 @@
   }
 
 
+  RadiographyDicomLayer::RadiographyDicomLayer(MessageBroker& broker, const RadiographyScene& scene) : RadiographyLayer(broker, scene)
+  {
+
+  }
+
   void RadiographyDicomLayer::SetDicomTags(const OrthancPlugins::FullOrthancDataset& dataset)
   {
     converter_.reset(new DicomFrameConverter);
@@ -103,7 +108,13 @@
     source_ = raii;
     ApplyConverter();
 
-    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
+  }
+
+
+  void RadiographyDicomLayer::SetDicomFrameConverter(DicomFrameConverter* converter)
+  {
+    converter_.reset(converter);
   }
 
   void RadiographyDicomLayer::Render(Orthanc::ImageAccessor& buffer,
--- a/Framework/Radiography/RadiographyDicomLayer.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Radiography/RadiographyDicomLayer.h	Thu May 16 09:11:14 2019 +0200
@@ -41,10 +41,7 @@
     void ApplyConverter();
 
   public:
-    RadiographyDicomLayer(MessageBroker& broker, const RadiographyScene& scene)
-      : RadiographyLayer(broker, scene)
-    {
-    }
+    RadiographyDicomLayer(MessageBroker& broker, const RadiographyScene& scene);
 
     void SetInstance(const std::string& instanceId, unsigned int frame)
     {
@@ -69,7 +66,9 @@
     const Orthanc::ImageAccessor* GetSourceImage() const {return source_.get();}  // currently need this access to serialize scene in plain old data to send to a WASM worker
 
     const DicomFrameConverter& GetDicomFrameConverter() const {return *converter_;} // currently need this access to serialize scene in plain old data to send to a WASM worker
-    void SetDicomFrameConverter(DicomFrameConverter* converter) {converter_.reset(converter);} // Takes ownership
+    
+     // Takes ownership
+    void SetDicomFrameConverter(DicomFrameConverter* converter);
 
     virtual void Render(Orthanc::ImageAccessor& buffer,
                         const AffineTransform2D& viewTransform,
--- a/Framework/Radiography/RadiographyLayer.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Radiography/RadiographyLayer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -141,7 +141,7 @@
   {
     prefferedPhotometricDisplayMode_ = prefferedPhotometricDisplayMode;
 
-    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
   void RadiographyLayer::SetCrop(unsigned int x,
@@ -163,7 +163,7 @@
     geometry_.SetCrop(x, y, width, height);
     UpdateTransform();
 
-    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
   void RadiographyLayer::SetGeometry(const Geometry& geometry)
@@ -175,7 +175,7 @@
       UpdateTransform();
     }
 
-    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
 
@@ -203,7 +203,7 @@
     geometry_.SetAngle(angle);
     UpdateTransform();
 
-    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
   void RadiographyLayer::SetFlipVertical(bool flip)
@@ -211,7 +211,7 @@
     geometry_.SetFlipVertical(flip);
     UpdateTransform();
 
-    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
   void RadiographyLayer::SetFlipHorizontal(bool flip)
@@ -219,7 +219,7 @@
     geometry_.SetFlipHorizontal(flip);
     UpdateTransform();
 
-    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
   void RadiographyLayer::SetSize(unsigned int width,
@@ -237,7 +237,7 @@
     height_ = height;
 
     UpdateTransform();
-    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
 
@@ -315,7 +315,7 @@
   {
     geometry_.SetPan(x, y);
     UpdateTransform();
-    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
 
@@ -324,7 +324,7 @@
   {
     geometry_.SetPixelSpacing(x, y);
     UpdateTransform();
-    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
 
--- a/Framework/Radiography/RadiographyLayer.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Radiography/RadiographyLayer.h	Thu May 16 09:11:14 2019 +0200
@@ -55,18 +55,7 @@
     friend class RadiographyScene;
 
   public:
-    class LayerEditedMessage :
-        public OriginMessage<MessageType_RadiographyLayer_Edited, RadiographyLayer>
-    {
-    private:
-
-    public:
-      LayerEditedMessage(const RadiographyLayer& origin) :
-        OriginMessage(origin)
-      {
-      }
-    };
-
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, LayerEditedMessage, RadiographyLayer);
 
     class Geometry
     {
--- a/Framework/Radiography/RadiographyMaskLayer.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Radiography/RadiographyMaskLayer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -63,7 +63,7 @@
       corners_.push_back(corner);
     invalidated_ = true;
 
-    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
   void RadiographyMaskLayer::SetCorners(const std::vector<Orthanc::ImageProcessing::ImagePoint>& corners)
@@ -71,7 +71,7 @@
     corners_ = corners;
     invalidated_ = true;
 
-    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
   void RadiographyMaskLayer::Render(Orthanc::ImageAccessor& buffer,
--- a/Framework/Radiography/RadiographyScene.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Radiography/RadiographyScene.cpp	Thu May 16 09:11:14 2019 +0200
@@ -140,8 +140,8 @@
     raii->SetIndex(index);
     layers_[index] = raii.release();
 
-    EmitMessage(GeometryChangedMessage(*this, *layer));
-    EmitMessage(ContentChangedMessage(*this, *layer));
+    BroadcastMessage(GeometryChangedMessage(*this, *layer));
+    BroadcastMessage(ContentChangedMessage(*this, *layer));
     layer->RegisterObserverCallback(new Callable<RadiographyScene, RadiographyLayer::LayerEditedMessage>(*this, &RadiographyScene::OnLayerEdited));
 
     return *layer;
@@ -149,7 +149,7 @@
 
   void RadiographyScene::OnLayerEdited(const RadiographyLayer::LayerEditedMessage& message)
   {
-    EmitMessage(RadiographyScene::LayerEditedMessage(*this, message.GetOrigin()));
+    BroadcastMessage(RadiographyScene::LayerEditedMessage(*this, message.GetOrigin()));
   }
 
   RadiographyScene::RadiographyScene(MessageBroker& broker) :
@@ -199,24 +199,37 @@
   {
     LOG(INFO) << "Removing layer: " << layerIndex;
 
-    if (layerIndex > countLayers_)
+    Layers::iterator found = layers_.find(layerIndex);
+
+    if (found == layers_.end())
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
-    delete layers_[layerIndex];
-    layers_.erase(layerIndex);
-    countLayers_--;
-    LOG(INFO) << "Removing layer, there are now : " << countLayers_ << " layers";
+    else
+    {
+      assert(found->second != NULL);
+      delete found->second;
+      
+      layers_.erase(found);
+      countLayers_--;
+      
+      LOG(INFO) << "Removing layer, there are now : " << countLayers_ << " layers";
+    }
   }
 
   const RadiographyLayer& RadiographyScene::GetLayer(size_t layerIndex) const
   {
-    if (layerIndex > countLayers_)
+    Layers::const_iterator found = layers_.find(layerIndex);
+    
+    if (found == layers_.end())
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
-
-    return *(layers_.at(layerIndex));
+    else
+    {
+      assert(found->second != NULL);
+      return *found->second;
+    }
   }
 
   bool RadiographyScene::GetWindowing(float& center,
@@ -253,7 +266,7 @@
     windowingCenter_ = center;
     windowingWidth_ = width;
 
-    EmitMessage(RadiographyScene::WindowingChangedMessage(*this));
+    BroadcastMessage(RadiographyScene::WindowingChangedMessage(*this));
   }
 
 
@@ -434,7 +447,7 @@
         windowingWidth_ = w;
       }
 
-      EmitMessage(GeometryChangedMessage(*this, *(layer->second)));
+      BroadcastMessage(GeometryChangedMessage(*this, *(layer->second)));
     }
   }
 
@@ -461,7 +474,7 @@
       reader->ReadFromMemory(content);
       dynamic_cast<RadiographyDicomLayer*>(layer->second)->SetSourceImage(reader.release());
 
-      EmitMessage(ContentChangedMessage(*this, *(layer->second)));
+      BroadcastMessage(ContentChangedMessage(*this, *(layer->second)));
     }
   }
 
--- a/Framework/Radiography/RadiographyScene.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Radiography/RadiographyScene.h	Thu May 16 09:11:14 2019 +0200
@@ -37,9 +37,10 @@
       public IObservable
   {
   public:
-    class GeometryChangedMessage :
-        public OriginMessage<MessageType_RadiographyScene_GeometryChanged, RadiographyScene>
+    class GeometryChangedMessage : public OriginMessage<RadiographyScene>
     {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+
     private:
       RadiographyLayer&        layer_;
 
@@ -57,9 +58,10 @@
       }
     };
 
-    class ContentChangedMessage :
-        public OriginMessage<MessageType_RadiographyScene_ContentChanged, RadiographyScene>
+    class ContentChangedMessage : public OriginMessage<RadiographyScene>
     {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+
     private:
       RadiographyLayer&        layer_;
 
@@ -77,9 +79,10 @@
       }
     };
 
-    class LayerEditedMessage :
-        public OriginMessage<MessageType_RadiographyScene_LayerEdited, RadiographyScene>
+    class LayerEditedMessage : public OriginMessage<RadiographyScene>
     {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+
     private:
       const RadiographyLayer&        layer_;
 
@@ -95,20 +98,12 @@
       {
         return layer_;
       }
-
     };
 
-    class WindowingChangedMessage :
-        public OriginMessage<MessageType_RadiographyScene_WindowingChanged, RadiographyScene>
-    {
 
-    public:
-      WindowingChangedMessage(const RadiographyScene& origin) :
-        OriginMessage(origin)
-      {
-      }
-    };
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, WindowingChangedMessage, RadiographyScene);
 
+    
     class LayerAccessor : public boost::noncopyable
     {
     private:
--- a/Framework/Radiography/RadiographySceneReader.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Radiography/RadiographySceneReader.cpp	Thu May 16 09:11:14 2019 +0200
@@ -21,6 +21,8 @@
 
 #include "RadiographySceneReader.h"
 
+#include <Framework/Toolbox/DicomFrameConverter.h>
+
 #include <Core/Images/FontRegistry.h>
 #include <Core/Images/PngReader.h>
 #include <Core/OrthancException.h>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/CairoCompositor.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,177 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "CairoCompositor.h"
+
+#include "Internals/CairoColorTextureRenderer.h"
+#include "Internals/CairoFloatTextureRenderer.h"
+#include "Internals/CairoInfoPanelRenderer.h"
+#include "Internals/CairoPolylineRenderer.h"
+#include "Internals/CairoTextRenderer.h"
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  cairo_t* CairoCompositor::GetCairoContext()
+  {
+    if (context_.get() == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return context_->GetObject();
+    }
+  }
+
+    
+  Internals::CompositorHelper::ILayerRenderer* CairoCompositor::Create(const ISceneLayer& layer)
+  {
+    switch (layer.GetType())
+    {
+      case ISceneLayer::Type_Polyline:
+        return new Internals::CairoPolylineRenderer(*this, layer);
+
+      case ISceneLayer::Type_InfoPanel:
+        return new Internals::CairoInfoPanelRenderer(*this, layer);
+
+      case ISceneLayer::Type_ColorTexture:
+        return new Internals::CairoColorTextureRenderer(*this, layer);
+
+      case ISceneLayer::Type_FloatTexture:
+        return new Internals::CairoFloatTextureRenderer(*this, layer);
+
+      case ISceneLayer::Type_Text:
+      {
+        const TextSceneLayer& l = dynamic_cast<const TextSceneLayer&>(layer);
+
+        Fonts::const_iterator found = fonts_.find(l.GetFontIndex());
+        if (found == fonts_.end())
+        {
+          return NULL;
+        }
+        else
+        {
+          assert(found->second != NULL);
+          return new Internals::CairoTextRenderer(*this, *found->second, l);
+        }
+      }
+
+      default:
+        return NULL;
+    }
+  }
+
+
+  CairoCompositor::CairoCompositor(const Scene2D& scene,
+                                   unsigned int canvasWidth,
+                                   unsigned int canvasHeight) :
+    helper_(scene, *this)
+  {
+    canvas_.SetSize(canvasWidth, canvasHeight, false);
+  }
+
+    
+  CairoCompositor::~CairoCompositor()
+  {
+    for (Fonts::iterator it = fonts_.begin(); it != fonts_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+  }
+
+
+  void CairoCompositor::SetFont(size_t index,
+                                GlyphBitmapAlphabet* dict)  // Takes ownership
+  {
+    if (dict == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+    else
+    {
+      std::auto_ptr<GlyphBitmapAlphabet> protection(dict);
+      
+      Fonts::iterator found = fonts_.find(index);
+
+      if (found == fonts_.end())
+      {
+        fonts_[index] = protection.release();
+      }
+      else
+      {
+        assert(found->second != NULL);
+        delete found->second;
+
+        found->second = protection.release();
+      }
+    }
+  }
+    
+
+#if ORTHANC_ENABLE_LOCALE == 1
+  void CairoCompositor::SetFont(size_t index,
+                                Orthanc::EmbeddedResources::FileResourceId resource,
+                                unsigned int fontSize,
+                                Orthanc::Encoding codepage)
+  {
+    FontRenderer renderer;
+    renderer.LoadFont(resource, fontSize);
+
+    std::auto_ptr<GlyphBitmapAlphabet> alphabet(new GlyphBitmapAlphabet);
+    alphabet->LoadCodepage(renderer, codepage);
+
+    SetFont(index, alphabet.release());
+  }
+#endif
+
+
+  void CairoCompositor::Refresh()
+  {
+    context_.reset(new CairoContext(canvas_));
+
+    // https://www.cairographics.org/FAQ/#clear_a_surface
+    cairo_set_source_rgba(context_->GetObject(), 0, 0, 0, 255);
+    cairo_paint(context_->GetObject());
+
+    helper_.Refresh(canvas_.GetWidth(), canvas_.GetHeight());
+    context_.reset();
+  }
+
+
+  Orthanc::ImageAccessor* CairoCompositor::RenderText(size_t fontIndex,
+                                                      const std::string& utf8) const
+  {
+    Fonts::const_iterator found = fonts_.find(fontIndex);
+
+    if (found == fonts_.end())
+    {
+      return NULL;
+    }
+    else
+    {
+      assert(found->second != NULL);
+      return found->second->RenderText(utf8);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/CairoCompositor.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,86 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Fonts/GlyphBitmapAlphabet.h"
+#include "../Viewport/CairoContext.h"
+#include "Internals/CompositorHelper.h"
+#include "Internals/ICairoContextProvider.h"
+
+namespace OrthancStone
+{
+  class CairoCompositor :
+    private Internals::CompositorHelper::IRendererFactory,
+    private Internals::ICairoContextProvider
+  {
+  private:
+    typedef std::map<size_t, GlyphBitmapAlphabet*>   Fonts;
+
+    Internals::CompositorHelper  helper_;
+    CairoSurface                 canvas_;
+    Fonts                        fonts_;
+
+    // Only valid during a call to "Refresh()"
+    std::auto_ptr<CairoContext>  context_;
+
+    virtual cairo_t* GetCairoContext();
+
+    virtual unsigned int GetCairoWidth()
+    {
+      return canvas_.GetWidth();
+    }
+
+    virtual unsigned int GetCairoHeight()
+    {
+      return canvas_.GetHeight();
+    }
+    
+    virtual Internals::CompositorHelper::ILayerRenderer* Create(const ISceneLayer& layer);
+
+  public:
+    CairoCompositor(const Scene2D& scene,
+                    unsigned int canvasWidth,
+                    unsigned int canvasHeight);
+    
+    ~CairoCompositor();
+
+    const CairoSurface& GetCanvas() const
+    {
+      return canvas_;
+    }
+    
+    void SetFont(size_t index,
+                 GlyphBitmapAlphabet* dict);  // Takes ownership
+
+#if ORTHANC_ENABLE_LOCALE == 1
+    void SetFont(size_t index,
+                 Orthanc::EmbeddedResources::FileResourceId resource,
+                 unsigned int fontSize,
+                 Orthanc::Encoding codepage);
+#endif
+
+    void Refresh();
+
+    Orthanc::ImageAccessor* RenderText(size_t fontIndex,
+                                       const std::string& utf8) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/ColorSceneLayer.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,100 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ISceneLayer.h"
+#include <Core/Enumerations.h>
+
+#include <stdint.h>
+
+namespace OrthancStone
+{
+  class ColorSceneLayer : public ISceneLayer
+  {
+  private:
+    uint8_t  red_;
+    uint8_t  green_;
+    uint8_t  blue_;
+    uint64_t revision_;
+
+  protected:
+    void BumpRevision()
+    {
+      // this is *not* thread-safe!!!  => (SJO) no problem, Stone assumes mono-threading
+      revision_++;
+    }
+
+  public:
+    ColorSceneLayer() :
+      red_(255),
+      green_(255),
+      blue_(255),
+      revision_(0)
+    {
+    }
+
+    virtual uint64_t GetRevision() const ORTHANC_OVERRIDE
+    {
+      return revision_;
+    }
+
+    void SetColor(uint8_t red,
+                  uint8_t green,
+                  uint8_t blue)
+    {
+      red_ = red;
+      green_ = green;
+      blue_ = blue;
+      BumpRevision();
+    }
+
+    uint8_t GetRed() const
+    {
+      return red_;
+    }
+
+    uint8_t GetGreen() const
+    {
+      return green_;
+    }
+
+    uint8_t GetBlue() const
+    {
+      return blue_;
+    }
+
+    float GetRedAsFloat() const
+    {
+      return static_cast<float>(red_) / 255.0f;
+    }
+
+    float GetGreenAsFloat() const
+    {
+      return static_cast<float>(green_) / 255.0f;
+    }
+
+    float GetBlueAsFloat() const
+    {
+      return static_cast<float>(blue_) / 255.0f;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/ColorTextureSceneLayer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,49 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "ColorTextureSceneLayer.h"
+
+#include <Core/OrthancException.h>
+#include <Core/Images/Image.h>
+
+
+namespace OrthancStone
+{
+  ColorTextureSceneLayer::ColorTextureSceneLayer(const Orthanc::ImageAccessor& texture)
+  {
+    if (texture.GetFormat() != Orthanc::PixelFormat_Grayscale8 &&
+        texture.GetFormat() != Orthanc::PixelFormat_RGBA32 &&
+        texture.GetFormat() != Orthanc::PixelFormat_RGB24)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+    }
+      
+    SetTexture(Orthanc::Image::Clone(texture));
+  }
+
+
+  ISceneLayer* ColorTextureSceneLayer::Clone() const
+  {
+    std::auto_ptr<ColorTextureSceneLayer> cloned(new ColorTextureSceneLayer(GetTexture()));
+    cloned->CopyParameters(*this);
+    return cloned.release();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/ColorTextureSceneLayer.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,40 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "TextureBaseSceneLayer.h"
+
+namespace OrthancStone
+{
+  class ColorTextureSceneLayer : public TextureBaseSceneLayer
+  {
+  public:
+    ColorTextureSceneLayer(const Orthanc::ImageAccessor& texture);
+
+    virtual ISceneLayer* Clone() const;
+
+    virtual Type GetType() const
+    {
+      return Type_ColorTexture;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/FloatTextureSceneLayer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,122 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "FloatTextureSceneLayer.h"
+
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  FloatTextureSceneLayer::FloatTextureSceneLayer(const Orthanc::ImageAccessor& texture)
+  {
+    {
+      std::auto_ptr<Orthanc::ImageAccessor> t(
+        new Orthanc::Image(Orthanc::PixelFormat_Float32, 
+                           texture.GetWidth(), 
+                           texture.GetHeight(), 
+                           false));
+
+      Orthanc::ImageProcessing::Convert(*t, texture);
+      SetTexture(t.release());
+    }
+
+    SetCustomWindowing(128, 256);
+  }
+
+
+  void FloatTextureSceneLayer::SetWindowing(ImageWindowing windowing)
+  {
+    if (windowing_ != windowing)
+    {
+      if (windowing == ImageWindowing_Custom)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+      else
+      {
+        windowing_ = windowing;
+        IncrementRevision();
+      }
+    }
+  }
+
+
+  void FloatTextureSceneLayer::SetCustomWindowing(float customCenter,
+                                                  float customWidth)
+  {
+    if (customWidth <= 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      windowing_ = ImageWindowing_Custom;
+      customCenter_ = customCenter;
+      customWidth_ = customWidth;
+      IncrementRevision();
+    }
+  }
+
+  
+  void FloatTextureSceneLayer::GetWindowing(float& targetCenter,
+                                            float& targetWidth) const
+  {
+    ::OrthancStone::ComputeWindowing(targetCenter, targetWidth,
+                                     windowing_, customCenter_, customWidth_);
+  }
+
+
+  void FloatTextureSceneLayer::FitRange()
+  {
+    float minValue, maxValue;
+    Orthanc::ImageProcessing::GetMinMaxFloatValue(minValue, maxValue, GetTexture());
+
+    float width;
+
+    assert(minValue <= maxValue);
+    if (LinearAlgebra::IsCloseToZero(maxValue - minValue))
+    {
+      width = 1;
+    }
+    else
+    {
+      width = maxValue - minValue;
+    }
+
+    SetCustomWindowing((minValue + maxValue) / 2.0f, width);
+  }
+
+    
+  ISceneLayer* FloatTextureSceneLayer::Clone() const
+  {
+    std::auto_ptr<FloatTextureSceneLayer> cloned
+      (new FloatTextureSceneLayer(GetTexture()));
+
+    cloned->CopyParameters(*this);
+    cloned->windowing_ = windowing_;
+    cloned->customCenter_ = customCenter_;
+    cloned->customWidth_ = customWidth_;
+
+    return cloned.release();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/FloatTextureSceneLayer.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,61 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "TextureBaseSceneLayer.h"
+
+namespace OrthancStone
+{
+  class FloatTextureSceneLayer : public TextureBaseSceneLayer
+  {
+  private:
+    ImageWindowing   windowing_;
+    float            customCenter_;
+    float            customWidth_;
+
+  public:
+    // The pixel format must be "Float32"
+    FloatTextureSceneLayer(const Orthanc::ImageAccessor& texture);
+
+    void SetWindowing(ImageWindowing windowing);
+
+    void SetCustomWindowing(float customCenter,
+                            float customWidth);
+
+    void GetWindowing(float& targetCenter,
+                      float& targetWidth) const;
+
+    ImageWindowing GetWindowingType() const
+    {
+      return windowing_;
+    }
+
+    void FitRange();
+
+    virtual ISceneLayer* Clone() const;
+
+    virtual Type GetType() const
+    {
+      return Type_FloatTexture;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/IPointerTracker.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,46 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "PointerEvent.h"
+
+namespace OrthancStone
+{
+  class IPointerTracker : public boost::noncopyable
+  {
+  public:
+    virtual ~IPointerTracker()
+    {
+    }
+
+    /**
+    This method will be repeatedly called during user interaction
+    */
+    virtual void Update(const PointerEvent& event) = 0;
+
+    /**
+    This method will be called when the tracker should commit its result
+    before being destroyed.
+    */
+    virtual void Release() = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/ISceneLayer.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,55 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Toolbox/Extent2D.h"
+
+#include <boost/noncopyable.hpp>
+#include <stdint.h>
+
+namespace OrthancStone
+{
+  class ISceneLayer : public boost::noncopyable
+  {
+  public:
+    enum Type
+    {
+      Type_InfoPanel,
+      Type_ColorTexture,
+      Type_Polyline,
+      Type_Text,
+      Type_FloatTexture
+    };
+
+    virtual ~ISceneLayer()
+    {
+    }
+
+    virtual ISceneLayer* Clone() const = 0;
+
+    virtual Type GetType() const = 0;
+
+    virtual bool GetBoundingBox(Extent2D& target) const = 0;
+
+    virtual uint64_t GetRevision() const = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/InfoPanelSceneLayer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,105 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "InfoPanelSceneLayer.h"
+
+#include <Core/Images/Image.h>
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  InfoPanelSceneLayer::InfoPanelSceneLayer(const Orthanc::ImageAccessor& texture,
+                                           BitmapAnchor anchor,
+                                           bool isLinearInterpolation) :
+    texture_(Orthanc::Image::Clone(texture)),
+    anchor_(anchor),
+    isLinearInterpolation_(isLinearInterpolation)
+  {
+    if (texture_->GetFormat() != Orthanc::PixelFormat_RGBA32 &&
+        texture_->GetFormat() != Orthanc::PixelFormat_RGB24)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+    }
+  }
+
+
+  void InfoPanelSceneLayer::ComputeAnchorLocation(int& x,
+                                                  int& y,
+                                                  BitmapAnchor anchor,
+                                                  unsigned int textureWidth,
+                                                  unsigned int textureHeight,
+                                                  unsigned int canvasWidth,
+                                                  unsigned int canvasHeight)
+  {
+    int tw = static_cast<int>(textureWidth);
+    int th = static_cast<int>(textureHeight);
+    int cw = static_cast<int>(canvasWidth);
+    int ch = static_cast<int>(canvasHeight);
+    
+    switch (anchor)
+    {
+      case BitmapAnchor_TopLeft:
+      case BitmapAnchor_CenterLeft:
+      case BitmapAnchor_BottomLeft:
+        x = 0;
+        break;
+          
+      case BitmapAnchor_TopCenter:
+      case BitmapAnchor_Center:
+      case BitmapAnchor_BottomCenter:
+        x = (cw - tw) / 2;
+        break;
+          
+      case BitmapAnchor_TopRight:
+      case BitmapAnchor_CenterRight:
+      case BitmapAnchor_BottomRight:
+        x = cw - tw;
+        break;
+          
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    switch (anchor)
+    {
+      case BitmapAnchor_TopLeft:
+      case BitmapAnchor_TopCenter:
+      case BitmapAnchor_TopRight:
+        y = 0;
+        break;
+          
+      case BitmapAnchor_CenterLeft:
+      case BitmapAnchor_Center:
+      case BitmapAnchor_CenterRight:
+        y = (ch - th) / 2;
+        break;
+          
+      case BitmapAnchor_BottomLeft:
+      case BitmapAnchor_BottomCenter:
+      case BitmapAnchor_BottomRight:
+        y = ch - th;
+        break;
+          
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/InfoPanelSceneLayer.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,88 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ISceneLayer.h"
+#include "../StoneEnumerations.h"
+
+#include <Core/Images/ImageAccessor.h>
+
+#include <memory>
+
+namespace OrthancStone
+{
+  class InfoPanelSceneLayer : public ISceneLayer
+  {
+  private:
+    std::auto_ptr<Orthanc::ImageAccessor>  texture_;
+    BitmapAnchor                           anchor_;
+    bool                                   isLinearInterpolation_;
+
+  public:
+    InfoPanelSceneLayer(const Orthanc::ImageAccessor& texture,
+                        BitmapAnchor anchor,
+                        bool isLinearInterpolation);
+
+    virtual ISceneLayer* Clone() const
+    {
+      return new InfoPanelSceneLayer(*texture_, anchor_, isLinearInterpolation_);
+    }
+
+    const Orthanc::ImageAccessor& GetTexture() const
+    {
+      return *texture_;
+    }
+
+    BitmapAnchor GetAnchor() const
+    {
+      return anchor_;
+    }
+
+    bool IsLinearInterpolation() const
+    {
+      return isLinearInterpolation_;
+    }
+
+    virtual Type GetType() const
+    {
+      return Type_InfoPanel;
+    }
+
+    virtual bool GetBoundingBox(Extent2D& target) const
+    {
+      return false;
+    }
+
+    virtual uint64_t GetRevision() const
+    {
+      return 0;
+    }
+
+    static void ComputeAnchorLocation(int& x,
+                                      int& y,
+                                      BitmapAnchor anchor,
+                                      unsigned int textureWidth,
+                                      unsigned int textureHeight,
+                                      unsigned int canvasWidth,
+                                      unsigned int canvasHeight);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/CairoBaseRenderer.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,63 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ICairoContextProvider.h"
+#include "CompositorHelper.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class CairoBaseRenderer : public CompositorHelper::ILayerRenderer
+    {
+    private:
+      ICairoContextProvider&      target_;
+      std::auto_ptr<ISceneLayer>  layer_;
+
+    protected:
+      template<typename T>
+      const T& GetLayer() const
+      {
+        return dynamic_cast<T&>(*layer_);
+      }
+
+      cairo_t* GetCairoContext() const
+      {
+        return target_.GetCairoContext();
+      }
+    
+    public:
+      CairoBaseRenderer(ICairoContextProvider& target,
+                        const ISceneLayer& layer) :
+        target_(target)
+      {
+        Update(layer);
+      }
+
+      virtual void Update(const ISceneLayer& layer)
+      {
+        layer_.reset(layer.Clone());
+      }    
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/CairoColorTextureRenderer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,79 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "CairoColorTextureRenderer.h"
+
+#include "../ColorTextureSceneLayer.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    CairoColorTextureRenderer::CairoColorTextureRenderer(ICairoContextProvider& target,
+                                                         const ISceneLayer& layer) :
+      target_(target)
+    {
+      Update(layer);
+    }
+
+    
+    void CairoColorTextureRenderer::Update(const ISceneLayer& layer)
+    {
+      const ColorTextureSceneLayer& l = dynamic_cast<const ColorTextureSceneLayer&>(layer);
+
+      texture_.Copy(l.GetTexture(), true);
+      textureTransform_ = l.GetTransform();
+      isLinearInterpolation_ = l.IsLinearInterpolation();
+    }
+
+    
+    void CairoColorTextureRenderer::Render(const AffineTransform2D& transform)
+    {
+      cairo_t* cr = target_.GetCairoContext();
+
+      AffineTransform2D t =
+        AffineTransform2D::Combine(transform, textureTransform_);
+      Matrix h = t.GetHomogeneousMatrix();
+      
+      cairo_save(cr);
+
+      cairo_matrix_t m;
+      cairo_matrix_init(&m, h(0, 0), h(1, 0), h(0, 1), h(1, 1), h(0, 2), h(1, 2));
+      cairo_transform(cr, &m);
+
+      cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
+      cairo_set_source_surface(cr, texture_.GetObject(), 0, 0);
+
+      if (isLinearInterpolation_)
+      {
+        cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_BILINEAR);
+      }
+      else
+      {
+        cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
+      }
+
+      cairo_paint(cr);
+
+      cairo_restore(cr);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/CairoColorTextureRenderer.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,49 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Viewport/CairoSurface.h"
+#include "CompositorHelper.h"
+#include "ICairoContextProvider.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class CairoColorTextureRenderer : public CompositorHelper::ILayerRenderer
+    {
+    private:
+      ICairoContextProvider&  target_;
+      CairoSurface            texture_;
+      AffineTransform2D       textureTransform_;
+      bool                    isLinearInterpolation_;
+    
+    public:
+      CairoColorTextureRenderer(ICairoContextProvider& target,
+                                const ISceneLayer& layer);
+
+      virtual void Update(const ISceneLayer& layer);
+    
+      virtual void Render(const AffineTransform2D& transform);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/CairoFloatTextureRenderer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,116 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "CairoFloatTextureRenderer.h"
+
+#include "../FloatTextureSceneLayer.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    void CairoFloatTextureRenderer::Update(const ISceneLayer& layer)
+    {
+      const FloatTextureSceneLayer& l = dynamic_cast<const FloatTextureSceneLayer&>(layer);
+
+      textureTransform_ = l.GetTransform();
+      isLinearInterpolation_ = l.IsLinearInterpolation();
+
+      float windowCenter, windowWidth;
+      l.GetWindowing(windowCenter, windowWidth);
+
+      const float a = windowCenter - windowWidth;
+      const float slope = 256.0f / (2.0f * windowWidth);
+
+      const Orthanc::ImageAccessor& source = l.GetTexture();
+      const unsigned int width = source.GetWidth();
+      const unsigned int height = source.GetHeight();
+      texture_.SetSize(width, height, false);
+
+      Orthanc::ImageAccessor target;
+      texture_.GetWriteableAccessor(target);
+
+      assert(source.GetFormat() == Orthanc::PixelFormat_Float32 &&
+             target.GetFormat() == Orthanc::PixelFormat_BGRA32 &&
+             sizeof(float) == 4);
+
+      for (unsigned int y = 0; y < height; y++)
+      {
+        const float* p = reinterpret_cast<const float*>(source.GetConstRow(y));
+        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
+
+        for (unsigned int x = 0; x < width; x++)
+        {
+          float v = (*p - a) * slope;
+          if (v <= 0)
+          {
+            v = 0;
+          }
+          else if (v >= 255)
+          {
+            v = 255;
+          }
+
+          uint8_t vv = static_cast<uint8_t>(v);
+
+          q[0] = vv;
+          q[1] = vv;
+          q[2] = vv;
+
+          p++;
+          q += 4;
+        }
+      }
+    }
+
+      
+    void CairoFloatTextureRenderer::Render(const AffineTransform2D& transform)
+    {
+      cairo_t* cr = target_.GetCairoContext();
+
+      AffineTransform2D t =
+        AffineTransform2D::Combine(transform, textureTransform_);
+      Matrix h = t.GetHomogeneousMatrix();
+      
+      cairo_save(cr);
+
+      cairo_matrix_t m;
+      cairo_matrix_init(&m, h(0, 0), h(1, 0), h(0, 1), h(1, 1), h(0, 2), h(1, 2));
+      cairo_transform(cr, &m);
+
+      cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
+      cairo_set_source_surface(cr, texture_.GetObject(), 0, 0);
+
+      if (isLinearInterpolation_)
+      {
+        cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_BILINEAR);
+      }
+      else
+      {
+        cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
+      }
+
+      cairo_paint(cr);
+
+      cairo_restore(cr);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/CairoFloatTextureRenderer.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,53 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Viewport/CairoSurface.h"
+#include "CompositorHelper.h"
+#include "ICairoContextProvider.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class CairoFloatTextureRenderer : public CompositorHelper::ILayerRenderer
+    {
+    private:
+      ICairoContextProvider&  target_;
+      CairoSurface            texture_;
+      AffineTransform2D       textureTransform_;
+      bool                    isLinearInterpolation_;
+    
+    public:
+      CairoFloatTextureRenderer(ICairoContextProvider& target,
+                                const ISceneLayer& layer) :
+        target_(target)
+      {
+        Update(layer);
+      }
+
+      virtual void Update(const ISceneLayer& layer);
+    
+      virtual void Render(const AffineTransform2D& transform);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/CairoInfoPanelRenderer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,73 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "CairoInfoPanelRenderer.h"
+
+#include "../InfoPanelSceneLayer.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    void CairoInfoPanelRenderer::Update(const ISceneLayer& layer)
+    {
+      const InfoPanelSceneLayer& l = dynamic_cast<const InfoPanelSceneLayer&>(layer);
+
+      texture_.Copy(l.GetTexture(), true);
+      anchor_ = l.GetAnchor();
+      isLinearInterpolation_ = l.IsLinearInterpolation();
+    }
+
+    
+    void CairoInfoPanelRenderer::Render(const AffineTransform2D& transform)
+    {
+      int dx, dy;
+      InfoPanelSceneLayer::ComputeAnchorLocation(
+        dx, dy, anchor_, texture_.GetWidth(), texture_.GetHeight(),
+        target_.GetCairoWidth(), target_.GetCairoHeight());
+
+      cairo_t* cr = target_.GetCairoContext();
+
+      cairo_save(cr);
+
+      cairo_matrix_t t;
+      cairo_matrix_init_identity(&t);
+      cairo_matrix_translate(&t, dx, dy);
+      cairo_transform(cr, &t);
+
+      cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
+      cairo_set_source_surface(cr, texture_.GetObject(), 0, 0);
+
+      if (isLinearInterpolation_)
+      {
+        cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_BILINEAR);
+      }
+      else
+      {
+        cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
+      }
+
+      cairo_paint(cr);
+
+      cairo_restore(cr);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/CairoInfoPanelRenderer.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,53 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Viewport/CairoSurface.h"
+#include "CompositorHelper.h"
+#include "ICairoContextProvider.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class CairoInfoPanelRenderer : public CompositorHelper::ILayerRenderer
+    {
+    private:
+      ICairoContextProvider& target_;
+      CairoSurface           texture_;
+      BitmapAnchor           anchor_;
+      bool                   isLinearInterpolation_;
+
+    public:
+      CairoInfoPanelRenderer(ICairoContextProvider& target,
+                             const ISceneLayer& layer) :
+        target_(target)
+      {
+        Update(layer);
+      }
+
+      virtual void Update(const ISceneLayer& layer);
+      
+      virtual void Render(const AffineTransform2D& transform);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/CairoPolylineRenderer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,70 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "CairoPolylineRenderer.h"
+
+#include "../PolylineSceneLayer.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    void CairoPolylineRenderer::Render(const AffineTransform2D& transform)
+    {
+      const PolylineSceneLayer& layer = GetLayer<PolylineSceneLayer>();
+      
+      cairo_t* cr = GetCairoContext();
+
+      cairo_set_source_rgb(cr, layer.GetRedAsFloat(), layer.GetGreenAsFloat(), layer.GetBlueAsFloat());
+      cairo_set_line_width(cr, layer.GetThickness());
+      
+      for (size_t i = 0; i < layer.GetChainsCount(); i++)
+      {
+        const PolylineSceneLayer::Chain& chain = layer.GetChain(i);
+
+        if (!chain.empty())
+        {
+          for (size_t j = 0; j < chain.size(); j++)
+          {
+            ScenePoint2D p = chain[j].Apply(transform);
+
+            if (j == 0)
+            {
+              cairo_move_to(cr, p.GetX(), p.GetY());
+            }
+            else
+            {
+              cairo_line_to(cr, p.GetX(), p.GetY());
+            }
+          }
+
+          if (layer.IsClosedChain(i))
+          {
+            ScenePoint2D p = chain[0].Apply(transform);
+            cairo_line_to(cr, p.GetX(), p.GetY());
+          }
+        }
+      }
+
+      cairo_stroke(cr);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/CairoPolylineRenderer.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,42 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "CairoBaseRenderer.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class CairoPolylineRenderer : public CairoBaseRenderer
+    {
+    public:
+      CairoPolylineRenderer(ICairoContextProvider& target,
+                            const ISceneLayer& layer) :
+        CairoBaseRenderer(target, layer)
+      {
+      }
+    
+      virtual void Render(const AffineTransform2D& transform);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/CairoTextRenderer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,114 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "CairoTextRenderer.h"
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    CairoTextRenderer::CairoTextRenderer(ICairoContextProvider& target,
+                                         const GlyphBitmapAlphabet& alphabet,
+                                         const TextSceneLayer& layer) :
+      CairoBaseRenderer(target, layer)
+    {
+      std::auto_ptr<Orthanc::ImageAccessor> source(alphabet.RenderText(layer.GetText()));
+
+      if (source.get() != NULL)
+      {
+        text_.SetSize(source->GetWidth(), source->GetHeight(), true);
+
+        Orthanc::ImageAccessor target;
+        text_.GetWriteableAccessor(target);
+        
+        if (source->GetFormat() != Orthanc::PixelFormat_Grayscale8 ||
+            target.GetFormat() != Orthanc::PixelFormat_BGRA32)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+        
+        const unsigned int width = source->GetWidth();
+        const unsigned int red = layer.GetRed();
+        const unsigned int green = layer.GetGreen();
+        const unsigned int blue = layer.GetBlue();
+
+        for (unsigned int y = 0; y < source->GetHeight(); y++)
+        {
+          const uint8_t* p = reinterpret_cast<const uint8_t*>(source->GetConstRow(y));
+          uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
+          
+          for (unsigned int x = 0; x < width; x++)
+          {
+            unsigned int alpha = *p;
+
+            // Premultiplied alpha
+            q[0] = static_cast<uint8_t>((blue * alpha) / 255);
+            q[1] = static_cast<uint8_t>((green * alpha) / 255);
+            q[2] = static_cast<uint8_t>((red * alpha) / 255);
+            q[3] = *p;
+            
+            p++;
+            q += 4;
+          }
+        }
+
+        cairo_surface_mark_dirty(text_.GetObject());
+      }
+    }
+
+      
+    void CairoTextRenderer::Render(const AffineTransform2D& transform)
+    {
+      if (text_.GetWidth() != 0 &&
+          text_.GetHeight() != 0)
+      {
+        const TextSceneLayer& layer = GetLayer<TextSceneLayer>();
+      
+        cairo_t* cr = GetCairoContext();
+        cairo_set_source_rgb(cr, layer.GetRedAsFloat(), layer.GetGreenAsFloat(), layer.GetBlueAsFloat());
+
+        double dx, dy;  // In pixels
+        ComputeAnchorTranslation(dx, dy, layer.GetAnchor(), text_.GetWidth(),
+                                 text_.GetHeight(), layer.GetBorder());
+      
+        double x = layer.GetX();
+        double y = layer.GetY();
+        transform.Apply(x, y);
+
+        cairo_save(cr);
+
+        cairo_matrix_t t;
+        cairo_matrix_init_identity(&t);
+        cairo_matrix_translate(&t, x + dx, y + dy);
+        cairo_transform(cr, &t);
+
+        cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
+        cairo_set_source_surface(cr, text_.GetObject(), 0, 0);
+        cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_BILINEAR);
+        cairo_paint(cr);
+
+        cairo_restore(cr);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/CairoTextRenderer.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,46 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Fonts/GlyphBitmapAlphabet.h"
+#include "../../Viewport/CairoSurface.h"
+#include "../TextSceneLayer.h"
+#include "CairoBaseRenderer.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class CairoTextRenderer : public CairoBaseRenderer
+    {
+    private:
+      CairoSurface  text_;
+    
+    public:
+      CairoTextRenderer(ICairoContextProvider& target,
+                        const GlyphBitmapAlphabet& alphabet,
+                        const TextSceneLayer& layer);
+    
+      virtual void Render(const AffineTransform2D& transform);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/CompositorHelper.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,156 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "CompositorHelper.h"
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class CompositorHelper::Item : public boost::noncopyable
+    {
+    private:
+      std::auto_ptr<ILayerRenderer>  renderer_;
+      const ISceneLayer&             layer_;
+      uint64_t                       layerIdentifier_;
+      uint64_t                       lastRevision_;
+
+    public:
+      Item(ILayerRenderer* renderer,     // Takes ownership
+           const ISceneLayer& layer,
+           uint64_t layerIdentifier) :
+        renderer_(renderer),
+        layer_(layer),
+        layerIdentifier_(layerIdentifier),
+        lastRevision_(layer.GetRevision())
+      {
+        if (renderer == NULL)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+        }
+      }
+
+      ILayerRenderer& GetRenderer() const
+      {
+        assert(renderer_.get() != NULL);
+        return *renderer_;
+      }
+
+      const ISceneLayer& GetLayer() const
+      {
+        return layer_;
+      }
+
+      uint64_t GetLayerIdentifier() const
+      {
+        return layerIdentifier_;
+      }
+
+      uint64_t GetLastRevision() const
+      {
+        return lastRevision_;
+      }
+
+      void UpdateRenderer()
+      {
+        assert(renderer_.get() != NULL);
+        renderer_->Update(layer_);
+        lastRevision_ = layer_.GetRevision();
+      }
+    };
+
+
+    void CompositorHelper::Visit(const ISceneLayer& layer,
+                                 uint64_t layerIdentifier,
+                                 int depth)
+    {
+      // "Visit()" is only applied to layers existing in the scene
+      assert(scene_.HasLayer(depth)); 
+
+      Content::iterator found = content_.find(depth);
+
+      assert(found == content_.end() ||
+             found->second != NULL);
+
+      if (found == content_.end() ||
+          found->second->GetLayerIdentifier() != layerIdentifier)
+      {
+        // This is the first time this layer is rendered, or the layer
+        // is not the same as before
+        if (found != content_.end())
+        {
+          delete found->second;
+          content_.erase(found);
+        }
+
+        std::auto_ptr<ILayerRenderer> renderer(factory_.Create(layer));
+
+        if (renderer.get() != NULL)
+        {
+          renderer->Render(sceneTransform_);
+          content_[depth] = new Item(renderer.release(), layer, layerIdentifier);
+        }
+      }
+      else
+      {
+        // This layer has already been rendered
+        assert(found->second->GetLastRevision() <= layer.GetRevision());
+        
+        if (found->second->GetLastRevision() < layer.GetRevision())
+        {
+          found->second->UpdateRenderer();
+        }
+
+        found->second->GetRenderer().Render(sceneTransform_);
+      }
+
+      // Check invariants
+      assert(content_.find(depth) == content_.end() ||
+             (content_[depth]->GetLayerIdentifier() == layerIdentifier &&
+              content_[depth]->GetLastRevision() == layer.GetRevision()));
+    }
+
+
+    CompositorHelper::~CompositorHelper()
+    {
+      for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
+      {
+        assert(it->second != NULL);
+        delete it->second;
+      }
+    }
+
+  
+    void CompositorHelper::Refresh(unsigned int canvasWidth,
+                                   unsigned int canvasHeight)
+    {
+      // Bring coordinate (0,0) to the center of the canvas
+      AffineTransform2D offset = AffineTransform2D::CreateOffset(
+        static_cast<double>(canvasWidth) / 2.0,
+        static_cast<double>(canvasHeight) / 2.0);
+
+      sceneTransform_ = AffineTransform2D::Combine(offset, scene_.GetSceneToCanvasTransform());
+      scene_.Apply(*this);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/CompositorHelper.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,85 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Scene2D.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class CompositorHelper : protected Scene2D::IVisitor
+    {
+    public:
+      class ILayerRenderer : public boost::noncopyable
+      {
+      public:
+        virtual ~ILayerRenderer()
+        {
+        }
+
+        virtual void Render(const AffineTransform2D& transform) = 0;
+
+        // "Update()" is only called if the type of the layer has not changed
+        virtual void Update(const ISceneLayer& layer) = 0;
+      };
+
+      class IRendererFactory : public boost::noncopyable
+      {
+      public:
+        virtual ~IRendererFactory()
+        {
+        }
+
+        virtual ILayerRenderer* Create(const ISceneLayer& layer) = 0;
+      };
+
+    private:
+      class Item;
+
+      typedef std::map<int, Item*>  Content;
+
+      const Scene2D&     scene_;
+      IRendererFactory&  factory_;
+      Content            content_;
+      AffineTransform2D  sceneTransform_;
+
+    protected:
+      virtual void Visit(const ISceneLayer& layer,
+                         uint64_t layerIdentifier,
+                         int depth);
+
+    public:
+      CompositorHelper(const Scene2D& scene,
+                       IRendererFactory& factory) :
+        scene_(scene),
+        factory_(factory)
+      {
+      }
+
+      ~CompositorHelper();
+
+      void Refresh(unsigned int canvasWidth,
+                   unsigned int canvasHeight);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/FixedPointAligner.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,48 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "FixedPointAligner.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    FixedPointAligner::FixedPointAligner(Scene2D& scene,
+                                         const ScenePoint2D& p) :
+      scene_(scene),
+      canvas_(p)
+    {
+      pivot_ = canvas_.Apply(scene_.GetCanvasToSceneTransform());
+    }
+
+    
+    void FixedPointAligner::Apply()
+    {
+      ScenePoint2D p = canvas_.Apply(scene_.GetCanvasToSceneTransform());
+
+      scene_.SetSceneToCanvasTransform(
+        AffineTransform2D::Combine(
+          scene_.GetSceneToCanvasTransform(),
+          AffineTransform2D::CreateOffset(p.GetX() - pivot_.GetX(),
+                                          p.GetY() - pivot_.GetY())));
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/FixedPointAligner.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,47 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Scene2D.h"
+#include "../ScenePoint2D.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    // During a mouse event that modifies the view of a scene, keeps
+    // one point (the pivot) at the same position on the canvas
+    class FixedPointAligner : public boost::noncopyable
+    {
+    private:
+      Scene2D&      scene_;
+      ScenePoint2D  pivot_;
+      ScenePoint2D  canvas_;
+
+    public:
+      FixedPointAligner(Scene2D& scene,
+                        const ScenePoint2D& p);
+
+      void Apply();
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/ICairoContextProvider.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,46 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <boost/noncopyable.hpp>
+#include <cairo.h>
+#include <stdint.h>
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class ICairoContextProvider : public boost::noncopyable
+    {
+    public:
+      virtual ~ICairoContextProvider()
+      {
+      }
+
+      virtual cairo_t* GetCairoContext() = 0;
+
+      virtual unsigned int GetCairoWidth() = 0;
+
+      virtual unsigned int GetCairoHeight() = 0;
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLAdvancedPolylineRenderer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,51 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OpenGLAdvancedPolylineRenderer.h"
+
+#include <Core/OrthancException.h>
+
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    void OpenGLAdvancedPolylineRenderer::LoadLayer(const PolylineSceneLayer& layer)
+    {
+      data_.reset(new OpenGLLinesProgram::Data(context_, layer));
+
+      if (data_.get() == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+    }
+
+
+    OpenGLAdvancedPolylineRenderer::OpenGLAdvancedPolylineRenderer(OpenGL::IOpenGLContext& context,
+                                                                   OpenGLLinesProgram& program,
+                                                                   const PolylineSceneLayer& layer) :
+      context_(context),
+      program_(program)
+    {
+      LoadLayer(layer);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLAdvancedPolylineRenderer.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,57 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "CompositorHelper.h"
+#include "OpenGLLinesProgram.h"
+
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class OpenGLAdvancedPolylineRenderer : public CompositorHelper::ILayerRenderer
+    {
+    private:
+      OpenGL::IOpenGLContext&                  context_;
+      OpenGLLinesProgram&                      program_;
+      std::auto_ptr<OpenGLLinesProgram::Data>  data_;
+
+      void LoadLayer(const PolylineSceneLayer& layer);
+
+    public:
+      OpenGLAdvancedPolylineRenderer(OpenGL::IOpenGLContext& context,
+                                     OpenGLLinesProgram& program,
+                                     const PolylineSceneLayer& layer);
+
+      virtual void Render(const AffineTransform2D& transform)
+      {
+        program_.Apply(*data_, transform, true, true);
+      }
+
+      virtual void Update(const ISceneLayer& layer)
+      {
+        LoadLayer(dynamic_cast<const PolylineSceneLayer&>(layer));
+      }
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLBasicPolylineRenderer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,90 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OpenGLBasicPolylineRenderer.h"
+
+#include "../../OpenGL/OpenGLIncludes.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    OpenGLBasicPolylineRenderer::OpenGLBasicPolylineRenderer(OpenGL::IOpenGLContext& context,
+      const PolylineSceneLayer& layer) :
+      context_(context)
+    {
+      layer_.Copy(layer);
+    }
+
+
+    void OpenGLBasicPolylineRenderer::Render(const AffineTransform2D& transform)
+    {
+      AffineTransform2D t = AffineTransform2D::Combine(
+        AffineTransform2D::CreateOpenGLClipspace(context_.GetCanvasWidth(), context_.GetCanvasHeight()),
+        transform);
+
+      glUseProgram(0);
+      glColor3ub(layer_.GetRed(), layer_.GetGreen(), layer_.GetBlue());
+
+      glBegin(GL_LINES);
+
+      for (size_t i = 0; i < layer_.GetChainsCount(); i++)
+      {
+        const PolylineSceneLayer::Chain& chain = layer_.GetChain(i);
+
+        if (chain.size() > 1)
+        {
+          ScenePoint2D previous = chain[0].Apply(t);
+
+          for (size_t j = 1; j < chain.size(); j++)
+          {
+            ScenePoint2D p = chain[j].Apply(t);
+
+            glVertex2f(static_cast<GLfloat>(previous.GetX()),
+                       static_cast<GLfloat>(previous.GetY()));
+            glVertex2f(static_cast<GLfloat>(p.GetX()), 
+                       static_cast<GLfloat>(p.GetY()));
+
+            previous = p;
+          }
+
+          if (layer_.IsClosedChain(i))
+          {
+            ScenePoint2D p = chain[0].Apply(t);
+
+            glVertex2f(static_cast<GLfloat>(previous.GetX()),
+                       static_cast<GLfloat>(previous.GetY()));
+            glVertex2f(static_cast<GLfloat>(p.GetX()),
+                       static_cast<GLfloat>(p.GetY()));
+          }
+        }
+      }
+
+      glEnd();
+    }
+
+
+    void OpenGLBasicPolylineRenderer::Update(const ISceneLayer& layer)
+    {
+      layer_.Copy(dynamic_cast<const PolylineSceneLayer&>(layer));
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLBasicPolylineRenderer.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,47 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../OpenGL/IOpenGLContext.h"
+#include "../PolylineSceneLayer.h"
+#include "CompositorHelper.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class OpenGLBasicPolylineRenderer : public CompositorHelper::ILayerRenderer
+    {
+    private:
+      OpenGL::IOpenGLContext&  context_;
+      PolylineSceneLayer       layer_;
+
+    public:
+      OpenGLBasicPolylineRenderer(OpenGL::IOpenGLContext& context,
+                                  const PolylineSceneLayer& layer);
+
+      virtual void Render(const AffineTransform2D& transform);
+
+      virtual void Update(const ISceneLayer& layer);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLColorTextureProgram.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,64 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OpenGLColorTextureProgram.h"
+#include "OpenGLShaderVersionDirective.h"
+
+static const char* FRAGMENT_SHADER = 
+  ORTHANC_STONE_OPENGL_SHADER_VERSION_DIRECTIVE
+  "uniform sampler2D u_texture;                       \n"
+  "varying vec2 v_texcoord;                           \n"
+  "void main()                                        \n"
+  "{                                                  \n"
+  "  gl_FragColor = texture2D(u_texture, v_texcoord); \n"
+  "}";
+
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    OpenGLColorTextureProgram::OpenGLColorTextureProgram(OpenGL::IOpenGLContext&  context) :
+      program_(context, FRAGMENT_SHADER)
+    {
+    }
+
+    
+    void OpenGLColorTextureProgram::Apply(OpenGL::OpenGLTexture& texture,
+                                          const AffineTransform2D& transform,
+                                          bool useAlpha)
+    {
+      OpenGLTextureProgram::Execution execution(program_, texture, transform);
+
+      if (useAlpha)
+      {
+        glEnable(GL_BLEND);
+        glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+        execution.DrawTriangles();
+        glDisable(GL_BLEND);
+      }
+      else
+      {
+        execution.DrawTriangles();
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLColorTextureProgram.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,43 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "OpenGLTextureProgram.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class OpenGLColorTextureProgram : public boost::noncopyable
+    {
+    private:
+      OpenGLTextureProgram  program_;
+
+    public:
+      OpenGLColorTextureProgram(OpenGL::IOpenGLContext&  context);
+
+      void Apply(OpenGL::OpenGLTexture& texture,
+                 const AffineTransform2D& transform,
+                 bool useAlpha);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLColorTextureRenderer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,62 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OpenGLColorTextureRenderer.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    void OpenGLColorTextureRenderer::LoadTexture(const ColorTextureSceneLayer& layer)
+    {
+      context_.MakeCurrent();
+      texture_.reset(new OpenGL::OpenGLTexture);
+      texture_->Load(layer.GetTexture(), layer.IsLinearInterpolation());
+      layerTransform_ = layer.GetTransform();
+    }
+
+    
+    OpenGLColorTextureRenderer::OpenGLColorTextureRenderer(OpenGL::IOpenGLContext& context,
+                                                           OpenGLColorTextureProgram& program,
+                                                           const ColorTextureSceneLayer& layer) :
+      context_(context),
+      program_(program)
+    {
+      LoadTexture(layer);
+    }
+
+    
+    void OpenGLColorTextureRenderer::Render(const AffineTransform2D& transform)
+    {
+      if (texture_.get() != NULL)
+      {
+        program_.Apply(*texture_, AffineTransform2D::Combine(transform, layerTransform_), true);
+      }
+    }
+
+    
+    void OpenGLColorTextureRenderer::Update(const ISceneLayer& layer)
+    {
+      // Should never happen (no revisions in color textures)
+      LoadTexture(dynamic_cast<const ColorTextureSceneLayer&>(layer));
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLColorTextureRenderer.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,52 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "OpenGLColorTextureProgram.h"
+#include "CompositorHelper.h"
+#include "../ColorTextureSceneLayer.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class OpenGLColorTextureRenderer : public CompositorHelper::ILayerRenderer
+    {
+    private:
+      OpenGL::IOpenGLContext&                context_;
+      OpenGLColorTextureProgram&             program_;
+      std::auto_ptr<OpenGL::OpenGLTexture>   texture_;
+      AffineTransform2D                      layerTransform_;
+
+      void LoadTexture(const ColorTextureSceneLayer& layer);
+
+    public:
+      OpenGLColorTextureRenderer(OpenGL::IOpenGLContext& context,
+                                 OpenGLColorTextureProgram& program,
+                                 const ColorTextureSceneLayer& layer);
+
+      virtual void Render(const AffineTransform2D& transform);
+
+      virtual void Update(const ISceneLayer& layer);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLFloatTextureProgram.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,148 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OpenGLFloatTextureProgram.h"
+#include "OpenGLShaderVersionDirective.h"
+
+#include <Core/OrthancException.h>
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+
+
+static const char* FRAGMENT_SHADER = 
+  ORTHANC_STONE_OPENGL_SHADER_VERSION_DIRECTIVE
+  "uniform float u_offset;                       \n"
+  "uniform float u_slope;                        \n"
+  "uniform float u_windowCenter;                 \n"
+  "uniform float u_windowWidth;                  \n"
+  "uniform sampler2D u_texture;                  \n"
+  "varying vec2 v_texcoord;                      \n"
+  "void main()                                   \n"
+  "{                                             \n"
+  "  vec4 t = texture2D(u_texture, v_texcoord);  \n"
+  "  float v = (t.r * 256.0 + t.g) * 256.0;      \n"
+  "  v = v * u_slope + u_offset;                 \n"  // (*)
+  "  float a = u_windowCenter - u_windowWidth;   \n"
+  "  float dy = 1.0 / (2.0 * u_windowWidth);     \n"
+  "  if (v <= a)                                 \n"
+  "    v = 0.0;                                  \n"
+  "  else                                        \n"
+  "  {                                           \n"
+  "    v = (v - a) * dy;                         \n"
+  "    if (v >= 1.0)                             \n"
+  "      v = 1.0;                                \n"
+  "  }                                           \n"
+  "  gl_FragColor = vec4(v, v, v, 1);            \n"
+  "}";
+
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    OpenGLFloatTextureProgram::Data::Data(const Orthanc::ImageAccessor& texture,
+                                          bool isLinearInterpolation)
+    {
+      if (texture.GetFormat() != Orthanc::PixelFormat_Float32)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+      }
+
+      float minValue, maxValue;
+      Orthanc::ImageProcessing::GetMinMaxFloatValue(minValue, maxValue, texture);
+
+      offset_ = minValue;
+
+      if (LinearAlgebra::IsCloseToZero(maxValue - minValue))
+      {
+        slope_ = 1;
+      }
+      else
+      {
+        slope_ = (maxValue - minValue) / 65536.0f;
+        assert(!LinearAlgebra::IsCloseToZero(slope_));
+      }
+
+      const unsigned int width = texture.GetWidth();
+      const unsigned int height = texture.GetHeight();
+
+      Orthanc::Image converted(Orthanc::PixelFormat_RGB24, width, height, true);
+
+      for (unsigned int y = 0; y < height; y++)
+      {
+        const float *p = reinterpret_cast<const float*>(texture.GetConstRow(y));
+        uint8_t *q = reinterpret_cast<uint8_t*>(converted.GetRow(y));
+
+        for (unsigned int x = 0; x < width; x++)
+        {
+          /**
+           * At (*), the floating-point "value" is reconstructed as
+           * "value = texture * slope + offset".
+           * <=> texture = (value - offset) / slope
+           **/
+
+          float texture = (*p - offset_) / slope_;
+          if (texture < 0)
+          {
+            texture = 0;
+          }
+          else if (texture >= 65535.0f)
+          {
+            texture = 65535.0f;
+          }
+
+          uint16_t t = static_cast<uint16_t>(texture);
+
+          q[0] = t / 256;  // red
+          q[1] = t % 256;  // green
+          q[2] = 0;        // blue is unused
+
+          p++;
+          q += 3;
+        }
+      }
+
+      texture_.Load(converted, isLinearInterpolation);
+    }
+
+    
+    OpenGLFloatTextureProgram::OpenGLFloatTextureProgram(OpenGL::IOpenGLContext&  context) :
+      program_(context, FRAGMENT_SHADER)
+    {
+    }
+
+
+    void OpenGLFloatTextureProgram::Apply(Data& data,
+                                          const AffineTransform2D& transform,
+                                          float windowCenter,
+                                          float windowWidth)
+    {
+      OpenGLTextureProgram::Execution execution(program_, data.GetTexture(), transform);
+
+      glUniform1f(execution.GetUniformLocation("u_slope"), data.GetSlope());
+      glUniform1f(execution.GetUniformLocation("u_offset"), data.GetOffset());
+      glUniform1f(execution.GetUniformLocation("u_windowCenter"), windowCenter);
+      glUniform1f(execution.GetUniformLocation("u_windowWidth"), windowWidth);
+
+      execution.DrawTriangles();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLFloatTextureProgram.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,72 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "OpenGLTextureProgram.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class OpenGLFloatTextureProgram : public boost::noncopyable
+    {
+    public:
+      class Data : public boost::noncopyable
+      {
+      private:
+        OpenGL::OpenGLTexture  texture_;
+        float                  offset_;
+        float                  slope_;
+
+      public:
+        Data(const Orthanc::ImageAccessor& texture,
+             bool isLinearInterpolation);
+
+        float GetOffset() const
+        {
+          return offset_;
+        }
+
+        float GetSlope() const
+        {
+          return slope_;
+        }
+
+        OpenGL::OpenGLTexture& GetTexture()
+        {
+          return texture_;
+        }
+      };
+
+    private:
+      OpenGLTextureProgram  program_;
+
+    public:
+      OpenGLFloatTextureProgram(OpenGL::IOpenGLContext&  context);
+
+      void Apply(Data& data,
+                 const AffineTransform2D& transform,
+                 float windowCenter,
+                 float windowWidth);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLFloatTextureRenderer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,67 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OpenGLFloatTextureRenderer.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    void OpenGLFloatTextureRenderer::UpdateInternal(const FloatTextureSceneLayer& layer,
+                                                    bool loadTexture)
+    {
+      if (loadTexture)
+      {
+        context_.MakeCurrent();
+        texture_.reset(new OpenGLFloatTextureProgram::Data(layer.GetTexture(), layer.IsLinearInterpolation()));
+      }
+
+      layerTransform_ = layer.GetTransform();
+      layer.GetWindowing(windowCenter_, windowWidth_);
+    }
+
+
+    OpenGLFloatTextureRenderer::OpenGLFloatTextureRenderer(OpenGL::IOpenGLContext& context,
+                                                           OpenGLFloatTextureProgram& program,
+                                                           const FloatTextureSceneLayer& layer) :
+      context_(context),
+      program_(program)
+    {
+      UpdateInternal(layer, true);
+    }
+
+
+    void OpenGLFloatTextureRenderer::Render(const AffineTransform2D& transform)
+    {
+      if (texture_.get() != NULL)
+      {
+        program_.Apply(*texture_, AffineTransform2D::Combine(transform, layerTransform_), 
+                       windowCenter_, windowWidth_);
+      }
+    }
+
+
+    void OpenGLFloatTextureRenderer::Update(const ISceneLayer& layer)
+    {
+      UpdateInternal(dynamic_cast<const FloatTextureSceneLayer&>(layer), false);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLFloatTextureRenderer.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,55 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "CompositorHelper.h"
+#include "OpenGLFloatTextureProgram.h"
+#include "../FloatTextureSceneLayer.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class OpenGLFloatTextureRenderer : public CompositorHelper::ILayerRenderer
+    {
+    private:
+      OpenGL::IOpenGLContext&                         context_;
+      OpenGLFloatTextureProgram&                      program_;
+      std::auto_ptr<OpenGLFloatTextureProgram::Data>  texture_;
+      AffineTransform2D                               layerTransform_;
+      float                                           windowCenter_;
+      float                                           windowWidth_;
+
+      void UpdateInternal(const FloatTextureSceneLayer& layer,
+                          bool loadTexture);
+
+    public:
+      OpenGLFloatTextureRenderer(OpenGL::IOpenGLContext& context,
+                                 OpenGLFloatTextureProgram& program,
+                                 const FloatTextureSceneLayer& layer);
+
+      virtual void Render(const AffineTransform2D& transform);
+
+      virtual void Update(const ISceneLayer& layer);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLInfoPanelRenderer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,62 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OpenGLInfoPanelRenderer.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    void OpenGLInfoPanelRenderer::LoadTexture(const InfoPanelSceneLayer& layer)
+    {
+      context_.MakeCurrent();
+      texture_.reset(new OpenGL::OpenGLTexture);
+      texture_->Load(layer.GetTexture(), layer.IsLinearInterpolation());
+      anchor_ = layer.GetAnchor();
+    }
+
+
+    OpenGLInfoPanelRenderer::OpenGLInfoPanelRenderer(OpenGL::IOpenGLContext& context,
+                                                     OpenGLColorTextureProgram& program,
+                                                     const InfoPanelSceneLayer& layer) :
+      context_(context),
+      program_(program)
+    {
+      LoadTexture(layer);
+    }
+
+    
+    void OpenGLInfoPanelRenderer::Render(const AffineTransform2D& transform)
+    {
+      if (texture_.get() != NULL)
+      {
+        int dx, dy;
+        InfoPanelSceneLayer::ComputeAnchorLocation(
+          dx, dy, anchor_, texture_->GetWidth(), texture_->GetHeight(),
+          context_.GetCanvasWidth(), context_.GetCanvasHeight());
+
+        // The position of this type of layer is layer: Ignore the
+        // "transform" coming from the scene
+        program_.Apply(*texture_, AffineTransform2D::CreateOffset(dx, dy), true);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLInfoPanelRenderer.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,55 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "CompositorHelper.h"
+#include "OpenGLColorTextureProgram.h"
+#include "../InfoPanelSceneLayer.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class OpenGLInfoPanelRenderer : public CompositorHelper::ILayerRenderer
+    {
+    private:
+      OpenGL::IOpenGLContext&               context_;
+      OpenGLColorTextureProgram&            program_;
+      std::auto_ptr<OpenGL::OpenGLTexture>  texture_;
+      BitmapAnchor                          anchor_;
+
+      void LoadTexture(const InfoPanelSceneLayer& layer);
+
+    public:
+      OpenGLInfoPanelRenderer(OpenGL::IOpenGLContext& context,
+                              OpenGLColorTextureProgram& program,
+                              const InfoPanelSceneLayer& layer);
+
+      virtual void Render(const AffineTransform2D& transform);
+
+      virtual void Update(const ISceneLayer& layer)
+      {
+        LoadTexture(dynamic_cast<const InfoPanelSceneLayer&>(layer));
+      }
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLLinesProgram.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,462 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OpenGLLinesProgram.h"
+#include "OpenGLShaderVersionDirective.h"
+
+#include <Core/OrthancException.h>
+
+
+static const unsigned int COMPONENTS_POSITION = 3;
+static const unsigned int COMPONENTS_MITER = 2;
+
+
+static const char* VERTEX_SHADER = 
+  ORTHANC_STONE_OPENGL_SHADER_VERSION_DIRECTIVE
+  "attribute vec2 a_miter_direction; \n"
+  "attribute vec4 a_position;        \n"
+  "uniform float u_thickness;        \n"
+  "uniform mat4 u_matrix;            \n"
+  "varying float v_distance;         \n"
+  "void main()                       \n"
+  "{                                 \n"
+  "  v_distance = a_position.z;      \n"
+  "  gl_Position = u_matrix * vec4(a_position.xy + a_position.z * a_miter_direction * u_thickness, 0, 1); \n"
+  "}";
+
+
+static const char* FRAGMENT_SHADER = 
+  ORTHANC_STONE_OPENGL_SHADER_VERSION_DIRECTIVE
+  "uniform bool u_antialiasing;           \n"
+  "uniform float u_antialiasing_start;    \n"
+  "uniform vec3 u_color;                  \n"
+  "varying float v_distance;              \n"   // Distance of the point to the segment
+  "void main()                            \n"
+  "{                                      \n"
+  "  float d = abs(v_distance);           \n"
+  "  if (!u_antialiasing ||               \n"
+  "      d <= u_antialiasing_start)       \n"
+  "    gl_FragColor = vec4(u_color, 1);   \n"
+  "  else if (d >= 1.0)                   \n"
+  "    gl_FragColor = vec4(0, 0, 0, 0);   \n"
+  "  else                                 \n"
+  "  {                                    \n"
+  "    float alpha = 1.0 - smoothstep(u_antialiasing_start, 1.0, d); \n"
+  "    gl_FragColor = vec4(u_color * alpha, alpha); \n"
+  "  }                                    \n"
+  "}";
+
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class OpenGLLinesProgram::Data::Segment
+    {
+    private:
+      bool    isEmpty_;
+      double  x1_;
+      double  y1_;
+      double  x2_;
+      double  y2_;
+      double  miterX1_;
+      double  miterY1_;
+      double  miterX2_;
+      double  miterY2_;
+
+      Vector  lineAbove_;  // In homogeneous coordinates (size = 3)
+      Vector  lineBelow_;
+
+    public:
+      Segment(const PolylineSceneLayer::Chain& chain,
+              size_t index1,
+              size_t index2) :
+        isEmpty_(false)
+      {
+        if (index1 >= chain.size() ||
+            index2 >= chain.size())
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+        else
+        {
+          const ScenePoint2D& p = chain[index1];
+          const ScenePoint2D& q = chain[index2];
+        
+          x1_ = p.GetX();
+          y1_ = p.GetY();
+          x2_ = q.GetX();
+          y2_ = q.GetY();
+
+          const double dx = x2_ - x1_;
+          const double dy = y2_ - y1_;
+          const double norm = sqrt(dx * dx + dy * dy);
+
+          if (LinearAlgebra::IsCloseToZero(norm))
+          {
+            isEmpty_ = true;
+          }
+          else
+          {
+            isEmpty_ = false;
+            const double normalX = -dy / norm;
+            const double normalY = dx / norm;
+
+            miterX1_ = normalX;
+            miterY1_ = normalY;
+            miterX2_ = normalX;
+            miterY2_ = normalY;
+
+            Vector a = LinearAlgebra::CreateVector(x1_ + normalX, y1_ + normalY, 1);
+            Vector b = LinearAlgebra::CreateVector(x2_ + normalX, y2_ + normalY, 1);
+            LinearAlgebra::CrossProduct(lineAbove_, a, b);
+
+            a = LinearAlgebra::CreateVector(x1_ - normalX, y1_ - normalY, 1);
+            b = LinearAlgebra::CreateVector(x2_ - normalX, y2_ - normalY, 1);
+            LinearAlgebra::CrossProduct(lineBelow_, a, b);
+          }
+        }
+      }
+
+      bool IsEmpty() const
+      {
+        return isEmpty_;
+      }
+
+      static double ComputeSignedArea(double x1,
+                                      double y1,
+                                      double x2,
+                                      double y2,
+                                      double x3,
+                                      double y3)
+      {
+        // This computes the signed area of a 2D triangle. This
+        // formula is e.g. used in the sorting algorithm of Graham's
+        // scan to compute the convex hull.
+        // https://en.wikipedia.org/wiki/Graham_scan
+        return (x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1);
+      }
+
+      static void CreateMiter(Segment& left,
+                              Segment& right)
+      {
+        if (!left.IsEmpty() &&
+            !right.IsEmpty())
+        {
+          Vector above, below;
+          LinearAlgebra::CrossProduct(above, left.lineAbove_, right.lineAbove_);
+          LinearAlgebra::CrossProduct(below, left.lineBelow_, right.lineBelow_);
+
+          if (!LinearAlgebra::IsCloseToZero(above[2]) &&
+              !LinearAlgebra::IsCloseToZero(below[2]))
+          {
+            // Back to inhomogeneous 2D coordinates
+            above /= above[2];
+            below /= below[2];
+
+            // Check whether "above" and "below" intersection points
+            // are on the half-plane defined by the endpoints of the
+            // two segments. This is an indicator of whether the angle
+            // is too acute.
+            double s1 = ComputeSignedArea(left.x1_, left.y1_,
+                                          above[0], above[1],
+                                          right.x2_, right.y2_);
+            double s2 = ComputeSignedArea(left.x1_, left.y1_,
+                                          below[0], below[1],
+                                          right.x2_, right.y2_);
+            
+            // The two signed areas must have the same sign
+            if (s1 * s2 >= 0)
+            {
+              left.miterX2_ = above[0] - left.x2_;
+              left.miterY2_ = above[1] - left.y2_;
+
+              right.miterX1_ = left.miterX2_;
+              right.miterY1_ = left.miterY2_;
+            }
+          }
+        }
+      }
+
+      void AddTriangles(std::vector<float>& coords,
+                        std::vector<float>& miterDirections)
+      {
+        if (isEmpty_)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+        }
+
+        // First triangle
+        coords.push_back(x1_);
+        coords.push_back(y1_);
+        coords.push_back(1);
+        coords.push_back(x2_);
+        coords.push_back(y2_);
+        coords.push_back(-1);
+        coords.push_back(x2_);
+        coords.push_back(y2_);
+        coords.push_back(1);
+
+        miterDirections.push_back(miterX1_);
+        miterDirections.push_back(miterY1_);
+        miterDirections.push_back(miterX2_);
+        miterDirections.push_back(miterY2_);
+        miterDirections.push_back(miterX2_);
+        miterDirections.push_back(miterY2_);
+        
+        // Second triangle
+        coords.push_back(x1_);
+        coords.push_back(y1_);
+        coords.push_back(1);
+        coords.push_back(x1_);
+        coords.push_back(y1_);
+        coords.push_back(-1);
+        coords.push_back(x2_);
+        coords.push_back(y2_);
+        coords.push_back(-1);
+
+        miterDirections.push_back(miterX1_);
+        miterDirections.push_back(miterY1_);
+        miterDirections.push_back(miterX1_);
+        miterDirections.push_back(miterY1_);
+        miterDirections.push_back(miterX2_);
+        miterDirections.push_back(miterY2_);
+      }        
+    };
+
+
+    OpenGLLinesProgram::Data::Data(OpenGL::IOpenGLContext& context,
+                                   const PolylineSceneLayer& layer) :
+      context_(context),
+      verticesCount_(0),
+      thickness_(layer.GetThickness()),
+      red_(layer.GetRedAsFloat()),
+      green_(layer.GetGreenAsFloat()),
+      blue_(layer.GetBlueAsFloat())
+    {
+      // High-level reference:
+      // https://mattdesl.svbtle.com/drawing-lines-is-hard
+      // https://forum.libcinder.org/topic/smooth-thick-lines-using-geometry-shader
+      
+      size_t countVertices = 0;
+      for (size_t i = 0; i < layer.GetChainsCount(); i++)
+      {
+        size_t countSegments = layer.GetChain(i).size() - 1;
+
+        if (layer.IsClosedChain(i))
+        {
+          countSegments++;
+        }
+        
+        // Each segment is made of 2 triangles. One triangle is
+        // defined by 3 points in 2D => 6 vertices per segment.
+        countVertices += countSegments * 2 * 3;
+      }
+
+      std::vector<float>  coords, miterDirections;
+      coords.reserve(countVertices * COMPONENTS_POSITION);
+      miterDirections.reserve(countVertices * COMPONENTS_MITER);
+
+      for (size_t i = 0; i < layer.GetChainsCount(); i++)
+      {
+        const PolylineSceneLayer::Chain& chain = layer.GetChain(i);
+
+        if (chain.size() > 1)
+        {
+          std::vector<Segment> segments;
+          for (size_t j = 1; j < chain.size(); j++)
+          {
+            segments.push_back(Segment(chain, j - 1, j));
+          }
+
+          if (layer.IsClosedChain(i))
+          {
+            segments.push_back(Segment(chain, chain.size() - 1, 0));
+          }
+
+          // Try and create nice miters
+          for (size_t j = 1; j < segments.size(); j++)
+          {
+            Segment::CreateMiter(segments[j - 1], segments[j]);
+          }
+
+          if (layer.IsClosedChain(i))
+          {
+            Segment::CreateMiter(segments.back(), segments.front());
+          }
+
+          for (size_t j = 0; j < segments.size(); j++)
+          {
+            if (!segments[j].IsEmpty())
+            {
+              segments[j].AddTriangles(coords, miterDirections);
+            }
+          }
+        }
+      }
+
+      if (!coords.empty())
+      {
+        verticesCount_ = coords.size() / COMPONENTS_POSITION;
+
+        context_.MakeCurrent();
+        glGenBuffers(2, buffers_);
+
+        glBindBuffer(GL_ARRAY_BUFFER, buffers_[0]);
+        glBufferData(GL_ARRAY_BUFFER, sizeof(float) * coords.size(), &coords[0], GL_STATIC_DRAW);
+
+        glBindBuffer(GL_ARRAY_BUFFER, buffers_[1]);
+        glBufferData(GL_ARRAY_BUFFER, sizeof(float) * miterDirections.size(), &miterDirections[0], GL_STATIC_DRAW);
+      }
+    }
+
+    
+    OpenGLLinesProgram::Data::~Data()
+    {
+      if (!IsEmpty())
+      {
+        context_.MakeCurrent();
+        glDeleteBuffers(2, buffers_);
+      }
+    }
+
+
+    GLuint OpenGLLinesProgram::Data::GetVerticesBuffer() const
+    {
+      if (IsEmpty())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return buffers_[0];
+      }
+    }
+
+    
+    GLuint OpenGLLinesProgram::Data::GetMiterDirectionsBuffer() const
+    {
+      if (IsEmpty())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return buffers_[1];
+      }
+    }
+
+
+    OpenGLLinesProgram::OpenGLLinesProgram(OpenGL::IOpenGLContext&  context) :
+      context_(context)
+    {
+
+      context_.MakeCurrent();
+
+      program_.reset(new OpenGL::OpenGLProgram);
+      program_->CompileShaders(VERTEX_SHADER, FRAGMENT_SHADER);
+    }
+
+
+    void OpenGLLinesProgram::Apply(const Data& data,
+                                   const AffineTransform2D& transform,
+                                   bool antialiasing,
+                                   bool scaleIndependantThickness)
+    {
+      if (!data.IsEmpty())
+      {
+        context_.MakeCurrent();
+        program_->Use();
+
+        GLint locationPosition = program_->GetAttributeLocation("a_position");
+        GLint locationMiterDirection = program_->GetAttributeLocation("a_miter_direction");
+
+        float m[16];
+        transform.ConvertToOpenGLMatrix(m, context_.GetCanvasWidth(), context_.GetCanvasHeight());
+
+        glUniformMatrix4fv(program_->GetUniformLocation("u_matrix"), 1, GL_FALSE, m);
+        glUniform3f(program_->GetUniformLocation("u_color"), 
+                    data.GetRed(), data.GetGreen(), data.GetBlue());
+
+        glBindBuffer(GL_ARRAY_BUFFER, data.GetVerticesBuffer());
+        glEnableVertexAttribArray(locationPosition);
+        glVertexAttribPointer(locationPosition, COMPONENTS_POSITION, GL_FLOAT, GL_FALSE, 0, 0);
+
+        glBindBuffer(GL_ARRAY_BUFFER, data.GetMiterDirectionsBuffer());
+        glEnableVertexAttribArray(locationMiterDirection);
+        glVertexAttribPointer(locationMiterDirection, COMPONENTS_MITER, GL_FLOAT, GL_FALSE, 0, 0);
+
+        glUniform1i(program_->GetUniformLocation("u_antialiasing"), (antialiasing ? 1 : 0));
+
+        const double zoom = transform.ComputeZoom();
+        const double thickness = data.GetThickness() / 2.0;
+        const double aliasingBorder = 2.0;  // Border for antialiasing ramp, in pixels
+        assert(aliasingBorder > 0);  // Prevent division by zero with "t1"
+              
+        if (scaleIndependantThickness)
+        {
+          if (antialiasing)
+          {
+            double t1 = std::max(thickness, aliasingBorder);
+            double t0 = std::max(0.0, thickness - aliasingBorder);
+            
+            glUniform1f(program_->GetUniformLocation("u_thickness"), t1 / zoom);
+            glUniform1f(program_->GetUniformLocation("u_antialiasing_start"), t0 / t1);
+          }
+          else
+          {
+            glUniform1f(program_->GetUniformLocation("u_thickness"), thickness / zoom);
+          }
+        }
+        else
+        {
+          if (antialiasing)
+          {
+            double t1 = std::max(thickness, aliasingBorder / zoom);
+            double t0 = std::max(0.0, thickness - aliasingBorder / zoom);
+
+            glUniform1f(program_->GetUniformLocation("u_thickness"), t1);
+            glUniform1f(program_->GetUniformLocation("u_antialiasing_start"), t0 / t1);
+          }
+          else
+          {
+            glUniform1f(program_->GetUniformLocation("u_thickness"), thickness);
+          }
+        }
+
+        if (antialiasing)
+        {
+          glEnable(GL_BLEND);
+          glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+          glDrawArrays(GL_TRIANGLES, 0, data.GetVerticesCount());
+          glDisable(GL_BLEND);
+        }
+        else
+        {
+          glDrawArrays(GL_TRIANGLES, 0, data.GetVerticesCount());
+        }
+
+        glDisableVertexAttribArray(locationPosition);
+        glDisableVertexAttribArray(locationMiterDirection);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLLinesProgram.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,103 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../OpenGL/IOpenGLContext.h"
+#include "../../OpenGL/OpenGLProgram.h"
+#include "../../Toolbox/AffineTransform2D.h"
+#include "../PolylineSceneLayer.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class OpenGLLinesProgram : public boost::noncopyable
+    {
+    public:
+      class Data : public boost::noncopyable
+      {
+      private:
+        class Segment;
+        
+        OpenGL::IOpenGLContext&  context_;
+        GLuint                   buffers_[2];
+        size_t                   verticesCount_;
+        float                    thickness_;
+        float                    red_;
+        float                    green_;
+        float                    blue_;
+
+      public:
+        Data(OpenGL::IOpenGLContext& context,
+             const PolylineSceneLayer& layer);
+        
+        ~Data();
+
+        bool IsEmpty() const
+        {
+          return verticesCount_ == 0;
+        }
+
+        const size_t GetVerticesCount() const
+        {
+          return verticesCount_;
+        }
+
+        GLuint GetVerticesBuffer() const;
+
+        GLuint GetMiterDirectionsBuffer() const;
+
+        float GetThickness() const
+        {
+          return thickness_;
+        }
+
+        float GetRed() const
+        {
+          return red_;
+        }
+
+        float GetGreen() const
+        {
+          return green_;
+        }
+
+        float GetBlue() const
+        {
+          return blue_;
+        }
+      };
+      
+    private:
+      OpenGL::IOpenGLContext&               context_;
+      std::auto_ptr<OpenGL::OpenGLProgram>  program_;
+
+    public:
+      OpenGLLinesProgram(OpenGL::IOpenGLContext&  context);
+
+      void Apply(const Data& data,
+                 const AffineTransform2D& transform,
+                 bool antialiasing,
+                 bool scaleIndependantThickness);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLShaderVersionDirective.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,8 @@
+#pragma once
+
+#if ORTHANC_ENABLE_WASM == 1
+// https://emscripten.org/docs/optimizing/Optimizing-WebGL.html
+#  define ORTHANC_STONE_OPENGL_SHADER_VERSION_DIRECTIVE "precision mediump float;\n"
+#else
+#  define ORTHANC_STONE_OPENGL_SHADER_VERSION_DIRECTIVE "#version 110\n"
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLTextProgram.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,193 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OpenGLTextProgram.h"
+#include "OpenGLShaderVersionDirective.h"
+
+#include "../../Fonts/OpenGLTextCoordinates.h"
+
+#include <Core/OrthancException.h>
+
+
+static const unsigned int COMPONENTS = 2;
+
+static const char* VERTEX_SHADER = 
+  ORTHANC_STONE_OPENGL_SHADER_VERSION_DIRECTIVE
+  "attribute vec2 a_texcoord;             \n"
+  "attribute vec4 a_position;             \n"
+  "uniform mat4 u_matrix;                 \n"
+  "varying vec2 v_texcoord;               \n"
+  "void main()                            \n"
+  "{                                      \n"
+  "  gl_Position = u_matrix * a_position; \n"
+  "  v_texcoord = a_texcoord;             \n"
+  "}";
+
+static const char* FRAGMENT_SHADER = 
+  ORTHANC_STONE_OPENGL_SHADER_VERSION_DIRECTIVE
+  "uniform sampler2D u_texture;                  \n"
+  "uniform vec3 u_color;                         \n"
+  "varying vec2 v_texcoord;                      \n"
+  "void main()                                   \n"
+  "{                                             \n"
+  "  vec4 v = texture2D(u_texture, v_texcoord);  \n"
+  "  gl_FragColor = vec4(u_color * v.w, v.w);    \n"   // Premultiplied alpha
+  "}";
+
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    OpenGLTextProgram::OpenGLTextProgram(OpenGL::IOpenGLContext&  context) :
+      context_(context)
+    {
+
+      context_.MakeCurrent();
+
+      program_.reset(new OpenGL::OpenGLProgram);
+      program_->CompileShaders(VERTEX_SHADER, FRAGMENT_SHADER);
+
+      positionLocation_ = program_->GetAttributeLocation("a_position");
+      textureLocation_ = program_->GetAttributeLocation("a_texcoord");
+    }
+
+
+    OpenGLTextProgram::Data::Data(OpenGL::IOpenGLContext& context,
+                                  const GlyphTextureAlphabet& alphabet,
+                                  const TextSceneLayer& layer) :
+      context_(context),
+      red_(layer.GetRedAsFloat()),
+      green_(layer.GetGreenAsFloat()),
+      blue_(layer.GetBlueAsFloat()),
+      x_(layer.GetX()),
+      y_(layer.GetY()),
+      border_(layer.GetBorder()),
+      anchor_(layer.GetAnchor())
+    {
+      OpenGL::OpenGLTextCoordinates coordinates(alphabet, layer.GetText());
+      textWidth_ = coordinates.GetTextWidth();
+      textHeight_ = coordinates.GetTextHeight();
+
+      if (coordinates.IsEmpty())
+      {
+        coordinatesCount_ = 0;
+      }
+      else
+      {
+        coordinatesCount_ = coordinates.GetRenderingCoords().size();
+
+        context_.MakeCurrent();
+        glGenBuffers(2, buffers_);
+
+        glBindBuffer(GL_ARRAY_BUFFER, buffers_[0]);
+        glBufferData(GL_ARRAY_BUFFER, sizeof(float) * coordinatesCount_,
+                     &coordinates.GetRenderingCoords() [0], GL_STATIC_DRAW);
+
+        glBindBuffer(GL_ARRAY_BUFFER, buffers_[1]);
+        glBufferData(GL_ARRAY_BUFFER, sizeof(float) * coordinatesCount_,
+                     &coordinates.GetTextureCoords() [0], GL_STATIC_DRAW);
+      }
+    }
+
+    
+    OpenGLTextProgram::Data::~Data()
+    {
+      if (!IsEmpty())
+      {
+        context_.MakeCurrent();
+        glDeleteBuffers(2, buffers_);
+      }
+    }
+
+
+    GLuint OpenGLTextProgram::Data::GetSceneLocationsBuffer() const
+    {
+      if (IsEmpty())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return buffers_[0];
+      }
+    }
+
+    
+    GLuint OpenGLTextProgram::Data::GetTextureLocationsBuffer() const
+    {
+      if (IsEmpty())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return buffers_[1];
+      }
+    }
+
+
+    void OpenGLTextProgram::Apply(OpenGL::OpenGLTexture& fontTexture,
+                                  const Data& data,
+                                  const AffineTransform2D& transform)
+    {
+      if (!data.IsEmpty())
+      {
+        context_.MakeCurrent();
+        program_->Use();
+
+        double dx, dy;  // In pixels
+        ComputeAnchorTranslation(dx, dy, data.GetAnchor(), 
+                                 data.GetTextWidth(), data.GetTextHeight(), data.GetBorder());
+      
+        double x = data.GetX();
+        double y = data.GetY();
+        transform.Apply(x, y);
+
+        const AffineTransform2D t = AffineTransform2D::CreateOffset(x + dx, y + dy);
+
+        float m[16];
+        t.ConvertToOpenGLMatrix(m, context_.GetCanvasWidth(), context_.GetCanvasHeight());
+
+        fontTexture.Bind(program_->GetUniformLocation("u_texture"));
+        glUniformMatrix4fv(program_->GetUniformLocation("u_matrix"), 1, GL_FALSE, m);
+        glUniform3f(program_->GetUniformLocation("u_color"), 
+                    data.GetRed(), data.GetGreen(), data.GetBlue());
+
+        glBindBuffer(GL_ARRAY_BUFFER, data.GetSceneLocationsBuffer());
+        glEnableVertexAttribArray(positionLocation_);
+        glVertexAttribPointer(positionLocation_, COMPONENTS, GL_FLOAT, GL_FALSE, 0, 0);
+
+        glBindBuffer(GL_ARRAY_BUFFER, data.GetTextureLocationsBuffer());
+        glEnableVertexAttribArray(textureLocation_);
+        glVertexAttribPointer(textureLocation_, COMPONENTS, GL_FLOAT, GL_FALSE, 0, 0);
+
+        glEnable(GL_BLEND);
+        glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+        glDrawArrays(GL_TRIANGLES, 0, data.GetCoordinatesCount() / COMPONENTS);
+        glDisable(GL_BLEND);
+
+        glDisableVertexAttribArray(positionLocation_);
+        glDisableVertexAttribArray(textureLocation_);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLTextProgram.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,135 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Fonts/GlyphTextureAlphabet.h"
+#include "../../OpenGL/IOpenGLContext.h"
+#include "../../OpenGL/OpenGLProgram.h"
+#include "../../OpenGL/OpenGLTexture.h"
+#include "../../Toolbox/AffineTransform2D.h"
+#include "../TextSceneLayer.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class OpenGLTextProgram : public boost::noncopyable
+    {
+    public:
+      class Data : public boost::noncopyable
+      {
+      private:
+        OpenGL::IOpenGLContext&  context_;
+        size_t                   coordinatesCount_;
+        GLuint                   buffers_[2];
+        float                    red_;
+        float                    green_;
+        float                    blue_;
+        double                   x_;
+        double                   y_;
+        double                   border_;
+        unsigned int             textWidth_;
+        unsigned int             textHeight_;
+        BitmapAnchor             anchor_;
+
+      public:
+        Data(OpenGL::IOpenGLContext& context,
+             const GlyphTextureAlphabet& alphabet,
+             const TextSceneLayer& layer);
+
+        ~Data();
+
+        bool IsEmpty() const
+        {
+          return coordinatesCount_ == 0;
+        }
+
+        size_t GetCoordinatesCount() const
+        {
+          return coordinatesCount_;
+        }
+
+        GLuint GetSceneLocationsBuffer() const;
+
+        GLuint GetTextureLocationsBuffer() const;
+
+        float GetRed() const
+        {
+          return red_;
+        }
+
+        float GetGreen() const
+        {
+          return green_;
+        }
+
+        float GetBlue() const
+        {
+          return blue_;
+        }
+
+        double GetX() const
+        {
+          return x_;
+        }
+
+        double GetY() const
+        {
+          return y_;
+        }
+
+        double GetBorder() const
+        {
+          return border_;
+        }
+
+        unsigned int GetTextWidth() const
+        {
+          return textWidth_;
+        }
+
+        unsigned int GetTextHeight() const
+        {
+          return textHeight_;
+        }
+
+        BitmapAnchor GetAnchor() const
+        {
+          return anchor_;
+        }
+      };
+      
+    private:
+      OpenGL::IOpenGLContext&               context_;
+      std::auto_ptr<OpenGL::OpenGLProgram>  program_;
+      GLint                                 positionLocation_;
+      GLint                                 textureLocation_;
+
+    public:
+      OpenGLTextProgram(OpenGL::IOpenGLContext&  context);
+
+      void Apply(OpenGL::OpenGLTexture& fontTexture,
+                 const Data& data,
+                 const AffineTransform2D& transform);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLTextRenderer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,62 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OpenGLTextRenderer.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    void OpenGLTextRenderer::LoadLayer(const TextSceneLayer& layer)
+    {
+      data_.reset(new OpenGLTextProgram::Data(context_, alphabet_, layer));
+    }
+
+
+    OpenGLTextRenderer::OpenGLTextRenderer(OpenGL::IOpenGLContext& context,
+                                           OpenGLTextProgram& program,
+                                           const GlyphTextureAlphabet& alphabet,
+                                           OpenGL::OpenGLTexture& texture,
+                                           const TextSceneLayer& layer) :
+      context_(context),
+      program_(program),
+      alphabet_(alphabet),
+      texture_(texture)
+    {
+      LoadLayer(layer);
+    }
+
+      
+    void OpenGLTextRenderer::Render(const AffineTransform2D& transform)
+    {
+      if (data_.get() != NULL)
+      {
+        program_.Apply(texture_, *data_, transform);
+      }
+    }
+
+      
+    void OpenGLTextRenderer::Update(const ISceneLayer& layer)
+    {
+      LoadLayer(dynamic_cast<const TextSceneLayer&>(layer));
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLTextRenderer.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,54 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "CompositorHelper.h"
+#include "OpenGLTextProgram.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class OpenGLTextRenderer : public CompositorHelper::ILayerRenderer
+    {
+    private:
+      OpenGL::IOpenGLContext&                 context_;
+      OpenGLTextProgram&                      program_;
+      const GlyphTextureAlphabet&             alphabet_;
+      OpenGL::OpenGLTexture&                  texture_;
+      std::auto_ptr<OpenGLTextProgram::Data>  data_;
+
+      void LoadLayer(const TextSceneLayer& layer);
+
+    public:
+      OpenGLTextRenderer(OpenGL::IOpenGLContext& context,
+                         OpenGLTextProgram& program,
+                         const GlyphTextureAlphabet& alphabet,
+                         OpenGL::OpenGLTexture& texture,
+                         const TextSceneLayer& layer);
+
+      virtual void Render(const AffineTransform2D& transform);
+
+      virtual void Update(const ISceneLayer& layer);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLTextureProgram.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,122 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OpenGLTextureProgram.h"
+#include "OpenGLShaderVersionDirective.h"
+
+static const unsigned int COMPONENTS = 2;
+static const unsigned int COUNT = 6;  // 2 triangles in 2D
+
+static const char* VERTEX_SHADER = 
+  ORTHANC_STONE_OPENGL_SHADER_VERSION_DIRECTIVE
+  "attribute vec2 a_texcoord;             \n"
+  "attribute vec4 a_position;             \n"
+  "uniform mat4 u_matrix;                 \n"
+  "varying vec2 v_texcoord;               \n"
+  "void main()                            \n"
+  "{                                      \n"
+  "  gl_Position = u_matrix * a_position; \n"
+  "  v_texcoord = a_texcoord;             \n"
+  "}";
+
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    void OpenGLTextureProgram::InitializeExecution(OpenGL::OpenGLTexture& texture,
+                                                   const AffineTransform2D& transform)
+    {
+      context_.MakeCurrent();
+      program_->Use();
+
+      AffineTransform2D scale = AffineTransform2D::CreateScaling
+        (texture.GetWidth(), texture.GetHeight());
+
+      AffineTransform2D t = AffineTransform2D::Combine(transform, scale);
+
+      float m[16];
+      t.ConvertToOpenGLMatrix(m, context_.GetCanvasWidth(), context_.GetCanvasHeight());
+
+      texture.Bind(program_->GetUniformLocation("u_texture"));
+      glUniformMatrix4fv(program_->GetUniformLocation("u_matrix"), 1, GL_FALSE, m);
+
+      glBindBuffer(GL_ARRAY_BUFFER, buffers_[0]);
+      glEnableVertexAttribArray(positionLocation_);
+      glVertexAttribPointer(positionLocation_, COMPONENTS, GL_FLOAT, GL_FALSE, 0, 0);
+
+      glBindBuffer(GL_ARRAY_BUFFER, buffers_[1]);
+      glEnableVertexAttribArray(textureLocation_);
+      glVertexAttribPointer(textureLocation_, COMPONENTS, GL_FLOAT, GL_FALSE, 0, 0);
+    }
+
+    
+    void OpenGLTextureProgram::FinalizeExecution()
+    {
+      glDisableVertexAttribArray(positionLocation_);
+      glDisableVertexAttribArray(textureLocation_);
+    }
+
+    
+    OpenGLTextureProgram::OpenGLTextureProgram(OpenGL::IOpenGLContext& context,
+                                               const char* fragmentShader) :
+      context_(context)
+    {
+      static const float POSITIONS[COMPONENTS * COUNT] = {
+        0, 0,
+        0, 1,
+        1, 0,
+        1, 0,
+        0, 1,
+        1, 1
+      };
+        
+      context_.MakeCurrent();
+
+      program_.reset(new OpenGL::OpenGLProgram);
+      program_->CompileShaders(VERTEX_SHADER, fragmentShader);
+
+      positionLocation_ = program_->GetAttributeLocation("a_position");
+      textureLocation_ = program_->GetAttributeLocation("a_texcoord");
+
+      glGenBuffers(2, buffers_);
+
+      glBindBuffer(GL_ARRAY_BUFFER, buffers_[0]);
+      glBufferData(GL_ARRAY_BUFFER, sizeof(float) * COMPONENTS * COUNT, POSITIONS, GL_STATIC_DRAW);
+
+      glBindBuffer(GL_ARRAY_BUFFER, buffers_[1]);
+      glBufferData(GL_ARRAY_BUFFER, sizeof(float) * COMPONENTS * COUNT, POSITIONS, GL_STATIC_DRAW);
+    }
+
+
+    OpenGLTextureProgram::~OpenGLTextureProgram()
+    {
+      context_.MakeCurrent();
+      glDeleteBuffers(2, buffers_);
+    }
+
+
+    void OpenGLTextureProgram::Execution::DrawTriangles()
+    {
+      glDrawArrays(GL_TRIANGLES, 0, COUNT);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLTextureProgram.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,81 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../OpenGL/IOpenGLContext.h"
+#include "../../OpenGL/OpenGLProgram.h"
+#include "../../OpenGL/OpenGLTexture.h"
+#include "../../Toolbox/AffineTransform2D.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class OpenGLTextureProgram : public boost::noncopyable
+    {
+    private:
+      OpenGL::IOpenGLContext&               context_;
+      std::auto_ptr<OpenGL::OpenGLProgram>  program_;
+      GLint                                 positionLocation_;
+      GLint                                 textureLocation_;
+      GLuint                                buffers_[2];
+
+      void InitializeExecution(OpenGL::OpenGLTexture& texture,
+                               const AffineTransform2D& transform);
+
+      void FinalizeExecution();
+
+    public:
+      OpenGLTextureProgram(OpenGL::IOpenGLContext& context,
+                           const char* fragmentShader);
+
+      ~OpenGLTextureProgram();
+
+      class Execution : public boost::noncopyable
+      {
+      private:
+        OpenGLTextureProgram&  that_;
+
+      public:
+        Execution(OpenGLTextureProgram& that,
+                  OpenGL::OpenGLTexture& texture,
+                  const AffineTransform2D& transform) :
+          that_(that)
+        {
+          that_.InitializeExecution(texture, transform);
+        }
+
+        ~Execution()
+        {
+          that_.FinalizeExecution();
+        }
+
+        void DrawTriangles();
+
+        GLint GetUniformLocation(const std::string& name)
+        {
+          return that_.program_->GetUniformLocation(name);
+        }
+      };
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/OpenGLCompositor.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,213 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OpenGLCompositor.h"
+
+#include "Internals/OpenGLAdvancedPolylineRenderer.h"
+#include "Internals/OpenGLBasicPolylineRenderer.h"
+#include "Internals/OpenGLColorTextureRenderer.h"
+#include "Internals/OpenGLFloatTextureRenderer.h"
+#include "Internals/OpenGLInfoPanelRenderer.h"
+#include "Internals/OpenGLTextRenderer.h"
+
+namespace OrthancStone
+{
+  class OpenGLCompositor::Font : public boost::noncopyable
+  {
+  private:
+    std::auto_ptr<GlyphTextureAlphabet>   alphabet_;
+    std::auto_ptr<OpenGL::OpenGLTexture>  texture_;
+
+  public:
+    Font(const GlyphBitmapAlphabet& dict)
+    {
+      alphabet_.reset(new GlyphTextureAlphabet(dict));
+      texture_.reset(new OpenGL::OpenGLTexture);
+
+      std::auto_ptr<Orthanc::ImageAccessor> bitmap(alphabet_->ReleaseTexture());
+      texture_->Load(*bitmap, true /* enable linear interpolation */);
+    }
+
+    OpenGL::OpenGLTexture& GetTexture() const
+    {
+      assert(texture_.get() != NULL);
+      return *texture_;
+    }
+
+    const GlyphTextureAlphabet& GetAlphabet() const
+    {
+      assert(alphabet_.get() != NULL);
+      return *alphabet_;
+    }
+  };
+
+
+  const OpenGLCompositor::Font* OpenGLCompositor::GetFont(size_t fontIndex) const
+  {
+    Fonts::const_iterator found = fonts_.find(fontIndex);
+
+    if (found == fonts_.end())
+    {
+      return NULL;  // Unknown font, nothing should be drawn
+    }
+    else
+    {
+      assert(found->second != NULL);
+      return found->second;
+    }
+  }
+
+
+  Internals::CompositorHelper::ILayerRenderer* OpenGLCompositor::Create(const ISceneLayer& layer)
+  {
+    switch (layer.GetType())
+    {
+      case ISceneLayer::Type_InfoPanel:
+        return new Internals::OpenGLInfoPanelRenderer
+          (context_, colorTextureProgram_, dynamic_cast<const InfoPanelSceneLayer&>(layer));
+
+      case ISceneLayer::Type_ColorTexture:
+        return new Internals::OpenGLColorTextureRenderer
+          (context_, colorTextureProgram_, dynamic_cast<const ColorTextureSceneLayer&>(layer));
+
+      case ISceneLayer::Type_FloatTexture:
+        return new Internals::OpenGLFloatTextureRenderer
+          (context_, floatTextureProgram_, dynamic_cast<const FloatTextureSceneLayer&>(layer));
+
+      case ISceneLayer::Type_Polyline:
+        return new Internals::OpenGLAdvancedPolylineRenderer
+          (context_, linesProgram_, dynamic_cast<const PolylineSceneLayer&>(layer));
+        //return new Internals::OpenGLBasicPolylineRenderer(context_, dynamic_cast<const PolylineSceneLayer&>(layer));
+
+      case ISceneLayer::Type_Text:
+      {
+        const TextSceneLayer& l = dynamic_cast<const TextSceneLayer&>(layer);
+        const Font* font = GetFont(l.GetFontIndex());
+        if (font == NULL)
+        {
+          return NULL;
+        }
+        else
+        {
+          return new Internals::OpenGLTextRenderer
+            (context_, textProgram_, font->GetAlphabet(), font->GetTexture(), l);
+        }
+      }
+
+      default:
+        return NULL;
+    }
+  }
+
+
+  OpenGLCompositor::OpenGLCompositor(OpenGL::IOpenGLContext& context,
+                                     const Scene2D& scene) :
+    context_(context),
+    helper_(scene, *this),
+    colorTextureProgram_(context),
+    floatTextureProgram_(context),
+    linesProgram_(context),
+    textProgram_(context),
+    canvasWidth_(0),
+    canvasHeight_(0)
+  {
+    UpdateSize();
+  }
+
+  
+  OpenGLCompositor::~OpenGLCompositor()
+  {
+    for (Fonts::iterator it = fonts_.begin(); it != fonts_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+  }
+
+  
+  void OpenGLCompositor::UpdateSize()
+  {
+    canvasWidth_ = context_.GetCanvasWidth();
+    canvasHeight_ = context_.GetCanvasHeight();
+
+    context_.MakeCurrent();
+    glViewport(0, 0, canvasWidth_, canvasHeight_);
+  }
+
+  
+  void OpenGLCompositor::Refresh()
+  {
+    context_.MakeCurrent();
+
+    glClearColor(0, 0, 0, 1);
+    glClear(GL_COLOR_BUFFER_BIT);
+
+    helper_.Refresh(canvasWidth_, canvasHeight_);
+
+    context_.SwapBuffer();
+  }
+
+  
+  void OpenGLCompositor::SetFont(size_t index,
+                                 const GlyphBitmapAlphabet& dict)
+  {
+    std::auto_ptr<Font> font(new Font(dict));
+      
+    Fonts::iterator found = fonts_.find(index);
+
+    if (found == fonts_.end())
+    {
+      fonts_[index] = font.release();
+    }
+    else
+    {
+      assert(found->second != NULL);
+      delete found->second;
+
+      found->second = font.release();
+    }
+  }
+  
+
+#if ORTHANC_ENABLE_LOCALE == 1
+  void OpenGLCompositor::SetFont(size_t index,
+                                 Orthanc::EmbeddedResources::FileResourceId resource,
+                                 unsigned int fontSize,
+                                 Orthanc::Encoding codepage)
+  {
+    FontRenderer renderer;
+    renderer.LoadFont(resource, fontSize);
+
+    GlyphBitmapAlphabet dict;
+    dict.LoadCodepage(renderer, codepage);
+
+    SetFont(index, dict);
+  }
+#endif
+
+
+  ScenePoint2D OpenGLCompositor::GetPixelCenterCoordinates(int x, int y) const
+  {
+    return ScenePoint2D(
+      static_cast<double>(x) + 0.5 - static_cast<double>(canvasWidth_) / 2.0,
+      static_cast<double>(y) + 0.5 - static_cast<double>(canvasHeight_) / 2.0);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/OpenGLCompositor.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,85 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "Internals/CompositorHelper.h"
+#include "Internals/OpenGLColorTextureProgram.h"
+#include "Internals/OpenGLFloatTextureProgram.h"
+#include "Internals/OpenGLLinesProgram.h"
+#include "Internals/OpenGLTextProgram.h"
+
+namespace OrthancStone
+{
+  class OpenGLCompositor : private Internals::CompositorHelper::IRendererFactory
+  {
+  private:
+    class Font;
+
+    typedef std::map<size_t, Font*>  Fonts;
+
+    OpenGL::IOpenGLContext&               context_;
+    Fonts                                 fonts_;
+    Internals::CompositorHelper           helper_;
+    Internals::OpenGLColorTextureProgram  colorTextureProgram_;
+    Internals::OpenGLFloatTextureProgram  floatTextureProgram_;
+    Internals::OpenGLLinesProgram         linesProgram_;
+    Internals::OpenGLTextProgram          textProgram_;
+    unsigned int                          canvasWidth_;
+    unsigned int                          canvasHeight_;
+    
+    const Font* GetFont(size_t fontIndex) const;
+
+    virtual Internals::CompositorHelper::ILayerRenderer* Create(const ISceneLayer& layer);
+
+  public:
+    OpenGLCompositor(OpenGL::IOpenGLContext& context,
+                     const Scene2D& scene);
+
+    ~OpenGLCompositor();
+
+    void UpdateSize();
+
+    void Refresh();
+
+    void SetFont(size_t index,
+                 const GlyphBitmapAlphabet& dict);
+
+#if ORTHANC_ENABLE_LOCALE == 1
+    void SetFont(size_t index,
+                 Orthanc::EmbeddedResources::FileResourceId resource,
+                 unsigned int fontSize,
+                 Orthanc::Encoding codepage);
+#endif
+
+    unsigned int GetCanvasWidth() const
+    {
+      return canvasWidth_;
+    }
+
+    unsigned int GetCanvasHeight() const
+    {
+      return canvasHeight_;
+    }
+
+    ScenePoint2D GetPixelCenterCoordinates(int x, int y) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/PanSceneTracker.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,46 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PanSceneTracker.h"
+
+namespace OrthancStone
+{
+  PanSceneTracker::PanSceneTracker(Scene2D& scene,
+                                   const PointerEvent& event) :
+    scene_(scene),
+    originalSceneToCanvas_(scene_.GetSceneToCanvasTransform()),
+    originalCanvasToScene_(scene_.GetCanvasToSceneTransform())
+  {
+    pivot_ = event.GetMainPosition().Apply(originalCanvasToScene_);
+  }
+
+
+  void PanSceneTracker::Update(const PointerEvent& event)
+  {
+    ScenePoint2D p = event.GetMainPosition().Apply(originalCanvasToScene_);
+      
+    scene_.SetSceneToCanvasTransform(
+      AffineTransform2D::Combine(
+        originalSceneToCanvas_,
+        AffineTransform2D::CreateOffset(p.GetX() - pivot_.GetX(),
+                                        p.GetY() - pivot_.GetY())));
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/PanSceneTracker.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,47 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IPointerTracker.h"
+#include "Scene2D.h"
+
+namespace OrthancStone
+{
+  class PanSceneTracker : public IPointerTracker
+  {
+  private:
+    Scene2D&           scene_;
+    ScenePoint2D       pivot_;
+    AffineTransform2D  originalSceneToCanvas_;
+    AffineTransform2D  originalCanvasToScene_;
+
+  public:
+    PanSceneTracker(Scene2D& scene,
+                    const PointerEvent& event);
+
+    virtual void Update(const PointerEvent& event);
+
+    virtual void Release()
+    {
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/PointerEvent.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,60 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PointerEvent.h"
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  PointerEvent::PointerEvent() :
+    hasAltModifier_(false),
+    hasControlModifier_(false),
+    hasShiftModifier_(false)
+  {
+  }
+
+  
+  ScenePoint2D PointerEvent::GetMainPosition() const
+  {
+    if (positions_.empty())
+    {
+      return ScenePoint2D(0, 0);
+    }
+    else
+    {
+      return positions_[0];
+    }
+  }
+    
+
+  ScenePoint2D PointerEvent::GetPosition(size_t index) const
+  {
+    if (index < positions_.size())
+    {
+      return positions_[index];
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/PointerEvent.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,92 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ScenePoint2D.h"
+
+#include <boost/noncopyable.hpp>
+#include <stdint.h>
+
+namespace OrthancStone
+{
+  class PointerEvent : public boost::noncopyable
+  {
+  private:
+    std::vector<ScenePoint2D>  positions_;
+    bool                       hasAltModifier_;
+    bool                       hasControlModifier_;
+    bool                       hasShiftModifier_;
+
+  public:
+    PointerEvent();
+
+    ScenePoint2D GetMainPosition() const;
+    
+    void AddPosition(const ScenePoint2D& p)
+    {
+      positions_.push_back(p);
+    }
+
+    void AddPosition(double x,
+                     double y)
+    {
+      positions_.push_back(ScenePoint2D(x, y));
+    }
+
+    size_t GetPositionsCount() const
+    {
+      return positions_.size();
+    }
+    
+    ScenePoint2D GetPosition(size_t index) const;
+
+    void SetAltModifier(bool value)
+    {
+      hasAltModifier_ = value;
+    }
+
+    bool HasAltModifier() const
+    {
+      return hasAltModifier_;
+    }
+
+    void SetControlModifier(bool value)
+    {
+      hasControlModifier_ = value;
+    }
+
+    bool HasControlModifier() const
+    {
+      return hasControlModifier_;
+    }
+
+    void SetShiftModifier(bool value)
+    {
+      hasShiftModifier_ = value;
+    }
+
+    bool HasShiftModifier() const
+    {
+      return hasShiftModifier_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/PolylineSceneLayer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,127 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PolylineSceneLayer.h"
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  ISceneLayer* PolylineSceneLayer::Clone() const
+  {
+    std::auto_ptr<PolylineSceneLayer> cloned(new PolylineSceneLayer);
+    cloned->Copy(*this);
+    return cloned.release();
+  }
+    
+
+  void PolylineSceneLayer::SetThickness(double thickness)
+  {
+    if (thickness <= 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      thickness_ = thickness;
+      BumpRevision();
+    }
+  }
+
+
+  void PolylineSceneLayer::Copy(const PolylineSceneLayer& from)
+  {
+    SetColor(from.GetRed(), from.GetGreen(), from.GetBlue());
+    chains_ = from.chains_;
+    closed_ = from.closed_;
+    thickness_ = from.thickness_;
+    BumpRevision();
+  }
+
+  
+  void PolylineSceneLayer::Reserve(size_t countChains)
+  {
+    chains_.reserve(countChains);
+    closed_.reserve(countChains);
+  }
+
+  
+  void PolylineSceneLayer::AddChain(const Chain& chain,
+                                    bool isClosed)
+  {
+    if (!chain.empty())
+    {
+      chains_.push_back(chain);
+      closed_.push_back(isClosed);
+      BumpRevision();
+    }
+  }
+
+
+  void PolylineSceneLayer::ClearAllChains()
+  {
+    chains_.clear();
+    closed_.clear();
+    BumpRevision();
+  }
+
+  const PolylineSceneLayer::Chain& PolylineSceneLayer::GetChain(size_t i) const
+  {
+    if (i < chains_.size())
+    {
+      return chains_[i];
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  
+  bool PolylineSceneLayer::IsClosedChain(size_t i) const
+  {
+    if (i < closed_.size())
+    {
+      return closed_[i];
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool PolylineSceneLayer::GetBoundingBox(Extent2D& target) const
+  {
+    target.Reset();
+
+    for (size_t i = 0; i < chains_.size(); i++)
+    {
+      for (size_t j = 0; j < chains_[i].size(); j++)
+      {
+        const ScenePoint2D& p = chains_[i][j];
+        target.AddPoint(p.GetX(), p.GetY());
+      }
+    }
+
+    return true;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/PolylineSceneLayer.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,82 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ColorSceneLayer.h"
+#include "ScenePoint2D.h"
+
+#include <vector>
+
+namespace OrthancStone
+{
+  class PolylineSceneLayer : public ColorSceneLayer
+  {
+  public:
+    typedef std::vector<ScenePoint2D>  Chain;
+
+  private:
+    std::vector<Chain>  chains_;
+    std::vector<bool>   closed_;
+    double              thickness_;
+
+  public:
+    PolylineSceneLayer() :
+      thickness_(1.0)
+    {
+    }
+
+    virtual ISceneLayer* Clone() const;
+
+    void SetThickness(double thickness);
+
+    double GetThickness() const
+    {
+      return thickness_;
+    }
+
+    void Copy(const PolylineSceneLayer& from);
+
+    void Reserve(size_t countChains);
+
+    void AddChain(const Chain& chain,
+                  bool isClosed);
+
+    void ClearAllChains();
+
+    size_t GetChainsCount() const
+    {
+      return chains_.size();
+    }
+
+    const Chain& GetChain(size_t i) const;
+
+    bool IsClosedChain(size_t i) const;
+
+    virtual Type GetType() const
+    {
+      return Type_Polyline;
+    }
+
+    virtual bool GetBoundingBox(Extent2D& target) const;
+   
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/RotateSceneTracker.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,62 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "RotateSceneTracker.h"
+
+namespace OrthancStone
+{
+  RotateSceneTracker::RotateSceneTracker(Scene2D& scene,
+                                         const PointerEvent& event) :
+    scene_(scene),
+    click_(event.GetMainPosition()),
+    aligner_(scene, click_),
+    isFirst_(true),
+    originalSceneToCanvas_(scene.GetSceneToCanvasTransform())
+  {
+  }
+
+
+  void RotateSceneTracker::Update(const PointerEvent& event)
+  {
+    ScenePoint2D p = event.GetMainPosition();
+    double dx = p.GetX() - click_.GetX();
+    double dy = p.GetY() - click_.GetY();
+
+    if (std::abs(dx) > 5.0 || 
+        std::abs(dy) > 5.0)
+    {
+      double a = atan2(dy, dx);
+
+      if (isFirst_)
+      {
+        referenceAngle_ = a;
+        isFirst_ = false;
+      }
+
+      scene_.SetSceneToCanvasTransform(
+        AffineTransform2D::Combine(
+          AffineTransform2D::CreateRotation(a - referenceAngle_),
+          originalSceneToCanvas_));
+      
+      aligner_.Apply();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/RotateSceneTracker.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,49 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IPointerTracker.h"
+#include "Internals/FixedPointAligner.h"
+
+namespace OrthancStone
+{
+  class RotateSceneTracker : public IPointerTracker
+  {
+  private:
+    Scene2D&                      scene_;
+    ScenePoint2D                  click_;
+    Internals::FixedPointAligner  aligner_;
+    double                        referenceAngle_;
+    bool                          isFirst_;
+    AffineTransform2D             originalSceneToCanvas_;
+
+  public:
+    RotateSceneTracker(Scene2D& scene,
+                       const PointerEvent& event);
+
+    virtual void Update(const PointerEvent& event);
+
+    virtual void Release()
+    {
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Scene2D.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,267 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "Scene2D.h"
+
+#include <Core/OrthancException.h>
+
+
+namespace OrthancStone
+{
+  class Scene2D::Item
+  {
+  private:
+    std::auto_ptr<ISceneLayer>  layer_;
+    uint64_t                    identifier_;
+
+  public:
+    Item(ISceneLayer* layer,
+         uint64_t identifier) :
+      layer_(layer),
+      identifier_(identifier)
+    {
+      if (layer == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+    }
+
+    ISceneLayer& GetLayer() const
+    {
+      if (layer_.get() == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return *layer_;
+      }
+    }
+
+    ISceneLayer* ReleaseLayer()
+    {
+      if (layer_.get() == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return layer_.release();
+      }
+    }
+
+    uint64_t GetIdentifier() const
+    {
+      return identifier_;
+    }
+  };
+  
+  
+  Scene2D::Scene2D(const Scene2D& other) 
+    : IObservable(other.GetBroker())
+    , sceneToCanvas_(other.sceneToCanvas_)
+    , canvasToScene_(other.canvasToScene_)
+    , layerCounter_(0)
+  {
+    for (Content::const_iterator it = other.content_.begin();
+         it != other.content_.end(); ++it)
+    {
+      content_[it->first] = new Item(it->second->GetLayer().Clone(), layerCounter_++);
+    }
+  }
+
+    
+  Scene2D::~Scene2D()
+  {
+    for (Content::iterator it = content_.begin(); 
+         it != content_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+  }
+
+
+  void Scene2D::SetLayer(int depth,
+                         ISceneLayer* layer)  // Takes ownership
+  {
+    LOG(INFO) << "SetLayer(" << depth << ", " <<
+      reinterpret_cast<intptr_t>(layer) << ")";
+    std::auto_ptr<Item> item(new Item(layer, layerCounter_++));
+
+    if (layer == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+
+    Content::iterator found = content_.find(depth);
+
+    if (found == content_.end())
+    {
+      content_[depth] = item.release();
+    }
+    else
+    {
+      assert(found->second != NULL);
+      delete found->second;
+      found->second = item.release();
+    }
+  }
+
+
+  void Scene2D::DeleteLayer(int depth)
+  {
+
+    Content::iterator found = content_.find(depth);
+
+    if (found != content_.end())
+    {
+      LOG(INFO) << "DeleteLayer --found-- (" << depth << ")";
+      assert(found->second != NULL);
+      delete found->second;
+      content_.erase(found);
+    }    
+  }
+
+  
+  bool Scene2D::HasLayer(int depth) const
+  {
+    return (content_.find(depth) != content_.end());
+  }
+
+
+  ISceneLayer& Scene2D::GetLayer(int depth) const
+  {
+    Content::const_iterator found = content_.find(depth);
+
+    if (found == content_.end())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(found->second != NULL);
+      return found->second->GetLayer();
+    }
+  }
+
+  
+  int Scene2D::GetMinDepth() const
+  {
+    if (content_.size() == 0)
+      return 0;
+    else
+      return content_.begin()->first;
+  }
+
+
+  int Scene2D::GetMaxDepth() const
+  {
+    if (content_.size() == 0)
+      return 0;
+    else
+      return content_.rbegin()->first;
+  }
+
+  ISceneLayer* Scene2D::ReleaseLayer(int depth)
+  {
+    Content::iterator found = content_.find(depth);
+
+    if (found == content_.end())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(found->second != NULL);
+
+      std::auto_ptr<ISceneLayer> layer(found->second->ReleaseLayer());
+      assert(layer.get() != NULL);
+
+      content_.erase(found);
+
+      return layer.release();
+    }    
+  }
+  
+  
+  void Scene2D::Apply(IVisitor& visitor) const
+  {
+    for (Content::const_iterator it = content_.begin(); 
+         it != content_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      visitor.Visit(it->second->GetLayer(), it->second->GetIdentifier(), it->first);
+    }
+  }
+
+
+  void Scene2D::SetSceneToCanvasTransform(const AffineTransform2D& transform)
+  {
+    // Make sure the transform is invertible before making any change
+    AffineTransform2D inverse = AffineTransform2D::Invert(transform);
+
+    sceneToCanvas_ = transform;
+    canvasToScene_ = inverse;
+    BroadcastMessage(SceneTransformChanged(*this));
+  }
+
+  void Scene2D::FitContent(unsigned int canvasWidth,
+                           unsigned int canvasHeight)
+  {
+    Extent2D extent;
+
+    for (Content::const_iterator it = content_.begin(); 
+         it != content_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      
+      Extent2D tmp;
+      if (it->second->GetLayer().GetBoundingBox(tmp))
+      {
+        extent.Union(tmp);
+      }
+    }
+
+    if (!extent.IsEmpty())
+    {
+      double zoomX = static_cast<double>(canvasWidth) / extent.GetWidth();
+      double zoomY = static_cast<double>(canvasHeight) / extent.GetHeight();
+
+      double zoom = std::min(zoomX, zoomY);
+      if (LinearAlgebra::IsCloseToZero(zoom))
+      {
+        zoom = 1;
+      }
+
+      double panX = extent.GetCenterX();
+      double panY = extent.GetCenterY();
+
+      // Bring the center of the scene to (0,0)
+      AffineTransform2D t1 = AffineTransform2D::CreateOffset(-panX, -panY);
+      
+      // Scale the scene
+      AffineTransform2D t2 = AffineTransform2D::CreateScaling(zoom, zoom);
+
+      SetSceneToCanvasTransform(AffineTransform2D::Combine(t2, t1));
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Scene2D.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,121 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ISceneLayer.h"
+#include "../Toolbox/AffineTransform2D.h"
+#include <Framework/Messages/IObservable.h>
+#include <Framework/Messages/IMessage.h>
+
+#include <map>
+
+namespace OrthancStone
+{
+  class Scene2D : public IObservable
+  {
+  public:
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, SceneTransformChanged, Scene2D);
+    
+    class IVisitor : public boost::noncopyable
+    {
+    public:
+      virtual ~IVisitor()
+      {
+      }
+
+      virtual void Visit(const ISceneLayer& layer,
+                         uint64_t layerIdentifier,
+                         int depth) = 0;
+    };
+
+  private:
+    class Item;
+    
+    typedef std::map<int, Item*>  Content;
+
+    Content            content_;
+    AffineTransform2D  sceneToCanvas_;
+    AffineTransform2D  canvasToScene_;
+    uint64_t           layerCounter_;
+
+    Scene2D(const Scene2D& other);
+    
+  public:
+    Scene2D(MessageBroker& broker) 
+      : IObservable(broker)
+      , layerCounter_(0)
+    {
+    }
+    
+    ~Scene2D();
+
+    Scene2D* Clone() const
+    {
+      return new Scene2D(*this);
+    }
+
+    void SetLayer(int depth,
+                  ISceneLayer* layer);  // Takes ownership
+
+    /**
+    Removes the layer at specified depth and deletes the underlying object
+    */
+    void DeleteLayer(int depth);
+
+    bool HasLayer(int depth) const;
+
+    ISceneLayer& GetLayer(int depth) const;
+
+    /**
+    Returns the minimum depth among all layers or 0 if there are no layers
+    */
+    int GetMinDepth() const;
+
+    /**
+    Returns the minimum depth among all layers or 0 if there are no layers
+    */
+    int GetMaxDepth() const;
+
+    /**
+    Removes the layer at specified depth and transfers the object 
+    ownership to the caller
+    */
+    ISceneLayer* ReleaseLayer(int depth);
+
+    void Apply(IVisitor& visitor) const;
+
+    const AffineTransform2D& GetSceneToCanvasTransform() const
+    {
+      return sceneToCanvas_;
+    }
+
+    const AffineTransform2D& GetCanvasToSceneTransform() const
+    {
+      return canvasToScene_;
+    }
+
+    void SetSceneToCanvasTransform(const AffineTransform2D& transform);
+
+    void FitContent(unsigned int canvasWidth,
+                    unsigned int canvasHeight);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/ScenePoint2D.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,67 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Toolbox/AffineTransform2D.h"
+
+
+namespace OrthancStone
+{
+  class ScenePoint2D
+  {
+  private:
+    double  x_;
+    double  y_;
+
+  public:
+    ScenePoint2D() :
+      x_(0),
+      y_(0)
+    {
+    }
+
+    ScenePoint2D(double x,
+                 double y) :
+      x_(x),
+      y_(y)
+    {
+    }
+
+    double GetX() const
+    {
+      return x_;
+    }
+
+    double GetY() const
+    {
+      return y_;
+    }
+
+    ScenePoint2D Apply(const AffineTransform2D& t) const
+    {
+      double x = x_;
+      double y = y_;
+      t.Apply(x, y);
+      return ScenePoint2D(x, y);
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/TextSceneLayer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,81 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "TextSceneLayer.h"
+
+namespace OrthancStone
+{
+  TextSceneLayer::TextSceneLayer() :
+    x_(0),
+    y_(0),
+    fontIndex_(0),
+    anchor_(BitmapAnchor_Center),
+    border_(0),
+    revision_(0)
+  {
+  }
+
+
+  ISceneLayer* TextSceneLayer::Clone() const
+  {
+    std::auto_ptr<TextSceneLayer> cloned(new TextSceneLayer);
+    cloned->SetColor(GetRed(), GetGreen(), GetBlue());
+    cloned->x_ = x_;
+    cloned->y_ = y_;
+    cloned->utf8_ = utf8_;
+    cloned->fontIndex_ = fontIndex_;
+    cloned->anchor_ = anchor_;
+    cloned->border_ = border_;
+    return cloned.release();
+  }
+
+  void TextSceneLayer::SetPosition(double x,
+                                   double y)
+  {
+    x_ = x;
+    y_ = y;
+    revision_ ++;
+  }
+
+  void TextSceneLayer::SetText(const std::string& utf8)
+  {
+    utf8_ = utf8;
+    revision_ ++;
+  }
+
+  void TextSceneLayer::SetFontIndex(size_t fontIndex)
+  {
+    fontIndex_ = fontIndex;
+    revision_ ++;
+  }
+
+  void TextSceneLayer::SetAnchor(BitmapAnchor anchor)
+  {
+    anchor_ = anchor;
+    revision_ ++;
+  }
+
+  void TextSceneLayer::SetBorder(unsigned int border)
+  {
+    border_ = border;
+    revision_ ++;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/TextSceneLayer.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,104 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ColorSceneLayer.h"
+#include "../StoneEnumerations.h"
+
+#include <memory>
+#include <string>
+
+namespace OrthancStone
+{
+  class TextSceneLayer : public ColorSceneLayer
+  {
+  private:
+    double         x_;
+    double         y_;
+    std::string    utf8_;
+    size_t         fontIndex_;
+    BitmapAnchor   anchor_;
+    unsigned int   border_;
+    uint64_t       revision_;
+  
+  public:
+    TextSceneLayer();
+
+    virtual ISceneLayer* Clone() const;
+
+    void SetPosition(double x,
+                     double y);
+
+    void SetText(const std::string& utf8);
+
+    void SetFontIndex(size_t fontIndex);
+
+    void SetAnchor(BitmapAnchor anchor);
+
+    void SetBorder(unsigned int border);
+
+    double GetX() const
+    {
+      return x_;
+    }
+    
+    double GetY() const
+    {
+      return y_;
+    }
+
+    unsigned int GetBorder() const
+    {
+      return border_;
+    }
+  
+    const std::string& GetText() const
+    {
+      return utf8_;
+    }
+
+    size_t GetFontIndex() const
+    {
+      return fontIndex_;
+    }
+
+    BitmapAnchor GetAnchor() const
+    {
+      return anchor_;
+    }
+
+    virtual Type GetType() const
+    {
+      return Type_Text;
+    }
+
+    virtual bool GetBoundingBox(Extent2D& target) const
+    {
+      return false;
+    }
+
+    virtual uint64_t GetRevision() const
+    {
+      return revision_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/TextureBaseSceneLayer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,170 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "TextureBaseSceneLayer.h"
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  void TextureBaseSceneLayer::SetTexture(Orthanc::ImageAccessor* texture)
+  {
+    if (texture == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+    else
+    {
+      texture_.reset(texture);
+      IncrementRevision();
+    }
+  }
+
+
+  void TextureBaseSceneLayer::CopyParameters(const TextureBaseSceneLayer& other)
+  {
+    originX_ = other.originX_;
+    originY_ = other.originY_;
+    pixelSpacingX_ = other.pixelSpacingX_;
+    pixelSpacingY_ = other.pixelSpacingY_;
+    angle_ = other.angle_;
+    isLinearInterpolation_ = other.isLinearInterpolation_;
+  }
+
+
+  TextureBaseSceneLayer::TextureBaseSceneLayer() :
+    originX_(0),
+    originY_(0),
+    pixelSpacingX_(1),
+    pixelSpacingY_(1),
+    angle_(0),
+    isLinearInterpolation_(false),
+    revision_(0)
+  {
+    if (pixelSpacingX_ <= 0 ||
+        pixelSpacingY_ <= 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  
+  void TextureBaseSceneLayer::SetOrigin(double x,
+                                        double y)
+  {
+    originX_ = x;
+    originY_ = y;
+    IncrementRevision();
+  }
+
+
+  void TextureBaseSceneLayer::SetPixelSpacing(double sx,
+                                              double sy)
+  {
+    if (sx <= 0 ||
+        sy <= 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      pixelSpacingX_ = sx;
+      pixelSpacingY_ = sy;
+      IncrementRevision();
+    }
+  }
+
+  
+  void TextureBaseSceneLayer::SetAngle(double angle)
+  {
+    angle_ = angle;
+    IncrementRevision();
+  }
+
+  
+  void TextureBaseSceneLayer::SetLinearInterpolation(bool isLinearInterpolation)
+  {
+    isLinearInterpolation_ = isLinearInterpolation;
+    IncrementRevision();
+  }
+    
+
+  const Orthanc::ImageAccessor& TextureBaseSceneLayer::GetTexture() const
+  {
+    if (!HasTexture())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *texture_;
+    }
+  }
+
+  
+  AffineTransform2D TextureBaseSceneLayer::GetTransform() const
+  {
+    return AffineTransform2D::Combine(
+      AffineTransform2D::CreateOffset(originX_, originY_),
+      AffineTransform2D::CreateRotation(angle_),
+      AffineTransform2D::CreateScaling(pixelSpacingX_, pixelSpacingY_),
+      AffineTransform2D::CreateOffset(-0.5, -0.5));
+  }
+
+  
+  bool TextureBaseSceneLayer::GetBoundingBox(Extent2D& target) const
+  {
+    if (texture_.get() == NULL)
+    {
+      return false;
+    }
+    else
+    {
+      const AffineTransform2D t = GetTransform();
+
+      target.Reset();
+    
+      double x, y;
+
+      x = 0;
+      y = 0;
+      t.Apply(x, y);
+      target.AddPoint(x, y);
+
+      x = static_cast<double>(texture_->GetWidth());
+      y = 0;
+      t.Apply(x, y);
+      target.AddPoint(x, y);
+
+      x = 0;
+      y = static_cast<double>(texture_->GetHeight());
+      t.Apply(x, y);
+      target.AddPoint(x, y);
+
+      x = static_cast<double>(texture_->GetWidth());
+      y = static_cast<double>(texture_->GetHeight());
+      t.Apply(x, y);
+      target.AddPoint(x, y);    
+
+      return true;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/TextureBaseSceneLayer.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,114 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ISceneLayer.h"
+#include "../Toolbox/AffineTransform2D.h"
+
+#include <Core/Images/ImageAccessor.h>
+
+namespace OrthancStone
+{
+  class TextureBaseSceneLayer : public ISceneLayer
+  {
+  private:
+    std::auto_ptr<Orthanc::ImageAccessor>  texture_;
+    double                                 originX_;
+    double                                 originY_;
+    double                                 pixelSpacingX_;
+    double                                 pixelSpacingY_;
+    double                                 angle_;
+    bool                                   isLinearInterpolation_;
+    uint64_t                               revision_;
+
+  protected:
+    void SetTexture(Orthanc::ImageAccessor* texture);
+
+    void IncrementRevision() 
+    {
+      revision_++;
+    }
+
+    void CopyParameters(const TextureBaseSceneLayer& other);
+
+  public:
+    TextureBaseSceneLayer();
+
+    // Center of the top-left pixel
+    void SetOrigin(double x,
+                   double y);
+
+    void SetPixelSpacing(double sx,
+                         double sy);
+
+    // In radians
+    void SetAngle(double angle);
+
+    void SetLinearInterpolation(bool isLinearInterpolation);
+    
+    double GetOriginX() const
+    {
+      return originX_;
+    }
+
+    double GetOriginY() const
+    {
+      return originY_;
+    }
+
+    double GetPixelSpacingX() const
+    {
+      return pixelSpacingX_;
+    }
+
+    double GetPixelSpacingY() const
+    {
+      return pixelSpacingY_;
+    }
+
+    double GetAngle() const
+    {
+      return angle_;
+    }
+
+    bool IsLinearInterpolation() const
+    {
+      return isLinearInterpolation_;
+    }
+
+    bool HasTexture() const
+    {
+      return (texture_.get() != NULL);
+    }
+
+    const Orthanc::ImageAccessor& GetTexture() const;
+
+    AffineTransform2D GetTransform() const;
+    
+    virtual bool GetBoundingBox(Extent2D& target) const;
+
+    virtual uint64_t GetRevision() const
+    {
+      return revision_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/ZoomSceneTracker.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,81 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "ZoomSceneTracker.h"
+
+namespace OrthancStone
+{
+  ZoomSceneTracker::ZoomSceneTracker(Scene2D& scene,
+                                     const PointerEvent& event,
+                                     unsigned int canvasHeight) :
+    scene_(scene),
+    clickY_(event.GetMainPosition().GetY()),
+    aligner_(scene, event.GetMainPosition()),
+    originalSceneToCanvas_(scene.GetSceneToCanvasTransform())
+  {
+    if (canvasHeight <= 3)
+    {
+      active_ = false;
+    }
+    else
+    {
+      normalization_ = 1.0 / static_cast<double>(canvasHeight - 1);
+      active_ = true;
+    }
+  }
+  
+
+  void ZoomSceneTracker::Update(const PointerEvent& event)
+  {
+    static const double MIN_ZOOM = -4;
+    static const double MAX_ZOOM = 4;
+      
+    if (active_)
+    {
+      double y = event.GetMainPosition().GetY();
+      double dy = static_cast<double>(y - clickY_) * normalization_;  // In the range [-1,1]
+      double z;
+
+      // Linear interpolation from [-1, 1] to [MIN_ZOOM, MAX_ZOOM]
+      if (dy < -1.0)
+      {
+        z = MIN_ZOOM;
+      }
+      else if (dy > 1.0)
+      {
+        z = MAX_ZOOM;
+      }
+      else
+      {
+        z = MIN_ZOOM + (MAX_ZOOM - MIN_ZOOM) * (dy + 1.0) / 2.0;
+      }
+
+      double zoom = pow(2.0, z);
+
+      scene_.SetSceneToCanvasTransform(
+        AffineTransform2D::Combine(
+          AffineTransform2D::CreateScaling(zoom, zoom),
+          originalSceneToCanvas_));
+
+      aligner_.Apply();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/ZoomSceneTracker.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,50 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IPointerTracker.h"
+#include "Internals/FixedPointAligner.h"
+
+namespace OrthancStone
+{
+  class ZoomSceneTracker : public IPointerTracker
+  {
+  private:
+    Scene2D&                      scene_;
+    double                        clickY_;
+    bool                          active_;
+    double                        normalization_;
+    Internals::FixedPointAligner  aligner_;
+    AffineTransform2D             originalSceneToCanvas_;
+
+  public:
+    ZoomSceneTracker(Scene2D& scene,
+                     const PointerEvent& event,
+                     unsigned int canvasHeight);
+
+    virtual void Update(const PointerEvent& event);
+
+    virtual void Release()
+    {
+    }
+  };
+}
--- a/Framework/SmartLoader.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/SmartLoader.cpp	Thu May 16 09:11:14 2019 +0200
@@ -95,7 +95,7 @@
         LOG(WARNING) << "ScheduleLayerCreation for CachedSlice (image is loaded): " << slice_->GetOrthancInstanceId();
 
         RendererFactory factory(*this);   
-        EmitMessage(IVolumeSlicer::LayerReadyMessage(*this, factory, slice_->GetGeometry()));
+        BroadcastMessage(IVolumeSlicer::LayerReadyMessage(*this, factory, slice_->GetGeometry()));
       }
       else
       {
@@ -174,7 +174,7 @@
 
     if (cachedSlice != NULL)
     {
-      EmitMessage(IVolumeSlicer::GeometryReadyMessage(*cachedSlice));
+      BroadcastMessage(IVolumeSlicer::GeometryReadyMessage(*cachedSlice));
     }
 
   }
@@ -242,7 +242,7 @@
     cachedSlices_[sliceKeyId] = boost::shared_ptr<CachedSlice>(cachedSlice);
 
     // re-emit original Layer message to observers
-    EmitMessage(message);
+    BroadcastMessage(message);
   }
 
 
@@ -264,7 +264,7 @@
     cachedSlices_[sliceKeyId] = cachedSlice;
 
     // re-emit original Layer message to observers
-    EmitMessage(message);
+    BroadcastMessage(message);
   }
 
 
@@ -286,6 +286,6 @@
     }
 
     // re-emit original Layer message to observers
-    EmitMessage(message);
+    BroadcastMessage(message);
   }
 }
--- a/Framework/StoneEnumerations.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/StoneEnumerations.cpp	Thu May 16 09:11:14 2019 +0200
@@ -27,20 +27,18 @@
 
 namespace OrthancStone
 {  
-  bool StringToSopClassUid(SopClassUid& result,
-                           const std::string& source)
+  SopClassUid StringToSopClassUid(const std::string& source)
   {
     std::string s = Orthanc::Toolbox::StripSpaces(source);
 
     if (s == "1.2.840.10008.5.1.4.1.1.481.2")
     {
-      result = SopClassUid_RTDose;
-      return true;
+      return SopClassUid_RTDose;
     }
     else
     {
-      //LOG(INFO) << "Unknown SOP class UID: " << source;
-      return false;
+      //LOG(INFO) << "Other SOP class UID: " << source;
+      return SopClassUid_Other;
     }
   }  
 
@@ -48,14 +46,14 @@
   void ComputeWindowing(float& targetCenter,
                         float& targetWidth,
                         ImageWindowing windowing,
-                        float defaultCenter,
-                        float defaultWidth)
+                        float customCenter,
+                        float customWidth)
   {
     switch (windowing)
     {
-      case ImageWindowing_Default:
-        targetCenter = defaultCenter;
-        targetWidth = defaultWidth;
+      case ImageWindowing_Custom:
+        targetCenter = customCenter;
+        targetWidth = customWidth;
         break;
 
       case ImageWindowing_Bone:
--- a/Framework/StoneEnumerations.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/StoneEnumerations.h	Thu May 16 09:11:14 2019 +0200
@@ -34,7 +34,6 @@
 
   enum ImageWindowing
   {
-    ImageWindowing_Default,
     ImageWindowing_Bone,
     ImageWindowing_Lung,
     ImageWindowing_Custom
@@ -99,6 +98,7 @@
 
   enum SopClassUid
   {
+    SopClassUid_Other,
     SopClassUid_RTDose
   };
 
@@ -115,67 +115,6 @@
     BitmapAnchor_TopRight
   };
 
-  enum MessageType
-  {
-    MessageType_Widget_GeometryChanged,
-    MessageType_Widget_ContentChanged,
-
-    MessageType_VolumeSlicer_GeometryReady,   // instance tags have been loaded
-    MessageType_VolumeSlicer_GeometryError,
-    MessageType_VolumeSlicer_ContentChanged,
-    MessageType_VolumeSlicer_SliceChanged,
-    MessageType_VolumeSlicer_LayerReady,      // layer is ready to be rendered
-    MessageType_VolumeSlicer_LayerError,
-
-    MessageType_DicomSeriesVolumeSlicer_FrameReady,      // pixels data of the frame have been loaded
-
-    MessageType_SliceViewerWidget_DisplayedSlice,  // The displayed slice has changed
-
-    MessageType_SliceLoader_GeometryReady,
-    MessageType_SliceLoader_GeometryError,
-    MessageType_SliceLoader_ImageReady,
-    MessageType_SliceLoader_ImageError,
-
-    MessageType_VolumeLoader_GeometryReady,
-    MessageType_VolumeLoader_GeometryError,
-    MessageType_VolumeLoader_ContentChanged,  // Content of several slices in the loader has changed
-
-    MessageType_SlicedVolume_GeometryReady,
-    MessageType_SlicedVolume_GeometryError,
-    MessageType_SlicedVolume_VolumeReady,
-    MessageType_SlicedVolume_ContentChanged,
-    MessageType_SlicedVolume_SliceContentChanged,
-
-    MessageType_HttpRequestSuccess,
-    MessageType_HttpRequestError,
-
-    MessageType_OrthancApi_InternalGetJsonResponseReady,
-    MessageType_OrthancApi_InternalGetJsonResponseError,
-
-    MessageType_OrthancApi_GenericGetJson_Ready,
-    MessageType_OrthancApi_GenericGetBinary_Ready,
-    MessageType_OrthancApi_GenericHttpError_Ready,
-    MessageType_OrthancApi_GenericEmptyResponse_Ready,
-
-    MessageType_RadiographyScene_GeometryChanged,
-    MessageType_RadiographyScene_ContentChanged,
-    MessageType_RadiographyScene_LayerEdited,
-    MessageType_RadiographyScene_WindowingChanged,
-
-    MessageType_RadiographyLayer_Edited,
-
-    MessageType_ViewportChanged,
-
-    MessageType_Timeout,
-
-    // used in unit tests only
-    MessageType_Test1,
-    MessageType_Test2,
-
-    MessageType_CustomMessage // Custom messages ids ust be greater than this (this one must remain in last position)
-  };
-
-  
   enum ControlPointType
   {
     ControlPoint_TopLeftCorner = 0,
@@ -193,14 +132,13 @@
   };
 
   
-  bool StringToSopClassUid(SopClassUid& result,
-                           const std::string& source);
+  SopClassUid StringToSopClassUid(const std::string& source);
 
   void ComputeWindowing(float& targetCenter,
                         float& targetWidth,
                         ImageWindowing windowing,
-                        float defaultCenter,
-                        float defaultWidth);
+                        float customCenter,
+                        float customWidth);
 
   void ComputeAnchorTranslation(double& deltaX /* out */,
                                 double& deltaY /* out */,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/StoneInitialization.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,61 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "StoneInitialization.h"
+
+#include <Core/OrthancException.h>
+
+#if !defined(ORTHANC_ENABLE_SDL)
+#  error Macro ORTHANC_ENABLE_SDL must be defined
+#endif
+
+#if ORTHANC_ENABLE_SDL == 1
+#  include "../Applications/Sdl/SdlWindow.h"
+#endif
+
+namespace OrthancStone
+{
+#if ORTHANC_ENABLE_LOGGING_PLUGIN == 1
+  void StoneInitialize(OrthancPluginContext* context)
+#else
+  void StoneInitialize()
+#endif
+  {
+#if ORTHANC_ENABLE_LOGGING_PLUGIN == 1
+    Orthanc::Logging::Initialize(context);
+#else
+    Orthanc::Logging::Initialize();
+#endif
+
+#if ORTHANC_ENABLE_SDL == 1
+    OrthancStone::SdlWindow::GlobalInitialize();
+#endif
+  }
+
+  void StoneFinalize()
+  {
+#if ORTHANC_ENABLE_SDL == 1
+    OrthancStone::SdlWindow::GlobalFinalize();
+#endif
+    
+    Orthanc::Logging::Finalize();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/StoneInitialization.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,35 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <Core/Logging.h>
+
+namespace OrthancStone
+{
+#if ORTHANC_ENABLE_LOGGING_PLUGIN == 1
+  void StoneInitialize(OrthancPluginContext* context);
+#else
+  void StoneInitialize();
+#endif
+
+  void StoneFinalize();
+}
--- a/Framework/Toolbox/AffineTransform2D.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Toolbox/AffineTransform2D.cpp	Thu May 16 09:11:14 2019 +0200
@@ -90,6 +90,76 @@
   }
 
 
+  void AffineTransform2D::ConvertToOpenGLMatrix(float target[16],
+                                                unsigned int canvasWidth,
+                                                unsigned int canvasHeight) const
+  {
+    const AffineTransform2D t = AffineTransform2D::Combine(
+      CreateOpenGLClipspace(canvasWidth, canvasHeight), *this);
+    
+    const Matrix source = t.GetHomogeneousMatrix();
+  
+    if (source.size1() != 3 ||
+        source.size2() != 3)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    // "z" must be in the [-1,1] range, otherwise the texture does not show up
+    float z = 0;
+
+    // Embed the 3x3 affine transform of the 2D plane into a 4x4
+    // matrix (3D) for OpenGL. The matrix must be transposed.
+
+    target[0] = static_cast<float>(source(0, 0)); 
+    target[1] = static_cast<float>(source(1, 0)); 
+    target[2] = 0; 
+    target[3] = static_cast<float>(source(2, 0));
+    target[4] = static_cast<float>(source(0, 1)); 
+    target[5] = static_cast<float>(source(1, 1));
+    target[6] = 0;
+    target[7] = static_cast<float>(source(2, 1));
+    target[8] = 0; 
+    target[9] = 0; 
+    target[10] = -1; 
+    target[11] = 0;
+    target[12] = static_cast<float>(source(0, 2)); 
+    target[13] = static_cast<float>(source(1, 2));
+    target[14] = -z;
+    target[15] = static_cast<float>(source(2, 2));
+  }
+
+
+  double AffineTransform2D::ComputeZoom() const
+  {
+    // Compute the length of the (0,0)-(1,1) diagonal (whose
+    // length is sqrt(2)) instead of the (0,0)-(1,0) unit segment,
+    // in order to cope with possible anisotropic zooming
+        
+    double x1 = 0;
+    double y1 = 0;
+    Apply(x1, y1);
+
+    double x2 = 1;
+    double y2 = 1;
+    Apply(x2, y2);
+
+    double dx = x2 - x1;
+    double dy = y2 - y1;
+
+    double zoom = sqrt(dx * dx + dy * dy) / sqrt(2.0);
+
+    if (LinearAlgebra::IsCloseToZero(zoom))
+    {
+      return 1;  // Default value if transform is ill-conditioned 
+    }
+    else
+    {
+      return zoom;
+    }
+  }    
+
+
   AffineTransform2D AffineTransform2D::Invert(const AffineTransform2D& a)
   {
     AffineTransform2D t;
@@ -163,4 +233,17 @@
 
     return t;
   }
+
+
+  AffineTransform2D AffineTransform2D::CreateOpenGLClipspace(unsigned int canvasWidth,
+                                                             unsigned int canvasHeight)
+  {
+    AffineTransform2D t;
+    t.matrix_(0, 0) = 2.0 / static_cast<double>(canvasWidth);
+    t.matrix_(0, 2) = -1.0;
+    t.matrix_(1, 1) = -2.0 / static_cast<double>(canvasHeight);
+    t.matrix_(1, 2) = 1.0;
+    
+    return t;
+  }
 }
--- a/Framework/Toolbox/AffineTransform2D.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Toolbox/AffineTransform2D.h	Thu May 16 09:11:14 2019 +0200
@@ -56,6 +56,12 @@
                const Orthanc::ImageAccessor& source,
                ImageInterpolation interpolation,
                bool clear) const;
+
+    void ConvertToOpenGLMatrix(float target[16],
+                               unsigned int canvasWidth,
+                               unsigned int canvasHeight) const;
+
+    double ComputeZoom() const;
     
     static AffineTransform2D Invert(const AffineTransform2D& a);
 
@@ -78,5 +84,8 @@
                                            double sy);
     
     static AffineTransform2D CreateRotation(double angle);
+
+    static AffineTransform2D CreateOpenGLClipspace(unsigned int canvasWidth,
+                                                   unsigned int canvasHeight);
   };
 }
--- a/Framework/Toolbox/BaseWebService.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Toolbox/BaseWebService.h	Thu May 16 09:11:14 2019 +0200
@@ -81,7 +81,7 @@
     class BaseWebServicePayload;
 
     bool          cacheEnabled_;
-    std::map<std::string, boost::shared_ptr<CachedHttpRequestSuccessMessage>> cache_;  // TODO: this is currently an infinite cache !
+    std::map<std::string, boost::shared_ptr<CachedHttpRequestSuccessMessage> > cache_;  // TODO: this is currently an infinite cache !
 
   public:
 
--- a/Framework/Toolbox/CoordinateSystem3D.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Toolbox/CoordinateSystem3D.cpp	Thu May 16 09:11:14 2019 +0200
@@ -187,4 +187,24 @@
   {
     return GeometryToolbox::IntersectPlaneAndLine(p, normal_, d_, origin, direction);
   }
+
+
+  bool CoordinateSystem3D::GetDistance(double& distance,
+                                       const CoordinateSystem3D& a,
+                                       const CoordinateSystem3D& b)
+  {
+    bool opposite;   // Ignored
+
+    if (OrthancStone::GeometryToolbox::IsParallelOrOpposite(
+          opposite, a.GetNormal(), b.GetNormal()))
+    {
+      distance = std::abs(a.ProjectAlongNormal(a.GetOrigin()) -
+                          a.ProjectAlongNormal(b.GetOrigin()));
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
 }
--- a/Framework/Toolbox/CoordinateSystem3D.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Toolbox/CoordinateSystem3D.h	Thu May 16 09:11:14 2019 +0200
@@ -102,5 +102,10 @@
     bool IntersectLine(Vector& p,
                        const Vector& origin,
                        const Vector& direction) const;
+
+    // Returns "false" is the two planes are not parallel
+    static bool GetDistance(double& distance,
+                            const CoordinateSystem3D& a,
+                            const CoordinateSystem3D& b);
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Toolbox/DynamicBitmap.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,37 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DynamicBitmap.h"
+
+#include <Core/Images/Image.h>
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  DynamicBitmap::DynamicBitmap(const Orthanc::ImageAccessor& bitmap) :
+    bitmap_(Orthanc::Image::Clone(bitmap))
+  {
+    if (bitmap_.get() == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Toolbox/DynamicBitmap.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,44 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <Core/IDynamicObject.h>
+#include <Core/Images/ImageAccessor.h>
+
+#include <memory>
+
+namespace OrthancStone
+{
+  class DynamicBitmap : public Orthanc::IDynamicObject
+  {
+  private:
+    std::auto_ptr<Orthanc::ImageAccessor>  bitmap_;
+
+  public:
+    DynamicBitmap(const Orthanc::ImageAccessor& bitmap);
+
+    const Orthanc::ImageAccessor& GetBitmap() const
+    {
+      return *bitmap_;
+    }
+  };
+}
--- a/Framework/Toolbox/IDelayedCallExecutor.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Toolbox/IDelayedCallExecutor.h	Thu May 16 09:11:14 2019 +0200
@@ -39,8 +39,7 @@
     MessageBroker& broker_;
     
   public:
-
-    typedef NoPayloadMessage<MessageType_Timeout> TimeoutMessage;
+    ORTHANC_STONE_DEFINE_EMPTY_MESSAGE(__FILE__, __LINE__, TimeoutMessage);
 
     IDelayedCallExecutor(MessageBroker& broker) :
       broker_(broker)
--- a/Framework/Toolbox/IWebService.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Toolbox/IWebService.h	Thu May 16 09:11:14 2019 +0200
@@ -45,8 +45,10 @@
   public:
     typedef std::map<std::string, std::string> HttpHeaders;
 
-    class HttpRequestSuccessMessage : public BaseMessage<MessageType_HttpRequestSuccess>
+    class HttpRequestSuccessMessage : public IMessage
     {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+
     private:
       const std::string&             uri_;
       const void*                    answer_;
@@ -97,8 +99,10 @@
     };
     
 
-    class HttpRequestErrorMessage : public BaseMessage<MessageType_HttpRequestError>
+    class HttpRequestErrorMessage : public IMessage
     {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+
     private:
       const std::string&              uri_;
       const Orthanc::IDynamicObject*  payload_;
--- a/Framework/Toolbox/LinearAlgebra.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Toolbox/LinearAlgebra.h	Thu May 16 09:11:14 2019 +0200
@@ -137,7 +137,7 @@
                        double y,
                        double threshold)
     {
-      return fabs(x - y) < threshold;
+      return fabs(x - y) <= threshold;
     }
 
     inline bool IsNear(double x,
--- a/Framework/Toolbox/OrthancApiClient.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Toolbox/OrthancApiClient.h	Thu May 16 09:11:14 2019 +0200
@@ -35,9 +35,10 @@
       public IObserver
   {
   public:
-    class JsonResponseReadyMessage :
-        public BaseMessage<MessageType_OrthancApi_GenericGetJson_Ready>
+    class JsonResponseReadyMessage : public IMessage
     {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+
     private:
       const std::string&              uri_;
       const Json::Value&              json_;
@@ -72,9 +73,10 @@
     };
     
 
-    class BinaryResponseReadyMessage :
-        public BaseMessage<MessageType_OrthancApi_GenericGetBinary_Ready>
+    class BinaryResponseReadyMessage : public IMessage
     {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+
     private:
       const std::string&              uri_;
       const void*                     answer_;
@@ -117,9 +119,10 @@
     };
 
 
-    class EmptyResponseReadyMessage :
-        public BaseMessage<MessageType_OrthancApi_GenericEmptyResponse_Ready>
+    class EmptyResponseReadyMessage : public IMessage
     {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+
     private:
       const std::string&              uri_;
       const Orthanc::IDynamicObject*  payload_;
--- a/Framework/Toolbox/OrthancSlicesLoader.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Toolbox/OrthancSlicesLoader.cpp	Thu May 16 09:11:14 2019 +0200
@@ -177,7 +177,7 @@
   {
     OrthancSlicesLoader::SliceImageReadyMessage msg
       (*this, operation.GetSliceIndex(), operation.GetSlice(), image, operation.GetQuality());
-    EmitMessage(msg);
+    BroadcastMessage(msg);
   }
   
   
@@ -185,43 +185,31 @@
   {
     OrthancSlicesLoader::SliceImageErrorMessage msg
       (*this, operation.GetSliceIndex(), operation.GetSlice(), operation.GetQuality());
-    EmitMessage(msg);
+    BroadcastMessage(msg);
   }
   
   
   void OrthancSlicesLoader::SortAndFinalizeSlices()
   {
-    bool ok = false;
-    
-    if (slices_.GetSliceCount() > 0)
-    {
-      Vector normal;
-      if (slices_.SelectNormal(normal))
-      {
-        slices_.FilterNormal(normal);
-        slices_.SetNormal(normal);
-        slices_.Sort();
-        ok = true;
-      }
-    }
+    bool ok = slices_.Sort();
     
     state_ = State_GeometryReady;
     
     if (ok)
     {
-      LOG(INFO) << "Loaded a series with " << slices_.GetSliceCount() << " slice(s)";
-      EmitMessage(SliceGeometryReadyMessage(*this));
+      LOG(INFO) << "Loaded a series with " << slices_.GetSlicesCount() << " slice(s)";
+      BroadcastMessage(SliceGeometryReadyMessage(*this));
     }
     else
     {
       LOG(ERROR) << "This series is empty";
-      EmitMessage(SliceGeometryErrorMessage(*this));
+      BroadcastMessage(SliceGeometryErrorMessage(*this));
     }
   }
   
   void OrthancSlicesLoader::OnGeometryError(const IWebService::HttpRequestErrorMessage& message)
   {
-    EmitMessage(SliceGeometryErrorMessage(*this));
+    BroadcastMessage(SliceGeometryErrorMessage(*this));
     state_ = State_Error;
   }
 
@@ -256,7 +244,8 @@
         std::auto_ptr<Slice> slice(new Slice);
         if (slice->ParseOrthancFrame(dicom, instances[i], frame))
         {
-          slices_.AddSlice(slice.release());
+          CoordinateSystem3D geometry = slice->GetGeometry();
+          slices_.AddSlice(geometry, slice.release());
         }
         else
         {
@@ -291,12 +280,13 @@
       std::auto_ptr<Slice> slice(new Slice);
       if (slice->ParseOrthancFrame(dicom, instanceId, frame))
       {
-        slices_.AddSlice(slice.release());
+        CoordinateSystem3D geometry = slice->GetGeometry();
+        slices_.AddSlice(geometry, slice.release());
       }
       else
       {
         LOG(WARNING) << "Skipping invalid multi-frame instance " << instanceId;
-        EmitMessage(SliceGeometryErrorMessage(*this));
+        BroadcastMessage(SliceGeometryErrorMessage(*this));
         return;
       }
     }
@@ -322,13 +312,16 @@
     if (slice->ParseOrthancFrame(dicom, instanceId, frame))
     {
       LOG(INFO) << "Loaded instance geometry " << instanceId;
-      slices_.AddSlice(slice.release());
-      EmitMessage(SliceGeometryReadyMessage(*this));
+
+      CoordinateSystem3D geometry = slice->GetGeometry();
+      slices_.AddSlice(geometry, slice.release());
+      
+      BroadcastMessage(SliceGeometryReadyMessage(*this));
     }
     else
     {
       LOG(WARNING) << "Skipping invalid instance " << instanceId;
-      EmitMessage(SliceGeometryErrorMessage(*this));
+      BroadcastMessage(SliceGeometryErrorMessage(*this));
     }
   }
   
@@ -717,14 +710,14 @@
   }
   
   
-  size_t OrthancSlicesLoader::GetSliceCount() const
+  size_t OrthancSlicesLoader::GetSlicesCount() const
   {
     if (state_ != State_GeometryReady)
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
     }
     
-    return slices_.GetSliceCount();
+    return slices_.GetSlicesCount();
   }
   
   
@@ -734,8 +727,8 @@
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
     }
-    
-    return slices_.GetSlice(index);
+
+    return dynamic_cast<const Slice&>(slices_.GetSlicePayload(index));
   }
   
   
@@ -746,8 +739,10 @@
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
     }
-    
-    return slices_.LookupSlice(index, plane);
+
+    double distance;
+    return (slices_.LookupClosestSlice(index, distance, plane) &&
+            distance <= GetSlice(index).GetThickness() / 2.0);
   }
   
   
--- a/Framework/Toolbox/OrthancSlicesLoader.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Toolbox/OrthancSlicesLoader.h	Thu May 16 09:11:14 2019 +0200
@@ -26,6 +26,7 @@
 #include "IWebService.h"
 #include "OrthancApiClient.h"
 #include "SlicesSorter.h"
+#include "Slice.h"
 
 #include <Core/Images/Image.h>
 
@@ -35,13 +36,14 @@
   class OrthancSlicesLoader : public IObservable, public IObserver
   {
   public:
-
-    typedef OriginMessage<MessageType_SliceLoader_GeometryReady, OrthancSlicesLoader> SliceGeometryReadyMessage;
-    typedef OriginMessage<MessageType_SliceLoader_GeometryError, OrthancSlicesLoader> SliceGeometryErrorMessage;
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, SliceGeometryReadyMessage, OrthancSlicesLoader);
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, SliceGeometryErrorMessage, OrthancSlicesLoader);
 
-    class SliceImageReadyMessage :
-      public OriginMessage<MessageType_SliceLoader_ImageReady, OrthancSlicesLoader>
+    
+    class SliceImageReadyMessage : public OriginMessage<OrthancSlicesLoader>
     {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+      
     private:
       unsigned int                   sliceIndex_;
       const Slice&                   slice_;
@@ -84,9 +86,10 @@
     };
     
 
-    class SliceImageErrorMessage : 
-      public OriginMessage<MessageType_SliceLoader_ImageError, OrthancSlicesLoader>
+    class SliceImageErrorMessage : public OriginMessage<OrthancSlicesLoader>
     {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+      
     private:
       const Slice&       slice_;
       unsigned int       sliceIndex_;
@@ -193,7 +196,7 @@
 
     bool IsGeometryReady() const;
 
-    size_t GetSliceCount() const;
+    size_t GetSlicesCount() const;
 
     const Slice& GetSlice(size_t index) const;
 
--- a/Framework/Toolbox/Slice.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Toolbox/Slice.cpp	Thu May 16 09:11:14 2019 +0200
@@ -194,20 +194,16 @@
       geometry_ = CoordinateSystem3D(position, orientation);
 
       bool ok = true;
-      SopClassUid tmp;
 
-      if (StringToSopClassUid(tmp, sopClassUid_))
+      switch (StringToSopClassUid(sopClassUid_))
       {
-        switch (tmp)
-        {
-          case SopClassUid_RTDose:
-            type_ = Type_OrthancRawFrame;
-            ok = ComputeRTDoseGeometry(dataset, frame);
-            break;
+        case SopClassUid_RTDose:
+          type_ = Type_OrthancRawFrame;
+          ok = ComputeRTDoseGeometry(dataset, frame);
+          break;
             
-          default:
-            break;
-        }
+        default:
+          break;
       }
 
       if (!ok)
--- a/Framework/Toolbox/Slice.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Toolbox/Slice.h	Thu May 16 09:11:14 2019 +0200
@@ -25,10 +25,13 @@
 #include "DicomFrameConverter.h"
 
 #include <Core/DicomFormat/DicomImageInformation.h>
+#include <Core/IDynamicObject.h>
 
 namespace OrthancStone
 {
-  class Slice : public boost::noncopyable
+  // TODO - Remove this class
+  class Slice :
+    public Orthanc::IDynamicObject  /* to be used as a payload of SlicesSorter */
   {
   private:
     enum Type
--- a/Framework/Toolbox/SlicesSorter.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Toolbox/SlicesSorter.cpp	Thu May 16 09:11:14 2019 +0200
@@ -30,25 +30,23 @@
   class SlicesSorter::SliceWithDepth : public boost::noncopyable
   {
   private:
-    std::auto_ptr<Slice>   slice_;
-    double                 depth_;
+    CoordinateSystem3D  geometry_;
+    double              depth_;
+
+    std::auto_ptr<Orthanc::IDynamicObject>   payload_;
 
   public:
-    SliceWithDepth(Slice* slice) :
-      slice_(slice),
-      depth_(0)
+    SliceWithDepth(const CoordinateSystem3D& geometry,
+                   Orthanc::IDynamicObject* payload) :
+      geometry_(geometry),
+      depth_(0),
+      payload_(payload)
     {
-      if (slice == NULL)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-      }
     }
 
     void SetNormal(const Vector& normal)
     {
-      assert(slice_.get() != NULL);
-      depth_ = boost::numeric::ublas::inner_prod
-        (slice_->GetGeometry().GetOrigin(), normal);
+      depth_ = boost::numeric::ublas::inner_prod(geometry_.GetOrigin(), normal);
     }
 
     double GetDepth() const
@@ -56,10 +54,26 @@
       return depth_;
     }
 
-    const Slice& GetSlice() const
+    const CoordinateSystem3D& GetGeometry() const
+    {
+      return geometry_;
+    }
+
+    bool HasPayload() const
     {
-      assert(slice_.get() != NULL);
-      return *slice_;
+      return (payload_.get() != NULL);
+    }
+
+    const Orthanc::IDynamicObject& GetPayload() const
+    {
+      if (HasPayload())
+      {
+        return *payload_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
     }
   };
 
@@ -84,21 +98,42 @@
   }
 
 
-  void SlicesSorter::AddSlice(Slice* slice)
+  void SlicesSorter::AddSlice(const CoordinateSystem3D& slice,
+                              Orthanc::IDynamicObject* payload)
   {
-    slices_.push_back(new SliceWithDepth(slice));
+    slices_.push_back(new SliceWithDepth(slice, payload));
   }
 
   
-  const Slice& SlicesSorter::GetSlice(size_t i) const
+  const SlicesSorter::SliceWithDepth& SlicesSorter::GetSlice(size_t i) const
   {
     if (i >= slices_.size())
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
+    else
+    {
+      assert(slices_[i] != NULL);
+      return *slices_[i];
+    }
+  }
 
-    assert(slices_[i] != NULL);
-    return slices_[i]->GetSlice();
+
+  const CoordinateSystem3D& SlicesSorter::GetSliceGeometry(size_t i) const
+  {
+    return GetSlice(i).GetGeometry();
+  }
+  
+  
+  bool SlicesSorter::HasSlicePayload(size_t i) const
+  {
+    return GetSlice(i).HasPayload();
+  }
+  
+    
+  const Orthanc::IDynamicObject& SlicesSorter::GetSlicePayload(size_t i) const
+  {
+    return GetSlice(i).GetPayload();
   }
 
   
@@ -113,7 +148,7 @@
   }
   
     
-  void SlicesSorter::Sort()
+  void SlicesSorter::SortInternal()
   {
     if (!hasNormal_)
     {
@@ -131,7 +166,7 @@
 
     for (size_t i = 0; i < slices_.size(); i++)
     {
-      if (GeometryToolbox::IsParallel(normal, slices_[i]->GetSlice().GetGeometry().GetNormal()))
+      if (GeometryToolbox::IsParallel(normal, slices_[i]->GetGeometry().GetNormal()))
       {
         // This slice is compatible with the selected normal
         slices_[pos] = slices_[i];
@@ -155,7 +190,7 @@
 
     bool found = false;
 
-    for (size_t i = 0; !found && i < GetSliceCount(); i++)
+    for (size_t i = 0; !found && i < GetSlicesCount(); i++)
     {
       const Vector& normal = GetSlice(i).GetGeometry().GetNormal();
 
@@ -190,8 +225,8 @@
     for (size_t i = 0; !found && i < normalCandidates.size(); i++)
     {
       unsigned int count = normalCount[i];
-      if (count == GetSliceCount() ||
-          count + 1 == GetSliceCount())
+      if (count == GetSlicesCount() ||
+          count + 1 == GetSlicesCount())
       {
         normal = normalCandidates[i];
         found = true;
@@ -202,21 +237,90 @@
   }
 
 
-  bool SlicesSorter::LookupSlice(size_t& index,
-                                 const CoordinateSystem3D& slice) const
+  bool SlicesSorter::Sort()
   {
-    // TODO Turn this linear-time lookup into a log-time lookup,
-    // keeping track of whether the slices are sorted along the normal
-
-    for (size_t i = 0; i < slices_.size(); i++)
+    if (GetSlicesCount() > 0)
     {
-      if (slices_[i]->GetSlice().ContainsPlane(slice))
+      Vector normal;
+      if (SelectNormal(normal))
       {
-        index = i;
+        FilterNormal(normal);
+        SetNormal(normal);
+        SortInternal();
         return true;
       }
     }
 
     return false;
   }
+
+
+  bool SlicesSorter::LookupClosestSlice(size_t& index,
+                                        double& distance,
+                                        const CoordinateSystem3D& slice) const
+  {
+    // TODO Turn this linear-time lookup into a log-time lookup,
+    // keeping track of whether the slices are sorted along the normal
+
+    bool found = false;
+    
+    distance = std::numeric_limits<double>::infinity();
+    
+    for (size_t i = 0; i < slices_.size(); i++)
+    {
+      assert(slices_[i] != NULL);
+
+      double tmp;
+      if (CoordinateSystem3D::GetDistance(tmp, slices_[i]->GetGeometry(), slice))
+      {
+        if (!found ||
+            tmp < distance)
+        {
+          index = i;
+          distance = tmp;
+          found = true;
+        }
+      }
+    }
+
+    return found;
+  }
+
+
+  double SlicesSorter::ComputeSpacingBetweenSlices() const
+  {
+    if (GetSlicesCount() <= 1)
+    {
+      // This is a volume that is empty or that contains one single
+      // slice: Choose a dummy z-dimension for voxels
+      return 1.0;
+    }
+    
+    const OrthancStone::CoordinateSystem3D& reference = GetSliceGeometry(0);
+
+    double referencePosition = reference.ProjectAlongNormal(reference.GetOrigin());
+        
+    double p = reference.ProjectAlongNormal(GetSliceGeometry(1).GetOrigin());
+    double spacingZ = p - referencePosition;
+
+    if (spacingZ <= 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls,
+                                      "Please call the Sort() method before");
+    }
+
+    for (size_t i = 1; i < GetSlicesCount(); i++)
+    {
+      OrthancStone::Vector p = reference.GetOrigin() + spacingZ * static_cast<double>(i) * reference.GetNormal();        
+      double d = boost::numeric::ublas::norm_2(p - GetSliceGeometry(i).GetOrigin());
+
+      if (!OrthancStone::LinearAlgebra::IsNear(d, 0, 0.001 /* tolerance expressed in mm */))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
+                                        "The origins of the slices of a volume image are not regularly spaced");
+      }
+    }
+
+    return spacingZ;
+  }
 }
--- a/Framework/Toolbox/SlicesSorter.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Toolbox/SlicesSorter.h	Thu May 16 09:11:14 2019 +0200
@@ -21,10 +21,13 @@
 
 #pragma once
 
-#include "Slice.h"
+#include "CoordinateSystem3D.h"
+
+#include <Core/IDynamicObject.h>
 
 namespace OrthancStone
 {
+  // TODO - Rename this as "PlanesSorter"
   class SlicesSorter : public boost::noncopyable
   {
   private:
@@ -36,6 +39,16 @@
     Slices  slices_;
     bool    hasNormal_;
     
+    const SliceWithDepth& GetSlice(size_t i) const;
+    
+    void SetNormal(const Vector& normal);
+    
+    void SortInternal();
+
+    void FilterNormal(const Vector& normal);
+    
+    bool SelectNormal(Vector& normal) const;
+
   public:
     SlicesSorter() : hasNormal_(false)
     {
@@ -48,24 +61,35 @@
       slices_.reserve(count);
     }
 
-    void AddSlice(Slice* slice);  // Takes ownership
+    void AddSlice(const CoordinateSystem3D& plane)
+    {
+      AddSlice(plane, NULL);
+    }
 
-    size_t GetSliceCount() const
+    void AddSlice(const CoordinateSystem3D& plane,
+                  Orthanc::IDynamicObject* payload);  // Takes ownership
+
+    size_t GetSlicesCount() const
     {
       return slices_.size();
     }
 
-    const Slice& GetSlice(size_t i) const;
+    const CoordinateSystem3D& GetSliceGeometry(size_t i) const;
 
-    void SetNormal(const Vector& normal);
+    bool HasSlicePayload(size_t i) const;
     
-    void Sort();
+    const Orthanc::IDynamicObject& GetSlicePayload(size_t i) const;
 
-    void FilterNormal(const Vector& normal);
+    // WARNING - Apply the sorting algorithm can reduce the number of
+    // slices. This is notably the case if all the slices are not
+    // parallel to the reference normal that will be selected.
+    bool Sort();
     
-    bool SelectNormal(Vector& normal) const;
+    bool LookupClosestSlice(size_t& index,
+                            double& distance,
+                            const CoordinateSystem3D& slice) const;
 
-    bool LookupSlice(size_t& index,
-                     const CoordinateSystem3D& slice) const;
+    // WARNING - The slices must have been sorted before calling this method
+    double ComputeSpacingBetweenSlices() const;
   };
 }
--- a/Framework/Viewport/IMouseTracker.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Viewport/IMouseTracker.h	Thu May 16 09:11:14 2019 +0200
@@ -62,7 +62,5 @@
     virtual void MouseMove(int x, 
                            int y,
                            const std::vector<Touch>& displayTouches) = 0;
-
-    virtual bool IsTouchTracker() const {return false;}
   };
 }
--- a/Framework/Viewport/IViewport.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Viewport/IViewport.h	Thu May 16 09:11:14 2019 +0200
@@ -35,7 +35,7 @@
   class IViewport : public IObservable
   {
   public:
-    typedef OriginMessage<MessageType_ViewportChanged, IViewport> ViewportChangedMessage;
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, ViewportChangedMessage, IViewport);
 
     IViewport(MessageBroker& broker) :
       IObservable(broker)
@@ -89,7 +89,7 @@
     // TODO Why should this be virtual?
     virtual void NotifyContentChanged()
     {
-      EmitMessage(ViewportChangedMessage(*this));
+      BroadcastMessage(ViewportChangedMessage(*this));
     }
   };
 }
--- a/Framework/Volumes/ISlicedVolume.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Volumes/ISlicedVolume.h	Thu May 16 09:11:14 2019 +0200
@@ -29,14 +29,16 @@
   class ISlicedVolume : public IObservable
   {
   public:
-    typedef OriginMessage<MessageType_SlicedVolume_ContentChanged, ISlicedVolume> ContentChangedMessage;
-    typedef OriginMessage<MessageType_SlicedVolume_GeometryError, ISlicedVolume> GeometryErrorMessage;
-    typedef OriginMessage<MessageType_SlicedVolume_GeometryReady, ISlicedVolume> GeometryReadyMessage;
-    typedef OriginMessage<MessageType_SlicedVolume_VolumeReady, ISlicedVolume> VolumeReadyMessage;
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, ContentChangedMessage, ISlicedVolume);
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, GeometryErrorMessage, ISlicedVolume);
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, GeometryReadyMessage, ISlicedVolume);
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, VolumeReadyMessage, ISlicedVolume);
 
-    class SliceContentChangedMessage :
-      public OriginMessage<MessageType_SlicedVolume_SliceContentChanged, ISlicedVolume>
+
+    class SliceContentChangedMessage : public OriginMessage<ISlicedVolume>
     {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+      
     private:
       size_t        sliceIndex_;
       const Slice&  slice_;
--- a/Framework/Volumes/IVolumeLoader.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Volumes/IVolumeLoader.h	Thu May 16 09:11:14 2019 +0200
@@ -28,9 +28,9 @@
   class IVolumeLoader : public IObservable
   {
   public:
-    typedef OriginMessage<MessageType_VolumeLoader_GeometryReady, IVolumeLoader> GeometryReadyMessage;
-    typedef OriginMessage<MessageType_VolumeLoader_GeometryError, IVolumeLoader> GeometryErrorMessage;
-    typedef OriginMessage<MessageType_VolumeLoader_ContentChanged, IVolumeLoader> ContentChangedMessage;
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, GeometryReadyMessage, IVolumeLoader);
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, GeometryErrorMessage, IVolumeLoader);
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, ContentChangedMessage, IVolumeLoader);
 
     IVolumeLoader(MessageBroker& broker) :
       IObservable(broker)
--- a/Framework/Volumes/ImageBuffer3D.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Volumes/ImageBuffer3D.cpp	Thu May 16 09:11:14 2019 +0200
@@ -118,8 +118,9 @@
   {
     LinearAlgebra::AssignVector(voxelDimensions_, 1, 1, 1);
 
-    LOG(INFO) << "Created an image of "
-              << (GetEstimatedMemorySize() / (1024ll * 1024ll)) << "MB";
+    LOG(INFO) << "Created a 3D image of size " << width << "x" << height
+              << "x" << depth << " in " << Orthanc::EnumerationToString(format)
+              << " (" << (GetEstimatedMemorySize() / (1024ll * 1024ll)) << "MB)";
   }
 
 
--- a/Framework/Volumes/StructureSetLoader.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Volumes/StructureSetLoader.cpp	Thu May 16 09:11:14 2019 +0200
@@ -44,7 +44,7 @@
     MessagingToolbox::ConvertDataset(slice, dataset);
     structureSet_->AddReferencedSlice(slice);
 
-    EmitMessage(ContentChangedMessage(*this));
+    BroadcastMessage(ContentChangedMessage(*this));
   }
   
 
@@ -63,7 +63,7 @@
                             new Callable<StructureSetLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &StructureSetLoader::OnLookupCompleted));
     }
 
-    EmitMessage(GeometryReadyMessage(*this));
+    BroadcastMessage(GeometryReadyMessage(*this));
   }
 
   
--- a/Framework/Volumes/VolumeReslicer.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Volumes/VolumeReslicer.cpp	Thu May 16 09:11:14 2019 +0200
@@ -655,8 +655,7 @@
                                     float rescaleSlope,
                                     float rescaleIntercept)
   {
-    if (windowing == ImageWindowing_Custom ||
-        windowing == ImageWindowing_Default)
+    if (windowing == ImageWindowing_Custom)
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
--- a/Framework/Widgets/SliceViewerWidget.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Widgets/SliceViewerWidget.cpp	Thu May 16 09:11:14 2019 +0200
@@ -527,7 +527,7 @@
       InvalidateAllLayers();   // TODO Removing this line avoid loading twice the image in WASM
     }
 
-    EmitMessage(DisplayedSliceMessage(*this, displayedSlice));
+    BroadcastMessage(DisplayedSliceMessage(*this, displayedSlice));
   }
 
 
@@ -541,7 +541,7 @@
       changedLayers_[i] = true;
       //layers_[i]->ScheduleLayerCreation(plane_);
     }
-    EmitMessage(GeometryChangedMessage(*this));
+    BroadcastMessage(GeometryChangedMessage(*this));
   }
   
 
@@ -579,7 +579,7 @@
       InvalidateLayer(index);
     }
     
-    EmitMessage(SliceViewerWidget::ContentChangedMessage(*this));
+    BroadcastMessage(SliceViewerWidget::ContentChangedMessage(*this));
   }
   
 
@@ -594,7 +594,7 @@
       }
     }
     
-    EmitMessage(SliceViewerWidget::ContentChangedMessage(*this));
+    BroadcastMessage(SliceViewerWidget::ContentChangedMessage(*this));
   }
   
   
@@ -607,7 +607,7 @@
       UpdateLayer(index, message.CreateRenderer(), message.GetSlice());
     }
     
-    EmitMessage(SliceViewerWidget::ContentChangedMessage(*this));
+    BroadcastMessage(SliceViewerWidget::ContentChangedMessage(*this));
   }
 
 
@@ -621,7 +621,7 @@
       // TODO
       //UpdateLayer(index, new SliceOutlineRenderer(slice), slice);
 
-      EmitMessage(SliceViewerWidget::ContentChangedMessage(*this));
+      BroadcastMessage(SliceViewerWidget::ContentChangedMessage(*this));
     }
   }
 
--- a/Framework/Widgets/SliceViewerWidget.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/Widgets/SliceViewerWidget.h	Thu May 16 09:11:14 2019 +0200
@@ -36,12 +36,15 @@
     public IObservable
   {
   public:
-    typedef OriginMessage<MessageType_Widget_GeometryChanged, SliceViewerWidget> GeometryChangedMessage;
-    typedef OriginMessage<MessageType_Widget_ContentChanged, SliceViewerWidget> ContentChangedMessage;
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, GeometryChangedMessage, SliceViewerWidget);
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, ContentChangedMessage, SliceViewerWidget);
+
 
     // TODO - Use this message in ReferenceLineSource
-    class DisplayedSliceMessage : public OriginMessage<MessageType_SliceViewerWidget_DisplayedSlice, SliceViewerWidget>
+    class DisplayedSliceMessage : public OriginMessage<SliceViewerWidget>
     {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+      
     private:
       const Slice& slice_;
 
--- a/Framework/dev.h	Tue May 14 18:24:12 2019 +0200
+++ b/Framework/dev.h	Thu May 16 09:11:14 2019 +0200
@@ -110,25 +110,25 @@
     {
       assert(&message.GetOrigin() == &loader_);
 
-      if (loader_.GetSliceCount() == 0)
+      if (loader_.GetSlicesCount() == 0)
       {
         LOG(ERROR) << "Empty volume image";
-        EmitMessage(ISlicedVolume::GeometryErrorMessage(*this));
+        BroadcastMessage(ISlicedVolume::GeometryErrorMessage(*this));
         return;
       }
 
-      for (size_t i = 1; i < loader_.GetSliceCount(); i++)
+      for (size_t i = 1; i < loader_.GetSlicesCount(); i++)
       {
         if (!IsCompatible(loader_.GetSlice(0), loader_.GetSlice(i)))
         {
-          EmitMessage(ISlicedVolume::GeometryErrorMessage(*this));
+          BroadcastMessage(ISlicedVolume::GeometryErrorMessage(*this));
           return;
         }
       }
 
       double spacingZ;
 
-      if (loader_.GetSliceCount() > 1)
+      if (loader_.GetSlicesCount() > 1)
       {
         spacingZ = GetDistance(loader_.GetSlice(0), loader_.GetSlice(1));
       }
@@ -139,13 +139,13 @@
         spacingZ = 1;
       }
 
-      for (size_t i = 1; i < loader_.GetSliceCount(); i++)
+      for (size_t i = 1; i < loader_.GetSlicesCount(); i++)
       {
         if (!LinearAlgebra::IsNear(spacingZ, GetDistance(loader_.GetSlice(i - 1), loader_.GetSlice(i)),
                                    0.001 /* this is expressed in mm */))
         {
           LOG(ERROR) << "The distance between successive slices is not constant in a volume image";
-          EmitMessage(ISlicedVolume::GeometryErrorMessage(*this));
+          BroadcastMessage(ISlicedVolume::GeometryErrorMessage(*this));
           return;
         }
       }
@@ -154,16 +154,16 @@
       unsigned int height = loader_.GetSlice(0).GetHeight();
       Orthanc::PixelFormat format = loader_.GetSlice(0).GetConverter().GetExpectedPixelFormat();
       LOG(INFO) << "Creating a volume image of size " << width << "x" << height
-                << "x" << loader_.GetSliceCount() << " in " << Orthanc::EnumerationToString(format);
+                << "x" << loader_.GetSlicesCount() << " in " << Orthanc::EnumerationToString(format);
 
-      image_.reset(new ImageBuffer3D(format, width, height, static_cast<unsigned int>(loader_.GetSliceCount()), computeRange_));
+      image_.reset(new ImageBuffer3D(format, width, height, static_cast<unsigned int>(loader_.GetSlicesCount()), computeRange_));
       image_->SetAxialGeometry(loader_.GetSlice(0).GetGeometry());
       image_->SetVoxelDimensions(loader_.GetSlice(0).GetPixelSpacingX(),
                                  loader_.GetSlice(0).GetPixelSpacingY(), spacingZ);
       image_->Clear();
 
-      downloadStack_.reset(new DownloadStack(static_cast<unsigned int>(loader_.GetSliceCount())));
-      pendingSlices_ = loader_.GetSliceCount();
+      downloadStack_.reset(new DownloadStack(static_cast<unsigned int>(loader_.GetSlicesCount())));
+      pendingSlices_ = loader_.GetSlicesCount();
 
       for (unsigned int i = 0; i < 4; i++)  // Limit to 4 simultaneous downloads
       {
@@ -172,7 +172,7 @@
 
       // TODO Check the DicomFrameConverter are constant
 
-      EmitMessage(ISlicedVolume::GeometryReadyMessage(*this));
+      BroadcastMessage(ISlicedVolume::GeometryReadyMessage(*this));
     }
 
 
@@ -181,7 +181,7 @@
       assert(&message.GetOrigin() == &loader_);
 
       LOG(ERROR) << "Unable to download a volume image";
-      EmitMessage(ISlicedVolume::GeometryErrorMessage(*this));
+      BroadcastMessage(ISlicedVolume::GeometryErrorMessage(*this));
     }
 
 
@@ -194,12 +194,12 @@
         Orthanc::ImageProcessing::Copy(writer.GetAccessor(), message.GetImage());
       }
 
-      EmitMessage(ISlicedVolume::SliceContentChangedMessage
+      BroadcastMessage(ISlicedVolume::SliceContentChangedMessage
                   (*this, message.GetSliceIndex(), message.GetSlice()));
 
       if (pendingSlices_ == 1)
       {
-        EmitMessage(ISlicedVolume::VolumeReadyMessage(*this));
+        BroadcastMessage(ISlicedVolume::VolumeReadyMessage(*this));
         pendingSlices_ = 0;
       }
       else if (pendingSlices_ > 1)
@@ -263,9 +263,9 @@
       loader_.ScheduleLoadFrame(instanceId, frame);
     }
 
-    virtual size_t GetSliceCount() const
+    virtual size_t GetSlicesCount() const
     {
-      return loader_.GetSliceCount();
+      return loader_.GetSlicesCount();
     }
 
     virtual const Slice& GetSlice(size_t index) const
@@ -317,7 +317,7 @@
     {
       double thickness;
 
-      size_t n = volume.GetSliceCount();
+      size_t n = volume.GetSlicesCount();
       if (n > 1)
       {
         const Slice& a = volume.GetSlice(0);
@@ -349,7 +349,7 @@
 
       width_ = axial.GetWidth();
       height_ = axial.GetHeight();
-      depth_ = volume.GetSliceCount();
+      depth_ = volume.GetSlicesCount();
 
       pixelSpacingX_ = axial.GetPixelSpacingX();
       pixelSpacingY_ = axial.GetPixelSpacingY();
@@ -364,7 +364,7 @@
       double axialThickness = ComputeAxialThickness(volume);
 
       width_ = axial.GetWidth();
-      height_ = static_cast<unsigned int>(volume.GetSliceCount());
+      height_ = static_cast<unsigned int>(volume.GetSlicesCount());
       depth_ = axial.GetHeight();
 
       pixelSpacingX_ = axial.GetPixelSpacingX();
@@ -372,7 +372,7 @@
       sliceThickness_ = axial.GetPixelSpacingY();
 
       Vector origin = axial.GetGeometry().GetOrigin();
-      origin += (static_cast<double>(volume.GetSliceCount() - 1) *
+      origin += (static_cast<double>(volume.GetSlicesCount() - 1) *
                 axialThickness * axial.GetGeometry().GetNormal());
 
       reference_ = CoordinateSystem3D(origin,
@@ -386,7 +386,7 @@
       double axialThickness = ComputeAxialThickness(volume);
 
       width_ = axial.GetHeight();
-      height_ = static_cast<unsigned int>(volume.GetSliceCount());
+      height_ = static_cast<unsigned int>(volume.GetSlicesCount());
       depth_ = axial.GetWidth();
 
       pixelSpacingX_ = axial.GetPixelSpacingY();
@@ -394,7 +394,7 @@
       sliceThickness_ = axial.GetPixelSpacingX();
 
       Vector origin = axial.GetGeometry().GetOrigin();
-      origin += (static_cast<double>(volume.GetSliceCount() - 1) *
+      origin += (static_cast<double>(volume.GetSlicesCount() - 1) *
                 axialThickness * axial.GetGeometry().GetNormal());
 
       reference_ = CoordinateSystem3D(origin,
@@ -406,7 +406,7 @@
     VolumeImageGeometry(const OrthancVolumeImage& volume,
                         VolumeProjection projection)
     {
-      if (volume.GetSliceCount() == 0)
+      if (volume.GetSlicesCount() == 0)
       {
         throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
       }
@@ -432,7 +432,7 @@
       }
     }
 
-    size_t GetSliceCount() const
+    size_t GetSlicesCount() const
     {
       return depth_;
     }
@@ -540,21 +540,21 @@
       coronalGeometry_.reset(new VolumeImageGeometry(volume_, VolumeProjection_Coronal));
       sagittalGeometry_.reset(new VolumeImageGeometry(volume_, VolumeProjection_Sagittal));
 
-      EmitMessage(IVolumeSlicer::GeometryReadyMessage(*this));
+      BroadcastMessage(IVolumeSlicer::GeometryReadyMessage(*this));
     }
 
     void OnGeometryError(const ISlicedVolume::GeometryErrorMessage& message)
     {
       assert(&message.GetOrigin() == &volume_);
 
-      EmitMessage(IVolumeSlicer::GeometryErrorMessage(*this));
+      BroadcastMessage(IVolumeSlicer::GeometryErrorMessage(*this));
     }
 
     void OnContentChanged(const ISlicedVolume::ContentChangedMessage& message)
     {
       assert(&message.GetOrigin() == &volume_);
 
-      EmitMessage(IVolumeSlicer::ContentChangedMessage(*this));
+      BroadcastMessage(IVolumeSlicer::ContentChangedMessage(*this));
     }
 
     void OnSliceContentChanged(const ISlicedVolume::SliceContentChangedMessage& message)
@@ -564,7 +564,7 @@
       //IVolumeSlicer::OnSliceContentChange(slice);
 
       // TODO Improve this?
-      EmitMessage(IVolumeSlicer::ContentChangedMessage(*this));
+      BroadcastMessage(IVolumeSlicer::ContentChangedMessage(*this));
     }
 
     const VolumeImageGeometry& GetProjectionGeometry(VolumeProjection projection)
@@ -697,14 +697,14 @@
 
           RendererFactory factory(*frame, *slice, isFullQuality);
 
-          EmitMessage(IVolumeSlicer::LayerReadyMessage(*this, factory, slice->GetGeometry()));
+          BroadcastMessage(IVolumeSlicer::LayerReadyMessage(*this, factory, slice->GetGeometry()));
           return;
         }
       }
 
       // Error
       CoordinateSystem3D slice;
-      EmitMessage(IVolumeSlicer::LayerErrorMessage(*this, slice));
+      BroadcastMessage(IVolumeSlicer::LayerErrorMessage(*this, slice));
     }
   };
 
@@ -728,7 +728,7 @@
           dynamic_cast<const OrthancVolumeImage&>(message.GetOrigin());
 
         slices_.reset(new VolumeImageGeometry(image, projection_));
-        SetSlice(slices_->GetSliceCount() / 2);
+        SetSlice(slices_->GetSlicesCount() / 2);
 
         widget_.FitContent();
       }
@@ -817,7 +817,7 @@
       return slices_.get() != NULL;
     }
 
-    size_t GetSliceCount() const
+    size_t GetSlicesCount() const
     {
       if (slices_.get() == NULL)
       {
@@ -825,7 +825,7 @@
       }
       else
       {
-        return slices_->GetSliceCount();
+        return slices_->GetSlicesCount();
       }
     }
 
@@ -840,9 +840,9 @@
           slice = 0;
         }
 
-        if (slice >= static_cast<int>(slices_->GetSliceCount()))
+        if (slice >= static_cast<int>(slices_->GetSlicesCount()))
         {
-          slice = static_cast<unsigned int>(slices_->GetSliceCount()) - 1;
+          slice = static_cast<unsigned int>(slices_->GetSlicesCount()) - 1;
         }
 
         if (slice != static_cast<int>(slice_))
@@ -906,7 +906,7 @@
       IVolumeSlicer(broker),
       otherPlane_(otherPlane)
     {
-      EmitMessage(IVolumeSlicer::GeometryReadyMessage(*this));
+      BroadcastMessage(IVolumeSlicer::GeometryReadyMessage(*this));
     }
 
     virtual bool GetExtent(std::vector<Vector>& points,
@@ -929,7 +929,7 @@
                                                viewportSlice.GetOrigin(), viewportSlice.GetNormal()))
       {
         // The two slice are parallel, don't try and display the intersection
-        EmitMessage(IVolumeSlicer::LayerErrorMessage(*this, reference.GetGeometry()));
+        BroadcastMessage(IVolumeSlicer::LayerErrorMessage(*this, reference.GetGeometry()));
       }
       else
       {
@@ -945,12 +945,12 @@
                                                  extent.GetX2(), extent.GetY2()))
         {
           RendererFactory factory(x1, y1, x2, y2, slice);
-          EmitMessage(IVolumeSlicer::LayerReadyMessage(*this, factory, reference.GetGeometry()));
+          BroadcastMessage(IVolumeSlicer::LayerReadyMessage(*this, factory, reference.GetGeometry()));
         }
         else
         {
           // Error: Parallel slices
-          EmitMessage(IVolumeSlicer::LayerErrorMessage(*this, reference.GetGeometry()));
+          BroadcastMessage(IVolumeSlicer::LayerErrorMessage(*this, reference.GetGeometry()));
         }
       }
     }
--- a/Platforms/Generic/DelayedCallCommand.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Platforms/Generic/DelayedCallCommand.cpp	Thu May 16 09:11:14 2019 +0200
@@ -36,7 +36,7 @@
     callback_(callback),
     payload_(payload),
     context_(context),
-    expirationTimePoint_(boost::chrono::system_clock::now() + boost::chrono::milliseconds(timeoutInMs)),
+    expirationTimePoint_(boost::posix_time::microsec_clock::local_time() + boost::posix_time::milliseconds(timeoutInMs)),
     timeoutInMs_(timeoutInMs)
   {
   }
@@ -44,9 +44,9 @@
 
   void DelayedCallCommand::Execute()
   {
-    while (boost::chrono::system_clock::now() < expirationTimePoint_)
+    while (boost::posix_time::microsec_clock::local_time() < expirationTimePoint_)
     {
-      boost::this_thread::sleep_for(boost::chrono::milliseconds(1));
+      boost::this_thread::sleep(boost::posix_time::milliseconds(1));
     }
   }
 
--- a/Platforms/Generic/DelayedCallCommand.h	Tue May 14 18:24:12 2019 +0200
+++ b/Platforms/Generic/DelayedCallCommand.h	Thu May 16 09:11:14 2019 +0200
@@ -27,7 +27,8 @@
 #include "../../Framework/Messages/IObservable.h"
 #include "../../Framework/Messages/ICallable.h"
 #include "../../Applications/Generic/NativeStoneApplicationContext.h"
-#include <boost/chrono.hpp>
+
+#include <boost/date_time/posix_time/posix_time.hpp>
 
 namespace OrthancStone
 {
@@ -37,7 +38,7 @@
     std::auto_ptr<MessageHandler<IDelayedCallExecutor::TimeoutMessage> >  callback_;
     std::auto_ptr<Orthanc::IDynamicObject>  payload_;
     NativeStoneApplicationContext&          context_;
-    boost::chrono::system_clock::time_point expirationTimePoint_;
+    boost::posix_time::ptime                expirationTimePoint_;
     unsigned int                            timeoutInMs_;
 
   public:
--- a/Platforms/Wasm/Defaults.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Platforms/Wasm/Defaults.cpp	Thu May 16 09:11:14 2019 +0200
@@ -5,9 +5,12 @@
 #include <Framework/dev.h>
 #include "Framework/Widgets/TestCairoWidget.h"
 #include <Framework/Viewport/WidgetViewport.h>
+#include <Applications/Wasm/StartupParametersBuilder.h>
+#include <Platforms/Wasm/WasmPlatformApplicationAdapter.h>
+#include <Core/Logging.h>
+
 #include <algorithm>
-#include "Applications/Wasm/StartupParametersBuilder.h"
-#include "Platforms/Wasm/WasmPlatformApplicationAdapter.h"
+
 
 static unsigned int width_ = 0;
 static unsigned int height_ = 0;
@@ -88,6 +91,10 @@
 
     printf("StartWasmApplication\n");
 
+    Orthanc::Logging::SetErrorWarnInfoTraceLoggingFunctions(
+      stone_console_error, stone_console_warning,
+      stone_console_info, stone_console_trace);
+
     // recreate a command line from uri arguments and parse it
     boost::program_options::variables_map parameters;
     boost::program_options::options_description options;
@@ -106,6 +113,16 @@
     printf("StartWasmApplication - completed\n");
   }
   
+  bool EMSCRIPTEN_KEEPALIVE WasmIsTraceLevelEnabled()
+  {
+    return Orthanc::Logging::IsTraceLevelEnabled();
+  }
+
+  bool EMSCRIPTEN_KEEPALIVE WasmIsInfoLevelEnabled()
+  {
+    return Orthanc::Logging::IsInfoLevelEnabled();
+  }
+  
   void EMSCRIPTEN_KEEPALIVE WasmDoAnimation()
   {
     for (auto viewport : viewports_) {
--- a/Platforms/Wasm/Defaults.h	Tue May 14 18:24:12 2019 +0200
+++ b/Platforms/Wasm/Defaults.h	Thu May 16 09:11:14 2019 +0200
@@ -18,7 +18,11 @@
   extern void ScheduleWebViewportRedrawFromCpp(ViewportHandle cppViewportHandle);
   extern void UpdateStoneApplicationStatusFromCppWithString(const char* statusUpdateMessage);
   extern void UpdateStoneApplicationStatusFromCppWithSerializedMessage(const char* statusUpdateMessage);
-  
+  extern void stone_console_error(const char*);
+  extern void stone_console_warning(const char*);
+  extern void stone_console_info(const char*);
+  extern void stone_console_trace(const char*);
+
   // C++ methods accessible from JS
   extern void EMSCRIPTEN_KEEPALIVE CreateWasmApplication(ViewportHandle cppViewportHandle);
   extern void EMSCRIPTEN_KEEPALIVE SetStartupParameter(const char* keyc, const char* value);
--- a/Platforms/Wasm/default-library.js	Tue May 14 18:24:12 2019 +0200
+++ b/Platforms/Wasm/default-library.js	Thu May 16 09:11:14 2019 +0200
@@ -1,21 +1,49 @@
 // this file contains the JS method you want to expose to C++ code
 
 mergeInto(LibraryManager.library, {
+
   ScheduleWebViewportRedrawFromCpp: function(cppViewportHandle) {
     window.ScheduleWebViewportRedraw(cppViewportHandle);
   },
+
   CreateWasmViewportFromCpp: function(htmlCanvasId) {
     return window.CreateWasmViewport(htmlCanvasId);
   },
+
   // each time the StoneApplication updates its status, it may signal it 
   // through this method. i.e, to change the status of a button in the web interface
   UpdateStoneApplicationStatusFromCppWithString: function(statusUpdateMessage) {
     var statusUpdateMessage_ = UTF8ToString(statusUpdateMessage);
     window.UpdateWebApplicationWithString(statusUpdateMessage_);
   },
+
   // same, but with a serialized message
   UpdateStoneApplicationStatusFromCppWithSerializedMessage: function(statusUpdateMessage) {
     var statusUpdateMessage_ = UTF8ToString(statusUpdateMessage);
     window.UpdateWebApplicationWithSerializedMessage(statusUpdateMessage_);
+  },
+
+  // These functions are called from C++ (through an extern declaration) 
+  // and call the standard logger that, here, routes to the console.
+
+  stone_console_error : function(message) {
+    var text = UTF8ToString(message);
+    window.errorFromCpp(text);
+  },
+
+  stone_console_warning : function(message) {
+    var text = UTF8ToString(message);
+    window.warningFromCpp(text);
+  },
+
+  stone_console_info: function(message) {
+    var text = UTF8ToString(message);
+    window.infoFromCpp(text);
+  },
+  
+  stone_console_trace : function(message) {
+    var text = UTF8ToString(message);
+    window.debugFromCpp(text);
   }
+
 });
--- a/Platforms/Wasm/logger.ts	Tue May 14 18:24:12 2019 +0200
+++ b/Platforms/Wasm/logger.ts	Thu May 16 09:11:14 2019 +0200
@@ -1,90 +1,111 @@
-export enum LogSource {
-  Cpp,
-  Typescript
-}
-
-export class StandardConsoleLogger {
-  public showSource: boolean = true;
-
-  public debug(...args: any[]): void {
-    this._debug(LogSource.Typescript, ...args);
-  }
-
-  public info(...args: any[]): void {
-    this._info(LogSource.Typescript, ...args);
-  }
-
-  public infoFromCpp(message: string): void {
-    this._info(LogSource.Cpp, message);
-  }
-
-  public warning(...args: any[]): void {
-    this._warning(LogSource.Typescript, ...args);
-  }
-
-  public error(...args: any[]): void {
-    this._error(LogSource.Typescript, ...args);
-  }
-
-  public errorFromCpp(message: string): void {
-    this._error(LogSource.Cpp, message);
-  }
-
-  public _debug(source: LogSource, ...args: any[]): void {
-    var output = this.getOutput(source, args);
-    console.debug(...output);
-  }
-
-  private _info(source: LogSource, ...args: any[]): void {
-    var output = this.getOutput(source, args);
-    console.info(...output);
-  }
-
-  public _warning(source: LogSource, ...args: any[]): void {
-    var output = this.getOutput(source, args);
-    console.warn(...output);
-  }
-
-  public _error(source: LogSource, ...args: any[]): void {
-    var output = this.getOutput(source, args);
-    console.error(...output);
-  }
-
-
-  private getOutput(source: LogSource, args: any[]): any[] {
-    var prefix = this.getPrefix();
-    var prefixAndSource = [];
-
-    if (prefix != null) {
-      prefixAndSource = [prefix];
-    } 
-
-    if (this.showSource) {
-      if (source == LogSource.Typescript) {
-        prefixAndSource = [...prefixAndSource, "TS "];
-      } else if (source == LogSource.Cpp) {
-        prefixAndSource = [...prefixAndSource, "C++"];
-      }
-    }
-
-    if (prefixAndSource.length > 0) {
-      prefixAndSource = [...prefixAndSource, "|"];
-    }
-
-    return [...prefixAndSource, ...args];
-  }
-
-  protected getPrefix(): string {
-    return null;
-  }
-}
-
-export class TimeConsoleLogger extends StandardConsoleLogger {
-  protected getPrefix(): string {
-    let now = new Date();
-    let timeString = now.getHours().toString().padStart(2, "0") + ":" + now.getMinutes().toString().padStart(2, "0") + ":" + now.getSeconds().toString().padStart(2, "0") + "." + now.getMilliseconds().toString().padStart(3, "0");
-    return timeString;
-  }
-}
-
-export var defaultLogger: StandardConsoleLogger = new TimeConsoleLogger();
+export enum LogSource {
+  Cpp,
+  Typescript
+}
+
+export class StandardConsoleLogger {
+  public showSource: boolean = true;
+
+  public debug(...args: any[]): void {
+    this._debug(LogSource.Typescript, ...args);
+  }
+
+  public debugFromCpp(...args: any[]): void {
+    this._debug(LogSource.Cpp, ...args);
+  }
+
+  public info(...args: any[]): void {
+    this._info(LogSource.Typescript, ...args);
+  }
+
+  public infoFromCpp(message: string): void {
+    this._info(LogSource.Cpp, message);
+  }
+
+  public warning(...args: any[]): void {
+    this._warning(LogSource.Typescript, ...args);
+  }
+
+  public warningFromCpp(message: string): void {
+    this._warning(LogSource.Cpp, message);
+  }
+
+  public error(...args: any[]): void {
+    this._error(LogSource.Typescript, ...args);
+  }
+
+  public errorFromCpp(message: string): void {
+    this._error(LogSource.Cpp, message);
+  }
+
+  public _debug(source: LogSource, ...args: any[]): void {
+    if ((<any> window).IsTraceLevelEnabled)
+    {
+      if ((<any> window).IsTraceLevelEnabled())
+      {
+        var output = this.getOutput(source, args);
+        console.debug(...output);
+      }
+    }
+  }
+
+  private _info(source: LogSource, ...args: any[]): void {
+    if ((<any> window).IsInfoLevelEnabled)
+    {
+      if ((<any> window).IsInfoLevelEnabled())
+      {
+        var output = this.getOutput(source, args);
+        console.info(...output);
+      }
+    }
+  }
+
+  public _warning(source: LogSource, ...args: any[]): void {
+    var output = this.getOutput(source, args);
+    console.warn(...output);
+  }
+
+  public _error(source: LogSource, ...args: any[]): void {
+    var output = this.getOutput(source, args);
+    console.error(...output);
+  }
+
+
+  private getOutput(source: LogSource, args: any[]): any[] {
+    var prefix = this.getPrefix();
+    var prefixAndSource = [];
+
+    if (prefix != null) {
+      prefixAndSource = [prefix];
+    } 
+
+    if (this.showSource) {
+      if (source == LogSource.Typescript) {
+        prefixAndSource = [...prefixAndSource, "TS "];
+      } else if (source == LogSource.Cpp) {
+        prefixAndSource = [...prefixAndSource, "C++"];
+      }
+    }
+
+    if (prefixAndSource.length > 0) {
+      prefixAndSource = [...prefixAndSource, "|"];
+    }
+
+    return [...prefixAndSource, ...args];
+  }
+
+  protected getPrefix(): string {
+    return null;
+  }
+}
+
+export class TimeConsoleLogger extends StandardConsoleLogger {
+  protected getPrefix(): string {
+    let now = new Date();
+    let timeString = now.getHours().toString().padStart(2, "0") + ":" + now.getMinutes().toString().padStart(2, "0") + ":" + now.getSeconds().toString().padStart(2, "0") + "." + now.getMilliseconds().toString().padStart(3, "0");
+    return timeString;
+  }
+}
+
+export var defaultLogger: StandardConsoleLogger = new TimeConsoleLogger();
+
--- a/Platforms/Wasm/stone-framework-loader.ts	Tue May 14 18:24:12 2019 +0200
+++ b/Platforms/Wasm/stone-framework-loader.ts	Thu May 16 09:11:14 2019 +0200
@@ -59,6 +59,11 @@
   {
     Logger.defaultLogger.debug('Initializing WebAssembly Module');
 
+    (<any> window).errorFromCpp = function(text:any) { Logger.defaultLogger.errorFromCpp(text); };
+    (<any> window).warningFromCpp = function(text:any) { Logger.defaultLogger.warningFromCpp(text); };
+    (<any> window).infoFromCpp = function(text:any) { Logger.defaultLogger.infoFromCpp(text); };
+    (<any> window).debugFromCpp = function(text:any) { Logger.defaultLogger.debugFromCpp(text); };
+
     // (<any> window).
     (<any> window).StoneFrameworkModule = {
       preRun: [ 
@@ -76,12 +81,6 @@
           callback();
         }
       ],
-      print: function(text : string) {
-        Logger.defaultLogger.infoFromCpp(text);
-      },
-      printErr: function(text : string) {
-        Logger.defaultLogger.errorFromCpp(text);
-      },
       totalDependencies: 0
     };
 
--- a/Platforms/Wasm/wasm-application-runner.ts	Tue May 14 18:24:12 2019 +0200
+++ b/Platforms/Wasm/wasm-application-runner.ts	Thu May 16 09:11:14 2019 +0200
@@ -27,7 +27,8 @@
     WasmDoAnimation();
   }
 
-  setTimeout(DoAnimationThread, 100);  // Update the viewport content every 100ms if need be
+  // Update the viewport content every 100ms if need be
+  setTimeout(DoAnimationThread, 100);  
 }
 
 function GetUriParameters(): Map<string, string> {
@@ -42,10 +43,12 @@
       var tmp = tokens[i].split('=');
       if (tmp.length == 2) {
         result[tmp[0]] = decodeURIComponent(tmp[1]);
+      } else if(tmp.length == 1) {
+        // if there is no '=', we treat ot afterwards as a flag-style param
+        result[tmp[0]] = "";
       }
     }
-
-    return result;
+  return result;
   }
   else {
     return new Map<string, string>();
@@ -65,6 +68,8 @@
 
   for (let key in parameters) {
     if (parameters.hasOwnProperty(key)) {
+      Logger.defaultLogger.debug(
+        `About to call SetStartupParameter("${key}","${parameters[key]}")`);
       SetStartupParameter(key, parameters[key]);
     }
   }
@@ -92,6 +97,8 @@
     CreateCppViewport = (<any> window).StoneFrameworkModule.cwrap('CreateCppViewport', 'number', []);
     ReleaseCppViewport = (<any> window).StoneFrameworkModule.cwrap('ReleaseCppViewport', null, ['number']);
     StartWasmApplication = (<any> window).StoneFrameworkModule.cwrap('StartWasmApplication', null, ['string']);
+    (<any> window).IsTraceLevelEnabled = (<any> window).StoneFrameworkModule.cwrap('WasmIsTraceLevelEnabled', 'boolean', null);
+    (<any> window).IsInfoLevelEnabled = (<any> window).StoneFrameworkModule.cwrap('WasmIsInfoLevelEnabled', 'boolean', null);
 
     (<any> window).WasmWebService_NotifyCachedSuccess = (<any> window).StoneFrameworkModule.cwrap('WasmWebService_NotifyCachedSuccess', null, ['number']);
     (<any> window).WasmWebService_NotifySuccess = (<any> window).StoneFrameworkModule.cwrap('WasmWebService_NotifySuccess', null, ['number', 'string', 'array', 'number', 'number']);
--- a/README.md	Tue May 14 18:24:12 2019 +0200
+++ b/README.md	Thu May 16 09:11:14 2019 +0200
@@ -236,9 +236,22 @@
   url="https://doi.org/10.1007/s10278-018-0082-y"
 }
 
-Various notes to be deleted
+Various notes to be sorted
 ---------------------------
-class BaseCommand : public ICommand
+How to build the newest (2019-04-29) SDL samples under Windows, *inside* a
+folder that is sibling to the orthanc-stone folder: 
+
+```
+cmake -G "Visual Studio 15 2017 Win64" -DMSVC_MULTIPLE_PROCESSES=ON -DENABLE_OPENGL=ON -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="$($pwd)\..\orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ../orthanc-stone/Samples/Sdl
+```
 
-RadiographySceneCommand
-GenericNoArgCommand
+And under Ubuntu (note the /mnt/c/osi/dev/orthanc folder):
+```
+cmake -G "Ninja" -DENABLE_OPENGL=ON -DSTATIC_BUILD=OFF -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="/mnt/c/osi/dev/orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ../orthanc-stone/Samples/Sdl
+```
+
+TODO trackers:
+- text overlay 50% --> ColorTextureLayer 50%
+- angle tracker: draw arcs
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/CMake/FreetypeConfiguration.cmake	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,87 @@
+if (STATIC_BUILD OR NOT USE_SYSTEM_FREETYPE)
+  set(FREETYPE_SOURCES_DIR ${CMAKE_BINARY_DIR}/freetype-2.9.1)
+  set(FREETYPE_URL "http://orthanc.osimis.io/ThirdPartyDownloads/freetype-2.9.1.tar.gz")
+  set(FREETYPE_MD5 "3adb0e35d3c100c456357345ccfa8056")
+
+  DownloadPackage(${FREETYPE_MD5} ${FREETYPE_URL} "${FREETYPE_SOURCES_DIR}")
+
+  include_directories(BEFORE
+    ${FREETYPE_SOURCES_DIR}/include/
+    )
+
+  add_definitions(
+    -DFT2_BUILD_LIBRARY
+    -DFT_CONFIG_OPTION_NO_ASSEMBLER
+    )
+    
+  set(FREETYPE_SOURCES
+    ${FREETYPE_SOURCES_DIR}/src/autofit/autofit.c
+    ${FREETYPE_SOURCES_DIR}/src/base/ftbase.c
+    ${FREETYPE_SOURCES_DIR}/src/base/ftbbox.c
+    ${FREETYPE_SOURCES_DIR}/src/base/ftbdf.c
+    ${FREETYPE_SOURCES_DIR}/src/base/ftbitmap.c
+    ${FREETYPE_SOURCES_DIR}/src/base/ftcid.c
+    ${FREETYPE_SOURCES_DIR}/src/base/ftfstype.c
+    ${FREETYPE_SOURCES_DIR}/src/base/ftgasp.c
+    ${FREETYPE_SOURCES_DIR}/src/base/ftglyph.c
+    ${FREETYPE_SOURCES_DIR}/src/base/ftgxval.c
+    ${FREETYPE_SOURCES_DIR}/src/base/ftinit.c
+    ${FREETYPE_SOURCES_DIR}/src/base/ftmm.c
+    ${FREETYPE_SOURCES_DIR}/src/base/ftotval.c
+    ${FREETYPE_SOURCES_DIR}/src/base/ftpatent.c
+    ${FREETYPE_SOURCES_DIR}/src/base/ftpfr.c
+    ${FREETYPE_SOURCES_DIR}/src/base/ftstroke.c
+    ${FREETYPE_SOURCES_DIR}/src/base/ftsynth.c
+    ${FREETYPE_SOURCES_DIR}/src/base/ftsystem.c
+    ${FREETYPE_SOURCES_DIR}/src/base/fttype1.c
+    ${FREETYPE_SOURCES_DIR}/src/base/ftwinfnt.c
+    ${FREETYPE_SOURCES_DIR}/src/bdf/bdf.c
+    ${FREETYPE_SOURCES_DIR}/src/bzip2/ftbzip2.c
+    ${FREETYPE_SOURCES_DIR}/src/cache/ftcache.c
+    ${FREETYPE_SOURCES_DIR}/src/cff/cff.c
+    ${FREETYPE_SOURCES_DIR}/src/cid/type1cid.c
+    ${FREETYPE_SOURCES_DIR}/src/gzip/ftgzip.c
+    ${FREETYPE_SOURCES_DIR}/src/lzw/ftlzw.c
+    ${FREETYPE_SOURCES_DIR}/src/pcf/pcf.c
+    ${FREETYPE_SOURCES_DIR}/src/pfr/pfr.c
+    ${FREETYPE_SOURCES_DIR}/src/psaux/psaux.c
+    ${FREETYPE_SOURCES_DIR}/src/pshinter/pshinter.c
+    ${FREETYPE_SOURCES_DIR}/src/psnames/psnames.c
+    ${FREETYPE_SOURCES_DIR}/src/raster/raster.c
+    ${FREETYPE_SOURCES_DIR}/src/sfnt/sfnt.c
+    ${FREETYPE_SOURCES_DIR}/src/smooth/smooth.c
+    ${FREETYPE_SOURCES_DIR}/src/truetype/truetype.c
+    ${FREETYPE_SOURCES_DIR}/src/type1/type1.c
+    ${FREETYPE_SOURCES_DIR}/src/type42/type42.c
+    ${FREETYPE_SOURCES_DIR}/src/winfonts/winfnt.c
+    )
+
+  if (CMAKE_SYSTEM_NAME STREQUAL "Windows")
+    list(APPEND FREETYPE_SOURCES
+      ${FREETYPE_SOURCES_DIR}/builds/windows/ftdebug.c
+      )
+  endif()
+
+  foreach(header
+      ${FREETYPE_SOURCES_DIR}/include/freetype/config/ftconfig.h
+      ${FREETYPE_SOURCES_DIR}/include/freetype/config/ftoption.h
+      )
+
+    set_source_files_properties(
+      ${FREETYPE_SOURCES}
+      PROPERTIES OBJECT_DEPENDS ${header}
+      )
+  endforeach()
+
+  source_group(ThirdParty\\Freetype REGULAR_EXPRESSION ${FREETYPE_SOURCES_DIR}/.*)
+
+else()
+  include(FindFreetype)
+
+  if (NOT FREETYPE_FOUND)
+    message(FATAL_ERROR "Please install the libfreetype6-dev package")
+  endif()
+
+  include_directories(${FREETYPE_INCLUDE_DIRS})
+  link_libraries(${FREETYPE_LIBRARIES})
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/CMake/GlewConfiguration.cmake	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,44 @@
+# Stone of Orthanc
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2019 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Affero General Public License
+# as published by the Free Software Foundation, either version 3 of
+# the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+if (STATIC_BUILD OR NOT USE_SYSTEM_GLEW)
+  SET(GLEW_SOURCES_DIR ${CMAKE_BINARY_DIR}/glew-2.1.0)
+  SET(GLEW_URL "http://orthanc.osimis.io/ThirdPartyDownloads/glew-2.1.0.tgz")
+  SET(GLEW_MD5 "b2ab12331033ddfaa50dc39345343980")
+  DownloadPackage(${GLEW_MD5} ${GLEW_URL} "${GLEW_SOURCES_DIR}")
+
+  set(GLEW_SOURCES
+    ${GLEW_SOURCES_DIR}/src/glew.c
+    )
+
+  include_directories(${GLEW_SOURCES_DIR}/include)
+
+  add_definitions(
+    -DGLEW_STATIC=1
+    )
+
+else()
+  include(FindGLEW)
+  if (NOT GLEW_FOUND)
+    message(FATAL_ERROR "Please install the libglew-dev package")
+  endif()
+  
+  include_directories(${GLEW_INCLUDE_DIRS})
+  link_libraries(${GLEW_LIBRARIES})
+endif()
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Tue May 14 18:24:12 2019 +0200
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Thu May 16 09:11:14 2019 +0200
@@ -56,6 +56,27 @@
     message(FATAL_ERROR "Cannot enable SSL in sandboxed environments")
   endif()
 endif()
+
+if (ENABLE_WASM)
+  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_WASM=1)
+else()
+  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()
+
+  add_definitions(-DORTHANC_ENABLE_WASM=0)
+endif()
   
 
 #####################################################################
@@ -67,6 +88,7 @@
 include(FindPkgConfig)
 include(${CMAKE_CURRENT_LIST_DIR}/BoostExtendedConfiguration.cmake)
 include(${CMAKE_CURRENT_LIST_DIR}/CairoConfiguration.cmake)
+include(${CMAKE_CURRENT_LIST_DIR}/FreetypeConfiguration.cmake)
 include(${CMAKE_CURRENT_LIST_DIR}/PixmanConfiguration.cmake)
 
 
@@ -87,21 +109,62 @@
 elseif(ENABLE_SDL)
   message("SDL is enabled")
   include(${CMAKE_CURRENT_LIST_DIR}/SdlConfiguration.cmake)
-  add_definitions(-DORTHANC_ENABLE_NATIVE=1)
-  add_definitions(-DORTHANC_ENABLE_QT=0)
-  add_definitions(-DORTHANC_ENABLE_SDL=1)
+  add_definitions(
+    -DORTHANC_ENABLE_NATIVE=1
+    -DORTHANC_ENABLE_QT=0
+    -DORTHANC_ENABLE_SDL=1
+    )
 elseif(ENABLE_QT)
   message("QT is enabled")
   include(${CMAKE_CURRENT_LIST_DIR}/QtConfiguration.cmake)
-  add_definitions(-DORTHANC_ENABLE_NATIVE=1)
-  add_definitions(-DORTHANC_ENABLE_QT=1)
-  add_definitions(-DORTHANC_ENABLE_SDL=0)
+  add_definitions(
+    -DORTHANC_ENABLE_NATIVE=1
+    -DORTHANC_ENABLE_QT=1
+    -DORTHANC_ENABLE_SDL=0
+    )
 else()
   message("SDL and QT are both disabled")
   unset(USE_SYSTEM_SDL CACHE)
-  add_definitions(-DORTHANC_ENABLE_SDL=0)
-  add_definitions(-DORTHANC_ENABLE_QT=0)
-  add_definitions(-DORTHANC_ENABLE_NATIVE=0)
+  add_definitions(
+    -DORTHANC_ENABLE_SDL=0
+    -DORTHANC_ENABLE_QT=0
+    -DORTHANC_ENABLE_NATIVE=0
+    )
+endif()
+
+
+if (ENABLE_OPENGL AND CMAKE_SYSTEM_NAME STREQUAL "Windows")
+  include(${CMAKE_CURRENT_LIST_DIR}/GlewConfiguration.cmake)
+  add_definitions(
+    -DORTHANC_ENABLE_GLEW=1
+    )
+else()
+  add_definitions(
+    -DORTHANC_ENABLE_GLEW=0
+    )
+endif()
+
+
+if (ENABLE_OPENGL)
+  if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+    # If including "FindOpenGL.cmake" using Emscripten (targeting
+    # WebAssembly), the "OPENGL_LIBRARIES" value incorrectly includes
+    # the "nul" library, which leads to warning message in Emscripten:
+    # 'shared:WARNING: emcc: cannot find library "nul"'.
+    include(FindOpenGL)
+    if (NOT OPENGL_FOUND)
+      message(FATAL_ERROR "Cannot find OpenGL on your system")
+    endif()
+
+    link_libraries(${OPENGL_LIBRARIES})
+  endif()
+
+  add_definitions(
+    -DGL_GLEXT_PROTOTYPES=1
+    -DORTHANC_ENABLE_OPENGL=1
+    )
+else()
+  add_definitions(-DORTHANC_ENABLE_OPENGL=0)  
 endif()
 
 
@@ -124,6 +187,8 @@
   add_definitions(-DCHECK_OBSERVERS_MESSAGES)
 endif()
 
+
+
 #####################################################################
 ## Embed the colormaps into the binaries
 #####################################################################
@@ -200,10 +265,11 @@
       )
     if (ENABLE_SDL)
       list(APPEND APPLICATIONS_SOURCES
-        ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlStoneApplicationRunner.cpp
+        ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlCairoSurface.cpp
         ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlEngine.cpp
-        ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlCairoSurface.cpp
+        ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlOpenGLWindow.cpp
         ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlOrthancSurface.cpp
+        ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlStoneApplicationRunner.cpp
         ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlWindow.cpp
         )
     endif()
@@ -243,6 +309,32 @@
   #${ORTHANC_STONE_ROOT}/Framework/Layers/SeriesFrameRendererFactory.cpp
   #${ORTHANC_STONE_ROOT}/Framework/Layers/SingleFrameRendererFactory.cpp
 
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/CairoCompositor.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ColorTextureSceneLayer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/FloatTextureSceneLayer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/InfoPanelSceneLayer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoColorTextureRenderer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoFloatTextureRenderer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoInfoPanelRenderer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoPolylineRenderer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoTextRenderer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CompositorHelper.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/FixedPointAligner.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PanSceneTracker.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PointerEvent.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PolylineSceneLayer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/RotateSceneTracker.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Scene2D.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/TextSceneLayer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/TextureBaseSceneLayer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ZoomSceneTracker.cpp
+
+  ${ORTHANC_STONE_ROOT}/Framework/Fonts/FontRenderer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Fonts/Glyph.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Fonts/GlyphAlphabet.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Fonts/GlyphBitmapAlphabet.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Fonts/GlyphTextureAlphabet.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Fonts/TextBoundingBox.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Layers/CircleMeasureTracker.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Layers/ColorFrameRenderer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Layers/DicomSeriesVolumeSlicer.cpp
@@ -273,12 +365,14 @@
   ${ORTHANC_STONE_ROOT}/Framework/SmartLoader.cpp
   ${ORTHANC_STONE_ROOT}/Framework/StoneEnumerations.cpp
   ${ORTHANC_STONE_ROOT}/Framework/StoneException.h
+  ${ORTHANC_STONE_ROOT}/Framework/StoneInitialization.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/AffineTransform2D.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/BaseWebService.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/CoordinateSystem3D.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/DicomFrameConverter.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/DicomStructureSet.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/DownloadStack.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/DynamicBitmap.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/Extent2D.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/FiniteProjectiveCamera.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/GeometryToolbox.cpp
@@ -320,6 +414,7 @@
   ${ORTHANC_STONE_ROOT}/Framework/Widgets/WidgetBase.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Widgets/WorldSceneWidget.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Widgets/ZoomMouseTracker.cpp
+
   ${ORTHANC_STONE_ROOT}/Framework/dev.h
 
   ${ORTHANC_STONE_ROOT}/Framework/Messages/ICallable.h
@@ -343,14 +438,45 @@
 
   # Mandatory components
   ${CAIRO_SOURCES}
+  ${FREETYPE_SOURCES}
   ${PIXMAN_SOURCES}
 
   # Optional components
   ${SDL_SOURCES}
   ${QT_SOURCES}
   ${BOOST_EXTENDED_SOURCES}
+  ${GLEW_SOURCES}
   )
 
+
+if (ENABLE_OPENGL)
+  list(APPEND ORTHANC_STONE_SOURCES
+    ${ORTHANC_STONE_ROOT}/Framework/Fonts/OpenGLTextCoordinates.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/OpenGL/OpenGLProgram.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/OpenGL/OpenGLShader.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/OpenGL/OpenGLTexture.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/OpenGLCompositor.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLAdvancedPolylineRenderer.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLBasicPolylineRenderer.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLColorTextureProgram.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLColorTextureRenderer.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLFloatTextureProgram.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLFloatTextureRenderer.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLInfoPanelRenderer.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLLinesProgram.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLTextProgram.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLTextRenderer.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLTextureProgram.cpp
+    )
+
+  if (ENABLE_WASM)
+    list(APPEND ORTHANC_STONE_SOURCES
+      ${ORTHANC_STONE_ROOT}/Framework/OpenGL/WebAssemblyOpenGLContext.cpp
+      )
+  endif()
+endif()
+
+
 include_directories(${ORTHANC_STONE_ROOT})
 
 
--- a/Resources/CMake/OrthancStoneParameters.cmake	Tue May 14 18:24:12 2019 +0200
+++ b/Resources/CMake/OrthancStoneParameters.cmake	Thu May 16 09:11:14 2019 +0200
@@ -27,9 +27,10 @@
 
 set(ENABLE_DCMTK OFF)
 set(ENABLE_GOOGLE_TEST ON)
+set(ENABLE_JPEG ON)
+set(ENABLE_OPENSSL_ENGINES ON)
+set(ENABLE_PNG ON)
 set(ENABLE_SQLITE OFF)
-set(ENABLE_JPEG ON)
-set(ENABLE_PNG ON)
 set(ENABLE_ZLIB ON)
 set(HAS_EMBEDDED_RESOURCES ON)
 
@@ -40,6 +41,8 @@
 
 # Advanced parameters to fine-tune linking against system libraries
 set(USE_SYSTEM_CAIRO ON CACHE BOOL "Use the system version of Cairo")
+set(USE_SYSTEM_FREETYPE ON CACHE BOOL "Use the system version of Freetype")
+set(USE_SYSTEM_GLEW ON CACHE BOOL "Use the system version of glew (for Windows only)")
 set(USE_SYSTEM_PIXMAN ON CACHE BOOL "Use the system version of Pixman")
 set(USE_SYSTEM_SDL ON CACHE BOOL "Use the system version of SDL2")
 
@@ -49,3 +52,5 @@
 ## the Stone of Orthanc
 #####################################################################
 
+set(ENABLE_OPENGL ON CACHE INTERNAL "Enable support of OpenGL")
+set(ENABLE_WASM OFF CACHE INTERNAL "Enable support of WebAssembly")
--- a/Resources/CMake/SdlConfiguration.cmake	Tue May 14 18:24:12 2019 +0200
+++ b/Resources/CMake/SdlConfiguration.cmake	Thu May 16 09:11:14 2019 +0200
@@ -21,7 +21,6 @@
   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)
--- a/Resources/CodeGeneration/stonegentool.py	Tue May 14 18:24:12 2019 +0200
+++ b/Resources/CodeGeneration/stonegentool.py	Thu May 16 09:11:14 2019 +0200
@@ -186,6 +186,7 @@
   RegisterTemplateFunction(template,NeedsCppConstruction)
   RegisterTemplateFunction(template, DefaultValueToTs)
   RegisterTemplateFunction(template, DefaultValueToCpp)
+  RegisterTemplateFunction(template, sorted)
   return template
 
 def MakeTemplateFromFile(templateFileName):
@@ -533,6 +534,11 @@
           lineNumber = schemaText.count("\n",0,i) + 1
           raise RuntimeError("Error at line " + str(lineNumber) + " in the schema: colons must be followed by a space or a newline!")
     schema = yaml.load(schemaText)
+    print("*******************************************")
+    print("*******************************************")
+    print(schema["struct EventBase"])
+    print("*******************************************")
+    print("*******************************************")
     return schema
 
 def GetTemplatingDictFromSchemaFilename(fn):
--- a/Resources/CodeGeneration/template.in.h.j2	Tue May 14 18:24:12 2019 +0200
+++ b/Resources/CodeGeneration/template.in.h.j2	Thu May 16 09:11:14 2019 +0200
@@ -394,9 +394,9 @@
 
   struct {{struct['name']}}
   {
-{% if struct %}{% if struct['fields'] %}{% for key in struct['fields']%}    {{CanonToCpp(struct['fields'][key]['type'])}} {{key}};
+{% if struct %}{% if struct['fields'] %}{% for key in sorted(struct['fields']) %}    {{CanonToCpp(struct['fields'][key]['type'])}} {{key}};
 {% endfor %}{% endif %}{% endif %}
-    {{struct['name']}}({% if struct %}{% if struct['fields'] %}{% for key in struct['fields']%}{{CanonToCpp(struct['fields'][key]['type'])}} {{key}} = {% if struct['fields'][key]['defaultValue'] %}{{DefaultValueToCpp(rootName,enums,struct['fields'][key])}} {%else%} {{CanonToCpp(struct['fields'][key]['type'])}}() {%endif%} {{ ", " if not loop.last }}{% endfor %}{% endif %}{% endif %})
+    {{struct['name']}}({% if struct %}{% if struct['fields'] %}{% for key in sorted(struct['fields']) %}{{CanonToCpp(struct['fields'][key]['type'])}} {{key}} = {% if struct['fields'][key]['defaultValue'] %}{{DefaultValueToCpp(rootName,enums,struct['fields'][key])}} {%else%} {{CanonToCpp(struct['fields'][key]['type'])}}() {%endif%} {{ ", " if not loop.last }}{% endfor %}{% endif %}{% endif %})
     {
 {% if struct %}{% if struct['fields'] %}{% for key in struct['fields']%}      this->{{key}} = {{key}};
 {% endfor %}{% endif %}{% endif %}    }
--- a/Resources/CodeGeneration/testCppHandler/main.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/Resources/CodeGeneration/testCppHandler/main.cpp	Thu May 16 09:11:14 2019 +0200
@@ -1,139 +1,139 @@
-#include <string>
-#include <fstream>
-#include <filesystem>
-#include <regex>
-using namespace std;
-namespace fs = std::filesystem;
-
-#include <boost/program_options.hpp>
-using namespace boost::program_options;
-
-#include "VsolMessages_generated.hpp"
-
-/**
-Transforms `str` by replacing occurrences of `oldStr` with `newStr`, using 
-plain text (*not* regular expressions.)
-*/
-static inline void ReplaceInString(
-  string& str,
-  const std::string& oldStr,
-  const std::string& newStr)
-{
-  std::string::size_type pos = 0u;
-  while ((pos = str.find(oldStr, pos)) != std::string::npos) {
-    str.replace(pos, oldStr.length(), newStr);
-    pos += newStr.length();
-  }
-}
-
-string SlurpFile(const string& fileName)
-{
-  ifstream ifs(fileName.c_str(), ios::in | ios::binary | ios::ate);
-
-  ifstream::pos_type fileSize = ifs.tellg();
-  ifs.seekg(0, ios::beg);
-
-  vector<char> bytes(fileSize);
-  ifs.read(bytes.data(), fileSize);
-
-  return string(bytes.data(), fileSize);
-}
-
-class MyHandler : public VsolMessages::IHandler
-{
-public:
-  virtual bool Handle(const VsolMessages::A& value) override
-  {
-    VsolMessages::StoneDumpValue(cout, value);
-    return true;
-  }
-  virtual bool Handle(const VsolMessages::B& value) override
-  {
-    VsolMessages::StoneDumpValue(cout, value);
-    return true;
-  }
-  virtual bool Handle(const VsolMessages::C& value) override
-  {
-    VsolMessages::StoneDumpValue(cout, value);
-    return true;
-  }
-  virtual bool Handle(const VsolMessages::Message1& value) override
-  {
-    VsolMessages::StoneDumpValue(cout, value);
-    return true;
-  }
-  virtual bool Handle(const VsolMessages::Message2& value) override
-  {
-    VsolMessages::StoneDumpValue(cout, value);
-    return true;
-  }
-};
-
-template<typename T>
-void ProcessPath(T filePath)
-{
-  cout << "+--------------------------------------------+\n";
-  cout << "| Processing: " << filePath.path().string() << "\n";
-  cout << "+--------------------------------------------+\n";
-  MyHandler handler;
-  auto contents = SlurpFile(filePath.path().string());
-  VsolMessages::StoneDispatchToHandler(contents, &handler);
-}
-
-int main(int argc, char** argv)
-{
-  try
-  {
-
-    options_description desc("Allowed options");
-    desc.add_options()
-      // First parameter describes option name/short name
-      // The second is parameter to option
-      // The third is description
-      ("help,h", "print usage message")
-      ("pattern,p", value<string>(), "pattern for input")
-      ;
-
-    variables_map vm;
-    store(parse_command_line(argc, argv, desc), vm);
-
-    if (vm.count("help"))
-    {
-      cout << desc << "\n";
-      return 0;
-    }
-
-    notify(vm);
-
-    string pattern = vm["pattern"].as<string>();
-
-    // tranform globbing pattern into regex
-    // we should deal with -, ., *...
-    string regexPatternStr = pattern;
-    cout << "Pattern is: " << regexPatternStr << endl;
-    ReplaceInString(regexPatternStr, "\\", "\\\\");
-    ReplaceInString(regexPatternStr, "-", "\\-");
-    ReplaceInString(regexPatternStr, ".", "\\.");
-    ReplaceInString(regexPatternStr, "*", ".*");
-    ReplaceInString(regexPatternStr, "?", ".");
-    cout << "Corresponding regex is: " << regexPatternStr << endl;
-
-    regex regexPattern(regexPatternStr);
-
-    for (auto& p : fs::directory_iterator("."))
-    {
-      auto fileName = p.path().filename().string();
-      if (regex_match(fileName, regexPattern))
-      {
-        ProcessPath(p);
-      }
-    }
-    return 0;
-
-
-  }
-  catch (exception& e)
-  {
-    cerr << e.what() << "\n";
-  }
+#include <string>
+#include <fstream>
+#include <filesystem>
+#include <regex>
+using namespace std;
+namespace fs = std::filesystem;
+
+#include <boost/program_options.hpp>
+using namespace boost::program_options;
+
+#include "VsolMessages_generated.hpp"
+
+/**
+Transforms `str` by replacing occurrences of `oldStr` with `newStr`, using 
+plain text (*not* regular expressions.)
+*/
+static inline void ReplaceInString(
+  string& str,
+  const std::string& oldStr,
+  const std::string& newStr)
+{
+  std::string::size_type pos = 0u;
+  while ((pos = str.find(oldStr, pos)) != std::string::npos) {
+    str.replace(pos, oldStr.length(), newStr);
+    pos += newStr.length();
+  }
+}
+
+string SlurpFile(const string& fileName)
+{
+  ifstream ifs(fileName.c_str(), ios::in | ios::binary | ios::ate);
+
+  ifstream::pos_type fileSize = ifs.tellg();
+  ifs.seekg(0, ios::beg);
+
+  vector<char> bytes(fileSize);
+  ifs.read(bytes.data(), fileSize);
+
+  return string(bytes.data(), fileSize);
+}
+
+class MyHandler : public VsolMessages::IHandler
+{
+public:
+  virtual bool Handle(const VsolMessages::A& value) override
+  {
+    VsolMessages::StoneDumpValue(cout, value);
+    return true;
+  }
+  virtual bool Handle(const VsolMessages::B& value) override
+  {
+    VsolMessages::StoneDumpValue(cout, value);
+    return true;
+  }
+  virtual bool Handle(const VsolMessages::C& value) override
+  {
+    VsolMessages::StoneDumpValue(cout, value);
+    return true;
+  }
+  virtual bool Handle(const VsolMessages::Message1& value) override
+  {
+    VsolMessages::StoneDumpValue(cout, value);
+    return true;
+  }
+  virtual bool Handle(const VsolMessages::Message2& value) override
+  {
+    VsolMessages::StoneDumpValue(cout, value);
+    return true;
+  }
+};
+
+template<typename T>
+void ProcessPath(T filePath)
+{
+  cout << "+--------------------------------------------+\n";
+  cout << "| Processing: " << filePath.path().string() << "\n";
+  cout << "+--------------------------------------------+\n";
+  MyHandler handler;
+  auto contents = SlurpFile(filePath.path().string());
+  VsolMessages::StoneDispatchToHandler(contents, &handler);
+}
+
+int main(int argc, char** argv)
+{
+  try
+  {
+
+    options_description desc("Allowed options");
+    desc.add_options()
+      // First parameter describes option name/short name
+      // The second is parameter to option
+      // The third is description
+      ("help,h", "print usage message")
+      ("pattern,p", value<string>(), "pattern for input")
+      ;
+
+    variables_map vm;
+    store(parse_command_line(argc, argv, desc), vm);
+
+    if (vm.count("help"))
+    {
+      cout << desc << "\n";
+      return 0;
+    }
+
+    notify(vm);
+
+    string pattern = vm["pattern"].as<string>();
+
+    // tranform globbing pattern into regex
+    // we should deal with -, ., *...
+    string regexPatternStr = pattern;
+    cout << "Pattern is: " << regexPatternStr << endl;
+    ReplaceInString(regexPatternStr, "\\", "\\\\");
+    ReplaceInString(regexPatternStr, "-", "\\-");
+    ReplaceInString(regexPatternStr, ".", "\\.");
+    ReplaceInString(regexPatternStr, "*", ".*");
+    ReplaceInString(regexPatternStr, "?", ".");
+    cout << "Corresponding regex is: " << regexPatternStr << endl;
+
+    regex regexPattern(regexPatternStr);
+
+    for (auto& p : fs::directory_iterator("."))
+    {
+      auto fileName = p.path().filename().string();
+      if (regex_match(fileName, regexPattern))
+      {
+        ProcessPath(p);
+      }
+    }
+    return 0;
+
+
+  }
+  catch (exception& e)
+  {
+    cerr << e.what() << "\n";
+  }
 }
\ No newline at end of file
--- a/Resources/Orthanc/DownloadOrthancFramework.cmake	Tue May 14 18:24:12 2019 +0200
+++ b/Resources/Orthanc/DownloadOrthancFramework.cmake	Thu May 16 09:11:14 2019 +0200
@@ -95,6 +95,16 @@
         set(ORTHANC_FRAMEWORK_MD5 "4429d8d9dea4ff6648df80ec3c64d79e")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.1")
         set(ORTHANC_FRAMEWORK_MD5 "099671538865e5da96208b37494d6718")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.2")
+        set(ORTHANC_FRAMEWORK_MD5 "8867050f3e9a1ce6157c1ea7a9433b1b")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.3")
+        set(ORTHANC_FRAMEWORK_MD5 "bf2f5ed1adb8b0fc5f10d278e68e1dfe")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.4")
+        set(ORTHANC_FRAMEWORK_MD5 "404baef5d4c43e7c5d9410edda8ef5a5")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.5")
+        set(ORTHANC_FRAMEWORK_MD5 "cfc437e0687ae4bd725fd93dc1f08bc4")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.6")
+        set(ORTHANC_FRAMEWORK_MD5 "3c29de1e289b5472342947168f0105c0")
       endif()
     endif()
   endif()
--- a/Resources/Orthanc/LinuxStandardBaseToolchain.cmake	Tue May 14 18:24:12 2019 +0200
+++ b/Resources/Orthanc/LinuxStandardBaseToolchain.cmake	Thu May 16 09:11:14 2019 +0200
@@ -1,4 +1,4 @@
-# LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../Resources/LinuxStandardBaseToolchain.cmake -DUSE_LEGACY_JSONCPP=ON
+# LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../Resources/LinuxStandardBaseToolchain.cmake -DUSE_LEGACY_JSONCPP=ON -DUSE_LEGACY_LIBICU=ON -DBOOST_LOCALE_BACKEND=icu
 
 INCLUDE(CMakeForceCompiler)
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/AngleMeasureTool.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,261 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "AngleMeasureTool.h"
+#include "MeasureToolsToolbox.h"
+
+#include <Core/Logging.h>
+
+#include <boost/math/constants/constants.hpp>
+
+extern void TrackerSample_SetInfoDisplayMessage(std::string key, std::string value);
+
+namespace OrthancStone
+{
+  AngleMeasureTool::~AngleMeasureTool()
+  {
+    // this measuring tool is a RABI for the corresponding visual layers
+    // stored in the 2D scene
+    Disable();
+    RemoveFromScene();
+  }
+
+  void AngleMeasureTool::RemoveFromScene()
+  {
+    if (layersCreated)
+    {
+      assert(GetScene().HasLayer(polylineZIndex_));
+      assert(GetScene().HasLayer(textZIndex_));
+      GetScene().DeleteLayer(polylineZIndex_);
+      GetScene().DeleteLayer(textZIndex_);
+    }
+  }
+
+  void AngleMeasureTool::SetSide1End(ScenePoint2D pt)
+  {
+    side1End_ = pt;
+    RefreshScene();
+  }
+
+  void AngleMeasureTool::SetSide2End(ScenePoint2D pt)
+  {
+    side2End_ = pt;
+    RefreshScene();
+  }
+
+  void AngleMeasureTool::SetCenter(ScenePoint2D pt)
+  {
+    center_ = pt;
+    RefreshScene();
+  }
+  
+  PolylineSceneLayer* AngleMeasureTool::GetPolylineLayer()
+  {
+    assert(GetScene().HasLayer(polylineZIndex_));
+    ISceneLayer* layer = &(GetScene().GetLayer(polylineZIndex_));
+    PolylineSceneLayer* concreteLayer = dynamic_cast<PolylineSceneLayer*>(layer);
+    assert(concreteLayer != NULL);
+    return concreteLayer;
+  }
+
+  TextSceneLayer* AngleMeasureTool::GetTextLayer()
+  {
+    assert(GetScene().HasLayer(textZIndex_));
+    ISceneLayer* layer = &(GetScene().GetLayer(textZIndex_));
+    TextSceneLayer* concreteLayer = dynamic_cast<TextSceneLayer*>(layer);
+    assert(concreteLayer != NULL);
+    return concreteLayer;
+  }
+
+
+  void AngleMeasureTool::RefreshScene()
+  {
+    if (IsEnabled())
+    {
+      // get the scaling factor 
+      const double pixelToScene =
+        GetScene().GetCanvasToSceneTransform().ComputeZoom();
+
+      if (!layersCreated)
+      {
+        // Create the layers if need be
+
+        assert(textZIndex_ == -1);
+        {
+          polylineZIndex_ = GetScene().GetMaxDepth() + 100;
+          //LOG(INFO) << "set polylineZIndex_ to: " << polylineZIndex_;
+          std::auto_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer());
+          GetScene().SetLayer(polylineZIndex_, layer.release());
+        }
+        {
+          textZIndex_ = GetScene().GetMaxDepth() + 100;
+          //LOG(INFO) << "set textZIndex_ to: " << textZIndex_;
+          std::auto_ptr<TextSceneLayer> layer(new TextSceneLayer());
+          GetScene().SetLayer(textZIndex_, layer.release());
+        }
+        layersCreated = true;
+      }
+      else
+      {
+        assert(GetScene().HasLayer(polylineZIndex_));
+        assert(GetScene().HasLayer(textZIndex_));
+      }
+      {
+        // Fill the polyline layer with the measurement line
+
+        PolylineSceneLayer* polylineLayer = GetPolylineLayer();
+        polylineLayer->ClearAllChains();
+        polylineLayer->SetColor(0, 223, 21);
+
+        // sides
+        {
+          {
+            PolylineSceneLayer::Chain chain;
+            chain.push_back(side1End_);
+            chain.push_back(center_);
+            polylineLayer->AddChain(chain, false);
+          }
+          {
+            PolylineSceneLayer::Chain chain;
+            chain.push_back(side2End_);
+            chain.push_back(center_);
+            polylineLayer->AddChain(chain, false);
+          }
+        }
+
+        // handles
+        {
+          //void AddSquare(PolylineSceneLayer::Chain& chain,const Scene2D& scene,const ScenePoint2D& centerS,const double& sideLength)
+
+          {
+            PolylineSceneLayer::Chain chain;
+            AddSquare(chain, GetScene(), side1End_, 10.0* pixelToScene); //TODO: take DPI into account
+            polylineLayer->AddChain(chain, true);
+          }
+
+          {
+            PolylineSceneLayer::Chain chain;
+            AddSquare(chain, GetScene(), side2End_, 10.0* pixelToScene); //TODO: take DPI into account
+            polylineLayer->AddChain(chain, true);
+          }
+        }
+
+        // arc
+        {
+          PolylineSceneLayer::Chain chain;
+
+          const double ARC_RADIUS_CANVAS_COORD = 30.0;
+          AddShortestArc(chain, GetScene(), side1End_, center_, side2End_, 
+            ARC_RADIUS_CANVAS_COORD*pixelToScene);
+          polylineLayer->AddChain(chain, false);
+        }
+      }
+      {
+        // Set the text layer
+
+        double p1cAngle = atan2(
+          side1End_.GetY() - center_.GetY(),
+          side1End_.GetX() - center_.GetX());
+
+        TrackerSample_SetInfoDisplayMessage("center_.GetX()",
+          boost::lexical_cast<std::string>(center_.GetX()));
+
+        TrackerSample_SetInfoDisplayMessage("center_.GetY()",
+          boost::lexical_cast<std::string>(center_.GetY()));
+
+        TrackerSample_SetInfoDisplayMessage("side1End_.GetX()",
+          boost::lexical_cast<std::string>(side1End_.GetX()));
+
+        TrackerSample_SetInfoDisplayMessage("side1End_.GetY()",
+          boost::lexical_cast<std::string>(side1End_.GetY()));
+
+        TrackerSample_SetInfoDisplayMessage("side2End_.GetX()",
+          boost::lexical_cast<std::string>(side2End_.GetX()));
+
+        TrackerSample_SetInfoDisplayMessage("side2End_.GetY()",
+          boost::lexical_cast<std::string>(side2End_.GetY()));
+
+        TrackerSample_SetInfoDisplayMessage("p1cAngle (deg)",
+          boost::lexical_cast<std::string>(RadiansToDegrees(p1cAngle)));
+
+        double p2cAngle = atan2(
+          side2End_.GetY() - center_.GetY(),
+          side2End_.GetX() - center_.GetX());
+
+        double delta = NormalizeAngle(p2cAngle - p1cAngle);
+        TrackerSample_SetInfoDisplayMessage("delta (deg)",
+          boost::lexical_cast<std::string>(RadiansToDegrees(delta)));
+
+        double theta = p1cAngle + delta/2;
+
+        TrackerSample_SetInfoDisplayMessage("theta (deg)",
+          boost::lexical_cast<std::string>(RadiansToDegrees(theta)));
+
+        TrackerSample_SetInfoDisplayMessage("p2cAngle (deg)",
+          boost::lexical_cast<std::string>(RadiansToDegrees(p2cAngle)));
+
+        const double TEXT_CENTER_DISTANCE_CANVAS_COORD = 90;
+
+        double offsetX = TEXT_CENTER_DISTANCE_CANVAS_COORD * cos(theta);
+        TrackerSample_SetInfoDisplayMessage("offsetX (pix)",
+          boost::lexical_cast<std::string>(offsetX));
+
+        double offsetY = TEXT_CENTER_DISTANCE_CANVAS_COORD * sin(theta);
+        TrackerSample_SetInfoDisplayMessage("offsetY (pix)",
+          boost::lexical_cast<std::string>(offsetY));
+
+        double pointX = center_.GetX() + offsetX * pixelToScene;
+        double pointY = center_.GetY() + offsetY * pixelToScene;
+        TrackerSample_SetInfoDisplayMessage("pointX",
+          boost::lexical_cast<std::string>(pointX));
+
+        TrackerSample_SetInfoDisplayMessage("pointY",
+          boost::lexical_cast<std::string>(pointY));
+
+        TextSceneLayer* textLayer = GetTextLayer();
+
+        char buf[64];
+        double angleDeg = RadiansToDegrees(delta);
+
+        TrackerSample_SetInfoDisplayMessage("angleDeg",
+          boost::lexical_cast<std::string>(angleDeg));
+
+        sprintf(buf, "%0.02f deg", angleDeg);
+        textLayer->SetText(buf);
+        textLayer->SetColor(0, 223, 21);
+
+        ScenePoint2D textAnchor;
+        //GetPositionOnBisectingLine(
+        //  textAnchor, side1End_, center_, side2End_, 40.0*pixelToScene);
+        textLayer->SetPosition(pointX, pointY);
+      }
+    }
+    else
+    {
+      if (layersCreated)
+      {
+        RemoveFromScene();
+        layersCreated = false;
+      }
+    }
+  }
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/AngleMeasureTool.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,74 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include "MeasureTools.h"
+
+#include <Framework/Scene2D/Scene2D.h>
+#include <Framework/Scene2D/ScenePoint2D.h>
+#include <Framework/Scene2D/PolylineSceneLayer.h>
+#include <Framework/Scene2D/TextSceneLayer.h>
+
+#include <boost/shared_ptr.hpp>
+#include <boost/weak_ptr.hpp>
+
+#include <vector>
+#include <cmath>
+
+namespace OrthancStone
+{
+  class AngleMeasureTool : public MeasureTool
+  {
+  public:
+    AngleMeasureTool(MessageBroker& broker, Scene2D& scene)
+      : MeasureTool(broker, scene)
+      , layersCreated(false)
+      , polylineZIndex_(-1)
+      , textZIndex_(-1)
+    {
+
+    }
+
+    ~AngleMeasureTool();
+
+    void SetSide1End(ScenePoint2D start);
+    void SetCenter(ScenePoint2D start);
+    void SetSide2End(ScenePoint2D start);
+
+  private:
+    PolylineSceneLayer* GetPolylineLayer();
+    TextSceneLayer*     GetTextLayer();
+    virtual void        RefreshScene() ORTHANC_OVERRIDE;
+    void                RemoveFromScene();
+
+  private:
+    ScenePoint2D side1End_;
+    ScenePoint2D side2End_;
+    ScenePoint2D center_;
+    bool         layersCreated;
+    int          polylineZIndex_;
+    int          textZIndex_;
+  };
+
+  typedef boost::shared_ptr<AngleMeasureTool> AngleMeasureToolPtr;
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/CreateAngleMeasureTracker.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,128 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "CreateAngleMeasureTracker.h"
+#include <Core/OrthancException.h>
+
+using namespace Orthanc;
+
+namespace OrthancStone
+{
+  CreateAngleMeasureTracker::CreateAngleMeasureTracker(
+    MessageBroker&                  broker,
+    Scene2D&                        scene,
+    std::vector<TrackerCommandPtr>& undoStack,
+    std::vector<MeasureToolPtr>&    measureTools,
+    const PointerEvent&             e)
+    : CreateMeasureTracker(scene, undoStack, measureTools)
+    , state_(CreatingSide1)
+  {
+    command_.reset(
+      new CreateAngleMeasureCommand(
+        broker,
+        scene,
+        measureTools,
+        e.GetMainPosition().Apply(scene.GetCanvasToSceneTransform())));
+  }
+
+  CreateAngleMeasureTracker::~CreateAngleMeasureTracker()
+  {
+  }
+
+  void CreateAngleMeasureTracker::PointerMove(const PointerEvent& event)
+  {
+    if (!active_)
+    {
+      throw OrthancException(ErrorCode_InternalError,
+        "Internal error: wrong state in CreateAngleMeasureTracker::"
+        "PointerMove: active_ == false");
+    }
+
+    ScenePoint2D scenePos = event.GetMainPosition().Apply(
+      scene_.GetCanvasToSceneTransform());
+
+    switch (state_)
+    {
+    case CreatingSide1:
+      GetCommand()->SetCenter(scenePos);
+      break;
+    case CreatingSide2:
+      GetCommand()->SetSide2End(scenePos);
+      break;
+    default:
+      throw OrthancException(ErrorCode_InternalError,
+        "Wrong state in CreateAngleMeasureTracker::"
+        "PointerMove: state_ invalid");
+    }
+    //LOG(TRACE) << "scenePos.GetX() = " << scenePos.GetX() << "     " <<
+    //  "scenePos.GetY() = " << scenePos.GetY();
+  }
+
+  void CreateAngleMeasureTracker::PointerUp(const PointerEvent& e)
+  {
+    // TODO: the current app does not prevent multiple PointerDown AND
+    // PointerUp to be sent to the tracker.
+    // Unless we augment the PointerEvent structure with the button index, 
+    // we cannot really tell if this pointer up event matches the initial
+    // pointer down event. Let's make it simple for now.
+
+    switch (state_)
+    {
+    case CreatingSide1:
+      state_ = CreatingSide2;
+      break;
+    case CreatingSide2:
+      throw OrthancException(ErrorCode_InternalError,
+        "Wrong state in CreateAngleMeasureTracker::"
+        "PointerUp: state_ == CreatingSide2 ; this should not happen");
+      break;
+    default:
+      throw OrthancException(ErrorCode_InternalError,
+        "Wrong state in CreateAngleMeasureTracker::"
+        "PointerMove: state_ invalid");
+    }
+  }
+
+  void CreateAngleMeasureTracker::PointerDown(const PointerEvent& e)
+  {
+    switch (state_)
+    {
+    case CreatingSide1:
+      throw OrthancException(ErrorCode_InternalError,
+        "Wrong state in CreateAngleMeasureTracker::"
+        "PointerDown: state_ == CreatingSide1 ; this should not happen");
+      break;
+    case CreatingSide2:
+      // we are done
+      active_ = false;
+      break;
+    default:
+      throw OrthancException(ErrorCode_InternalError,
+        "Wrong state in CreateAngleMeasureTracker::"
+        "PointerMove: state_ invalid");
+    }
+  }
+
+  CreateAngleMeasureCommandPtr CreateAngleMeasureTracker::GetCommand()
+  {
+    return boost::dynamic_pointer_cast<CreateAngleMeasureCommand>(command_);
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/CreateAngleMeasureTracker.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,65 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include "MeasureTrackers.h"
+#include "MeasureCommands.h"
+
+#include <vector>
+
+namespace OrthancStone
+{
+  class CreateAngleMeasureTracker : public CreateMeasureTracker
+  {
+  public:
+    /**
+    When you create this tracker, you need to supply it with the undo stack
+    where it will store the commands that perform the actual measure tool
+    creation and modification.
+    In turn, a container for these commands to store the actual measuring
+    must be supplied, too
+    */
+    CreateAngleMeasureTracker(
+      MessageBroker&                  broker,
+      Scene2D&                        scene,
+      std::vector<TrackerCommandPtr>& undoStack,
+      std::vector<MeasureToolPtr>&    measureTools,
+      const PointerEvent&             e);
+
+    ~CreateAngleMeasureTracker();
+
+    virtual void PointerMove(const PointerEvent& e) ORTHANC_OVERRIDE;
+    virtual void PointerUp(const PointerEvent& e) ORTHANC_OVERRIDE;
+    virtual void PointerDown(const PointerEvent& e) ORTHANC_OVERRIDE;
+
+  private:
+    CreateAngleMeasureCommandPtr GetCommand();
+
+    enum State
+    {
+      CreatingSide1,
+      CreatingSide2,
+      Finished // just for debug
+    };
+    State state_;
+
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/CreateCircleMeasureTracker.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,23 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+namespace OrthancStone
+{
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/CreateCircleMeasureTracker.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,25 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+namespace OrthancStone
+{
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/CreateLineMeasureTracker.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,91 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "CreateLineMeasureTracker.h"
+#include <Core/OrthancException.h>
+
+using namespace Orthanc;
+
+namespace OrthancStone
+{
+  CreateLineMeasureTracker::CreateLineMeasureTracker(
+    MessageBroker&                  broker,
+    Scene2D&                        scene,
+    std::vector<TrackerCommandPtr>& undoStack,
+    std::vector<MeasureToolPtr>&    measureTools,
+    const PointerEvent&             e)
+    : CreateMeasureTracker(scene, undoStack, measureTools)
+  {
+    command_.reset(
+      new CreateLineMeasureCommand(
+        broker,
+        scene,
+        measureTools,
+        e.GetMainPosition().Apply(scene.GetCanvasToSceneTransform())));
+  }
+
+  CreateLineMeasureTracker::~CreateLineMeasureTracker()
+  {
+
+  }
+
+  void CreateLineMeasureTracker::PointerMove(const PointerEvent& event)
+  {
+    if (!active_)
+    {
+      throw OrthancException(ErrorCode_InternalError,
+        "Internal error: wrong state in CreateLineMeasureTracker::"
+        "PointerMove: active_ == false");
+    }
+
+    ScenePoint2D scenePos = event.GetMainPosition().Apply(
+      scene_.GetCanvasToSceneTransform());
+
+    //LOG(TRACE) << "scenePos.GetX() = " << scenePos.GetX() << "     " <<
+    //  "scenePos.GetY() = " << scenePos.GetY();
+
+    CreateLineMeasureTracker* concreteThis =
+      dynamic_cast<CreateLineMeasureTracker*>(this);
+    assert(concreteThis != NULL);
+    GetCommand()->SetEnd(scenePos);
+  }
+
+  void CreateLineMeasureTracker::PointerUp(const PointerEvent& e)
+  {
+    // TODO: the current app does not prevent multiple PointerDown AND
+    // PointerUp to be sent to the tracker.
+    // Unless we augment the PointerEvent structure with the button index, 
+    // we cannot really tell if this pointer up event matches the initial
+    // pointer down event. Let's make it simple for now.
+    active_ = false;
+  }
+
+  void CreateLineMeasureTracker::PointerDown(const PointerEvent& e)
+  {
+    LOG(WARNING) << "Additional touches (fingers, pen, mouse buttons...) "
+      "are ignored when the line measure creation tracker is active";
+  }
+
+  CreateLineMeasureCommandPtr CreateLineMeasureTracker::GetCommand()
+  {
+    return boost::dynamic_pointer_cast<CreateLineMeasureCommand>(command_);
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/CreateLineMeasureTracker.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,53 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include "MeasureTrackers.h"
+
+namespace OrthancStone
+{
+  class CreateLineMeasureTracker : public CreateMeasureTracker
+  {
+  public:
+    /**
+    When you create this tracker, you need to supply it with the undo stack
+    where it will store the commands that perform the actual measure tool
+    creation and modification.
+    In turn, a container for these commands to store the actual measuring
+    must be supplied, too
+    */
+    CreateLineMeasureTracker(
+      MessageBroker&                  broker,
+      Scene2D&                        scene,
+      std::vector<TrackerCommandPtr>& undoStack,
+      std::vector<MeasureToolPtr>&    measureTools,
+      const PointerEvent&             e);
+
+    ~CreateLineMeasureTracker();
+
+    virtual void PointerMove(const PointerEvent& e) ORTHANC_OVERRIDE;
+    virtual void PointerUp(const PointerEvent& e) ORTHANC_OVERRIDE;
+    virtual void PointerDown(const PointerEvent& e) ORTHANC_OVERRIDE;
+
+  private:
+    CreateLineMeasureCommandPtr GetCommand();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/CreateMeasureTracker.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,20 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/CreateMeasureTracker.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,22 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/CreateSimpleTrackerAdapter.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,77 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "IFlexiblePointerTracker.h"
+#include <Framework/Scene2D/IPointerTracker.h>
+
+
+namespace OrthancStone
+{
+  namespace 
+  {
+    class SimpleTrackerAdapter : public IFlexiblePointerTracker
+    {
+    public:
+      SimpleTrackerAdapter(PointerTrackerPtr wrappedTracker)
+        : wrappedTracker_(wrappedTracker)
+        , active_(true)
+      {
+      }
+
+      virtual void PointerMove(const PointerEvent& event) ORTHANC_OVERRIDE
+      {
+        if(active_)
+          wrappedTracker_->Update(event);
+      };
+      virtual void PointerUp(const PointerEvent& event) ORTHANC_OVERRIDE
+      {
+        if (wrappedTracker_)
+        {
+          wrappedTracker_->Release();
+          wrappedTracker_ = NULL;
+        }
+        active_ = false;
+      }
+      virtual void PointerDown(const PointerEvent& event) ORTHANC_OVERRIDE
+      {
+        // nothing to do atm
+      }
+      virtual bool IsActive() const ORTHANC_OVERRIDE
+      {
+        return active_;
+      }
+
+      virtual void Cancel() ORTHANC_OVERRIDE
+      {
+        wrappedTracker_ = NULL;
+        active_ = false;
+      }
+
+    private:
+      PointerTrackerPtr wrappedTracker_;
+      bool active_;
+    };
+  }
+
+  FlexiblePointerTrackerPtr CreateSimpleTrackerAdapter(PointerTrackerPtr t)
+  {
+    return FlexiblePointerTrackerPtr(new SimpleTrackerAdapter(t));
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/EditAngleMeasureTracker.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,23 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+namespace OrthancStone
+{
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/EditAngleMeasureTracker.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,25 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+namespace OrthancStone
+{
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/EditCircleMeasureTracker.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,23 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+namespace OrthancStone
+{
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/EditCircleMeasureTracker.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,25 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+namespace OrthancStone
+{
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/EditLineMeasureTracker.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,23 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+namespace OrthancStone
+{
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/EditLineMeasureTracker.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,25 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+namespace OrthancStone
+{
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/IFlexiblePointerTracker.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,84 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <Framework/Scene2D/PointerEvent.h>
+#include <boost/shared_ptr.hpp>
+
+namespace OrthancStone
+{
+  class IPointerTracker;
+  typedef boost::shared_ptr<IPointerTracker> PointerTrackerPtr;
+
+  /**
+  This interface represents a flexible mouse tracker that can respond to 
+  several events and is not automatically deleted upon mouse up or when touch
+  interaction is suspended : for instance, a stateful tracker with a two-step 
+  interaction like: click & drag --> mouse up --> drag --> mouse click 
+  (for instance, for an angle measuring tracker or an ellipse tracker)
+  */
+  class IFlexiblePointerTracker : public boost::noncopyable
+  {
+  public:
+    virtual ~IFlexiblePointerTracker() {}
+
+    /**
+    This method will be repeatedly called during user interaction
+    */
+    virtual void PointerMove(const PointerEvent& event) = 0;
+
+    /**
+    This method will be called when a touch/pointer is removed (mouse up, 
+    pen lift, finger removed...)
+    */
+    virtual void PointerUp(const PointerEvent& event) = 0;
+
+    /**
+    This method will be called when a touch/pointer is added (mouse down, 
+    pen or finger press)
+    */
+    virtual void PointerDown(const PointerEvent& event) = 0;
+
+    /**
+    This method will be repeatedly called by the tracker owner (for instance,
+    the application) to check whether the tracker must keep on receiving 
+    interaction or if its job is done and it should be deleted.
+    */
+    virtual bool IsActive() const = 0;
+
+    /**
+    This will be called if the tracker needs to be dismissed without committing
+    its changes to the underlying model. If the model has been modified during
+    tracker lifetime, it must be restored to its initial value
+    */
+    virtual void Cancel() = 0;
+  };
+
+  typedef boost::shared_ptr<IFlexiblePointerTracker> FlexiblePointerTrackerPtr;
+
+  /**
+  This factory adopts the supplied simple tracker and creates a flexible 
+  tracker wrapper around it.
+  */
+  FlexiblePointerTrackerPtr CreateSimpleTrackerAdapter(PointerTrackerPtr);
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/LineMeasureTool.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,200 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "LineMeasureTool.h"
+#include "MeasureToolsToolbox.h"
+
+#include <Core/Logging.h>
+
+
+namespace OrthancStone
+{
+  LineMeasureTool::~LineMeasureTool()
+  {
+    // this measuring tool is a RABI for the corresponding visual layers
+    // stored in the 2D scene
+    Disable();
+    RemoveFromScene();
+  }
+
+  void LineMeasureTool::RemoveFromScene()
+  {
+    if (layersCreated)
+    {
+      assert(GetScene().HasLayer(polylineZIndex_));
+      assert(GetScene().HasLayer(textZIndex_));
+      GetScene().DeleteLayer(polylineZIndex_);
+      GetScene().DeleteLayer(textZIndex_);
+    }
+  }
+
+
+  void LineMeasureTool::SetStart(ScenePoint2D start)
+  {
+    start_ = start;
+    RefreshScene();
+  }
+
+  void LineMeasureTool::SetEnd(ScenePoint2D end)
+  {
+    end_ = end;
+    RefreshScene();
+  }
+
+  void LineMeasureTool::Set(ScenePoint2D start, ScenePoint2D end)
+  {
+    start_ = start;
+    end_ = end;
+    RefreshScene();
+  }
+
+  PolylineSceneLayer* LineMeasureTool::GetPolylineLayer()
+  {
+    assert(GetScene().HasLayer(polylineZIndex_));
+    ISceneLayer* layer = &(GetScene().GetLayer(polylineZIndex_));
+    PolylineSceneLayer* concreteLayer = dynamic_cast<PolylineSceneLayer*>(layer);
+    assert(concreteLayer != NULL);
+    return concreteLayer;
+  }
+
+  TextSceneLayer* LineMeasureTool::GetTextLayer()
+  {
+    assert(GetScene().HasLayer(textZIndex_));
+    ISceneLayer* layer = &(GetScene().GetLayer(textZIndex_));
+    TextSceneLayer* concreteLayer = dynamic_cast<TextSceneLayer*>(layer);
+    assert(concreteLayer != NULL);
+    return concreteLayer;
+  }
+
+  void LineMeasureTool::RefreshScene()
+  {
+    if (IsEnabled())
+    {
+      if (!layersCreated)
+      {
+        // Create the layers if need be
+
+        assert(textZIndex_ == -1);
+        {
+          polylineZIndex_ = GetScene().GetMaxDepth() + 100;
+          //LOG(INFO) << "set polylineZIndex_ to: " << polylineZIndex_;
+          std::auto_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer());
+          GetScene().SetLayer(polylineZIndex_, layer.release());
+        }
+        {
+          textZIndex_ = GetScene().GetMaxDepth() + 100;
+          //LOG(INFO) << "set textZIndex_ to: " << textZIndex_;
+          std::auto_ptr<TextSceneLayer> layer(new TextSceneLayer());
+          GetScene().SetLayer(textZIndex_, layer.release());
+        }
+        layersCreated = true;
+      }
+      else
+      {
+        assert(GetScene().HasLayer(polylineZIndex_));
+        assert(GetScene().HasLayer(textZIndex_));
+      }
+      {
+        // Fill the polyline layer with the measurement line
+
+        PolylineSceneLayer* polylineLayer = GetPolylineLayer();
+        polylineLayer->ClearAllChains();
+        polylineLayer->SetColor(0, 223, 21);
+
+        {
+          PolylineSceneLayer::Chain chain;
+          chain.push_back(start_);
+          chain.push_back(end_);
+          polylineLayer->AddChain(chain, false);
+        }
+
+        // handles
+        {
+          //void AddSquare(PolylineSceneLayer::Chain& chain,const Scene2D& scene,const ScenePoint2D& centerS,const double& sideLength)
+
+          {
+            PolylineSceneLayer::Chain chain;
+            AddSquare(chain, GetScene(), start_, 10.0); //TODO: take DPI into account
+            polylineLayer->AddChain(chain, true);
+          }
+
+          {
+            PolylineSceneLayer::Chain chain;
+            AddSquare(chain, GetScene(), end_, 10.0); //TODO: take DPI into account
+            polylineLayer->AddChain(chain, true);
+          }
+
+          //ScenePoint2D startC = start_.Apply(GetScene().GetSceneToCanvasTransform());
+          //double squareSize = 10.0; 
+          //double startHandleLX = startC.GetX() - squareSize/2;
+          //double startHandleTY = startC.GetY() - squareSize / 2;
+          //double startHandleRX = startC.GetX() + squareSize / 2;
+          //double startHandleBY = startC.GetY() + squareSize / 2;
+          //ScenePoint2D startLTC(startHandleLX, startHandleTY);
+          //ScenePoint2D startRTC(startHandleRX, startHandleTY);
+          //ScenePoint2D startRBC(startHandleRX, startHandleBY);
+          //ScenePoint2D startLBC(startHandleLX, startHandleBY);
+
+          //ScenePoint2D startLT = startLTC.Apply(GetScene().GetCanvasToSceneTransform());
+          //ScenePoint2D startRT = startRTC.Apply(GetScene().GetCanvasToSceneTransform());
+          //ScenePoint2D startRB = startRBC.Apply(GetScene().GetCanvasToSceneTransform());
+          //ScenePoint2D startLB = startLBC.Apply(GetScene().GetCanvasToSceneTransform());
+
+          //PolylineSceneLayer::Chain chain;
+          //chain.push_back(startLT);
+          //chain.push_back(startRT);
+          //chain.push_back(startRB);
+          //chain.push_back(startLB);
+          //polylineLayer->AddChain(chain, true);
+        }
+
+      }
+      {
+        // Set the text layer proporeties
+
+        TextSceneLayer* textLayer = GetTextLayer();
+        double deltaX = end_.GetX() - start_.GetX();
+        double deltaY = end_.GetY() - start_.GetY();
+        double squareDist = deltaX * deltaX + deltaY * deltaY;
+        double dist = sqrt(squareDist);
+        char buf[64];
+        sprintf(buf, "%0.02f units", dist);
+        textLayer->SetText(buf);
+        textLayer->SetColor(0, 223, 21);
+
+        // TODO: for now we simply position the text overlay at the middle
+        // of the measuring segment
+        double midX = 0.5*(end_.GetX() + start_.GetX());
+        double midY = 0.5*(end_.GetY() + start_.GetY());
+        textLayer->SetPosition(midX, midY);
+      }
+    }
+    else
+    {
+      if (layersCreated)
+      {
+        RemoveFromScene();
+        layersCreated = false;
+      }
+    }
+  }
+
+
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/LineMeasureTool.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,72 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include "MeasureTools.h"
+
+#include <Framework/Scene2D/Scene2D.h>
+#include <Framework/Scene2D/ScenePoint2D.h>
+#include <Framework/Scene2D/PolylineSceneLayer.h>
+#include <Framework/Scene2D/TextSceneLayer.h>
+
+#include <boost/shared_ptr.hpp>
+#include <boost/weak_ptr.hpp>
+
+#include <vector>
+#include <cmath>
+
+namespace OrthancStone
+{
+  class LineMeasureTool : public MeasureTool
+  {
+  public:
+    LineMeasureTool(MessageBroker& broker, Scene2D& scene)
+      : MeasureTool(broker, scene)
+      , layersCreated(false)
+      , polylineZIndex_(-1)
+      , textZIndex_(-1)
+    {
+
+    }
+
+    ~LineMeasureTool();
+
+    void SetStart(ScenePoint2D start);
+    void SetEnd(ScenePoint2D end);
+    void Set(ScenePoint2D start, ScenePoint2D end);
+
+  private:
+    PolylineSceneLayer* GetPolylineLayer();
+    TextSceneLayer*     GetTextLayer();
+    virtual void        RefreshScene() ORTHANC_OVERRIDE;
+    void                RemoveFromScene();
+
+  private:
+    ScenePoint2D start_;
+    ScenePoint2D end_;
+    bool         layersCreated;
+    int          polylineZIndex_;
+    int          textZIndex_;
+  };
+
+  typedef boost::shared_ptr<LineMeasureTool> LineMeasureToolPtr;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/MeasureCommands.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,93 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "MeasureCommands.h"
+
+namespace OrthancStone
+{
+  void CreateMeasureCommand::Undo()
+  {
+    // simply disable the measure tool upon undo
+    GetMeasureTool()->Disable();
+  }
+
+  void CreateMeasureCommand::Redo()
+  {
+    GetMeasureTool()->Enable();
+  }
+
+  CreateMeasureCommand::CreateMeasureCommand(
+    Scene2D& scene, MeasureToolList& measureTools)
+    : TrackerCommand(scene)
+    , measureTools_(measureTools)
+  {
+
+  }
+
+  CreateMeasureCommand::~CreateMeasureCommand()
+  {
+    // deleting the command should not change the model state
+    // we thus leave it as is
+  }
+
+  CreateLineMeasureCommand::CreateLineMeasureCommand(
+    MessageBroker&    broker, 
+    Scene2D&          scene, 
+    MeasureToolList&  measureTools, 
+    ScenePoint2D      point)
+    : CreateMeasureCommand(scene, measureTools)
+    , measureTool_(new LineMeasureTool(broker,scene))
+  {
+    measureTools_.push_back(measureTool_);
+    measureTool_->Set(point, point);
+  }
+
+  void CreateLineMeasureCommand::SetEnd(ScenePoint2D scenePos)
+  {
+    measureTool_->SetEnd(scenePos);
+  }
+
+  CreateAngleMeasureCommand::CreateAngleMeasureCommand(
+    MessageBroker&    broker, 
+    Scene2D&          scene, 
+    MeasureToolList&  measureTools, 
+    ScenePoint2D      point)
+    : CreateMeasureCommand(scene, measureTools)
+    , measureTool_(new AngleMeasureTool(broker,scene))
+  {
+    measureTools_.push_back(measureTool_);
+    measureTool_->SetSide1End(point);
+    measureTool_->SetCenter(point);
+    measureTool_->SetSide2End(point);
+  }
+
+  /** This method sets center*/
+  void CreateAngleMeasureCommand::SetCenter(ScenePoint2D scenePos)
+  {
+    measureTool_->SetCenter(scenePos);
+  }
+
+  /** This method sets end of side 2*/
+  void CreateAngleMeasureCommand::SetSide2End(ScenePoint2D scenePos)
+  {
+    measureTool_->SetSide2End(scenePos);
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/MeasureCommands.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,118 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+#pragma once
+
+#include <Framework/Scene2D/Scene2D.h>
+#include <boost/shared_ptr.hpp>
+
+// to be moved into Stone
+#include "MeasureTools.h"
+#include "LineMeasureTool.h"
+#include "AngleMeasureTool.h"
+
+namespace OrthancStone
+{
+  //class LineMeasureTool;
+  //typedef boost::shared_ptr<LineMeasureTool> LineMeasureToolPtr;
+  //class AngleMeasureTool;
+  //typedef boost::shared_ptr<AngleMeasureTool> AngleMeasureToolPtr;
+   
+  class TrackerCommand
+  {
+  public:
+    TrackerCommand(Scene2D& scene) : scene_(scene)
+    {
+
+    }
+    virtual void Undo() = 0;
+    virtual void Redo() = 0;
+    Scene2D& GetScene()
+    {
+      return scene_;
+    }
+
+  protected:
+    Scene2D& scene_;
+  private:
+    TrackerCommand(const TrackerCommand&);
+    TrackerCommand& operator=(const TrackerCommand&);
+  };
+
+  typedef boost::shared_ptr<TrackerCommand> TrackerCommandPtr;
+  
+  class CreateMeasureCommand : public TrackerCommand
+  {
+  public:
+    CreateMeasureCommand(Scene2D& scene, MeasureToolList& measureTools);
+    ~CreateMeasureCommand();
+    virtual void Undo() ORTHANC_OVERRIDE;
+    virtual void Redo() ORTHANC_OVERRIDE;
+  protected:
+    MeasureToolList& measureTools_;
+  private:
+    /** Must be implemented by the subclasses that create the actual tool */
+    virtual MeasureToolPtr GetMeasureTool() = 0;
+  };
+
+  typedef boost::shared_ptr<CreateMeasureCommand> CreateMeasureCommandPtr;
+
+  class CreateLineMeasureCommand : public CreateMeasureCommand
+  {
+  public:
+    CreateLineMeasureCommand(
+      MessageBroker& broker, Scene2D& scene, MeasureToolList& measureTools, ScenePoint2D point);
+    
+    // the starting position is set in the ctor
+    void SetEnd(ScenePoint2D scenePos);
+
+  private:
+    virtual MeasureToolPtr GetMeasureTool() ORTHANC_OVERRIDE
+    {
+      return measureTool_;
+    }
+    LineMeasureToolPtr measureTool_;
+  };
+
+  typedef boost::shared_ptr<CreateLineMeasureCommand> CreateLineMeasureCommandPtr;
+
+  class CreateAngleMeasureCommand : public CreateMeasureCommand
+  {
+  public:
+    /** Ctor sets end of side 1*/
+    CreateAngleMeasureCommand(
+      MessageBroker& broker, Scene2D& scene, MeasureToolList& measureTools, ScenePoint2D point);
+
+    /** This method sets center*/
+    void SetCenter(ScenePoint2D scenePos);
+
+    /** This method sets end of side 2*/
+    void SetSide2End(ScenePoint2D scenePos);
+
+  private:
+    virtual MeasureToolPtr GetMeasureTool() ORTHANC_OVERRIDE
+    {
+      return measureTool_;
+    }
+    AngleMeasureToolPtr measureTool_;
+  };
+
+  typedef boost::shared_ptr<CreateAngleMeasureCommand> CreateAngleMeasureCommandPtr;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/MeasureTools.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,75 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "MeasureTools.h"
+
+#include <Core/Logging.h>
+
+#include <boost/math/constants/constants.hpp>
+
+namespace OrthancStone
+{
+
+  MeasureTool::~MeasureTool()
+  {
+
+  }
+
+  void MeasureTool::Enable()
+  {
+    enabled_ = true;
+    RefreshScene();
+  }
+
+  void MeasureTool::Disable()
+  {
+    enabled_ = false;
+    RefreshScene();
+  }
+
+  bool MeasureTool::IsEnabled() const
+  {
+    return enabled_;
+  }
+
+  OrthancStone::Scene2D& MeasureTool::GetScene()
+  {
+    return scene_;
+  }
+
+  MeasureTool::MeasureTool(MessageBroker& broker, Scene2D& scene)
+    : IObserver(broker)
+    , scene_(scene)
+    , enabled_(true)
+  {
+    scene_.RegisterObserverCallback(
+      new Callable<MeasureTool, Scene2D::SceneTransformChanged>
+      (*this, &MeasureTool::OnSceneTransformChanged));
+  }
+
+  void MeasureTool::OnSceneTransformChanged(
+    const Scene2D::SceneTransformChanged& message)
+  {
+    RefreshScene();
+  }
+
+
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/MeasureTools.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,89 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include <Framework/Scene2D/Scene2D.h>
+#include <Framework/Scene2D/ScenePoint2D.h>
+#include <Framework/Scene2D/PolylineSceneLayer.h>
+#include <Framework/Scene2D/TextSceneLayer.h>
+
+#include <boost/shared_ptr.hpp>
+#include <boost/weak_ptr.hpp>
+
+#include <vector>
+#include <cmath>
+
+namespace OrthancStone
+{
+  class MeasureTool : public IObserver
+  {
+  public:
+    virtual ~MeasureTool();
+
+    /**
+    Enabled tools are rendered in the scene.
+    */
+    void Enable();
+
+    /**
+    Disabled tools are not rendered in the scene. This is useful to be able
+    to use them as their own memento in command stacks (when a measure tool
+    creation command has been undone, the measure remains alive in the
+    command object but is disabled so that it can be redone later on easily)
+    */
+    void Disable();
+
+    /**
+    This method is called when the scene transform changes. It allows to 
+    recompute the visual elements whose content depend upon the scene transform
+    */
+    void OnSceneTransformChanged(const Scene2D::SceneTransformChanged& message);
+
+  protected:
+    MeasureTool(MessageBroker& broker, Scene2D& scene);
+    
+    /**
+    This is the meat of the tool: this method must [create (if needed) and]
+    update the layers and their data according to the measure tool kind and
+    current state. This is repeatedly called during user interaction
+    */
+    virtual void RefreshScene() = 0;
+
+    Scene2D& GetScene();
+
+    /**
+    enabled_ is not accessible by subclasses because there is a state machine
+    that we do not wanna mess with
+    */
+    bool IsEnabled() const;
+
+  private:
+    Scene2D& scene_;
+    bool     enabled_;
+  };
+
+  typedef boost::shared_ptr<MeasureTool> MeasureToolPtr;
+  typedef std::vector<MeasureToolPtr> MeasureToolList;
+}
+
+
+extern void TrackerSample_SetInfoDisplayMessage(
+  std::string key, std::string value);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/MeasureToolsToolbox.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,278 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "MeasureToolsToolbox.h"
+
+#include <boost/math/constants/constants.hpp>
+
+namespace
+{
+  double g_pi = boost::math::constants::pi<double>();
+}
+
+namespace OrthancStone
+{
+  double RadiansToDegrees(double angleRad)
+  {
+    static const double factor = 180.0 / g_pi;
+    return angleRad * factor;
+  }
+
+  void AddSquare(PolylineSceneLayer::Chain& chain,
+    const Scene2D&      scene,
+    const ScenePoint2D& centerS,
+    const double&       sideLength)
+  {
+    chain.clear();
+    chain.reserve(4);
+    ScenePoint2D centerC = centerS.Apply(scene.GetSceneToCanvasTransform());
+    //TODO: take DPI into account 
+    double handleLX = centerC.GetX() - sideLength / 2;
+    double handleTY = centerC.GetY() - sideLength / 2;
+    double handleRX = centerC.GetX() + sideLength / 2;
+    double handleBY = centerC.GetY() + sideLength / 2;
+    ScenePoint2D LTC(handleLX, handleTY);
+    ScenePoint2D RTC(handleRX, handleTY);
+    ScenePoint2D RBC(handleRX, handleBY);
+    ScenePoint2D LBC(handleLX, handleBY);
+
+    ScenePoint2D startLT = LTC.Apply(scene.GetCanvasToSceneTransform());
+    ScenePoint2D startRT = RTC.Apply(scene.GetCanvasToSceneTransform());
+    ScenePoint2D startRB = RBC.Apply(scene.GetCanvasToSceneTransform());
+    ScenePoint2D startLB = LBC.Apply(scene.GetCanvasToSceneTransform());
+
+    chain.push_back(startLT);
+    chain.push_back(startRT);
+    chain.push_back(startRB);
+    chain.push_back(startLB);
+  }
+#if 0
+  void AddArc(
+      PolylineSceneLayer::Chain& chain
+    , const Scene2D&      scene
+    , const ScenePoint2D& p1
+    , const ScenePoint2D& c
+    , const ScenePoint2D& p2
+    , const double&       radiusS
+    , const bool          clockwise
+    , const int           subdivisionsCount)
+  {
+    double p1cAngle = atan2(p1.GetY() - c.GetY(), p1.GetX() - c.GetX());
+    double p2cAngle = atan2(p2.GetY() - c.GetY(), p2.GetX() - c.GetX());
+    AddArc(
+      chain, scene, c, radiusS, p1cAngle, p2cAngle, 
+      clockwise, subdivisionsCount);
+  }
+#endif
+
+  void AddShortestArc(
+      PolylineSceneLayer::Chain& chain
+    , const Scene2D&             scene
+    , const ScenePoint2D&        p1
+    , const ScenePoint2D&        c
+    , const ScenePoint2D&        p2
+    , const double&              radiusS
+    , const int                  subdivisionsCount)
+  {
+    double p1cAngle = atan2(p1.GetY() - c.GetY(), p1.GetX() - c.GetX());
+    double p2cAngle = atan2(p2.GetY() - c.GetY(), p2.GetX() - c.GetX());
+    AddShortestArc(
+      chain, scene, c, radiusS, p1cAngle, p2cAngle, subdivisionsCount);
+  }
+
+  void GetPositionOnBisectingLine(
+    ScenePoint2D&       result
+    , const ScenePoint2D& p1
+    , const ScenePoint2D& c
+    , const ScenePoint2D& p2
+    , const double d)
+  {
+    // TODO: fix correct half-plane
+    double p1cAngle = atan2(p1.GetY() - c.GetY(), p1.GetX() - c.GetX());
+    double p2cAngle = atan2(p2.GetY() - c.GetY(), p2.GetX() - c.GetX());
+    double angle = 0.5*(p1cAngle + p2cAngle);
+    double unitVectorX = cos(angle);
+    double unitVectorY = sin(angle);
+    double posX = c.GetX() + d * unitVectorX;
+    double posY = c.GetX() + d * unitVectorY;
+    result = ScenePoint2D(posX, posY);
+  }
+   
+
+  void AddShortestArc(
+      PolylineSceneLayer::Chain&  chain
+    , const Scene2D&              scene
+    , const ScenePoint2D&         centerS
+    , const double&               radiusS
+    , const double                startAngleRad
+    , const double                endAngleRad
+    , const int                   subdivisionsCount)
+  {
+    // this gives a signed difference between angle which
+    // is the smallest difference (in magnitude) between 
+    // the angles
+    double delta = NormalizeAngle(endAngleRad-startAngleRad);
+
+    chain.clear();
+    chain.reserve(subdivisionsCount + 1);
+
+    double angleIncr = delta/static_cast<double>(subdivisionsCount);
+
+    double theta = startAngleRad;
+    for (int i = 0; i < subdivisionsCount + 1; ++i)
+    {
+      double offsetX = radiusS * cos(theta);
+      double offsetY = radiusS * sin(theta);
+      double pointX = centerS.GetX() + offsetX;
+      double pointY = centerS.GetY() + offsetY;
+      chain.push_back(ScenePoint2D(pointX, pointY));
+      theta += angleIncr;
+    }
+  }
+
+#if 0
+  void AddArc(
+      PolylineSceneLayer::Chain& chain
+    , const Scene2D&      scene
+    , const ScenePoint2D& centerS
+    , const double&       radiusS
+    , const double        startAngleRad
+    , const double        endAngleRad
+    , const bool          clockwise
+    , const int           subdivisionsCount)
+  {
+    double startAngleRadN = NormalizeAngle(startAngleRad);
+    double endAngleRadN = NormalizeAngle(endAngleRad);
+
+    double angle1Rad = std::min(startAngleRadN, endAngleRadN);
+    double angle2Rad = std::max(startAngleRadN, endAngleRadN);
+
+    // now we are sure angle1Rad < angle2Rad
+    // this means that if we draw from 1 to 2, it will be clockwise (
+    // increasing angles).
+    // let's fix this:
+    if (!clockwise)
+    {
+      angle2Rad -= 2 * g_pi;
+      // now we are sure angle2Rad < angle1Rad (since they were normalized) 
+      // and, thus, going from 1 to 2 means the angle values will DECREASE,
+      // which is the definition of anticlockwise
+    }
+
+    chain.clear();
+    chain.reserve(subdivisionsCount + 1);
+
+    double angleIncr = (angle2Rad - angle1Rad)
+      / static_cast<double>(subdivisionsCount);
+
+    double theta = angle1Rad;
+    for (int i = 0; i < subdivisionsCount + 1; ++i)
+    {
+      double offsetX = radiusS * cos(theta);
+      double offsetY = radiusS * sin(theta);
+      double pointX = centerS.GetX() + offsetX;
+      double pointY = centerS.GetY() + offsetY;
+      chain.push_back(ScenePoint2D(pointX, pointY));
+      theta += angleIncr;
+    }
+  }
+#endif
+
+  void AddCircle(PolylineSceneLayer::Chain& chain,
+    const Scene2D&      scene,
+    const ScenePoint2D& centerS,
+    const double&       radiusS,
+    const int           numSubdivisions)
+  {
+    //ScenePoint2D centerC = centerS.Apply(scene.GetSceneToCanvasTransform());
+    //TODO: take DPI into account
+
+    // TODO: automatically compute the number for segments for smooth 
+    // display based on the radius in pixels.
+
+    chain.clear();
+    chain.reserve(numSubdivisions);
+
+    double angleIncr = (2.0 * g_pi)
+      / static_cast<double>(numSubdivisions);
+
+    double theta = 0;
+    for (int i = 0; i < numSubdivisions; ++i)
+    {
+      double offsetX = radiusS * cos(theta);
+      double offsetY = radiusS * sin(theta);
+      double pointX = centerS.GetX() + offsetX;
+      double pointY = centerS.GetY() + offsetY;
+      chain.push_back(ScenePoint2D(pointX, pointY));
+      theta += angleIncr;
+    }
+  }
+
+  double NormalizeAngle(double angle)
+  {
+    double retAngle = angle;
+    while (retAngle < -1.0*g_pi)
+      retAngle += 2 * g_pi;
+    while (retAngle >= g_pi)
+      retAngle -= 2 * g_pi;
+    return retAngle;
+  }
+
+  double MeasureAngle(const ScenePoint2D& p1, const ScenePoint2D& c, const ScenePoint2D& p2)
+  {
+    double p1cAngle = atan2(p1.GetY() - c.GetY(), p1.GetX() - c.GetX());
+    double p2cAngle = atan2(p2.GetY() - c.GetY(), p2.GetX() - c.GetX());
+    double delta = p2cAngle - p1cAngle;
+    return NormalizeAngle(delta);
+  }
+
+
+#if 0
+  void AddEllipse(PolylineSceneLayer::Chain& chain,
+    const Scene2D&      scene,
+    const ScenePoint2D& centerS,
+    const double&       halfHAxis,
+    const double&       halfVAxis)
+  {
+    chain.clear();
+    chain.reserve(4);
+    ScenePoint2D centerC = centerS.Apply(scene.GetSceneToCanvasTransform());
+    //TODO: take DPI into account
+    double handleLX = centerC.GetX() - sideLength / 2;
+    double handleTY = centerC.GetY() - sideLength / 2;
+    double handleRX = centerC.GetX() + sideLength / 2;
+    double handleBY = centerC.GetY() + sideLength / 2;
+    ScenePoint2D LTC(handleLX, handleTY);
+    ScenePoint2D RTC(handleRX, handleTY);
+    ScenePoint2D RBC(handleRX, handleBY);
+    ScenePoint2D LBC(handleLX, handleBY);
+
+    ScenePoint2D startLT = LTC.Apply(scene.GetCanvasToSceneTransform());
+    ScenePoint2D startRT = RTC.Apply(scene.GetCanvasToSceneTransform());
+    ScenePoint2D startRB = RBC.Apply(scene.GetCanvasToSceneTransform());
+    ScenePoint2D startLB = LBC.Apply(scene.GetCanvasToSceneTransform());
+
+    chain.push_back(startLT);
+    chain.push_back(startRT);
+    chain.push_back(startRB);
+    chain.push_back(startLB);
+}
+#endif
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/MeasureToolsToolbox.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,173 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include <Framework/Scene2D/PolylineSceneLayer.h>
+#include <Framework/Scene2D/Scene2D.h>
+
+namespace OrthancStone
+{
+
+  /**
+  This function will create a square around the center point supplied in
+  scene coordinates, with a side length given in canvas coordinates. The
+  square sides are parallel to the canvas boundaries.
+  */
+  void AddSquare(PolylineSceneLayer::Chain& chain,
+    const Scene2D&      scene,
+    const ScenePoint2D& centerS,
+    const double&       sideLength);
+
+
+  /**
+    Creates an arc centered on c that goes
+    - from a point r1:
+      - so that r1 belongs to the p1,c line
+      - so that the distance from c to r1 equals radius
+    - to a point r2:
+      - so that r2 belongs to the p2,c line
+      - so that the distance from c to r2 equals radius
+    - that follows the shortest among the two possible paths
+
+    Warning: the existing chain content will be wiped out.
+  */
+  void AddShortestArc(
+      PolylineSceneLayer::Chain&  chain
+    , const Scene2D&              scene
+    , const ScenePoint2D&         p1
+    , const ScenePoint2D&         c
+    , const ScenePoint2D&         p2
+    , const double&               radiusS
+    , const int                   subdivisionsCount = 63);
+
+  /**
+    Creates an arc (open curve) with "numSubdivisions" (N + 1 points) from 
+    start angle to end angle, by following the shortest arc.
+
+    Warning: the existing chain content will be wiped out.
+  */
+  void AddShortestArc(
+      PolylineSceneLayer::Chain&  chain
+    , const Scene2D&              scene
+    , const ScenePoint2D&         centerS
+    , const double&               radiusS
+    , const double                startAngleRad
+    , const double                endAngleRad
+    , const int                   subdivisionsCount = 63);
+
+#if 0
+  /**
+    Creates an arc centered on c that goes
+    - from a point r1:
+      - so that r1 belongs to the p1,c line
+      - so that the distance from c to r1 equals radius
+    - to a point r2:
+      - so that r2 belongs to the p2,c line
+      - so that the distance from c to r2 equals radius
+
+    if clockwise is true, the arc is drawn from r1 to r2 with increasing 
+    angle values. Otherwise, the angle values decrease.
+
+    Warning: the existing chain content will be wiped out.
+  */
+
+  void AddArc(
+      PolylineSceneLayer::Chain& chain
+    , const Scene2D&             scene
+    , const ScenePoint2D&        p1
+    , const ScenePoint2D&        c
+    , const ScenePoint2D&        p2
+    , const double&              radiusS
+    , const bool                 clockwise
+    , const int                  subdivisionsCount = 63);
+ 
+  /**
+    Creates an arc (open curve) with "numSubdivisions" (N + 1 points) from 
+    start angle to end angle with the supplied radius.
+
+    if clockwise is true, the arc is drawn from start to end by increasing the
+    angle values.
+
+    Otherwise, the angle value decreases from start to end.
+
+    Warning: the existing chain content will be wiped out.
+  */
+  void AddArc(
+      PolylineSceneLayer::Chain& chain
+    , const Scene2D&      scene
+    , const ScenePoint2D& centerS
+    , const double&       radiusS
+    , const double        startAngleRad
+    , const double        endAngleRad
+    , const bool          clockwise
+    , const int           subdivisionsCount = 63);
+#endif
+  /**
+    Creates a circle (closed curve) with "numSubdivisions"
+    (N points)
+
+    Warning: the existing chain content will be wiped out.
+  */
+  void AddCircle(PolylineSceneLayer::Chain& chain,
+    const Scene2D&      scene,
+    const ScenePoint2D& centerS,
+    const double&       radiusS,
+    const int           numSubdivisions = 63);
+
+  /**
+    Adds or subtracts 2*pi as many times as need to shift the specified
+    angle to a value such as: -pi <= value < pi
+   */
+  double NormalizeAngle(double angle);
+
+  /**
+    Returns the angle magnitude between the p1,c and p2,c lines. 
+    The returned angle is between 0 and 2*pi
+
+    If the angle is between 0 and pi, this means that the shortest arc 
+    from p1 to p2 is clockwise.
+
+    If the angle is between pi and 2*pi, this means that the shortest arc
+    from p1 to p2 is COUNTERclockwise.
+
+  */
+  double MeasureAngle(
+      const ScenePoint2D& p1
+    , const ScenePoint2D& c
+    , const ScenePoint2D& p2);
+
+  /**
+  RadiansToDegrees
+  */
+  double RadiansToDegrees(double angleRad);
+
+  /**
+  This function will return the coordinates of a point that:
+  - belongs to the two bisecting lines of the p1 c p2 angle.
+  - is a distance d from c.
+  Among the four possible points, the one returned will be the one belonging
+  to the *smallest* half-plane defined by the [c,p1[ and [c,p2[ half-lines.
+  */
+  void GetPositionOnBisectingLine(
+      ScenePoint2D&       result
+    , const ScenePoint2D& p1
+    , const ScenePoint2D& c
+    , const ScenePoint2D& p2
+    , const double d);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/MeasureTrackers.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,65 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "MeasureTrackers.h"
+#include <Core/OrthancException.h>
+
+using namespace Orthanc;
+
+namespace OrthancStone
+{
+
+  CreateMeasureTracker::CreateMeasureTracker(
+    Scene2D&                        scene,
+    std::vector<TrackerCommandPtr>& undoStack,
+    std::vector<MeasureToolPtr>&    measureTools)
+    : scene_(scene)
+    , active_(true)
+    , undoStack_(undoStack)
+    , measureTools_(measureTools)
+    , commitResult_(true)
+  {
+  }
+
+  void CreateMeasureTracker::Cancel()
+  {
+    commitResult_ = false;
+    active_ = false;
+  }
+
+  bool CreateMeasureTracker::IsActive() const
+  {
+    return active_;
+  }
+
+  CreateMeasureTracker::~CreateMeasureTracker()
+  {
+    // if the tracker completes successfully, we add the command
+    // to the undo stack
+
+    // otherwise, we simply undo it
+    if (commitResult_)
+      undoStack_.push_back(command_);
+    else
+      command_->Undo();
+  }
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Common/MeasureTrackers.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,57 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include "IFlexiblePointerTracker.h"
+#include "../../Framework/Scene2D/Scene2D.h"
+#include "../../Framework/Scene2D/PointerEvent.h"
+
+#include "MeasureTools.h"
+#include "MeasureCommands.h"
+
+#include <vector>
+
+namespace OrthancStone
+{
+  class CreateMeasureTracker : public IFlexiblePointerTracker
+  {
+  public:
+    virtual void Cancel() ORTHANC_OVERRIDE;
+    virtual bool IsActive() const ORTHANC_OVERRIDE;
+  protected:
+    CreateMeasureTracker(
+      Scene2D&                        scene,
+      std::vector<TrackerCommandPtr>& undoStack,
+      std::vector<MeasureToolPtr>&    measureTools);
+
+    ~CreateMeasureTracker();
+  
+  protected:
+    CreateMeasureCommandPtr         command_;
+    Scene2D&                        scene_;
+    bool                            active_;
+  private:
+    std::vector<TrackerCommandPtr>& undoStack_;
+    std::vector<MeasureToolPtr>&    measureTools_;
+    bool                            commitResult_;
+  };
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/BasicScene.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,377 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+// From Stone
+#include "../../Applications/Sdl/SdlOpenGLWindow.h"
+#include "../../Framework/Scene2D/CairoCompositor.h"
+#include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
+#include "../../Framework/Scene2D/OpenGLCompositor.h"
+#include "../../Framework/Scene2D/PanSceneTracker.h"
+#include "../../Framework/Scene2D/RotateSceneTracker.h"
+#include "../../Framework/Scene2D/Scene2D.h"
+#include "../../Framework/Scene2D/ZoomSceneTracker.h"
+#include "../../Framework/StoneInitialization.h"
+#include "../../Framework/Messages/MessageBroker.h"
+
+// From Orthanc framework
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Core/Images/PngWriter.h>
+
+#include <SDL.h>
+#include <stdio.h>
+
+static const unsigned int FONT_SIZE = 32;
+static const int LAYER_POSITION = 150;
+
+
+void PrepareScene(OrthancStone::Scene2D& scene)
+{
+  using namespace OrthancStone;
+
+  // Texture of 2x2 size
+  {
+    Orthanc::Image i(Orthanc::PixelFormat_RGB24, 2, 2, false);
+    
+    uint8_t *p = reinterpret_cast<uint8_t*>(i.GetRow(0));
+    p[0] = 255;
+    p[1] = 0;
+    p[2] = 0;
+
+    p[3] = 0;
+    p[4] = 255;
+    p[5] = 0;
+
+    p = reinterpret_cast<uint8_t*>(i.GetRow(1));
+    p[0] = 0;
+    p[1] = 0;
+    p[2] = 255;
+
+    p[3] = 255;
+    p[4] = 0;
+    p[5] = 0;
+
+    scene.SetLayer(12, new ColorTextureSceneLayer(i));
+
+    std::auto_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
+    l->SetOrigin(-3, 2);
+    l->SetPixelSpacing(1.5, 1);
+    l->SetAngle(20.0 / 180.0 * M_PI);
+    scene.SetLayer(14, l.release());
+  }
+
+  // Texture of 1x1 size
+  {
+    Orthanc::Image i(Orthanc::PixelFormat_RGB24, 1, 1, false);
+    
+    uint8_t *p = reinterpret_cast<uint8_t*>(i.GetRow(0));
+    p[0] = 255;
+    p[1] = 0;
+    p[2] = 0;
+
+    std::auto_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
+    l->SetOrigin(-2, 1);
+    l->SetAngle(20.0 / 180.0 * M_PI);
+    scene.SetLayer(13, l.release());
+  }
+
+  // Some lines
+  {
+    std::auto_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer);
+
+    layer->SetThickness(1);
+
+    PolylineSceneLayer::Chain chain;
+    chain.push_back(ScenePoint2D(0 - 0.5, 0 - 0.5));
+    chain.push_back(ScenePoint2D(0 - 0.5, 2 - 0.5));
+    chain.push_back(ScenePoint2D(2 - 0.5, 2 - 0.5));
+    chain.push_back(ScenePoint2D(2 - 0.5, 0 - 0.5));
+    layer->AddChain(chain, true);
+
+    chain.clear();
+    chain.push_back(ScenePoint2D(-5, -5));
+    chain.push_back(ScenePoint2D(5, -5));
+    chain.push_back(ScenePoint2D(5, 5));
+    chain.push_back(ScenePoint2D(-5, 5));
+    layer->AddChain(chain, true);
+
+    double dy = 1.01;
+    chain.clear();
+    chain.push_back(ScenePoint2D(-4, -4));
+    chain.push_back(ScenePoint2D(4, -4 + dy));
+    chain.push_back(ScenePoint2D(-4, -4 + 2.0 * dy));
+    chain.push_back(ScenePoint2D(4, 2));
+    layer->AddChain(chain, false);
+
+    layer->SetColor(0,255, 255);
+    scene.SetLayer(50, layer.release());
+  }
+
+  // Some text
+  {
+    std::auto_ptr<TextSceneLayer> layer(new TextSceneLayer);
+    layer->SetText("Hello");
+    scene.SetLayer(100, layer.release());
+  }
+}
+
+
+void TakeScreenshot(const std::string& target,
+                    const OrthancStone::Scene2D& scene,
+                    unsigned int canvasWidth,
+                    unsigned int canvasHeight)
+{
+  // Take a screenshot, then save it as PNG file
+  OrthancStone::CairoCompositor compositor(scene, canvasWidth, canvasHeight);
+  compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, FONT_SIZE, Orthanc::Encoding_Latin1);
+  compositor.Refresh();
+
+  Orthanc::ImageAccessor canvas;
+  compositor.GetCanvas().GetReadOnlyAccessor(canvas);
+
+  Orthanc::Image png(Orthanc::PixelFormat_RGB24, canvas.GetWidth(), canvas.GetHeight(), false);
+  Orthanc::ImageProcessing::Convert(png, canvas);
+        
+  Orthanc::PngWriter writer;
+  writer.WriteToFile(target, png);
+}
+
+
+void HandleApplicationEvent(OrthancStone::Scene2D& scene,
+                            const OrthancStone::OpenGLCompositor& compositor,
+                            const SDL_Event& event,
+                            std::auto_ptr<OrthancStone::IPointerTracker>& activeTracker)
+{
+  if (event.type == SDL_MOUSEMOTION)
+  {
+    int scancodeCount = 0;
+    const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
+
+    if (activeTracker.get() == NULL &&
+        SDL_SCANCODE_LCTRL < scancodeCount &&
+        keyboardState[SDL_SCANCODE_LCTRL])
+    {
+      // The "left-ctrl" key is down, while no tracker is present
+
+      OrthancStone::PointerEvent e;
+      e.AddPosition(compositor.GetPixelCenterCoordinates(event.button.x, event.button.y));
+
+      OrthancStone::ScenePoint2D p = e.GetMainPosition().Apply(scene.GetCanvasToSceneTransform());
+
+      char buf[64];
+      sprintf(buf, "(%0.02f,%0.02f)", p.GetX(), p.GetY());
+
+      if (scene.HasLayer(LAYER_POSITION))
+      {
+        OrthancStone::TextSceneLayer& layer =
+          dynamic_cast<OrthancStone::TextSceneLayer&>(scene.GetLayer(LAYER_POSITION));
+        layer.SetText(buf);
+        layer.SetPosition(p.GetX(), p.GetY());
+      }
+      else
+      {
+        std::auto_ptr<OrthancStone::TextSceneLayer> layer(new OrthancStone::TextSceneLayer);
+        layer->SetColor(0, 255, 0);
+        layer->SetText(buf);
+        layer->SetBorder(20);
+        layer->SetAnchor(OrthancStone::BitmapAnchor_BottomCenter);
+        layer->SetPosition(p.GetX(), p.GetY());
+        scene.SetLayer(LAYER_POSITION, layer.release());
+      }
+    }
+    else
+    {
+      scene.DeleteLayer(LAYER_POSITION);
+    }
+  }
+  else if (event.type == SDL_MOUSEBUTTONDOWN)
+  {
+    OrthancStone::PointerEvent e;
+    e.AddPosition(compositor.GetPixelCenterCoordinates(event.button.x, event.button.y));
+
+    switch (event.button.button)
+    {
+      case SDL_BUTTON_MIDDLE:
+        activeTracker.reset(new OrthancStone::PanSceneTracker(scene, e));
+        break;
+
+      case SDL_BUTTON_RIGHT:
+        activeTracker.reset(new OrthancStone::ZoomSceneTracker(scene, e, 
+                                                               compositor.GetCanvasHeight()));
+        break;
+
+      case SDL_BUTTON_LEFT:
+        activeTracker.reset(new OrthancStone::RotateSceneTracker(scene, e));
+        break;
+
+      default:
+        break;
+    }
+  }
+  else if (event.type == SDL_KEYDOWN &&
+           event.key.repeat == 0 /* Ignore key bounce */)
+  {
+    switch (event.key.keysym.sym)
+    {
+      case SDLK_s:
+        scene.FitContent(compositor.GetCanvasWidth(), 
+                         compositor.GetCanvasHeight());
+        break;
+              
+      case SDLK_c:
+        TakeScreenshot("screenshot.png", scene, 
+                       compositor.GetCanvasWidth(), 
+                       compositor.GetCanvasHeight());
+        break;
+              
+      default:
+        break;
+    }
+  }
+}
+
+
+static void GLAPIENTRY
+OpenGLMessageCallback(GLenum source,
+                      GLenum type,
+                      GLuint id,
+                      GLenum severity,
+                      GLsizei length,
+                      const GLchar* message,
+                      const void* userParam )
+{
+  if (severity != GL_DEBUG_SEVERITY_NOTIFICATION)
+  {
+    fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
+            ( type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : "" ),
+            type, severity, message );
+  }
+}
+
+
+void Run(OrthancStone::Scene2D& scene)
+{
+  OrthancStone::SdlOpenGLWindow window("Hello", 1024, 768);
+
+  scene.FitContent(window.GetCanvasWidth(), window.GetCanvasHeight());
+  
+  glEnable(GL_DEBUG_OUTPUT);
+  glDebugMessageCallback(OpenGLMessageCallback, 0);
+
+  OrthancStone::OpenGLCompositor compositor(window, scene);
+  compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, 
+                     FONT_SIZE, Orthanc::Encoding_Latin1);
+
+  std::auto_ptr<OrthancStone::IPointerTracker>  tracker;
+
+  bool stop = false;
+  while (!stop)
+  {
+    compositor.Refresh();
+
+    SDL_Event event;
+    while (!stop &&
+           SDL_PollEvent(&event))
+    {
+      if (event.type == SDL_QUIT)
+      {
+        stop = true;
+        break;
+      }
+      else if (event.type == SDL_MOUSEMOTION)
+      {
+        if (tracker.get() != NULL)
+        {
+          OrthancStone::PointerEvent e;
+          e.AddPosition(compositor.GetPixelCenterCoordinates(event.button.x, event.button.y));
+          tracker->Update(e);
+        }
+      }
+      else if (event.type == SDL_MOUSEBUTTONUP)
+      {
+        if (tracker.get() != NULL)
+        {
+          tracker->Release();
+          tracker.reset(NULL);
+        }
+      }
+      else if (event.type == SDL_WINDOWEVENT &&
+               event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
+      {
+        tracker.reset(NULL);
+        compositor.UpdateSize();
+      }
+      else if (event.type == SDL_KEYDOWN &&
+               event.key.repeat == 0 /* Ignore key bounce */)
+      {
+        switch (event.key.keysym.sym)
+        {
+          case SDLK_f:
+            window.GetWindow().ToggleMaximize();
+            break;
+              
+          case SDLK_q:
+            stop = true;
+            break;
+
+          default:
+            break;
+        }
+      }
+      
+      HandleApplicationEvent(scene, compositor, event, tracker);
+    }
+
+    SDL_Delay(1);
+  }
+}
+
+
+
+
+/**
+ * IMPORTANT: The full arguments to "main()" are needed for SDL on
+ * Windows. Otherwise, one gets the linking error "undefined reference
+ * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
+ **/
+int main(int argc, char* argv[])
+{
+  OrthancStone::StoneInitialize();
+  Orthanc::Logging::EnableInfoLevel(true);
+
+  try
+  {
+    OrthancStone::MessageBroker broker;
+    OrthancStone::Scene2D scene(broker);
+    PrepareScene(scene);
+    Run(scene);
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "EXCEPTION: " << e.What();
+  }
+
+  OrthancStone::StoneFinalize();
+
+  return 0;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/CMakeLists.txt	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,128 @@
+cmake_minimum_required(VERSION 2.8.3)
+
+#####################################################################
+## Configuration of the Orthanc framework
+#####################################################################
+
+# This CMake file defines the "ORTHANC_STONE_VERSION" macro, so it
+# must be the first inclusion
+include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/Version.cmake)
+
+if (ORTHANC_STONE_VERSION STREQUAL "mainline")
+  set(ORTHANC_FRAMEWORK_VERSION "mainline")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
+else()
+  set(ORTHANC_FRAMEWORK_VERSION "1.5.7")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
+endif()
+
+set(ORTHANC_FRAMEWORK_SOURCE "${ORTHANC_FRAMEWORK_DEFAULT_SOURCE}" CACHE STRING "Source of the Orthanc source code (can be \"hg\", \"archive\", \"web\" or \"path\")")
+set(ORTHANC_FRAMEWORK_ARCHIVE "" CACHE STRING "Path to the Orthanc archive, if ORTHANC_FRAMEWORK_SOURCE is \"archive\"")
+set(ORTHANC_FRAMEWORK_ROOT "" CACHE STRING "Path to the Orthanc source directory, if ORTHANC_FRAMEWORK_SOURCE is \"path\"")
+
+
+#####################################################################
+## Configuration of the Stone framework
+#####################################################################
+
+include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/OrthancStoneParameters.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake)
+
+DownloadPackage(
+  "a24b8136b8f3bb93f166baf97d9328de"
+  "http://orthanc.osimis.io/ThirdPartyDownloads/ubuntu-font-family-0.83.zip"
+  "${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83")
+
+set(ORTHANC_STONE_APPLICATION_RESOURCES
+  UBUNTU_FONT  ${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83/Ubuntu-R.ttf
+  )
+
+SET(ENABLE_SDL_CONSOLE OFF CACHE BOOL "Enable the use of the MIT-licensed SDL_Console")
+SET(ENABLE_GOOGLE_TEST OFF)
+SET(ENABLE_LOCALE ON)
+SET(ENABLE_SDL ON)
+SET(ENABLE_WEB_CLIENT ON)
+SET(ORTHANC_SANDBOXED OFF)
+LIST(APPEND ORTHANC_BOOST_COMPONENTS program_options)
+
+include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/OrthancStoneConfiguration.cmake)
+
+#####################################################################
+## Build the samples
+#####################################################################
+
+add_library(OrthancStone STATIC
+  ${ORTHANC_STONE_SOURCES}
+  )
+
+add_executable(BasicScene
+  BasicScene.cpp
+  )
+
+target_link_libraries(BasicScene OrthancStone)
+
+if(ENABLE_SDL_CONSOLE)
+  add_definitions(
+    -DENABLE_SDL_CONSOLE=1
+    )
+  LIST(APPEND TRACKERSAMPLE_SOURCE "../../../SDL-Console/SDL_Console.c")
+  LIST(APPEND TRACKERSAMPLE_SOURCE "../../../SDL-Console/SDL_Console.h")
+endif()
+
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/AngleMeasureTool.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/AngleMeasureTool.h")
+
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/CreateAngleMeasureTracker.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/CreateAngleMeasureTracker.h")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/CreateCircleMeasureTracker.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/CreateCircleMeasureTracker.h")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/CreateLineMeasureTracker.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/CreateLineMeasureTracker.h")
+
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/CreateMeasureTracker.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/CreateMeasureTracker.h")
+
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/CreateSimpleTrackerAdapter.cpp")
+
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/EditAngleMeasureTracker.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/EditAngleMeasureTracker.h")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/EditCircleMeasureTracker.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/EditCircleMeasureTracker.h")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/EditLineMeasureTracker.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/EditLineMeasureTracker.h")
+
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/IFlexiblePointerTracker.h")
+
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/LineMeasureTool.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/LineMeasureTool.h")
+
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/MeasureCommands.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/MeasureCommands.h")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/MeasureTools.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/MeasureTools.h")
+
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/MeasureToolsToolbox.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/MeasureToolsToolbox.h")
+
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/MeasureTrackers.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "../Common/MeasureTrackers.h")
+
+LIST(APPEND TRACKERSAMPLE_SOURCE "TrackerSample.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "TrackerSampleApp.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "TrackerSampleApp.h")
+
+if (MSVC AND MSVC_VERSION GREATER 1700)
+  LIST(APPEND TRACKERSAMPLE_SOURCE "cpp.hint")
+endif()
+
+add_executable(TrackerSample
+  ${TRACKERSAMPLE_SOURCE}
+  )
+
+target_link_libraries(TrackerSample OrthancStone)
+
+add_executable(Loader
+  Loader.cpp
+  )
+
+target_link_libraries(Loader OrthancStone)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/Loader.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,1962 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+// From Stone
+#include "../../Framework/Messages/ICallable.h"
+#include "../../Framework/Messages/IMessage.h"
+#include "../../Framework/Messages/IObservable.h"
+#include "../../Framework/Messages/MessageBroker.h"
+#include "../../Framework/StoneInitialization.h"
+#include "../../Framework/Toolbox/GeometryToolbox.h"
+#include "../../Framework/Volumes/ImageBuffer3D.h"
+#include "../../Framework/Toolbox/SlicesSorter.h"
+
+// From Orthanc framework
+#include <Core/Compression/GzipCompressor.h>
+#include <Core/Compression/ZlibCompressor.h>
+#include <Core/DicomFormat/DicomArray.h>
+#include <Core/DicomFormat/DicomImageInformation.h>
+#include <Core/HttpClient.h>
+#include <Core/IDynamicObject.h>
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Core/Images/JpegReader.h>
+#include <Core/Images/PamReader.h>
+#include <Core/Images/PngReader.h>
+#include <Core/Images/PngWriter.h>
+#include <Core/Logging.h>
+#include <Core/MultiThreading/SharedMessageQueue.h>
+#include <Core/OrthancException.h>
+#include <Core/Toolbox.h>
+#include <Core/SystemToolbox.h>
+
+#include <json/reader.h>
+#include <json/value.h>
+#include <json/writer.h>
+
+#include <list>
+#include <stdio.h>
+
+
+
+namespace Refactoring
+{
+  class IOracleCommand : public boost::noncopyable
+  {
+  public:
+    enum Type
+    {
+      Type_OrthancRestApi,
+      Type_GetOrthancImage,
+      Type_GetOrthancWebViewerJpeg
+    };
+
+    virtual ~IOracleCommand()
+    {
+    }
+
+    virtual Type GetType() const = 0;
+  };
+
+
+  class IMessageEmitter : public boost::noncopyable
+  {
+  public:
+    virtual ~IMessageEmitter()
+    {
+    }
+
+    virtual void EmitMessage(const OrthancStone::IObserver& observer,
+                             const OrthancStone::IMessage& message) = 0;
+  };
+
+
+  class IOracle : public boost::noncopyable
+  {
+  public:
+    virtual ~IOracle()
+    {
+    }
+
+    virtual void Schedule(const OrthancStone::IObserver& receiver,
+                          IOracleCommand* command) = 0;  // Takes ownership
+  };
+
+
+
+
+  class OracleCommandWithPayload : public IOracleCommand
+  {
+  private:
+    std::auto_ptr<Orthanc::IDynamicObject>  payload_;
+
+  public:
+    void SetPayload(Orthanc::IDynamicObject* payload)
+    {
+      if (payload == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+      else
+      {
+        payload_.reset(payload);
+      }    
+    }
+
+    bool HasPayload() const
+    {
+      return (payload_.get() != NULL);
+    }
+
+    const Orthanc::IDynamicObject& GetPayload() const
+    {
+      if (HasPayload())
+      {
+        return *payload_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+  };
+
+
+
+  class OracleCommandExceptionMessage : public OrthancStone::IMessage
+  {
+    ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+
+  private:
+    const IOracleCommand&       command_;
+    Orthanc::OrthancException   exception_;
+
+  public:
+    OracleCommandExceptionMessage(const IOracleCommand& command,
+                                  const Orthanc::OrthancException& exception) :
+      command_(command),
+      exception_(exception)
+    {
+    }
+
+    OracleCommandExceptionMessage(const IOracleCommand& command,
+                                  const Orthanc::ErrorCode& error) :
+      command_(command),
+      exception_(error)
+    {
+    }
+
+    const IOracleCommand& GetCommand() const
+    {
+      return command_;
+    }
+    
+    const Orthanc::OrthancException& GetException() const
+    {
+      return exception_;
+    }
+  };
+  
+
+  typedef std::map<std::string, std::string>  HttpHeaders;
+
+  class OrthancRestApiCommand : public OracleCommandWithPayload
+  {
+  public:
+    class SuccessMessage : public OrthancStone::OriginMessage<OrthancRestApiCommand>
+    {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+      
+    private:
+      HttpHeaders   headers_;
+      std::string   answer_;
+
+    public:
+      SuccessMessage(const OrthancRestApiCommand& command,
+                     const HttpHeaders& answerHeaders,
+                     std::string& answer  /* will be swapped to avoid a memcpy() */) :
+        OriginMessage(command),
+        headers_(answerHeaders),
+        answer_(answer)
+      {
+      }
+
+      const std::string& GetAnswer() const
+      {
+        return answer_;
+      }
+
+      void ParseJsonBody(Json::Value& target) const
+      {
+        Json::Reader reader;
+        if (!reader.parse(answer_, target))
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+      }
+
+      const HttpHeaders&  GetAnswerHeaders() const
+      {
+        return headers_;
+      }
+    };
+
+
+  private:
+    Orthanc::HttpMethod  method_;
+    std::string          uri_;
+    std::string          body_;
+    HttpHeaders          headers_;
+    unsigned int         timeout_;
+
+    std::auto_ptr< OrthancStone::MessageHandler<SuccessMessage> >  successCallback_;
+    std::auto_ptr< OrthancStone::MessageHandler<OracleCommandExceptionMessage> >  failureCallback_;
+
+  public:
+    OrthancRestApiCommand() :
+      method_(Orthanc::HttpMethod_Get),
+      uri_("/"),
+      timeout_(10)
+    {
+    }
+
+    virtual Type GetType() const
+    {
+      return Type_OrthancRestApi;
+    }
+
+    void SetMethod(Orthanc::HttpMethod method)
+    {
+      method_ = method;
+    }
+
+    void SetUri(const std::string& uri)
+    {
+      uri_ = uri;
+    }
+
+    void SetBody(const std::string& body)
+    {
+      body_ = body;
+    }
+
+    void SetBody(const Json::Value& json)
+    {
+      Json::FastWriter writer;
+      body_ = writer.write(json);
+    }
+
+    void SetHttpHeaders(const HttpHeaders& headers)
+    {
+      headers_ = headers;
+    }
+
+    void SetHttpHeader(const std::string& key,
+                       const std::string& value)
+    {
+      headers_[key] = value;
+    }
+
+    Orthanc::HttpMethod GetMethod() const
+    {
+      return method_;
+    }
+
+    const std::string& GetUri() const
+    {
+      return uri_;
+    }
+
+    const std::string& GetBody() const
+    {
+      if (method_ == Orthanc::HttpMethod_Post ||
+          method_ == Orthanc::HttpMethod_Put)
+      {
+        return body_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    const HttpHeaders& GetHttpHeaders() const
+    {
+      return headers_;
+    }
+
+    void SetTimeout(unsigned int seconds)
+    {
+      timeout_ = seconds;
+    }
+
+    unsigned int GetTimeout() const
+    {
+      return timeout_;
+    }
+  };
+
+
+
+
+  class GetOrthancImageCommand : public OracleCommandWithPayload
+  {
+  public:
+    class SuccessMessage : public OrthancStone::OriginMessage<GetOrthancImageCommand>
+    {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+      
+    private:
+      std::auto_ptr<Orthanc::ImageAccessor>  image_;
+      Orthanc::MimeType                      mime_;
+
+    public:
+      SuccessMessage(const GetOrthancImageCommand& command,
+                     Orthanc::ImageAccessor* image,   // Takes ownership
+                     Orthanc::MimeType mime) :
+        OriginMessage(command),
+        image_(image),
+        mime_(mime)
+      {
+        if (image == NULL)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+        }
+      }
+
+      const Orthanc::ImageAccessor& GetImage() const
+      {
+        return *image_;
+      }
+
+      Orthanc::MimeType GetMimeType() const
+      {
+        return mime_;
+      }
+    };
+
+
+  private:
+    std::string    uri_;
+    HttpHeaders    headers_;
+    unsigned int   timeout_;
+
+    std::auto_ptr< OrthancStone::MessageHandler<SuccessMessage> >  successCallback_;
+    std::auto_ptr< OrthancStone::MessageHandler<OracleCommandExceptionMessage> >  failureCallback_;
+
+  public:
+    GetOrthancImageCommand() :
+      uri_("/"),
+      timeout_(10)
+    {
+    }
+
+    virtual Type GetType() const
+    {
+      return Type_GetOrthancImage;
+    }
+
+    void SetUri(const std::string& uri)
+    {
+      uri_ = uri;
+    }
+
+    void SetHttpHeader(const std::string& key,
+                       const std::string& value)
+    {
+      headers_[key] = value;
+    }
+
+    const std::string& GetUri() const
+    {
+      return uri_;
+    }
+
+    const HttpHeaders& GetHttpHeaders() const
+    {
+      return headers_;
+    }
+
+    void SetTimeout(unsigned int seconds)
+    {
+      timeout_ = seconds;
+    }
+
+    unsigned int GetTimeout() const
+    {
+      return timeout_;
+    }
+
+    void ProcessHttpAnswer(IMessageEmitter& emitter,
+                           const OrthancStone::IObserver& receiver,
+                           const std::string& answer,
+                           const HttpHeaders& answerHeaders) const
+    {
+      Orthanc::MimeType contentType = Orthanc::MimeType_Binary;
+
+      for (HttpHeaders::const_iterator it = answerHeaders.begin(); 
+           it != answerHeaders.end(); ++it)
+      {
+        std::string s;
+        Orthanc::Toolbox::ToLowerCase(s, it->first);
+
+        if (s == "content-type")
+        {
+          contentType = Orthanc::StringToMimeType(it->second);
+          break;
+        }
+      }
+
+      std::auto_ptr<Orthanc::ImageAccessor> image;
+
+      switch (contentType)
+      {
+        case Orthanc::MimeType_Png:
+        {
+          image.reset(new Orthanc::PngReader);
+          dynamic_cast<Orthanc::PngReader&>(*image).ReadFromMemory(answer);
+          break;
+        }
+
+        case Orthanc::MimeType_Pam:
+        {
+          image.reset(new Orthanc::PamReader);
+          dynamic_cast<Orthanc::PamReader&>(*image).ReadFromMemory(answer);
+          break;
+        }
+
+        case Orthanc::MimeType_Jpeg:
+        {
+          image.reset(new Orthanc::JpegReader);
+          dynamic_cast<Orthanc::JpegReader&>(*image).ReadFromMemory(answer);
+          break;
+        }
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol,
+                                          "Unsupported HTTP Content-Type for an image: " + 
+                                          std::string(Orthanc::EnumerationToString(contentType)));
+      }
+
+      SuccessMessage message(*this, image.release(), contentType);
+      emitter.EmitMessage(receiver, message);
+    }
+  };
+
+
+
+  class GetOrthancWebViewerJpegCommand : public OracleCommandWithPayload
+  {
+  public:
+    class SuccessMessage : public OrthancStone::OriginMessage<GetOrthancWebViewerJpegCommand>
+    {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+      
+    private:
+      std::auto_ptr<Orthanc::ImageAccessor>  image_;
+
+    public:
+      SuccessMessage(const GetOrthancWebViewerJpegCommand& command,
+                     Orthanc::ImageAccessor* image) :   // Takes ownership
+        OriginMessage(command),
+        image_(image)
+      {
+        if (image == NULL)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+        }
+      }
+
+      const Orthanc::ImageAccessor& GetImage() const
+      {
+        return *image_;
+      }
+    };
+
+  private:
+    std::string           instanceId_;
+    unsigned int          frame_;
+    unsigned int          quality_;
+    HttpHeaders           headers_;
+    unsigned int          timeout_;
+    Orthanc::PixelFormat  expectedFormat_;
+
+    std::auto_ptr< OrthancStone::MessageHandler<SuccessMessage> >  successCallback_;
+    std::auto_ptr< OrthancStone::MessageHandler<OracleCommandExceptionMessage> >  failureCallback_;
+
+  public:
+    GetOrthancWebViewerJpegCommand() :
+      frame_(0),
+      quality_(95),
+      timeout_(10),
+      expectedFormat_(Orthanc::PixelFormat_Grayscale8)
+    {
+    }
+
+    virtual Type GetType() const
+    {
+      return Type_GetOrthancWebViewerJpeg;
+    }
+
+    void SetExpectedFormat(Orthanc::PixelFormat format)
+    {
+      expectedFormat_ = format;
+    }
+
+    void SetInstance(const std::string& instanceId)
+    {
+      instanceId_ = instanceId;
+    }
+
+    void SetFrame(unsigned int frame)
+    {
+      frame_ = frame;
+    }
+
+    void SetQuality(unsigned int quality)
+    {
+      if (quality <= 0 ||
+          quality > 100)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+      else
+      {
+        quality_ = quality;
+      }
+    }
+
+    void SetHttpHeader(const std::string& key,
+                       const std::string& value)
+    {
+      headers_[key] = value;
+    }
+
+    Orthanc::PixelFormat GetExpectedFormat() const
+    {
+      return expectedFormat_;
+    }
+
+    const std::string& GetInstanceId() const
+    {
+      return instanceId_;
+    }
+
+    unsigned int GetFrame() const
+    {
+      return frame_;
+    }
+
+    unsigned int GetQuality() const
+    {
+      return quality_;
+    }
+
+    const HttpHeaders& GetHttpHeaders() const
+    {
+      return headers_;
+    }
+
+    void SetTimeout(unsigned int seconds)
+    {
+      timeout_ = seconds;
+    }
+
+    unsigned int GetTimeout() const
+    {
+      return timeout_;
+    }
+
+    std::string GetUri() const
+    {
+      return ("/web-viewer/instances/jpeg" + boost::lexical_cast<std::string>(quality_) +
+              "-" + instanceId_ + "_" + boost::lexical_cast<std::string>(frame_));
+    }
+
+    void ProcessHttpAnswer(IMessageEmitter& emitter,
+                           const OrthancStone::IObserver& receiver,
+                           const std::string& answer) const
+    {
+      // This code comes from older "OrthancSlicesLoader::ParseSliceImageJpeg()"
+      
+      Json::Value encoded;
+
+      {
+        Json::Reader reader;
+        if (!reader.parse(answer, encoded))
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+      }
+
+      if (encoded.type() != Json::objectValue ||
+          !encoded.isMember("Orthanc") ||
+          encoded["Orthanc"].type() != Json::objectValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+    
+      const Json::Value& info = encoded["Orthanc"];
+      if (!info.isMember("PixelData") ||
+          !info.isMember("Stretched") ||
+          !info.isMember("Compression") ||
+          info["Compression"].type() != Json::stringValue ||
+          info["PixelData"].type() != Json::stringValue ||
+          info["Stretched"].type() != Json::booleanValue ||
+          info["Compression"].asString() != "Jpeg")
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+    
+      bool isSigned = false;
+      bool isStretched = info["Stretched"].asBool();
+    
+      if (info.isMember("IsSigned"))
+      {
+        if (info["IsSigned"].type() != Json::booleanValue)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+        else
+        {
+          isSigned = info["IsSigned"].asBool();
+        }
+      }
+    
+      std::auto_ptr<Orthanc::ImageAccessor> reader;
+    
+      {
+        std::string jpeg;
+        Orthanc::Toolbox::DecodeBase64(jpeg, info["PixelData"].asString());
+      
+        reader.reset(new Orthanc::JpegReader);
+        dynamic_cast<Orthanc::JpegReader&>(*reader).ReadFromMemory(jpeg);
+      }
+    
+      if (reader->GetFormat() == Orthanc::PixelFormat_RGB24)  // This is a color image
+      {
+        if (expectedFormat_ != Orthanc::PixelFormat_RGB24)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+      
+        if (isSigned || isStretched)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+        else
+        {
+          SuccessMessage message(*this, reader.release());
+          emitter.EmitMessage(receiver, message);
+          return;
+        }
+      }
+    
+      if (reader->GetFormat() != Orthanc::PixelFormat_Grayscale8)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+    
+      if (!isStretched)
+      {
+        if (expectedFormat_ != reader->GetFormat())
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+        else
+        {
+          SuccessMessage message(*this, reader.release());
+          emitter.EmitMessage(receiver, message);
+          return;
+        }
+      }
+    
+      int32_t stretchLow = 0;
+      int32_t stretchHigh = 0;
+    
+      if (!info.isMember("StretchLow") ||
+          !info.isMember("StretchHigh") ||
+          info["StretchLow"].type() != Json::intValue ||
+          info["StretchHigh"].type() != Json::intValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+    
+      stretchLow = info["StretchLow"].asInt();
+      stretchHigh = info["StretchHigh"].asInt();
+    
+      if (stretchLow < -32768 ||
+          stretchHigh > 65535 ||
+          (stretchLow < 0 && stretchHigh > 32767))
+      {
+        // This range cannot be represented with a uint16_t or an int16_t
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+    
+      // Decode a grayscale JPEG 8bpp image coming from the Web viewer
+      std::auto_ptr<Orthanc::ImageAccessor> image
+        (new Orthanc::Image(expectedFormat_, reader->GetWidth(), reader->GetHeight(), false));
+
+      Orthanc::ImageProcessing::Convert(*image, *reader);
+      reader.reset();
+    
+      float scaling = static_cast<float>(stretchHigh - stretchLow) / 255.0f;
+    
+      if (!OrthancStone::LinearAlgebra::IsCloseToZero(scaling))
+      {
+        float offset = static_cast<float>(stretchLow) / scaling;
+        Orthanc::ImageProcessing::ShiftScale(*image, offset, scaling, true);
+      }
+    
+      SuccessMessage message(*this, image.release());
+      emitter.EmitMessage(receiver, message);
+    }
+  };
+
+
+
+
+
+  class NativeOracle : public IOracle
+  {
+  private:
+    class Item : public Orthanc::IDynamicObject
+    {
+    private:
+      const OrthancStone::IObserver&  receiver_;
+      std::auto_ptr<IOracleCommand>   command_;
+
+    public:
+      Item(const OrthancStone::IObserver& receiver,
+           IOracleCommand* command) :
+        receiver_(receiver),
+        command_(command)
+      {
+        if (command == NULL)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+        }
+      }
+
+      const OrthancStone::IObserver& GetReceiver() const
+      {
+        return receiver_;
+      }
+
+      const IOracleCommand& GetCommand() const
+      {
+        assert(command_.get() != NULL);
+        return *command_;
+      }
+    };
+
+
+    enum State
+    {
+      State_Setup,
+      State_Running,
+      State_Stopped
+    };
+
+
+    IMessageEmitter&               emitter_;
+    Orthanc::WebServiceParameters  orthanc_;
+    Orthanc::SharedMessageQueue    queue_;
+    State                          state_;
+    boost::mutex                   mutex_;
+    std::vector<boost::thread*>    workers_;
+
+
+    void CopyHttpHeaders(Orthanc::HttpClient& client,
+                         const HttpHeaders& headers)
+    {
+      for (HttpHeaders::const_iterator it = headers.begin(); it != headers.end(); it++ )
+      {
+        client.AddHeader(it->first, it->second);
+      }
+    }
+
+
+    void DecodeAnswer(std::string& answer,
+                      const HttpHeaders& headers)
+    {
+      Orthanc::HttpCompression contentEncoding = Orthanc::HttpCompression_None;
+
+      for (HttpHeaders::const_iterator it = headers.begin(); 
+           it != headers.end(); ++it)
+      {
+        std::string s;
+        Orthanc::Toolbox::ToLowerCase(s, it->first);
+
+        if (s == "content-encoding")
+        {
+          if (it->second == "gzip")
+          {
+            contentEncoding = Orthanc::HttpCompression_Gzip;
+          }
+          else 
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol,
+                                            "Unsupported HTTP Content-Encoding: " + it->second);
+          }
+
+          break;
+        }
+      }
+
+      if (contentEncoding == Orthanc::HttpCompression_Gzip)
+      {
+        std::string compressed;
+        answer.swap(compressed);
+          
+        Orthanc::GzipCompressor compressor;
+        compressor.Uncompress(answer, compressed.c_str(), compressed.size());
+      }
+    }
+
+
+    void Execute(const OrthancStone::IObserver& receiver,
+                 const OrthancRestApiCommand& command)
+    {
+      Orthanc::HttpClient client(orthanc_, command.GetUri());
+      client.SetMethod(command.GetMethod());
+      client.SetTimeout(command.GetTimeout());
+
+      CopyHttpHeaders(client, command.GetHttpHeaders());
+
+      if (command.GetMethod() == Orthanc::HttpMethod_Post ||
+          command.GetMethod() == Orthanc::HttpMethod_Put)
+      {
+        client.SetBody(command.GetBody());
+      }
+
+      std::string answer;
+      HttpHeaders answerHeaders;
+      client.ApplyAndThrowException(answer, answerHeaders);
+
+      DecodeAnswer(answer, answerHeaders);
+
+      OrthancRestApiCommand::SuccessMessage message(command, answerHeaders, answer);
+      emitter_.EmitMessage(receiver, message);
+    }
+
+
+    void Execute(const OrthancStone::IObserver& receiver,
+                 const GetOrthancImageCommand& command)
+    {
+      Orthanc::HttpClient client(orthanc_, command.GetUri());
+      client.SetTimeout(command.GetTimeout());
+
+      CopyHttpHeaders(client, command.GetHttpHeaders());
+
+      std::string answer;
+      HttpHeaders answerHeaders;
+      client.ApplyAndThrowException(answer, answerHeaders);
+
+      DecodeAnswer(answer, answerHeaders);
+
+      command.ProcessHttpAnswer(emitter_, receiver, answer, answerHeaders);
+    }
+
+
+    void Execute(const OrthancStone::IObserver& receiver,
+                 const GetOrthancWebViewerJpegCommand& command)
+    {
+      Orthanc::HttpClient client(orthanc_, command.GetUri());
+      client.SetTimeout(command.GetTimeout());
+
+      CopyHttpHeaders(client, command.GetHttpHeaders());
+
+      std::string answer;
+      HttpHeaders answerHeaders;
+      client.ApplyAndThrowException(answer, answerHeaders);
+
+      DecodeAnswer(answer, answerHeaders);
+
+      command.ProcessHttpAnswer(emitter_, receiver, answer);
+    }
+
+
+    void Step()
+    {
+      std::auto_ptr<Orthanc::IDynamicObject>  object(queue_.Dequeue(100));
+
+      if (object.get() != NULL)
+      {
+        const Item& item = dynamic_cast<Item&>(*object);
+
+        try
+        {
+          switch (item.GetCommand().GetType())
+          {
+            case IOracleCommand::Type_OrthancRestApi:
+              Execute(item.GetReceiver(), 
+                      dynamic_cast<const OrthancRestApiCommand&>(item.GetCommand()));
+              break;
+
+            case IOracleCommand::Type_GetOrthancImage:
+              Execute(item.GetReceiver(), 
+                      dynamic_cast<const GetOrthancImageCommand&>(item.GetCommand()));
+              break;
+
+            case IOracleCommand::Type_GetOrthancWebViewerJpeg:
+              Execute(item.GetReceiver(), 
+                      dynamic_cast<const GetOrthancWebViewerJpegCommand&>(item.GetCommand()));
+              break;
+
+            default:
+              throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+          }
+        }
+        catch (Orthanc::OrthancException& e)
+        {
+          LOG(ERROR) << "Exception within the oracle: " << e.What();
+          emitter_.EmitMessage(item.GetReceiver(), OracleCommandExceptionMessage(item.GetCommand(), e));
+        }
+        catch (...)
+        {
+          LOG(ERROR) << "Native exception within the oracle";
+          emitter_.EmitMessage(item.GetReceiver(), OracleCommandExceptionMessage
+                               (item.GetCommand(), Orthanc::ErrorCode_InternalError));
+        }
+      }
+    }
+
+
+    static void Worker(NativeOracle* that)
+    {
+      assert(that != NULL);
+      
+      for (;;)
+      {
+        {
+          boost::mutex::scoped_lock lock(that->mutex_);
+          if (that->state_ != State_Running)
+          {
+            return;
+          }
+        }
+
+        that->Step();
+      }
+    }
+
+
+    void StopInternal()
+    {
+      {
+        boost::mutex::scoped_lock lock(mutex_);
+
+        if (state_ == State_Setup ||
+            state_ == State_Stopped)
+        {
+          return;
+        }
+        else
+        {
+          state_ = State_Stopped;
+        }
+      }
+
+      for (size_t i = 0; i < workers_.size(); i++)
+      {
+        if (workers_[i] != NULL)
+        {
+          if (workers_[i]->joinable())
+          {
+            workers_[i]->join();
+          }
+
+          delete workers_[i];
+        }
+      } 
+    }
+
+
+  public:
+    NativeOracle(IMessageEmitter& emitter) :
+    emitter_(emitter),
+      state_(State_Setup),
+      workers_(4)
+    {
+    }
+
+    virtual ~NativeOracle()
+    {
+      StopInternal();
+    }
+
+    void SetOrthancParameters(const Orthanc::WebServiceParameters& orthanc)
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      if (state_ != State_Setup)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        orthanc_ = orthanc;
+      }
+    }
+
+    void SetWorkersCount(unsigned int count)
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      if (count <= 0)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+      else if (state_ != State_Setup)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        workers_.resize(count);
+      }
+    }
+
+    void Start()
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      if (state_ != State_Setup)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        state_ = State_Running;
+
+        for (unsigned int i = 0; i < workers_.size(); i++)
+        {
+          workers_[i] = new boost::thread(Worker, this);
+        }
+      }      
+    }
+
+    void Stop()
+    {
+      StopInternal();
+    }
+
+    virtual void Schedule(const OrthancStone::IObserver& receiver,
+                          IOracleCommand* command)
+    {
+      queue_.Enqueue(new Item(receiver, command));
+    }
+  };
+
+
+
+  class NativeApplicationContext : public IMessageEmitter
+  {
+  private:
+    boost::shared_mutex            mutex_;
+    OrthancStone::MessageBroker    broker_;
+    OrthancStone::IObservable      oracleObservable_;
+
+  public:
+    NativeApplicationContext() :
+      oracleObservable_(broker_)
+    {
+    }
+
+
+    virtual void EmitMessage(const OrthancStone::IObserver& observer,
+                             const OrthancStone::IMessage& message)
+    {
+      try
+      {
+        boost::unique_lock<boost::shared_mutex>  lock(mutex_);
+        oracleObservable_.EmitMessage(observer, message);
+      }
+      catch (Orthanc::OrthancException& e)
+      {
+        LOG(ERROR) << "Exception while emitting a message: " << e.What();
+      }
+    }
+
+
+    class ReaderLock : public boost::noncopyable
+    {
+    private:
+      NativeApplicationContext&                that_;
+      boost::shared_lock<boost::shared_mutex>  lock_;
+
+    public:
+      ReaderLock(NativeApplicationContext& that) : 
+      that_(that),
+      lock_(that.mutex_)
+      {
+      }
+    };
+
+
+    class WriterLock : public boost::noncopyable
+    {
+    private:
+      NativeApplicationContext&                that_;
+      boost::unique_lock<boost::shared_mutex>  lock_;
+
+    public:
+      WriterLock(NativeApplicationContext& that) : 
+      that_(that),
+      lock_(that.mutex_)
+      {
+      }
+
+      OrthancStone::MessageBroker& GetBroker() 
+      {
+        return that_.broker_;
+      }
+
+      OrthancStone::IObservable& GetOracleObservable()
+      {
+        return that_.oracleObservable_;
+      }
+    };
+  };
+
+
+
+  class DicomInstanceParameters :
+    public Orthanc::IDynamicObject  /* to be used as a payload of SlicesSorter */
+  {
+  private:
+    struct Data   // Struct to ease the copy constructor
+    {
+      Orthanc::DicomImageInformation    imageInformation_;
+      OrthancStone::SopClassUid         sopClassUid_;
+      double                            thickness_;
+      double                            pixelSpacingX_;
+      double                            pixelSpacingY_;
+      OrthancStone::CoordinateSystem3D  geometry_;
+      OrthancStone::Vector              frameOffsets_;
+      bool                              isColor_;
+      bool                              hasRescale_;
+      double                            rescaleOffset_;
+      double                            rescaleSlope_;
+      bool                              hasDefaultWindowing_;
+      float                             defaultWindowingCenter_;
+      float                             defaultWindowingWidth_;
+      Orthanc::PixelFormat              expectedPixelFormat_;
+
+      void ComputeDoseOffsets(const Orthanc::DicomMap& dicom)
+      {
+        // http://dicom.nema.org/medical/Dicom/2016a/output/chtml/part03/sect_C.8.8.3.2.html
+
+        {
+          std::string increment;
+
+          if (dicom.CopyToString(increment, Orthanc::DICOM_TAG_FRAME_INCREMENT_POINTER, false))
+          {
+            Orthanc::Toolbox::ToUpperCase(increment);
+            if (increment != "3004,000C")  // This is the "Grid Frame Offset Vector" tag
+            {
+              LOG(ERROR) << "RT-DOSE: Bad value for the \"FrameIncrementPointer\" tag";
+              return;
+            }
+          }
+        }
+
+        if (!OrthancStone::LinearAlgebra::ParseVector(frameOffsets_, dicom, Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR) ||
+            frameOffsets_.size() < imageInformation_.GetNumberOfFrames())
+        {
+          LOG(ERROR) << "RT-DOSE: No information about the 3D location of some slice(s)";
+          frameOffsets_.clear();
+        }
+        else
+        {
+          if (frameOffsets_.size() >= 2)
+          {
+            thickness_ = frameOffsets_[1] - frameOffsets_[0];
+
+            if (thickness_ < 0)
+            {
+              thickness_ = -thickness_;
+            }
+          }
+        }
+      }
+
+      Data(const Orthanc::DicomMap& dicom) :
+        imageInformation_(dicom)
+      {
+        if (imageInformation_.GetNumberOfFrames() <= 0)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+            
+        std::string s;
+        if (!dicom.CopyToString(s, Orthanc::DICOM_TAG_SOP_CLASS_UID, false))
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+        else
+        {
+          sopClassUid_ = OrthancStone::StringToSopClassUid(s);
+        }
+
+        if (!dicom.ParseDouble(thickness_, Orthanc::DICOM_TAG_SLICE_THICKNESS))
+        {
+          thickness_ = 100.0 * std::numeric_limits<double>::epsilon();
+        }
+
+        OrthancStone::GeometryToolbox::GetPixelSpacing(pixelSpacingX_, pixelSpacingY_, dicom);
+
+        std::string position, orientation;
+        if (dicom.CopyToString(position, Orthanc::DICOM_TAG_IMAGE_POSITION_PATIENT, false) &&
+            dicom.CopyToString(orientation, Orthanc::DICOM_TAG_IMAGE_ORIENTATION_PATIENT, false))
+        {
+          geometry_ = OrthancStone::CoordinateSystem3D(position, orientation);
+        }
+
+        if (sopClassUid_ == OrthancStone::SopClassUid_RTDose)
+        {
+          ComputeDoseOffsets(dicom);
+        }
+
+        isColor_ = (imageInformation_.GetPhotometricInterpretation() != Orthanc::PhotometricInterpretation_Monochrome1 &&
+                    imageInformation_.GetPhotometricInterpretation() != Orthanc::PhotometricInterpretation_Monochrome2);
+
+        double doseGridScaling;
+
+        if (dicom.ParseDouble(rescaleOffset_, Orthanc::DICOM_TAG_RESCALE_INTERCEPT) &&
+            dicom.ParseDouble(rescaleSlope_, Orthanc::DICOM_TAG_RESCALE_SLOPE))
+        {
+          hasRescale_ = true;
+        }
+        else if (dicom.ParseDouble(doseGridScaling, Orthanc::DICOM_TAG_DOSE_GRID_SCALING))
+        {
+          hasRescale_ = true;
+          rescaleOffset_ = 0;
+          rescaleSlope_ = doseGridScaling;
+        }
+        else
+        {
+          hasRescale_ = false;
+        }
+
+        OrthancStone::Vector c, w;
+        if (OrthancStone::LinearAlgebra::ParseVector(c, dicom, Orthanc::DICOM_TAG_WINDOW_CENTER) &&
+            OrthancStone::LinearAlgebra::ParseVector(w, dicom, Orthanc::DICOM_TAG_WINDOW_WIDTH) &&
+            c.size() > 0 && 
+            w.size() > 0)
+        {
+          hasDefaultWindowing_ = true;
+          defaultWindowingCenter_ = static_cast<float>(c[0]);
+          defaultWindowingWidth_ = static_cast<float>(w[0]);
+        }
+        else
+        {
+          hasDefaultWindowing_ = false;
+        }
+
+        if (sopClassUid_ == OrthancStone::SopClassUid_RTDose)
+        {
+          switch (imageInformation_.GetBitsStored())
+          {
+            case 16:
+              expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale16;
+              break;
+
+            case 32:
+              expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale32;
+              break;
+
+            default:
+              throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+          } 
+        }
+        else if (isColor_)
+        {
+          expectedPixelFormat_ = Orthanc::PixelFormat_RGB24;
+        }
+        else if (imageInformation_.IsSigned())
+        {
+          expectedPixelFormat_ = Orthanc::PixelFormat_SignedGrayscale16;
+        }
+        else
+        {
+          expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale16;
+        }
+      }
+
+      OrthancStone::CoordinateSystem3D  GetFrameGeometry(unsigned int frame) const
+      {
+        if (frame == 0)
+        {
+          return geometry_;
+        }
+        else if (frame >= imageInformation_.GetNumberOfFrames())
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+        else if (sopClassUid_ == OrthancStone::SopClassUid_RTDose)
+        {
+          if (frame >= frameOffsets_.size())
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+          }
+
+          return OrthancStone::CoordinateSystem3D(
+            geometry_.GetOrigin() + frameOffsets_[frame] * geometry_.GetNormal(),
+            geometry_.GetAxisX(),
+            geometry_.GetAxisY());
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+        }
+      }
+
+      // TODO - Is this necessary?
+      bool FrameContainsPlane(unsigned int frame,
+                              const OrthancStone::CoordinateSystem3D& plane) const
+      {
+        if (frame >= imageInformation_.GetNumberOfFrames())
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+
+        OrthancStone::CoordinateSystem3D tmp = geometry_;
+
+        if (frame != 0)
+        {
+          tmp = GetFrameGeometry(frame);
+        }
+
+        double distance;
+
+        return (OrthancStone::CoordinateSystem3D::GetDistance(distance, tmp, plane) &&
+                distance <= thickness_ / 2.0);
+      }
+    };
+    
+    Data  data_;
+
+
+  public:
+    DicomInstanceParameters(const DicomInstanceParameters& other) :
+      data_(other.data_)
+    {
+    }
+
+    
+    DicomInstanceParameters(const Orthanc::DicomMap& dicom) :
+      data_(dicom)
+    {
+    }
+
+    const Orthanc::DicomImageInformation& GetImageInformation() const
+    {
+      return data_.imageInformation_;
+    }
+
+    OrthancStone::SopClassUid GetSopClassUid() const
+    {
+      return data_.sopClassUid_;
+    }
+
+    double GetThickness() const
+    {
+      return data_.thickness_;
+    }
+
+    double GetPixelSpacingX() const
+    {
+      return data_.pixelSpacingX_;
+    }
+
+    double GetPixelSpacingY() const
+    {
+      return data_.pixelSpacingY_;
+    }
+
+    const OrthancStone::CoordinateSystem3D&  GetGeometry() const
+    {
+      return data_.geometry_;
+    }
+
+    OrthancStone::CoordinateSystem3D  GetFrameGeometry(unsigned int frame) const
+    {
+      return data_.GetFrameGeometry(frame);
+    }
+
+    // TODO - Is this necessary?
+    bool FrameContainsPlane(unsigned int frame,
+                            const OrthancStone::CoordinateSystem3D& plane) const
+    {
+      return data_.FrameContainsPlane(frame, plane);
+    }
+
+    bool IsColor() const
+    {
+      return data_.isColor_;
+    }
+
+    bool HasRescale() const
+    {
+      return data_.hasRescale_;
+    }
+
+    double GetRescaleOffset() const
+    {
+      if (data_.hasRescale_)
+      {
+        return data_.rescaleOffset_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    double GetRescaleSlope() const
+    {
+      if (data_.hasRescale_)
+      {
+        return data_.rescaleSlope_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    bool HasDefaultWindowing() const
+    {
+      return data_.hasDefaultWindowing_;
+    }
+
+    float GetDefaultWindowingCenter() const
+    {
+      if (data_.hasDefaultWindowing_)
+      {
+        return data_.defaultWindowingCenter_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    float GetDefaultWindowingWidth() const
+    {
+      if (data_.hasDefaultWindowing_)
+      {
+        return data_.defaultWindowingWidth_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    Orthanc::PixelFormat GetExpectedPixelFormat() const
+    {
+      return data_.expectedPixelFormat_;
+    }
+  };
+
+
+  class DicomVolumeImage : public boost::noncopyable
+  {
+  private:
+    std::auto_ptr<OrthancStone::ImageBuffer3D>  image_;
+    std::vector<DicomInstanceParameters*>       slices_;
+
+    static const DicomInstanceParameters&
+    GetSliceParameters(const OrthancStone::SlicesSorter& slices,
+                       size_t index)
+    {
+      return dynamic_cast<const DicomInstanceParameters&>(slices.GetSlicePayload(index));
+    }
+
+    static void CheckSlice(const OrthancStone::SlicesSorter& slices,
+                           size_t index,
+                           const OrthancStone::CoordinateSystem3D& reference,
+                           const DicomInstanceParameters& a)
+    {
+      const OrthancStone::CoordinateSystem3D& slice = slices.GetSliceGeometry(index);
+      const DicomInstanceParameters& b = GetSliceParameters(slices, index);
+      
+      if (!OrthancStone::GeometryToolbox::IsParallel(reference.GetNormal(), slice.GetNormal()))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
+                                        "A slice in the volume image is not parallel to the others");
+      }
+
+      if (a.GetExpectedPixelFormat() != b.GetExpectedPixelFormat())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat,
+                                        "The pixel format changes across the slices of the volume image");
+      }
+
+      if (a.GetImageInformation().GetWidth() != b.GetImageInformation().GetWidth() ||
+          a.GetImageInformation().GetHeight() != b.GetImageInformation().GetHeight())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize,
+                                        "The width/height of slices are not constant in the volume image");
+      }
+
+      if (!OrthancStone::LinearAlgebra::IsNear(a.GetPixelSpacingX(), b.GetPixelSpacingX()) ||
+          !OrthancStone::LinearAlgebra::IsNear(a.GetPixelSpacingY(), b.GetPixelSpacingY()))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
+                                        "The pixel spacing of the slices change across the volume image");
+      }
+    }
+
+    
+    static void CheckVolume(const OrthancStone::SlicesSorter& slices)
+    {
+      for (size_t i = 0; i < slices.GetSlicesCount(); i++)
+      {
+        if (GetSliceParameters(slices, i).GetImageInformation().GetNumberOfFrames() != 1)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
+                                          "This class does not support multi-frame images");
+        }
+      }
+
+      if (slices.GetSlicesCount() != 0)
+      {
+        const OrthancStone::CoordinateSystem3D& reference = slices.GetSliceGeometry(0);
+        const DicomInstanceParameters& dicom = GetSliceParameters(slices, 0);
+
+        for (size_t i = 1; i < slices.GetSlicesCount(); i++)
+        {
+          CheckSlice(slices, i, reference, dicom);
+        }
+      }
+    }
+
+
+    void Clear()
+    {
+      image_.reset();
+      
+      for (size_t i = 0; i < slices_.size(); i++)
+      {
+        assert(slices_[i] != NULL);
+        delete slices_[i];
+      }
+    }
+
+    
+  public:
+    DicomVolumeImage()
+    {
+    }
+
+    ~DicomVolumeImage()
+    {
+      Clear();
+    }
+
+    // WARNING: The payload of "slices" must be of class "DicomInstanceParameters"
+    void SetGeometry(OrthancStone::SlicesSorter& slices)
+    {
+      Clear();
+      
+      if (!slices.Sort())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
+                                        "Cannot sort the 3D slices of a DICOM series");          
+      }
+
+      slices_.reserve(slices.GetSlicesCount());
+
+      for (size_t i = 0; i < slices.GetSlicesCount(); i++)
+      {
+        slices_.push_back(new DicomInstanceParameters(GetSliceParameters(slices, i)));
+      }
+
+      CheckVolume(slices);
+
+      const double spacingZ = slices.ComputeSpacingBetweenSlices();
+      LOG(INFO) << "Computed spacing between slices: " << spacingZ << "mm";
+      
+      const DicomInstanceParameters& parameters = GetSliceParameters(slices, 0);
+
+      image_.reset(new OrthancStone::ImageBuffer3D(parameters.GetExpectedPixelFormat(),
+                                                   parameters.GetImageInformation().GetWidth(),
+                                                   parameters.GetImageInformation().GetHeight(),
+                                                   slices.GetSlicesCount(), false /* don't compute range */));      
+
+      image_->SetAxialGeometry(slices.GetSliceGeometry(0));
+      image_->SetVoxelDimensions(parameters.GetPixelSpacingX(), parameters.GetPixelSpacingY(), spacingZ);
+      image_->Clear();
+    }
+
+    bool IsGeometryReady() const
+    {
+      return (image_.get() != NULL);
+    }
+
+    const OrthancStone::ImageBuffer3D& GetImage() const
+    {
+      if (!IsGeometryReady())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return *image_;
+      }
+    }      
+  };
+  
+  
+
+  class AxialVolumeOrthancLoader : public OrthancStone::IObserver
+  {
+  private:
+    class MessageHandler : public Orthanc::IDynamicObject
+    {
+    public:
+      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) const = 0;
+    };
+
+    void Handle(const OrthancRestApiCommand::SuccessMessage& message)
+    {
+      dynamic_cast<const MessageHandler&>(message.GetOrigin().GetPayload()).Handle(message);
+    }
+
+
+    class LoadSeriesGeometryHandler : public MessageHandler
+    {
+    private:
+      AxialVolumeOrthancLoader&  that_;
+
+    public:
+      LoadSeriesGeometryHandler(AxialVolumeOrthancLoader& that) :
+      that_(that)
+      {
+      }
+
+      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) const
+      {
+        Json::Value value;
+        message.ParseJsonBody(value);
+
+        if (value.type() != Json::objectValue)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+        }
+
+        Json::Value::Members instances = value.getMemberNames();
+
+        OrthancStone::SlicesSorter slices;
+        
+        for (size_t i = 0; i < instances.size(); i++)
+        {
+          Orthanc::DicomMap dicom;
+          dicom.FromDicomAsJson(value[instances[i]]);
+
+          std::auto_ptr<DicomInstanceParameters> instance(new DicomInstanceParameters(dicom));
+
+          OrthancStone::CoordinateSystem3D geometry = instance->GetGeometry();
+          slices.AddSlice(geometry, instance.release());
+        }
+
+        that_.image_.SetGeometry(slices);
+      }
+    };
+
+
+    class LoadInstanceGeometryHandler : public MessageHandler
+    {
+    private:
+      AxialVolumeOrthancLoader&  that_;
+
+    public:
+      LoadInstanceGeometryHandler(AxialVolumeOrthancLoader& that) :
+      that_(that)
+      {
+      }
+
+      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) const
+      {
+        Json::Value value;
+        message.ParseJsonBody(value);
+
+        if (value.type() != Json::objectValue)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+        }
+
+        Orthanc::DicomMap dicom;
+        dicom.FromDicomAsJson(value);
+
+        DicomInstanceParameters instance(dicom);
+      }
+    };
+
+
+    bool              active_;
+    DicomVolumeImage  image_;
+
+  public:
+    AxialVolumeOrthancLoader(OrthancStone::IObservable& oracle) :
+      IObserver(oracle.GetBroker()),
+      active_(false)
+    {
+      oracle.RegisterObserverCallback(
+        new OrthancStone::Callable<AxialVolumeOrthancLoader, OrthancRestApiCommand::SuccessMessage>
+        (*this, &AxialVolumeOrthancLoader::Handle));
+    }
+
+    void LoadSeries(IOracle& oracle,
+                    const std::string& seriesId)
+    {
+      if (active_)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+
+      active_ = true;
+
+      std::auto_ptr<Refactoring::OrthancRestApiCommand> command(new Refactoring::OrthancRestApiCommand);
+      command->SetUri("/series/" + seriesId + "/instances-tags");
+      command->SetPayload(new LoadSeriesGeometryHandler(*this));
+
+      oracle.Schedule(*this, command.release());
+    }
+
+    void LoadInstance(IOracle& oracle,
+                      const std::string& instanceId)
+    {
+      if (active_)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+
+      active_ = true;
+
+      // Tag "3004-000c" is "Grid Frame Offset Vector", which is
+      // mandatory to read RT DOSE, but is too long to be returned by default
+
+      // TODO => Should be part of a second call if needed
+
+      std::auto_ptr<Refactoring::OrthancRestApiCommand> command(new Refactoring::OrthancRestApiCommand);
+      command->SetUri("/instances/" + instanceId + "/tags?ignore-length=3004-000c");
+      command->SetPayload(new LoadInstanceGeometryHandler(*this));
+
+      oracle.Schedule(*this, command.release());
+    }
+  };
+
+}
+
+
+
+class Toto : public OrthancStone::IObserver
+{
+private:
+  void Handle(const Refactoring::OrthancRestApiCommand::SuccessMessage& message)
+  {
+    Json::Value v;
+    message.ParseJsonBody(v);
+
+    printf("ICI [%s]\n", v.toStyledString().c_str());
+  }
+
+  void Handle(const Refactoring::GetOrthancImageCommand::SuccessMessage& message)
+  {
+    printf("IMAGE %dx%d\n", message.GetImage().GetWidth(), message.GetImage().GetHeight());
+  }
+
+  void Handle(const Refactoring::GetOrthancWebViewerJpegCommand::SuccessMessage& message)
+  {
+    printf("WebViewer %dx%d\n", message.GetImage().GetWidth(), message.GetImage().GetHeight());
+  }
+
+  void Handle(const Refactoring::OracleCommandExceptionMessage& message)
+  {
+    printf("EXCEPTION: [%s] on command type %d\n", message.GetException().What(), message.GetCommand().GetType());
+
+    switch (message.GetCommand().GetType())
+    {
+      case Refactoring::IOracleCommand::Type_GetOrthancWebViewerJpeg:
+        printf("URI: [%s]\n", dynamic_cast<const Refactoring::GetOrthancWebViewerJpegCommand&>
+               (message.GetCommand()).GetUri().c_str());
+        break;
+      
+      default:
+        break;
+    }
+  }
+
+public:
+  Toto(OrthancStone::IObservable& oracle) :
+    IObserver(oracle.GetBroker())
+  {
+    oracle.RegisterObserverCallback
+      (new OrthancStone::Callable
+       <Toto, Refactoring::OrthancRestApiCommand::SuccessMessage>(*this, &Toto::Handle));
+
+    oracle.RegisterObserverCallback
+      (new OrthancStone::Callable
+       <Toto, Refactoring::GetOrthancImageCommand::SuccessMessage>(*this, &Toto::Handle));
+
+    oracle.RegisterObserverCallback
+      (new OrthancStone::Callable
+      <Toto, Refactoring::GetOrthancWebViewerJpegCommand::SuccessMessage>(*this, &Toto::Handle));
+
+    oracle.RegisterObserverCallback
+      (new OrthancStone::Callable
+       <Toto, Refactoring::OracleCommandExceptionMessage>(*this, &Toto::Handle));
+  }
+};
+
+
+void Run(Refactoring::NativeApplicationContext& context)
+{
+  std::auto_ptr<Toto> toto;
+  std::auto_ptr<Refactoring::AxialVolumeOrthancLoader> loader1, loader2;
+
+  {
+    Refactoring::NativeApplicationContext::WriterLock lock(context);
+    toto.reset(new Toto(lock.GetOracleObservable()));
+    loader1.reset(new Refactoring::AxialVolumeOrthancLoader(lock.GetOracleObservable()));
+    loader2.reset(new Refactoring::AxialVolumeOrthancLoader(lock.GetOracleObservable()));
+  }
+
+  Refactoring::NativeOracle oracle(context);
+
+  {
+    Orthanc::WebServiceParameters p;
+    //p.SetUrl("http://localhost:8043/");
+    p.SetCredentials("orthanc", "orthanc");
+    oracle.SetOrthancParameters(p);
+  }
+
+  oracle.Start();
+
+  if (1)
+  {
+    Json::Value v = Json::objectValue;
+    v["Level"] = "Series";
+    v["Query"] = Json::objectValue;
+
+    std::auto_ptr<Refactoring::OrthancRestApiCommand>  command(new Refactoring::OrthancRestApiCommand);
+    command->SetMethod(Orthanc::HttpMethod_Post);
+    command->SetUri("/tools/find");
+    command->SetBody(v);
+
+    oracle.Schedule(*toto, command.release());
+  }
+  
+  if (1)
+  {
+    std::auto_ptr<Refactoring::GetOrthancImageCommand>  command(new Refactoring::GetOrthancImageCommand);
+    command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Jpeg)));
+    command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/preview");
+    oracle.Schedule(*toto, command.release());
+  }
+  
+  if (1)
+  {
+    std::auto_ptr<Refactoring::GetOrthancImageCommand>  command(new Refactoring::GetOrthancImageCommand);
+    command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Png)));
+    command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/preview");
+    oracle.Schedule(*toto, command.release());
+  }
+  
+  if (1)
+  {
+    std::auto_ptr<Refactoring::GetOrthancImageCommand>  command(new Refactoring::GetOrthancImageCommand);
+    command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Png)));
+    command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/image-uint16");
+    oracle.Schedule(*toto, command.release());
+  }
+  
+  if (1)
+  {
+    std::auto_ptr<Refactoring::GetOrthancImageCommand>  command(new Refactoring::GetOrthancImageCommand);
+    command->SetHttpHeader("Accept-Encoding", "gzip");
+    command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam)));
+    command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/image-uint16");
+    oracle.Schedule(*toto, command.release());
+  }
+  
+  if (1)
+  {
+    std::auto_ptr<Refactoring::GetOrthancImageCommand>  command(new Refactoring::GetOrthancImageCommand);
+    command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam)));
+    command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/image-uint16");
+    oracle.Schedule(*toto, command.release());
+  }
+
+  if (1)
+  {
+    std::auto_ptr<Refactoring::GetOrthancWebViewerJpegCommand>  command(new Refactoring::GetOrthancWebViewerJpegCommand);
+    command->SetHttpHeader("Accept-Encoding", "gzip");
+    command->SetInstance("e6c7c20b-c9f65d7e-0d76f2e2-830186f2-3e3c600e");
+    command->SetQuality(90);
+    oracle.Schedule(*toto, command.release());
+  }
+
+
+  // 2017-11-17-Anonymized
+  //loader1->LoadSeries(oracle, "cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618");  // CT
+  loader2->LoadInstance(oracle, "41029085-71718346-811efac4-420e2c15-d39f99b6");  // RT-DOSE
+
+  // Delphine
+  loader1->LoadSeries(oracle, "5990e39c-51e5f201-fe87a54c-31a55943-e59ef80e");  // CT
+
+  LOG(WARNING) << "...Waiting for Ctrl-C...";
+  Orthanc::SystemToolbox::ServerBarrier();
+  //boost::this_thread::sleep(boost::posix_time::seconds(1));
+
+  oracle.Stop();
+}
+
+
+
+/**
+ * IMPORTANT: The full arguments to "main()" are needed for SDL on
+ * Windows. Otherwise, one gets the linking error "undefined reference
+ * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
+ **/
+int main(int argc, char* argv[])
+{
+  OrthancStone::StoneInitialize();
+  Orthanc::Logging::EnableInfoLevel(true);
+
+  try
+  {
+    Refactoring::NativeApplicationContext context;
+    Run(context);
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "EXCEPTION: " << e.What();
+  }
+
+  OrthancStone::StoneFinalize();
+
+  return 0;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/TrackerSample.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,98 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "TrackerSampleApp.h"
+
+ // From Stone
+#include "../../Applications/Sdl/SdlOpenGLWindow.h"
+#include "../../Framework/Scene2D/CairoCompositor.h"
+#include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
+#include "../../Framework/Scene2D/OpenGLCompositor.h"
+#include "../../Framework/StoneInitialization.h"
+
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+
+
+#include <boost/shared_ptr.hpp>
+#include <boost/weak_ptr.hpp>
+
+#include <SDL.h>
+#include <stdio.h>
+
+/*
+TODO:
+
+- to decouple the trackers from the sample, we need to supply them with
+  the scene rather than the app
+
+- in order to do that, we need a GetNextFreeZIndex function (or something 
+  along those lines) in the scene object
+
+*/
+
+
+using namespace Orthanc;
+using namespace OrthancStone;
+
+
+
+
+boost::weak_ptr<TrackerSampleApp> g_app;
+
+void TrackerSample_SetInfoDisplayMessage(std::string key, std::string value)
+{
+  boost::shared_ptr<TrackerSampleApp> app = g_app.lock();
+  if (app)
+  {
+    app->SetInfoDisplayMessage(key, value);
+  }
+}
+
+/**
+ * IMPORTANT: The full arguments to "main()" are needed for SDL on
+ * Windows. Otherwise, one gets the linking error "undefined reference
+ * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
+ **/
+int main(int argc, char* argv[])
+{
+  StoneInitialize();
+  Orthanc::Logging::EnableInfoLevel(true);
+  Orthanc::Logging::EnableTraceLevel(true);
+
+  try
+  {
+    MessageBroker broker;
+    boost::shared_ptr<TrackerSampleApp> app(new TrackerSampleApp(broker));
+    g_app = app;
+    app->PrepareScene();
+    app->Run();
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "EXCEPTION: " << e.What();
+  }
+
+  StoneFinalize();
+
+  return 0;
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/TrackerSampleApp.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,580 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "TrackerSampleApp.h"
+
+#include "../Common/CreateLineMeasureTracker.h"
+#include "../Common/CreateAngleMeasureTracker.h"
+
+#include "../../Applications/Sdl/SdlOpenGLWindow.h"
+
+#include "../../Framework/Scene2D/PanSceneTracker.h"
+#include "../../Framework/Scene2D/RotateSceneTracker.h"
+#include "../../Framework/Scene2D/Scene2D.h"
+#include "../../Framework/Scene2D/ZoomSceneTracker.h"
+#include "../../Framework/Scene2D/CairoCompositor.h"
+#include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
+#include "../../Framework/Scene2D/OpenGLCompositor.h"
+#include "../../Framework/StoneInitialization.h"
+
+ // From Orthanc framework
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Core/Images/PngWriter.h>
+
+#include <SDL.h>
+#include <stdio.h>
+
+using namespace Orthanc;
+
+namespace OrthancStone
+{
+  const char* MeasureToolToString(size_t i)
+  {
+    static const char* descs[] = {
+      "GuiTool_Rotate",
+      "GuiTool_Pan",
+      "GuiTool_Zoom",
+      "GuiTool_LineMeasure",
+      "GuiTool_CircleMeasure",
+      "GuiTool_AngleMeasure",
+      "GuiTool_EllipseMeasure",
+      "GuiTool_LAST"
+    };
+    if (i >= GuiTool_LAST)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Wrong tool index");
+    }
+    return descs[i];
+  }
+
+  Scene2D& TrackerSampleApp::GetScene()
+  {
+    return scene_;
+  }
+
+  void TrackerSampleApp::SelectNextTool()
+  {
+    currentTool_ = static_cast<GuiTool>(currentTool_ + 1);
+    if (currentTool_ == GuiTool_LAST)
+      currentTool_ = static_cast<GuiTool>(0);;
+    printf("Current tool is now: %s\n", MeasureToolToString(currentTool_));
+  }
+
+  void TrackerSampleApp::DisplayInfoText()
+  {
+    // do not try to use stuff too early!
+    if (compositor_.get() == NULL)
+      return;
+
+    std::stringstream msg;
+    for (auto kv : infoTextMap_)
+    {
+      msg << kv.first << " : " << kv.second << std::endl;
+    }
+    auto msgS = msg.str();
+
+    TextSceneLayer* layerP = NULL;
+    if (scene_.HasLayer(FIXED_INFOTEXT_LAYER_ZINDEX))
+    {
+      TextSceneLayer& layer = dynamic_cast<TextSceneLayer&>(
+        scene_.GetLayer(FIXED_INFOTEXT_LAYER_ZINDEX));
+      layerP = &layer;
+    }
+    else
+    {
+      std::auto_ptr<TextSceneLayer> layer(new TextSceneLayer);
+      layerP = layer.get();
+      layer->SetColor(0, 255, 0);
+      layer->SetFontIndex(1);
+      layer->SetBorder(20);
+      layer->SetAnchor(BitmapAnchor_TopLeft);
+      //layer->SetPosition(0,0);
+      scene_.SetLayer(FIXED_INFOTEXT_LAYER_ZINDEX, layer.release());
+    }
+    // position the fixed info text in the upper right corner
+    layerP->SetText(msgS.c_str());
+    double cX = compositor_->GetCanvasWidth() * (-0.5);
+    double cY = compositor_->GetCanvasHeight() * (-0.5);
+    scene_.GetCanvasToSceneTransform().Apply(cX,cY);
+    layerP->SetPosition(cX, cY);
+  }
+
+  void TrackerSampleApp::DisplayFloatingCtrlInfoText(const PointerEvent& e)
+  {
+    ScenePoint2D p = e.GetMainPosition().Apply(scene_.GetCanvasToSceneTransform());
+
+    char buf[128];
+    sprintf(buf, "S:(%0.02f,%0.02f) C:(%0.02f,%0.02f)", 
+      p.GetX(), p.GetY(), 
+      e.GetMainPosition().GetX(), e.GetMainPosition().GetY());
+
+    if (scene_.HasLayer(FLOATING_INFOTEXT_LAYER_ZINDEX))
+    {
+      TextSceneLayer& layer =
+        dynamic_cast<TextSceneLayer&>(scene_.GetLayer(FLOATING_INFOTEXT_LAYER_ZINDEX));
+      layer.SetText(buf);
+      layer.SetPosition(p.GetX(), p.GetY());
+    }
+    else
+    {
+      std::auto_ptr<TextSceneLayer> layer(new TextSceneLayer);
+      layer->SetColor(0, 255, 0);
+      layer->SetText(buf);
+      layer->SetBorder(20);
+      layer->SetAnchor(BitmapAnchor_BottomCenter);
+      layer->SetPosition(p.GetX(), p.GetY());
+      scene_.SetLayer(FLOATING_INFOTEXT_LAYER_ZINDEX, layer.release());
+    }
+  }
+
+  void TrackerSampleApp::HideInfoText()
+  {
+    scene_.DeleteLayer(FLOATING_INFOTEXT_LAYER_ZINDEX);
+  }
+
+  void TrackerSampleApp::HandleApplicationEvent(
+    const SDL_Event & event)
+  {
+    DisplayInfoText();
+
+    if (event.type == SDL_MOUSEMOTION)
+    {
+      int scancodeCount = 0;
+      const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
+
+      if (activeTracker_.get() == NULL &&
+        SDL_SCANCODE_LCTRL < scancodeCount &&
+        keyboardState[SDL_SCANCODE_LCTRL])
+      {
+        // The "left-ctrl" key is down, while no tracker is present
+        // Let's display the info text
+        PointerEvent e;
+        e.AddPosition(compositor_->GetPixelCenterCoordinates(
+          event.button.x, event.button.y));
+
+        DisplayFloatingCtrlInfoText(e);
+      }
+      else
+      {
+        HideInfoText();
+        //LOG(TRACE) << "(event.type == SDL_MOUSEMOTION)";
+        if (activeTracker_.get() != NULL)
+        {
+          //LOG(TRACE) << "(activeTracker_.get() != NULL)";
+          PointerEvent e;
+          e.AddPosition(compositor_->GetPixelCenterCoordinates(
+            event.button.x, event.button.y));
+          
+          //LOG(TRACE) << "event.button.x = " << event.button.x << "     " <<
+          //  "event.button.y = " << event.button.y;
+          //LOG(TRACE) << "activeTracker_->PointerMove(e); " <<
+          //  e.GetMainPosition().GetX() << " " << e.GetMainPosition().GetY();
+          
+          activeTracker_->PointerMove(e);
+          if (!activeTracker_->IsActive())
+            activeTracker_ = NULL;
+        }
+      }
+    }
+    else if (event.type == SDL_MOUSEBUTTONUP)
+    {
+      if (activeTracker_)
+      {
+        PointerEvent e;
+        e.AddPosition(compositor_->GetPixelCenterCoordinates(event.button.x, event.button.y));
+        activeTracker_->PointerUp(e);
+        if (!activeTracker_->IsActive())
+          activeTracker_ = NULL;
+      }
+    }
+    else if (event.type == SDL_MOUSEBUTTONDOWN)
+    {
+      PointerEvent e;
+      e.AddPosition(compositor_->GetPixelCenterCoordinates(
+        event.button.x, event.button.y));
+      if (activeTracker_)
+      {
+        activeTracker_->PointerDown(e);
+        if (!activeTracker_->IsActive())
+          activeTracker_ = NULL;
+      }
+      else
+      {
+        // we ATTEMPT to create a tracker if need be
+        activeTracker_ = CreateSuitableTracker(event, e);
+      }
+    }
+    else if (event.type == SDL_KEYDOWN &&
+      event.key.repeat == 0 /* Ignore key bounce */)
+    {
+      switch (event.key.keysym.sym)
+      {
+      case SDLK_ESCAPE:
+        if (activeTracker_)
+        {
+          activeTracker_->Cancel();
+          if (!activeTracker_->IsActive())
+            activeTracker_ = NULL;
+        }
+        break;
+
+      case SDLK_t:
+        if (!activeTracker_)
+          SelectNextTool();
+        else
+        {
+          LOG(WARNING) << "You cannot change the active tool when an interaction"
+            " is taking place";
+        }
+        break;
+
+      case SDLK_s:
+        scene_.FitContent(compositor_->GetCanvasWidth(),
+          compositor_->GetCanvasHeight());
+        break;
+
+      case SDLK_c:
+        TakeScreenshot(
+          "screenshot.png",
+          compositor_->GetCanvasWidth(),
+          compositor_->GetCanvasHeight());
+        break;
+
+      default:
+        break;
+      }
+    }
+  }
+
+
+  void TrackerSampleApp::OnSceneTransformChanged(const Scene2D::SceneTransformChanged& message)
+  {
+    DisplayInfoText();
+  }
+
+  FlexiblePointerTrackerPtr TrackerSampleApp::CreateSuitableTracker(
+    const SDL_Event & event,
+    const PointerEvent & e)
+  {
+    switch (event.button.button)
+    {
+    case SDL_BUTTON_MIDDLE:
+      return CreateSimpleTrackerAdapter(PointerTrackerPtr(
+        new PanSceneTracker(scene_, e)));
+
+    case SDL_BUTTON_RIGHT:
+      return CreateSimpleTrackerAdapter(PointerTrackerPtr(
+        new ZoomSceneTracker(scene_, e, compositor_->GetCanvasHeight())));
+
+    case SDL_BUTTON_LEFT:
+    {
+      //LOG(TRACE) << "CreateSuitableTracker: case SDL_BUTTON_LEFT:";
+      // TODO: we need to iterate on the set of measuring tool and perform
+      // a hit test to check if a tracker needs to be created for edition.
+      // Otherwise, depending upon the active tool, we might want to create
+      // a "measuring tool creation" tracker
+
+      // TODO: if there are conflicts, we should prefer a tracker that 
+      // pertains to the type of measuring tool currently selected (TBD?)
+      FlexiblePointerTrackerPtr hitTestTracker = TrackerHitTest(e);
+
+      if (hitTestTracker != NULL)
+      {
+        //LOG(TRACE) << "hitTestTracker != NULL";
+        return hitTestTracker;
+      }
+      else
+      {
+        switch (currentTool_)
+        {
+        case GuiTool_Rotate:
+          //LOG(TRACE) << "Creating RotateSceneTracker";
+          return CreateSimpleTrackerAdapter(PointerTrackerPtr(
+            new RotateSceneTracker(scene_, e)));
+        case GuiTool_Pan:
+          return CreateSimpleTrackerAdapter(PointerTrackerPtr(
+            new PanSceneTracker(scene_, e)));
+        case GuiTool_Zoom:
+          return CreateSimpleTrackerAdapter(PointerTrackerPtr(
+            new ZoomSceneTracker(scene_, e, compositor_->GetCanvasHeight())));
+        //case GuiTool_AngleMeasure:
+        //  return new AngleMeasureTracker(scene_, measureTools_, undoStack_, e);
+        //case GuiTool_CircleMeasure:
+        //  return new CircleMeasureTracker(scene_, measureTools_, undoStack_, e);
+        //case GuiTool_EllipseMeasure:
+        //  return new EllipseMeasureTracker(scene_, measureTools_, undoStack_, e);
+        case GuiTool_LineMeasure:
+          return FlexiblePointerTrackerPtr(new CreateLineMeasureTracker(
+            IObserver::GetBroker(), scene_, undoStack_, measureTools_, e));
+        case GuiTool_AngleMeasure:
+          return FlexiblePointerTrackerPtr(new CreateAngleMeasureTracker(
+            IObserver::GetBroker(), scene_, undoStack_, measureTools_, e));
+          return NULL;
+        case GuiTool_CircleMeasure:
+          LOG(ERROR) << "Not implemented yet!";
+          return NULL;
+        case GuiTool_EllipseMeasure:
+          LOG(ERROR) << "Not implemented yet!";
+          return NULL;
+        default:
+          throw OrthancException(ErrorCode_InternalError, "Wrong tool!");
+        }
+      }
+    }
+    default:
+      return NULL;
+    }
+  }
+
+
+  TrackerSampleApp::TrackerSampleApp(MessageBroker& broker) : IObserver(broker)
+    , scene_(broker)
+    , currentTool_(GuiTool_Rotate)
+  {
+    scene_.RegisterObserverCallback(
+      new Callable<TrackerSampleApp, Scene2D::SceneTransformChanged>
+      (*this, &TrackerSampleApp::OnSceneTransformChanged));
+
+    TEXTURE_2x2_1_ZINDEX = 1;
+    TEXTURE_1x1_ZINDEX = 2;
+    TEXTURE_2x2_2_ZINDEX = 3;
+    LINESET_1_ZINDEX = 4;
+    LINESET_2_ZINDEX = 5;
+    FLOATING_INFOTEXT_LAYER_ZINDEX = 6;
+    FIXED_INFOTEXT_LAYER_ZINDEX = 7;
+
+
+  }
+
+  void TrackerSampleApp::PrepareScene()
+  {
+    // Texture of 2x2 size
+    {
+      Orthanc::Image i(Orthanc::PixelFormat_RGB24, 2, 2, false);
+
+      uint8_t* p = reinterpret_cast<uint8_t*>(i.GetRow(0));
+      p[0] = 255;
+      p[1] = 0;
+      p[2] = 0;
+
+      p[3] = 0;
+      p[4] = 255;
+      p[5] = 0;
+
+      p = reinterpret_cast<uint8_t*>(i.GetRow(1));
+      p[0] = 0;
+      p[1] = 0;
+      p[2] = 255;
+
+      p[3] = 255;
+      p[4] = 0;
+      p[5] = 0;
+
+      scene_.SetLayer(TEXTURE_2x2_1_ZINDEX, new ColorTextureSceneLayer(i));
+
+      std::auto_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
+      l->SetOrigin(-3, 2);
+      l->SetPixelSpacing(1.5, 1);
+      l->SetAngle(20.0 / 180.0 * M_PI);
+      scene_.SetLayer(TEXTURE_2x2_2_ZINDEX, l.release());
+    }
+
+    // Texture of 1x1 size
+    {
+      Orthanc::Image i(Orthanc::PixelFormat_RGB24, 1, 1, false);
+
+      uint8_t* p = reinterpret_cast<uint8_t*>(i.GetRow(0));
+      p[0] = 255;
+      p[1] = 0;
+      p[2] = 0;
+
+      std::auto_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
+      l->SetOrigin(-2, 1);
+      l->SetAngle(20.0 / 180.0 * M_PI);
+      scene_.SetLayer(TEXTURE_1x1_ZINDEX, l.release());
+    }
+
+    // Some lines
+    {
+      std::auto_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer);
+
+      layer->SetThickness(1);
+
+      PolylineSceneLayer::Chain chain;
+      chain.push_back(ScenePoint2D(0 - 0.5, 0 - 0.5));
+      chain.push_back(ScenePoint2D(0 - 0.5, 2 - 0.5));
+      chain.push_back(ScenePoint2D(2 - 0.5, 2 - 0.5));
+      chain.push_back(ScenePoint2D(2 - 0.5, 0 - 0.5));
+      layer->AddChain(chain, true);
+
+      chain.clear();
+      chain.push_back(ScenePoint2D(-5, -5));
+      chain.push_back(ScenePoint2D(5, -5));
+      chain.push_back(ScenePoint2D(5, 5));
+      chain.push_back(ScenePoint2D(-5, 5));
+      layer->AddChain(chain, true);
+
+      double dy = 1.01;
+      chain.clear();
+      chain.push_back(ScenePoint2D(-4, -4));
+      chain.push_back(ScenePoint2D(4, -4 + dy));
+      chain.push_back(ScenePoint2D(-4, -4 + 2.0 * dy));
+      chain.push_back(ScenePoint2D(4, 2));
+      layer->AddChain(chain, false);
+
+      layer->SetColor(0, 255, 255);
+      scene_.SetLayer(LINESET_1_ZINDEX, layer.release());
+    }
+
+    // Some text
+    {
+      std::auto_ptr<TextSceneLayer> layer(new TextSceneLayer);
+      layer->SetText("Hello");
+      scene_.SetLayer(LINESET_2_ZINDEX, layer.release());
+    }
+  }
+
+
+  void TrackerSampleApp::DisableTracker()
+  {
+    if (activeTracker_)
+    {
+      activeTracker_->Cancel();
+      activeTracker_ = NULL;
+    }
+  }
+
+  void TrackerSampleApp::TakeScreenshot(const std::string& target,
+    unsigned int canvasWidth,
+    unsigned int canvasHeight)
+  {
+    CairoCompositor compositor(scene_, canvasWidth, canvasHeight);
+    compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, FONT_SIZE_0, Orthanc::Encoding_Latin1);
+    compositor.Refresh();
+
+    Orthanc::ImageAccessor canvas;
+    compositor.GetCanvas().GetReadOnlyAccessor(canvas);
+
+    Orthanc::Image png(Orthanc::PixelFormat_RGB24, canvas.GetWidth(), canvas.GetHeight(), false);
+    Orthanc::ImageProcessing::Convert(png, canvas);
+
+    Orthanc::PngWriter writer;
+    writer.WriteToFile(target, png);
+  }
+
+
+  FlexiblePointerTrackerPtr TrackerSampleApp::TrackerHitTest(const PointerEvent & e)
+  {
+    // std::vector<MeasureToolPtr> measureTools_;
+    return nullptr;
+  }
+
+  static void GLAPIENTRY
+    OpenGLMessageCallback(GLenum source,
+      GLenum type,
+      GLuint id,
+      GLenum severity,
+      GLsizei length,
+      const GLchar* message,
+      const void* userParam)
+  {
+    if (severity != GL_DEBUG_SEVERITY_NOTIFICATION)
+    {
+      fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
+        (type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : ""),
+        type, severity, message);
+    }
+  }
+
+  static bool g_stopApplication = false;
+  
+  void TrackerSampleApp::Run()
+  {
+    // False means we do NOT let Windows treat this as a legacy application
+    // that needs to be scaled
+    SdlOpenGLWindow window("Hello", 1024, 1024, false);
+
+    GetScene().FitContent(window.GetCanvasWidth(), window.GetCanvasHeight());
+
+    glEnable(GL_DEBUG_OUTPUT);
+    glDebugMessageCallback(OpenGLMessageCallback, 0);
+
+    compositor_.reset(new OpenGLCompositor(window, GetScene()));
+
+    compositor_->SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT,
+      FONT_SIZE_0, Orthanc::Encoding_Latin1);
+    compositor_->SetFont(1, Orthanc::EmbeddedResources::UBUNTU_FONT,
+      FONT_SIZE_1, Orthanc::Encoding_Latin1);
+
+    while (!g_stopApplication)
+    {
+      compositor_->Refresh();
+
+      SDL_Event event;
+      while (!g_stopApplication && SDL_PollEvent(&event))
+      {
+        if (event.type == SDL_QUIT)
+        {
+          g_stopApplication = true;
+          break;
+        }
+        else if (event.type == SDL_WINDOWEVENT &&
+          event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
+        {
+          DisableTracker(); // was: tracker.reset(NULL);
+          compositor_->UpdateSize();
+        }
+        else if (event.type == SDL_KEYDOWN &&
+          event.key.repeat == 0 /* Ignore key bounce */)
+        {
+          switch (event.key.keysym.sym)
+          {
+          case SDLK_f:
+            window.GetWindow().ToggleMaximize();
+            break;
+
+          case SDLK_q:
+            g_stopApplication = true;
+            break;
+          default:
+            break;
+          }
+        }
+        HandleApplicationEvent(event);
+      }
+      SDL_Delay(1);
+    }
+    compositor_.reset(NULL);
+  }
+
+  void TrackerSampleApp::SetInfoDisplayMessage(
+    std::string key, std::string value)
+  {
+    if (value == "")
+      infoTextMap_.erase(key);
+    else
+      infoTextMap_[key] = value;
+    DisplayInfoText();
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/TrackerSampleApp.h	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,134 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include <Framework/Scene2D/OpenGLCompositor.h>
+#include <Framework/Messages/IObserver.h>
+
+#include "../Common/IFlexiblePointerTracker.h"
+#include "../Common/MeasureTools.h"
+
+#include <SDL.h>
+
+#include <boost/make_shared.hpp>
+#include <boost/shared_ptr.hpp>
+#include <boost/enable_shared_from_this.hpp>
+
+namespace OrthancStone
+{
+  class TrackerCommand;
+  typedef boost::shared_ptr<TrackerCommand> TrackerCommandPtr;
+
+  enum GuiTool
+  {
+    GuiTool_Rotate = 0,
+    GuiTool_Pan,
+    GuiTool_Zoom,
+    GuiTool_LineMeasure,
+    GuiTool_CircleMeasure,
+    GuiTool_AngleMeasure,
+    GuiTool_EllipseMeasure,
+    GuiTool_LAST
+  };
+
+  const char* MeasureToolToString(size_t i);
+
+  static const unsigned int FONT_SIZE_0 = 32;
+  static const unsigned int FONT_SIZE_1 = 24;
+
+  class Scene2D;
+
+  class TrackerSampleApp : public IObserver
+    , public boost::enable_shared_from_this<TrackerSampleApp>
+  {
+  public:
+    // 12 because.
+    TrackerSampleApp(MessageBroker& broker);
+    void PrepareScene();
+    void Run();
+    void SetInfoDisplayMessage(std::string key, std::string value);
+    void DisableTracker();
+
+    Scene2D& GetScene();
+
+    void HandleApplicationEvent(const SDL_Event& event);
+
+    /**
+    This method is called when the scene transform changes. It allows to
+    recompute the visual elements whose content depend upon the scene transform
+    */
+    void OnSceneTransformChanged(const Scene2D::SceneTransformChanged& message);
+
+  private:
+    void SelectNextTool();
+
+
+    FlexiblePointerTrackerPtr TrackerHitTest(const PointerEvent& e);
+
+    FlexiblePointerTrackerPtr CreateSuitableTracker(
+      const SDL_Event& event,
+      const PointerEvent& e);
+
+    void TakeScreenshot(
+      const std::string& target,
+      unsigned int canvasWidth,
+      unsigned int canvasHeight);
+
+    /**
+      This adds the command at the top of the undo stack
+    */
+    void Commit(TrackerCommandPtr cmd);
+    void Undo();
+    void Redo();
+
+  private:
+    void DisplayFloatingCtrlInfoText(const PointerEvent& e);
+    void DisplayInfoText();
+    void HideInfoText();
+
+  private:
+    std::auto_ptr<OpenGLCompositor> compositor_;
+    /**
+    WARNING: the measuring tools do store a reference to the scene, and it 
+    paramount that the scene gets destroyed AFTER the measurement tools.
+    */
+    Scene2D scene_;
+
+    std::map<std::string, std::string> infoTextMap_;
+    FlexiblePointerTrackerPtr activeTracker_;
+    std::vector<TrackerCommandPtr> undoStack_;
+
+    // we store the measure tools here so that they don't get deleted
+    std::vector<MeasureToolPtr> measureTools_;
+
+    //static const int LAYER_POSITION = 150;
+
+
+    int TEXTURE_2x2_1_ZINDEX;
+    int TEXTURE_1x1_ZINDEX;
+    int TEXTURE_2x2_2_ZINDEX;
+    int LINESET_1_ZINDEX;
+    int LINESET_2_ZINDEX;
+    int FLOATING_INFOTEXT_LAYER_ZINDEX;
+    int FIXED_INFOTEXT_LAYER_ZINDEX;
+
+    GuiTool currentTool_;
+  };
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/cpp.hint	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,2 @@
+#define ORTHANC_OVERRIDE
+#define ORTHANC_STONE_DEFINE_EMPTY_MESSAGE(FILE, LINE, NAME) class NAME : public ::OrthancStone::IMessage {};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/WebAssembly/BasicScene.cpp	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,416 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+
+#include <emscripten.h>
+#include <emscripten/html5.h>
+
+// From Stone
+#include "../../Applications/Sdl/SdlOpenGLWindow.h"
+#include "../../Framework/Scene2D/CairoCompositor.h"
+#include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
+#include "../../Framework/Scene2D/OpenGLCompositor.h"
+#include "../../Framework/Scene2D/PanSceneTracker.h"
+#include "../../Framework/Scene2D/RotateSceneTracker.h"
+#include "../../Framework/Scene2D/Scene2D.h"
+#include "../../Framework/Scene2D/ZoomSceneTracker.h"
+#include "../../Framework/StoneInitialization.h"
+#include "../../Framework/OpenGL/WebAssemblyOpenGLContext.h"
+
+// From Orthanc framework
+#include <Core/Images/Image.h>
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+
+#include <stdio.h>
+
+static const unsigned int FONT_SIZE = 32;
+
+
+void PrepareScene(OrthancStone::Scene2D& scene)
+{
+  using namespace OrthancStone;
+
+  // Texture of 2x2 size
+  if (1)
+  {
+    Orthanc::Image i(Orthanc::PixelFormat_RGB24, 2, 2, false);
+    
+    uint8_t *p = reinterpret_cast<uint8_t*>(i.GetRow(0));
+    p[0] = 255;
+    p[1] = 0;
+    p[2] = 0;
+
+    p[3] = 0;
+    p[4] = 255;
+    p[5] = 0;
+
+    p = reinterpret_cast<uint8_t*>(i.GetRow(1));
+    p[0] = 0;
+    p[1] = 0;
+    p[2] = 255;
+
+    p[3] = 255;
+    p[4] = 0;
+    p[5] = 0;
+
+    scene.SetLayer(12, new ColorTextureSceneLayer(i));
+
+    std::auto_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
+    l->SetOrigin(-3, 2);
+    l->SetPixelSpacing(1.5, 1);
+    l->SetAngle(20.0 / 180.0 * M_PI);
+    scene.SetLayer(14, l.release());
+  }
+
+  // Texture of 1x1 size
+  if (1)
+  {
+    Orthanc::Image i(Orthanc::PixelFormat_RGB24, 1, 1, false);
+    
+    uint8_t *p = reinterpret_cast<uint8_t*>(i.GetRow(0));
+    p[0] = 255;
+    p[1] = 0;
+    p[2] = 0;
+
+    std::auto_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
+    l->SetOrigin(-2, 1);
+    l->SetAngle(20.0 / 180.0 * M_PI);
+    scene.SetLayer(13, l.release());
+  }
+
+  // Some lines
+  {
+    std::auto_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer);
+
+    layer->SetThickness(1);
+
+    PolylineSceneLayer::Chain chain;
+    chain.push_back(ScenePoint2D(0 - 0.5, 0 - 0.5));
+    chain.push_back(ScenePoint2D(0 - 0.5, 2 - 0.5));
+    chain.push_back(ScenePoint2D(2 - 0.5, 2 - 0.5));
+    chain.push_back(ScenePoint2D(2 - 0.5, 0 - 0.5));
+    layer->AddChain(chain, true);
+
+    chain.clear();
+    chain.push_back(ScenePoint2D(-5, -5));
+    chain.push_back(ScenePoint2D(5, -5));
+    chain.push_back(ScenePoint2D(5, 5));
+    chain.push_back(ScenePoint2D(-5, 5));
+    layer->AddChain(chain, true);
+
+    double dy = 1.01;
+    chain.clear();
+    chain.push_back(ScenePoint2D(-4, -4));
+    chain.push_back(ScenePoint2D(4, -4 + dy));
+    chain.push_back(ScenePoint2D(-4, -4 + 2.0 * dy));
+    chain.push_back(ScenePoint2D(4, 2));
+    layer->AddChain(chain, false);
+
+    layer->SetColor(0,255, 255);
+    scene.SetLayer(50, layer.release());
+  }
+
+  // Some text
+  if (1)
+  {
+    std::auto_ptr<TextSceneLayer> layer(new TextSceneLayer);
+    layer->SetText("Hello");
+    scene.SetLayer(100, layer.release());
+  }
+}
+
+
+
+
+namespace OrthancStone
+{
+  class WebAssemblyViewport : public boost::noncopyable
+  {
+  private:
+    OpenGL::WebAssemblyOpenGLContext  context_;
+    Scene2D                           scene_;
+    OpenGLCompositor                  compositor_;
+
+    void SetupEvents(const std::string& canvas);
+
+  public:
+    WebAssemblyViewport(MessageBroker& broker,
+                        const std::string& canvas) :
+      context_(canvas),
+      scene_(broker),
+      compositor_(context_, scene_)
+    {
+      compositor_.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, 
+                          FONT_SIZE, Orthanc::Encoding_Latin1);
+      SetupEvents(canvas);
+    }
+
+    Scene2D& GetScene()
+    {
+      return scene_;
+    }
+
+    void UpdateSize()
+    {
+      context_.UpdateSize();
+      compositor_.UpdateSize();
+      Refresh();
+    }
+
+    void Refresh()
+    {
+      compositor_.Refresh();
+    }
+
+    const std::string& GetCanvasIdentifier() const
+    {
+      return context_.GetCanvasIdentifier();
+    }
+
+    ScenePoint2D GetPixelCenterCoordinates(int x, int y) const
+    {
+      return compositor_.GetPixelCenterCoordinates(x, y);
+    }
+
+    unsigned int GetCanvasWidth() const
+    {
+      return context_.GetCanvasWidth();
+    }
+
+    unsigned int GetCanvasHeight() const
+    {
+      return context_.GetCanvasHeight();
+    }
+  };
+
+
+
+  class ActiveTracker : public boost::noncopyable
+  {
+  private:
+    std::auto_ptr<IPointerTracker>  tracker_;
+    std::string                     canvasIdentifier_;
+    bool                            insideCanvas_;
+    
+  public:
+    ActiveTracker(IPointerTracker* tracker,
+                  const WebAssemblyViewport& viewport) :
+      tracker_(tracker),
+      canvasIdentifier_(viewport.GetCanvasIdentifier()),
+      insideCanvas_(true)
+    {
+      if (tracker_.get() == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+    }
+
+    void Update(const PointerEvent& event)
+    {
+      tracker_->Update(event);
+    }
+
+    void Release()
+    {
+      tracker_->Release();
+    }
+  };
+}
+
+
+
+static OrthancStone::PointerEvent* ConvertMouseEvent(const EmscriptenMouseEvent& source,
+                                                     OrthancStone::WebAssemblyViewport& viewport)
+{
+  std::auto_ptr<OrthancStone::PointerEvent> target(new OrthancStone::PointerEvent);
+
+  target->AddPosition(viewport.GetPixelCenterCoordinates(source.targetX, source.targetY));
+  target->SetAltModifier(source.altKey);
+  target->SetControlModifier(source.ctrlKey);
+  target->SetShiftModifier(source.shiftKey);
+
+  return target.release();
+}
+
+
+std::auto_ptr<OrthancStone::ActiveTracker>      tracker_;
+
+
+EM_BOOL OnMouseEvent(int eventType, 
+                     const EmscriptenMouseEvent *mouseEvent, 
+                     void *userData)
+{
+  if (mouseEvent != NULL &&
+      userData != NULL)
+  {
+    OrthancStone::WebAssemblyViewport& viewport = 
+      *reinterpret_cast<OrthancStone::WebAssemblyViewport*>(userData);
+
+    switch (eventType)
+    {
+      case EMSCRIPTEN_EVENT_CLICK:
+      {
+        static unsigned int count = 0;
+        char buf[64];
+        sprintf(buf, "click %d", count++);
+
+        std::auto_ptr<OrthancStone::TextSceneLayer> layer(new OrthancStone::TextSceneLayer);
+        layer->SetText(buf);
+        viewport.GetScene().SetLayer(100, layer.release());
+        viewport.Refresh();
+        break;
+      }
+
+      case EMSCRIPTEN_EVENT_MOUSEDOWN:
+      {
+        std::auto_ptr<OrthancStone::IPointerTracker> t;
+
+        {
+          std::auto_ptr<OrthancStone::PointerEvent> event(ConvertMouseEvent(*mouseEvent, viewport));
+
+          switch (mouseEvent->button)
+          {
+            case 0:  // Left button
+              t.reset(new OrthancStone::RotateSceneTracker(viewport.GetScene(), *event));
+              break;
+
+            case 1:  // Middle button
+              t.reset(new OrthancStone::PanSceneTracker(viewport.GetScene(), *event));
+              break;
+
+            case 2:  // Right button
+              t.reset(new OrthancStone::ZoomSceneTracker
+                      (viewport.GetScene(), *event, viewport.GetCanvasWidth()));
+              break;
+
+            default:
+              break;
+          }
+        }
+
+        if (t.get() != NULL)
+        {
+          tracker_.reset(new OrthancStone::ActiveTracker(t.release(), viewport));
+          viewport.Refresh();
+        }
+
+        break;
+      }
+
+      case EMSCRIPTEN_EVENT_MOUSEMOVE:
+        if (tracker_.get() != NULL)
+        {
+          std::auto_ptr<OrthancStone::PointerEvent> event(ConvertMouseEvent(*mouseEvent, viewport));
+          tracker_->Update(*event);
+          viewport.Refresh();
+        }
+        break;
+
+      case EMSCRIPTEN_EVENT_MOUSEUP:
+        if (tracker_.get() != NULL)
+        {
+          tracker_->Release();
+          viewport.Refresh();
+          tracker_.reset();
+        }
+        break;
+
+      default:
+        break;
+    }
+  }
+
+  return true;
+}
+
+
+void OrthancStone::WebAssemblyViewport::SetupEvents(const std::string& canvas)
+{
+  if (0)
+  {
+    emscripten_set_click_callback(canvas.c_str(), this, false, OnMouseEvent);
+  }
+  else
+  {
+    emscripten_set_mousedown_callback(canvas.c_str(), this, false, OnMouseEvent);
+    emscripten_set_mousemove_callback(canvas.c_str(), this, false, OnMouseEvent);
+    emscripten_set_mouseup_callback(canvas.c_str(), this, false, OnMouseEvent);
+  }
+}
+
+
+
+
+std::auto_ptr<OrthancStone::WebAssemblyViewport>  viewport1_;
+std::auto_ptr<OrthancStone::WebAssemblyViewport>  viewport2_;
+std::auto_ptr<OrthancStone::WebAssemblyViewport>  viewport3_;
+OrthancStone::MessageBroker  broker_;
+
+
+EM_BOOL OnWindowResize(int eventType, const EmscriptenUiEvent *uiEvent, void *userData)
+{
+  if (viewport1_.get() != NULL)
+  {
+    viewport1_->UpdateSize();
+  }
+  
+  if (viewport2_.get() != NULL)
+  {
+    viewport2_->UpdateSize();
+  }
+  
+  if (viewport3_.get() != NULL)
+  {
+    viewport3_->UpdateSize();
+  }
+  
+  return true;
+}
+
+
+
+extern "C"
+{
+  int main(int argc, char const *argv[]) 
+  {
+    OrthancStone::StoneInitialize();
+    EM_ASM(window.dispatchEvent(new CustomEvent("WebAssemblyLoaded")););
+  }
+
+  EMSCRIPTEN_KEEPALIVE
+  void Initialize()
+  {
+    viewport1_.reset(new OrthancStone::WebAssemblyViewport(broker_, "mycanvas1"));
+    PrepareScene(viewport1_->GetScene());
+    viewport1_->UpdateSize();
+
+    viewport2_.reset(new OrthancStone::WebAssemblyViewport(broker_, "mycanvas2"));
+    PrepareScene(viewport2_->GetScene());
+    viewport2_->UpdateSize();
+
+    viewport3_.reset(new OrthancStone::WebAssemblyViewport(broker_, "mycanvas3"));
+    PrepareScene(viewport3_->GetScene());
+    viewport3_->UpdateSize();
+
+    emscripten_set_resize_callback("#window", NULL, false, OnWindowResize);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/WebAssembly/BasicScene.html	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,69 @@
+<!doctype html>
+<html lang="en-us">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+    <!-- Disable pinch zoom on mobile devices -->
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+    <meta name="HandheldFriendly" content="true" />
+    
+    
+    <title>Stone of Orthanc</title>
+
+    <style>
+      html, body {
+      width: 100%;
+      height: 100%;
+      margin: 0px;
+      border: 0;
+      overflow: hidden; /*  Disable scrollbars */
+      display: block;  /* No floating content on sides */
+      }
+
+      #mycanvas1 {
+      position:absolute;
+      left:0%;
+      top:0%;
+      background-color: red;
+      width: 50%;
+      height: 100%;
+      }
+
+      #mycanvas2 {
+      position:absolute;
+      left:50%;
+      top:0%;
+      background-color: green;
+      width: 50%;
+      height: 50%;
+      }
+
+      #mycanvas3 {
+      position:absolute;
+      left:50%;
+      top:50%;
+      background-color: blue;
+      width: 50%;
+      height: 50%;
+      }
+    </style>
+  </head>
+  <body>
+    <canvas id="mycanvas1" oncontextmenu="return false;"></canvas>
+    <canvas id="mycanvas2" oncontextmenu="return false;"></canvas>
+    <canvas id="mycanvas3" oncontextmenu="return false;"></canvas>
+
+    <script type="text/javascript">
+      if (!('WebAssembly' in window)) {
+      alert('Sorry, your browser does not support WebAssembly :(');
+      } else {
+      window.addEventListener('WebAssemblyLoaded', function() {
+      Module.ccall('Initialize', null, null, null);
+      });
+      }
+    </script>
+
+    <script type="text/javascript" async src="BasicScene.js"></script>
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/WebAssembly/CMakeLists.txt	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,89 @@
+cmake_minimum_required(VERSION 2.8.3)
+
+
+#####################################################################
+## Configuration of the Emscripten compiler for WebAssembly target
+#####################################################################
+
+set(WASM_FLAGS "-s WASM=1 -s FETCH=1")
+
+set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WASM_FLAGS}")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_FLAGS}")
+set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]'")
+set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ERROR_ON_UNDEFINED_SYMBOLS=1")
+set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1")
+set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ASSERTIONS=1 -s DISABLE_EXCEPTION_CATCHING=0")
+
+
+#####################################################################
+## Configuration of the Orthanc framework
+#####################################################################
+
+# This CMake file defines the "ORTHANC_STONE_VERSION" macro, so it
+# must be the first inclusion
+include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/Version.cmake)
+
+if (ORTHANC_STONE_VERSION STREQUAL "mainline")
+  set(ORTHANC_FRAMEWORK_VERSION "mainline")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
+else()
+  set(ORTHANC_FRAMEWORK_VERSION "1.5.7")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
+endif()
+
+set(ORTHANC_FRAMEWORK_SOURCE "${ORTHANC_FRAMEWORK_DEFAULT_SOURCE}" CACHE STRING "Source of the Orthanc source code (can be \"hg\", \"archive\", \"web\" or \"path\")")
+set(ORTHANC_FRAMEWORK_ARCHIVE "" CACHE STRING "Path to the Orthanc archive, if ORTHANC_FRAMEWORK_SOURCE is \"archive\"")
+set(ORTHANC_FRAMEWORK_ROOT "" CACHE STRING "Path to the Orthanc source directory, if ORTHANC_FRAMEWORK_SOURCE is \"path\"")
+
+
+#####################################################################
+## Configuration of the Stone framework
+#####################################################################
+
+include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/OrthancStoneParameters.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake)
+
+DownloadPackage(
+  "a24b8136b8f3bb93f166baf97d9328de"
+  "http://orthanc.osimis.io/ThirdPartyDownloads/ubuntu-font-family-0.83.zip"
+  "${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83")
+
+set(ORTHANC_STONE_APPLICATION_RESOURCES
+  UBUNTU_FONT  ${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83/Ubuntu-R.ttf
+  )
+
+SET(ENABLE_GOOGLE_TEST OFF)
+SET(ENABLE_LOCALE ON)
+SET(ORTHANC_SANDBOXED ON)
+SET(ENABLE_WASM ON)
+
+include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/OrthancStoneConfiguration.cmake)
+
+
+#####################################################################
+## Build the samples
+#####################################################################
+
+add_library(OrthancStone STATIC
+  ${ORTHANC_STONE_SOURCES}
+  )
+
+add_executable(BasicScene
+  BasicScene.cpp
+  )
+
+target_link_libraries(BasicScene OrthancStone)
+
+install(
+  TARGETS BasicScene
+  RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}
+  )
+
+install(
+  FILES
+  ${CMAKE_CURRENT_BINARY_DIR}/BasicScene.wasm
+  ${CMAKE_SOURCE_DIR}/BasicScene.html
+  ${CMAKE_SOURCE_DIR}/Configuration.json
+  ${CMAKE_SOURCE_DIR}/index.html
+  DESTINATION ${CMAKE_INSTALL_PREFIX}
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/WebAssembly/Configuration.json	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,17 @@
+{
+  "Plugins": [
+    "/usr/local/share/orthanc/plugins/libOrthancWebViewer.so",
+    "/usr/local/share/orthanc/plugins/libServeFolders.so"
+  ],
+  "StorageDirectory" : "/var/lib/orthanc/db",
+  "IndexDirectory" : "/var/lib/orthanc/db",
+  "RemoteAccessAllowed" : true,
+  "AuthenticationEnabled" : false,
+  "ServeFolders" : {
+    "AllowCache" : false,
+    "GenerateETag" : true,
+    "Folders" : {
+      "/stone" : "/root/stone"
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/WebAssembly/NOTES.txt	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,4 @@
+$ source ~/Downloads/emsdk/emsdk_env.sh
+$ cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Release -DALLOW_DOWNLOADS=ON .. -DCMAKE_INSTALL_PREFIX=/tmp/stone
+$ ninja install
+$ sudo docker run -p 4242:4242 -p 8042:8042 --rm -v /tmp/stone:/root/stone:ro jodogne/orthanc-plugins:1.5.6 /root/stone/Configuration.json --verbose
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/WebAssembly/index.html	Thu May 16 09:11:14 2019 +0200
@@ -0,0 +1,15 @@
+<!doctype html>
+<html lang="en-us">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+    <title>Stone of Orthanc</title>
+  </head>
+  <body>
+    <h1>Available samples</h1>
+    <ul>
+      <li><a href="BasicScene.html">Basic scene</a></li>
+    </ul>
+  </body>
+</html>
--- a/UnitTestsSources/TestMessageBroker.cpp	Tue May 14 18:24:12 2019 +0200
+++ b/UnitTestsSources/TestMessageBroker.cpp	Thu May 16 09:11:14 2019 +0200
@@ -34,32 +34,25 @@
   using namespace OrthancStone;
 
 
-  enum CustomMessageType
-  {
-    CustomMessageType_First = MessageType_CustomMessage + 1,
-
-    CustomMessageType_Completed,
-    CustomMessageType_Increment
-  };
-
-
   class MyObservable : public IObservable
   {
   public:
-    struct MyCustomMessage: public BaseMessage<CustomMessageType_Completed>
+    struct MyCustomMessage : public IMessage
     {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+
       int payload_;
 
-      MyCustomMessage(int payload)
-        : BaseMessage(),
-          payload_(payload)
-      {}
+      MyCustomMessage(int payload) :
+        payload_(payload)
+      {
+      }
     };
 
-    MyObservable(MessageBroker& broker)
-      : IObservable(broker)
-    {}
-
+    MyObservable(MessageBroker& broker) :
+      IObservable(broker)
+    {
+    }
   };
 
   class MyObserver : public IObserver
@@ -94,15 +87,18 @@
   class MyPromiseSource : public IObservable
   {
     Promise* currentPromise_;
+
   public:
-    struct MyPromiseMessage: public BaseMessage<MessageType_Test1>
+    struct MyPromiseMessage: public IMessage
     {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+
       int increment;
 
-      MyPromiseMessage(int increment)
-        : BaseMessage(),
-          increment(increment)
-      {}
+      MyPromiseMessage(int increment) :
+        increment(increment)
+      {
+      }
     };
 
     MyPromiseSource(MessageBroker& broker)
@@ -160,18 +156,18 @@
   observable.RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(observer, &MyObserver::HandleCompletedMessage));
 
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(12));
   ASSERT_EQ(12, testCounter);
 
   // the connection is permanent; if we emit the same message again, the observer will be notified again
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(20));
   ASSERT_EQ(20, testCounter);
 
   // Unregister the observer; make sure it's not called anymore
   observable.Unregister(&observer);
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(20));
   ASSERT_EQ(0, testCounter);
 }
 
@@ -186,12 +182,12 @@
   intermediate.RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(observer, &MyObserver::HandleCompletedMessage));
 
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(12));
   ASSERT_EQ(12, testCounter);
 
   // the connection is permanent; if we emit the same message again, the observer will be notified again
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(20));
   ASSERT_EQ(20, testCounter);
 }
 
@@ -205,7 +201,7 @@
   observable.RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(*observer, &MyObserver::HandleCompletedMessage));
 
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(12));
   ASSERT_EQ(12, testCounter);
 
   // delete the observer and check that the callback is not called anymore
@@ -213,7 +209,7 @@
 
   // the connection is permanent; if we emit the same message again, the observer will be notified again
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(20));
   ASSERT_EQ(0, testCounter);
 }
 
@@ -228,12 +224,12 @@
   intermediate->RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(observer, &MyObserver::HandleCompletedMessage));
 
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(12));
   ASSERT_EQ(12, testCounter);
 
   delete intermediate;
 
-  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(20));
   ASSERT_EQ(12, testCounter);
 }
 
@@ -248,12 +244,12 @@
   intermediate.RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(observer, &MyObserver::HandleCompletedMessage));
 
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(12));
   ASSERT_EQ(12, testCounter);
 
   // the connection is permanent; if we emit the same message again, the observer will be notified again
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(20));
   ASSERT_EQ(20, testCounter);
 }
 
@@ -324,7 +320,7 @@
   observable.RegisterObserverCallback(new LambdaCallable<MyObservable::MyCustomMessage>(*observer, [&](const MyObservable::MyCustomMessage& message) {testCounter += 2 * message.payload_;}));
 
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(12));
   ASSERT_EQ(24, testCounter);
 
   // delete the observer and check that the callback is not called anymore
@@ -332,7 +328,7 @@
 
   // the connection is permanent; if we emit the same message again, the observer will be notified again
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(20));
   ASSERT_EQ(0, testCounter);
 }
 
@@ -362,7 +358,7 @@
   MyObserverWithLambda*   observer = new MyObserverWithLambda(broker, 3, observable);
 
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(12));
   ASSERT_EQ(36, testCounter);
 
   // delete the observer and check that the callback is not called anymore
@@ -370,7 +366,7 @@
 
   // the connection is permanent; if we emit the same message again, the observer will be notified again
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(20));
   ASSERT_EQ(0, testCounter);
 }