changeset 605:7a7e36c52d62

Merged am-dev into default
author Alain Mazy <am@osimis.io>
date Mon, 29 Apr 2019 15:24:59 +0200
parents 86dfde451f4c (diff) 8432926e9db9 (current diff)
children d9c0a66304cb
files Framework/Radiography/RadiographyScene.cpp
diffstat 125 files changed, 9416 insertions(+), 132 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Mon Apr 29 12:01:55 2019 +0200
+++ b/.hgignore	Mon Apr 29 15:24:59 2019 +0200
@@ -1,28 +1,31 @@
-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/__pycache__
 Resources/CodeGeneration/build/
 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/
--- a/Applications/Generic/NativeStoneApplicationRunner.cpp	Mon Apr 29 12:01:55 2019 +0200
+++ b/Applications/Generic/NativeStoneApplicationRunner.cpp	Mon Apr 29 15:24:59 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).run(), parameters);
+      boost::program_options::store(
+        boost::program_options::command_line_parser(argc, argv).
+          options(options).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;
     }
 
@@ -120,12 +124,11 @@
     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;
+                << "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 << options << "\n";
       return error ? -1 : 0;
@@ -138,21 +141,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 +175,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 +205,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 +268,4 @@
 
     return (success ? 0 : -1);
   }
-
 }
--- a/Applications/Samples/CMakeLists.txt	Mon Apr 29 12:01:55 2019 +0200
+++ b/Applications/Samples/CMakeLists.txt	Mon Apr 29 15:24:59 2019 +0200
@@ -216,7 +216,6 @@
       ${SIMPLE_VIEWER_APPLICATION_SOURCES}
       )
     target_link_libraries(OrthancStoneSimpleViewer OrthancStone)
-
 endif()
 
 #####################################################################
--- a/Applications/Samples/SampleMainNative.cpp	Mon Apr 29 12:01:55 2019 +0200
+++ b/Applications/Samples/SampleMainNative.cpp	Mon Apr 29 15:24:59 2019 +0200
@@ -42,3 +42,4 @@
   return qtAppRunner.Execute(argc, argv);
 #endif
 }
+
--- a/Applications/Samples/rt-viewer-demo/main.cpp	Mon Apr 29 12:01:55 2019 +0200
+++ b/Applications/Samples/rt-viewer-demo/main.cpp	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 2019 +0200
@@ -0,0 +1,87 @@
+/**
+ * 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
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  SdlOpenGLWindow::SdlOpenGLWindow(const char* title,
+                                   unsigned int width,
+                                   unsigned int height) :
+    window_(title, width, height, true /* enable OpenGL */)
+  {
+    context_ = SDL_GL_CreateContext(window_.GetObject());
+    
+    if (context_ == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                      "Cannot initialize OpenGL");
+    }
+  }
+
+  
+  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()
+  {
+    int w = 0;
+    SDL_GL_GetDrawableSize(window_.GetObject(), &w, NULL);
+    return static_cast<unsigned int>(w);
+  }
+
+  
+  unsigned int SdlOpenGLWindow::GetCanvasHeight()
+  {
+    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	Mon Apr 29 15:24:59 2019 +0200
@@ -0,0 +1,59 @@
+/**
+ * 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);
+
+    ~SdlOpenGLWindow();
+
+    SdlWindow& GetWindow()
+    {
+      return window_;
+    }
+
+    virtual void MakeCurrent();
+
+    virtual void SwapBuffer();
+
+    virtual unsigned int GetCanvasWidth();
+
+    virtual unsigned int GetCanvasHeight();
+  };
+}
+
+#endif
--- a/Applications/Wasm/StartupParametersBuilder.cpp	Mon Apr 29 12:01:55 2019 +0200
+++ b/Applications/Wasm/StartupParametersBuilder.cpp	Mon Apr 29 15:24:59 2019 +0200
@@ -1,43 +1,66 @@
 #include "StartupParametersBuilder.h"
+#include <iostream>
 
 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] = "Toto.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];
+        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).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).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	Mon Apr 29 12:01:55 2019 +0200
+++ b/Applications/Wasm/StartupParametersBuilder.h	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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/RenderStyle.cpp	Mon Apr 29 12:01:55 2019 +0200
+++ b/Framework/Layers/RenderStyle.cpp	Mon Apr 29 15:24:59 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;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/OpenGL/IOpenGLContext.h	Mon Apr 29 15:24:59 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() = 0;
+
+      virtual unsigned int GetCanvasHeight() = 0;
+    };
+  }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/OpenGL/OpenGLIncludes.h	Mon Apr 29 15:24:59 2019 +0200
@@ -0,0 +1,45 @@
+/**
+ * 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 SDL
+#  if ORTHANC_ENABLE_SDL == 1
+#    include <SDL_opengl.h>
+#  else
+#    error Stone cannot be compiled on Windows without SDL
+#  endif
+#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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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_RGBA;
+          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	Mon Apr 29 15:24:59 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);
+    };
+  }
+}
--- a/Framework/Radiography/RadiographyScene.cpp	Mon Apr 29 12:01:55 2019 +0200
+++ b/Framework/Radiography/RadiographyScene.cpp	Mon Apr 29 15:24:59 2019 +0200
@@ -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,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/CairoCompositor.cpp	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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 "ISceneLayer.h"
+
+#include <stdint.h>
+
+namespace OrthancStone
+{
+  class ColorSceneLayer : public ISceneLayer
+  {
+  private:
+    uint8_t  red_;
+    uint8_t  green_;
+    uint8_t  blue_;
+
+  public:
+    ColorSceneLayer() :
+      red_(255),
+      green_(255),
+      blue_(255)
+    {
+    }
+
+    void SetColor(uint8_t red,
+                  uint8_t green,
+                  uint8_t blue)
+    {
+      red_ = red;
+      green_ = green;
+      blue_ = blue;
+    }
+
+    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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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
+
+#include "TextureBaseSceneLayer.h"
+
+namespace OrthancStone
+{
+  class FloatTextureSceneLayer : public TextureBaseSceneLayer
+  {
+  private:
+    ImageWindowing   windowing_;
+    float            customCenter_;
+    float            customWidth_;
+
+  public:
+    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	Mon Apr 29 15:24:59 2019 +0200
@@ -0,0 +1,39 @@
+/**
+ * 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()
+    {
+    }
+
+    virtual void Update(const PointerEvent& event) = 0;
+
+    virtual void Release() = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/ISceneLayer.h	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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 "FixedPointAligner.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    FixedPointAligner::FixedPointAligner(Scene2D& scene,
+                                         const ScenePoint2D& p,
+                                         unsigned int canvasWidth,
+                                         unsigned int canvasHeight) :
+      scene_(scene)
+    {
+      canvas_ = ScenePoint2D(p.GetX() - static_cast<double>(canvasWidth) / 2.0,
+                             p.GetY() - static_cast<double>(canvasHeight) / 2.0);
+      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	Mon Apr 29 15:24:59 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 "../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,
+                        unsigned int canvasWidth,
+                        unsigned int canvasHeight);
+
+      void Apply();
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/ICairoContextProvider.h	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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/>.
+ **/
+
+
+#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(previous.GetX(), previous.GetY());
+            glVertex2f(p.GetX(), p.GetY());
+
+            previous = p;
+          }
+
+          if (layer_.IsClosedChain(i))
+          {
+            ScenePoint2D p = chain[0].Apply(t);
+
+            glVertex2f(previous.GetX(), previous.GetY());
+            glVertex2f(p.GetX(), 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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/>.
+ **/
+
+
+#include "OpenGLColorTextureProgram.h"
+
+
+static const char* FRAGMENT_SHADER = 
+  "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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 2019 +0200
@@ -0,0 +1,146 @@
+/**
+ * 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 <Core/OrthancException.h>
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+
+
+static const char* FRAGMENT_SHADER = 
+  "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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 2019 +0200
@@ -0,0 +1,459 @@
+/**
+ * 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 <Core/OrthancException.h>
+
+
+static const unsigned int COMPONENTS_POSITION = 3;
+static const unsigned int COMPONENTS_MITER = 2;
+
+
+static const char* VERTEX_SHADER = 
+  "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 = 
+  "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	Mon Apr 29 15:24:59 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/OpenGLTextProgram.cpp	Mon Apr 29 15:24:59 2019 +0200
@@ -0,0 +1,190 @@
+/**
+ * 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 "../../Fonts/OpenGLTextCoordinates.h"
+
+#include <Core/OrthancException.h>
+
+
+static const unsigned int COMPONENTS = 2;
+
+static const char* VERTEX_SHADER = 
+  "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 = 
+  "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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 2019 +0200
@@ -0,0 +1,120 @@
+/**
+ * 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"
+
+static const unsigned int COMPONENTS = 2;
+static const unsigned int COUNT = 6;  // 2 triangles in 2D
+
+static const char* VERTEX_SHADER = 
+  "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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 2019 +0200
@@ -0,0 +1,205 @@
+/**
+ * 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
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/OpenGLCompositor.h	Mon Apr 29 15:24:59 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 "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
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/PanSceneTracker.cpp	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 2019 +0200
@@ -0,0 +1,69 @@
+/**
+ * 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];
+    }
+  }
+    
+
+  // Add the center of the pixel
+  void PointerEvent::AddIntegerPosition(int x,
+                                        int y)
+  {
+    AddPosition(static_cast<double>(x) + 0.5,
+                static_cast<double>(y) + 0.5);
+  }
+    
+    
+  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	Mon Apr 29 15:24:59 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/>.
+ **/
+
+
+#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(double x,
+                     double y)
+    {
+      positions_.push_back(ScenePoint2D(x, y));
+    }
+
+    // Add the center of the pixel
+    void AddIntegerPosition(int x,
+                            int 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	Mon Apr 29 15:24:59 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 "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;
+    }
+  }
+
+
+  void PolylineSceneLayer::Copy(const PolylineSceneLayer& from)
+  {
+    SetColor(from.GetRed(), from.GetGreen(), from.GetBlue());
+    chains_ = from.chains_;
+    closed_ = from.closed_;
+    thickness_ = from.thickness_;
+  }
+
+  
+  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);
+    }
+  }
+
+
+  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	Mon Apr 29 15:24:59 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 "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);
+
+    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;
+    
+    virtual uint64_t GetRevision() const
+    {
+      return 0;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/RotateSceneTracker.cpp	Mon Apr 29 15:24:59 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 "RotateSceneTracker.h"
+
+namespace OrthancStone
+{
+  RotateSceneTracker::RotateSceneTracker(Scene2D& scene,
+                                         const PointerEvent& event,
+                                         unsigned int canvasWidth,
+                                         unsigned int canvasHeight) :
+    scene_(scene),
+    click_(event.GetMainPosition()),
+    aligner_(scene, click_, canvasWidth, canvasHeight),
+    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	Mon Apr 29 15:24:59 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 "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,
+                       unsigned int canvasWidth,
+                       unsigned int canvasHeight);
+
+    virtual void Update(const PointerEvent& event);
+
+    virtual void Release()
+    {
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Scene2D.cpp	Mon Apr 29 15:24:59 2019 +0200
@@ -0,0 +1,205 @@
+/**
+ * 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
+    {
+      assert(layer_.get() != NULL);
+      return *layer_;
+    }
+
+    uint64_t GetIdentifier() const
+    {
+      return identifier_;
+    }
+  };
+  
+  
+  Scene2D::Scene2D(const Scene2D& other) :
+    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
+  {
+    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())
+    {
+      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_BadSequenceOfCalls);
+    }
+    else
+    {
+      assert(found->second != NULL);
+      return found->second->GetLayer();
+    }
+  }
+
+  
+  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;
+  }
+
+
+  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	Mon Apr 29 15:24:59 2019 +0200
@@ -0,0 +1,97 @@
+/**
+ * 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 <map>
+
+namespace OrthancStone
+{
+  class Scene2D : public boost::noncopyable
+  {
+  public:
+    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() :
+      layerCounter_(0)
+    {
+    }
+    
+    ~Scene2D();
+
+    Scene2D* Clone() const
+    {
+      return new Scene2D(*this);
+    }
+
+    void SetLayer(int depth,
+                  ISceneLayer* layer);  // Takes ownership
+
+    void DeleteLayer(int depth);
+
+    bool HasLayer(int depth) const;
+
+    ISceneLayer& GetLayer(int depth) const;
+
+    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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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/>.
+ **/
+
+
+#include "ZoomSceneTracker.h"
+
+namespace OrthancStone
+{
+  ZoomSceneTracker::ZoomSceneTracker(Scene2D& scene,
+                                     const PointerEvent& event,
+                                     unsigned int canvasWidth,
+                                     unsigned int canvasHeight) :
+    scene_(scene),
+    clickY_(event.GetMainPosition().GetY()),
+    aligner_(scene, event.GetMainPosition(), canvasWidth, canvasHeight),
+    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	Mon Apr 29 15:24:59 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 "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 canvasWidth,
+                     unsigned int canvasHeight);
+
+    virtual void Update(const PointerEvent& event);
+
+    virtual void Release()
+    {
+    }
+  };
+}
--- a/Framework/StoneEnumerations.cpp	Mon Apr 29 12:01:55 2019 +0200
+++ b/Framework/StoneEnumerations.cpp	Mon Apr 29 15:24:59 2019 +0200
@@ -48,14 +48,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	Mon Apr 29 12:01:55 2019 +0200
+++ b/Framework/StoneEnumerations.h	Mon Apr 29 15:24:59 2019 +0200
@@ -34,7 +34,6 @@
 
   enum ImageWindowing
   {
-    ImageWindowing_Default,
     ImageWindowing_Bone,
     ImageWindowing_Lung,
     ImageWindowing_Custom
@@ -199,8 +198,8 @@
   void ComputeWindowing(float& targetCenter,
                         float& targetWidth,
                         ImageWindowing windowing,
-                        float defaultCenter,
-                        float defaultWidth);
+                        float customCenter,
+                        float customWidth);
 
   void ComputeAnchorTranslation(double& deltaX /* out */,
                                 double& deltaY /* out */,
--- a/Framework/Toolbox/AffineTransform2D.cpp	Mon Apr 29 12:01:55 2019 +0200
+++ b/Framework/Toolbox/AffineTransform2D.cpp	Mon Apr 29 15:24:59 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	Mon Apr 29 12:01:55 2019 +0200
+++ b/Framework/Toolbox/AffineTransform2D.h	Mon Apr 29 15:24:59 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	Mon Apr 29 12:01:55 2019 +0200
+++ b/Framework/Toolbox/BaseWebService.h	Mon Apr 29 15:24:59 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:
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Toolbox/DynamicBitmap.cpp	Mon Apr 29 15:24:59 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	Mon Apr 29 15:24:59 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/Viewport/IMouseTracker.h	Mon Apr 29 12:01:55 2019 +0200
+++ b/Framework/Viewport/IMouseTracker.h	Mon Apr 29 15:24:59 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/Volumes/VolumeReslicer.cpp	Mon Apr 29 12:01:55 2019 +0200
+++ b/Framework/Volumes/VolumeReslicer.cpp	Mon Apr 29 15:24:59 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/Platforms/Generic/DelayedCallCommand.cpp	Mon Apr 29 12:01:55 2019 +0200
+++ b/Platforms/Generic/DelayedCallCommand.cpp	Mon Apr 29 15:24:59 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	Mon Apr 29 12:01:55 2019 +0200
+++ b/Platforms/Generic/DelayedCallCommand.h	Mon Apr 29 15:24:59 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	Mon Apr 29 12:01:55 2019 +0200
+++ b/Platforms/Wasm/Defaults.cpp	Mon Apr 29 15:24:59 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	Mon Apr 29 12:01:55 2019 +0200
+++ b/Platforms/Wasm/Defaults.h	Mon Apr 29 15:24:59 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	Mon Apr 29 12:01:55 2019 +0200
+++ b/Platforms/Wasm/default-library.js	Mon Apr 29 15:24:59 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	Mon Apr 29 12:01:55 2019 +0200
+++ b/Platforms/Wasm/logger.ts	Mon Apr 29 15:24:59 2019 +0200
@@ -10,6 +10,10 @@
     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);
   }
@@ -22,6 +26,10 @@
     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);
   }
@@ -31,13 +39,25 @@
   }
 
   public _debug(source: LogSource, ...args: any[]): void {
-    var output = this.getOutput(source, args);
-    console.debug(...output);
+    if ((<any> window).IsTraceLevelEnabled)
+    {
+      if ((<any> window).IsTraceLevelEnabled())
+      {
+        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);
+    if ((<any> window).IsInfoLevelEnabled)
+    {
+      if ((<any> window).IsInfoLevelEnabled())
+      {
+        var output = this.getOutput(source, args);
+        console.info(...output);
+      }
+    }
   }
 
   public _warning(source: LogSource, ...args: any[]): void {
@@ -88,3 +108,4 @@
 }
 
 export var defaultLogger: StandardConsoleLogger = new TimeConsoleLogger();
+
--- a/Platforms/Wasm/stone-framework-loader.ts	Mon Apr 29 12:01:55 2019 +0200
+++ b/Platforms/Wasm/stone-framework-loader.ts	Mon Apr 29 15:24:59 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: [ 
--- a/Platforms/Wasm/wasm-application-runner.ts	Mon Apr 29 12:01:55 2019 +0200
+++ b/Platforms/Wasm/wasm-application-runner.ts	Mon Apr 29 15:24:59 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']);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/CMake/FreetypeConfiguration.cmake	Mon Apr 29 15:24:59 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()
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Mon Apr 29 12:01:55 2019 +0200
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Mon Apr 29 15:24:59 2019 +0200
@@ -67,6 +67,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)
 
 
@@ -105,6 +106,23 @@
 endif()
 
 
+if (ENABLE_OPENGL)
+  include(FindOpenGL)
+  if (NOT OPENGL_FOUND)
+    message(FATAL_ERROR "Cannot find OpenGL on your system")
+  endif()
+
+  link_libraries(${OPENGL_LIBRARIES})
+
+  add_definitions(
+    -DGL_GLEXT_PROTOTYPES=1
+    -DORTHANC_ENABLE_OPENGL=1
+    )
+else()
+  add_definitions(-DORTHANC_ENABLE_OPENGL=0)  
+endif()
+
+
 
 #####################################################################
 ## Configuration of the C/C++ macros
@@ -124,6 +142,8 @@
   add_definitions(-DCHECK_OBSERVERS_MESSAGES)
 endif()
 
+
+
 #####################################################################
 ## Embed the colormaps into the binaries
 #####################################################################
@@ -200,10 +220,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 +264,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
@@ -279,6 +326,7 @@
   ${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 +368,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,6 +392,7 @@
 
   # Mandatory components
   ${CAIRO_SOURCES}
+  ${FREETYPE_SOURCES}
   ${PIXMAN_SOURCES}
 
   # Optional components
@@ -351,6 +401,29 @@
   ${BOOST_EXTENDED_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
+    )
+endif()
+
+
 include_directories(${ORTHANC_STONE_ROOT})
 
 
--- a/Resources/CMake/OrthancStoneParameters.cmake	Mon Apr 29 12:01:55 2019 +0200
+++ b/Resources/CMake/OrthancStoneParameters.cmake	Mon Apr 29 15:24:59 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,7 @@
 
 # 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_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 +51,4 @@
 ## the Stone of Orthanc
 #####################################################################
 
+set(ENABLE_OPENGL ON CACHE INTERNAL "Enable support of OpenGL")
--- a/Resources/Orthanc/DownloadOrthancFramework.cmake	Mon Apr 29 12:01:55 2019 +0200
+++ b/Resources/Orthanc/DownloadOrthancFramework.cmake	Mon Apr 29 15:24:59 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	Mon Apr 29 12:01:55 2019 +0200
+++ b/Resources/Orthanc/LinuxStandardBaseToolchain.cmake	Mon Apr 29 15:24:59 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/Sdl/BasicScene.cpp	Mon Apr 29 15:24:59 2019 +0200
@@ -0,0 +1,371 @@
+/**
+ * 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/OpenGLCompositor.h"
+#include "../../Framework/Scene2D/Scene2D.h"
+#include "../../Framework/Scene2D/PanSceneTracker.h"
+#include "../../Framework/Scene2D/RotateSceneTracker.h"
+#include "../../Framework/Scene2D/ZoomSceneTracker.h"
+#include "../../Framework/Scene2D/ColorTextureSceneLayer.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 SDL_Event& event,
+                            std::auto_ptr<OrthancStone::IPointerTracker>& activeTracker,
+                            unsigned int windowWidth,
+                            unsigned int windowHeight)
+{
+  bool hasPositionLayer = false;
+  
+  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.AddIntegerPosition(event.button.x - static_cast<int>(windowWidth) / 2,
+                           event.button.y - static_cast<int>(windowHeight) / 2);
+      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());
+      }
+
+      hasPositionLayer = true;
+    }
+  }
+  else if (event.type == SDL_MOUSEBUTTONDOWN)
+  {
+    OrthancStone::PointerEvent e;
+    e.AddIntegerPosition(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, windowWidth, windowHeight));
+        break;
+
+      case SDL_BUTTON_LEFT:
+        activeTracker.reset(new OrthancStone::RotateSceneTracker
+                            (scene, e, windowWidth, windowHeight));
+        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(windowWidth, windowHeight);
+        break;
+              
+      case SDLK_c:
+        TakeScreenshot("screenshot.png", scene, windowWidth, windowHeight);
+        break;
+              
+      default:
+        break;
+    }
+  }
+
+  if (!hasPositionLayer)
+  {
+    scene.DeleteLayer(LAYER_POSITION);
+  }
+}
+
+
+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.AddIntegerPosition(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, event, tracker, window.GetCanvasWidth(), window.GetCanvasHeight());
+    }
+
+    SDL_Delay(1);
+  }
+}
+
+
+int main()
+{
+  Orthanc::Logging::Initialize();
+  OrthancStone::SdlWindow::GlobalInitialize();
+
+  try
+  {
+    OrthancStone::Scene2D scene;
+    PrepareScene(scene);
+    Run(scene);
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "EXCEPTION: " << e.What();
+  }
+
+  OrthancStone::SdlWindow::GlobalFinalize();
+  Orthanc::Logging::Finalize();
+
+  return 0;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/CMakeLists.txt	Mon Apr 29 15:24:59 2019 +0200
@@ -0,0 +1,71 @@
+cmake_minimum_required(VERSION 2.8.3)
+
+if(MSVC)
+  add_definitions(/MP)
+  if (CMAKE_BUILD_TYPE MATCHES DEBUG)
+    add_definitions(/JMC)
+  endif()
+endif()
+
+#####################################################################
+## 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(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)
+
+include_directories(${ORTHANC_STONE_ROOT})
+
+
+#####################################################################
+## Build the samples
+#####################################################################
+
+add_library(OrthancStone STATIC
+  ${ORTHANC_STONE_SOURCES}
+  )
+
+add_executable(BasicScene
+  BasicScene.cpp
+  )
+
+target_link_libraries(BasicScene OrthancStone)