changeset 430:b85f635f1eb5 am-vsol-upgrade

added serialization for RadiographyScene
author am@osimis.io
date Thu, 29 Nov 2018 15:11:19 +0100
parents c7fb700a7d12 (current diff) 3f9017db1738 (diff)
children 26b90b110719
files Applications/Samples/SingleFrameEditorApplication.h Framework/Radiography/RadiographyAlphaLayer.cpp Framework/Radiography/RadiographyAlphaLayer.h Framework/Radiography/RadiographyDicomLayer.cpp Framework/Radiography/RadiographyDicomLayer.h Framework/Radiography/RadiographyLayer.cpp Framework/Radiography/RadiographyLayer.h Framework/Radiography/RadiographyLayerCropTracker.cpp Framework/Radiography/RadiographyLayerMoveTracker.cpp Framework/Radiography/RadiographyLayerResizeTracker.cpp Framework/Radiography/RadiographyLayerRotateTracker.cpp Framework/Radiography/RadiographyScene.cpp Framework/Radiography/RadiographyScene.h Framework/Radiography/RadiographySceneReader.cpp Framework/Radiography/RadiographySceneReader.h Framework/Radiography/RadiographySceneWriter.cpp Framework/Radiography/RadiographySceneWriter.h Framework/Radiography/RadiographyTextLayer.cpp Framework/Radiography/RadiographyTextLayer.h Framework/StoneEnumerations.h Framework/Toolbox/OrthancApiClient.cpp Resources/CMake/OrthancStoneConfiguration.cmake
diffstat 22 files changed, 1395 insertions(+), 582 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/Samples/SingleFrameEditorApplication.h	Wed Nov 28 10:46:32 2018 +0100
+++ b/Applications/Samples/SingleFrameEditorApplication.h	Thu Nov 29 15:11:19 2018 +0100
@@ -31,6 +31,8 @@
 #include "../../Framework/Radiography/RadiographySceneCommand.h"
 #include "../../Framework/Radiography/RadiographyWidget.h"
 #include "../../Framework/Radiography/RadiographyWindowingTracker.h"
+#include "../../Framework/Radiography/RadiographySceneWriter.h"
+#include "../../Framework/Radiography/RadiographySceneReader.h"
 
 #include <Core/HttpClient.h>
 #include <Core/Images/FontRegistry.h>
@@ -48,8 +50,8 @@
   namespace Samples
   {
     class RadiographyEditorInteractor :
-      public IWorldSceneInteractor,
-      public IObserver
+        public IWorldSceneInteractor,
+        public IObserver
     {
     private:
       enum Tool
@@ -60,7 +62,7 @@
         Tool_Resize,
         Tool_Windowing
       };
-        
+
 
       StoneApplicationContext*  context_;
       UndoRedoStack             undoRedoStack_;
@@ -71,8 +73,8 @@
       {
         return 10.0;
       }
-    
-         
+
+
     public:
       RadiographyEditorInteractor(MessageBroker& broker) :
         IObserver(broker),
@@ -85,7 +87,7 @@
       {
         context_ = &context;
       }
-    
+
       virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& worldWidget,
                                                           const ViewportGeometry& view,
                                                           MouseButton button,
@@ -101,16 +103,16 @@
         if (button == MouseButton_Left)
         {
           size_t selected;
-        
+
           if (tool_ == Tool_Windowing)
           {
             return new RadiographyWindowingTracker(
-              undoRedoStack_, widget.GetScene(),
-              viewportX, viewportY,
-              RadiographyWindowingTracker::Action_DecreaseWidth,
-              RadiographyWindowingTracker::Action_IncreaseWidth,
-              RadiographyWindowingTracker::Action_DecreaseCenter,
-              RadiographyWindowingTracker::Action_IncreaseCenter);
+                  undoRedoStack_, widget.GetScene(),
+                  viewportX, viewportY,
+                  RadiographyWindowingTracker::Action_DecreaseWidth,
+                  RadiographyWindowingTracker::Action_IncreaseWidth,
+                  RadiographyWindowingTracker::Action_DecreaseCenter,
+                  RadiographyWindowingTracker::Action_IncreaseCenter);
           }
           else if (!widget.LookupSelectedLayer(selected))
           {
@@ -133,23 +135,23 @@
             {
               switch (tool_)
               {
-                case Tool_Crop:
-                  return new RadiographyLayerCropTracker
+              case Tool_Crop:
+                return new RadiographyLayerCropTracker
                     (undoRedoStack_, widget.GetScene(), view, selected, x, y, corner);
 
-                case Tool_Resize:
-                  return new RadiographyLayerResizeTracker
+              case Tool_Resize:
+                return new RadiographyLayerResizeTracker
                     (undoRedoStack_, widget.GetScene(), selected, x, y, corner,
                      (modifiers & KeyboardModifiers_Shift));
 
-                default:
-                  throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+              default:
+                throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
               }
             }
             else
             {
               size_t layer;
-            
+
               if (widget.GetScene().LookupLayer(layer, x, y))
               {
                 widget.Select(layer);
@@ -158,7 +160,7 @@
               {
                 widget.Unselect();
               }
-            
+
               return NULL;
             }
           }
@@ -172,18 +174,18 @@
               {
                 switch (tool_)
                 {
-                  case Tool_Move:
-                    return new RadiographyLayerMoveTracker
+                case Tool_Move:
+                  return new RadiographyLayerMoveTracker
                       (undoRedoStack_, widget.GetScene(), layer, x, y,
                        (modifiers & KeyboardModifiers_Shift));
 
-                  case Tool_Rotate:
-                    return new RadiographyLayerRotateTracker
+                case Tool_Rotate:
+                  return new RadiographyLayerRotateTracker
                       (undoRedoStack_, widget.GetScene(), view, layer, x, y,
                        (modifiers & KeyboardModifiers_Shift));
-                
-                  default:
-                    break;
+
+                default:
+                  break;
                 }
 
                 return NULL;
@@ -232,14 +234,14 @@
              tool_ == Tool_Resize))
         {
           RadiographyScene::LayerAccessor accessor(widget.GetScene(), selected);
-        
+
           Corner corner;
           if (accessor.GetLayer().LookupCorner(corner, x, y, view.GetZoom(), GetHandleSize()))
           {
             accessor.GetLayer().GetCorner(x, y, corner);
-          
+
             double z = 1.0 / view.GetZoom();
-          
+
             context.SetSourceColor(255, 0, 0);
             cairo_t* cr = context.GetObject();
             cairo_set_line_width(cr, 2.0 * z);
@@ -270,118 +272,141 @@
 
         switch (keyChar)
         {
-          case 'a':
-            widget.FitContent();
-            break;
+        case 'a':
+          widget.FitContent();
+          break;
+
+        case 'c':
+          tool_ = Tool_Crop;
+          break;
 
-          case 'c':
-            tool_ = Tool_Crop;
-            break;
+        case 'd':
+        {
+          // dump to json and reload
+          Json::Value snapshot;
+          RadiographySceneWriter writer;
+          writer.Write(snapshot, widget.GetScene());
 
-          case 'e':
-          {
-            Orthanc::DicomMap tags;
+          LOG(INFO) << "JSON export was successful: "
+                    << snapshot.toStyledString();
+
+          boost::shared_ptr<RadiographyScene> scene(new RadiographyScene(GetBroker()));
+          RadiographySceneReader reader(*scene, context_->GetOrthancApiClient());
+
+          Orthanc::FontRegistry fontRegistry;
+          fontRegistry.AddFromResource(Orthanc::EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16);
+
+          reader.SetFontRegistry(fontRegistry);
+          reader.Read(snapshot);
 
-            // Minimal set of tags to generate a valid CR image
-            tags.SetValue(Orthanc::DICOM_TAG_ACCESSION_NUMBER, "NOPE", false);
-            tags.SetValue(Orthanc::DICOM_TAG_BODY_PART_EXAMINED, "PELVIS", false);
-            tags.SetValue(Orthanc::DICOM_TAG_INSTANCE_NUMBER, "1", false);
-            //tags.SetValue(Orthanc::DICOM_TAG_LATERALITY, "", false);
-            tags.SetValue(Orthanc::DICOM_TAG_MANUFACTURER, "OSIMIS", false);
-            tags.SetValue(Orthanc::DICOM_TAG_MODALITY, "CR", false);
-            tags.SetValue(Orthanc::DICOM_TAG_PATIENT_BIRTH_DATE, "20000101", false);
-            tags.SetValue(Orthanc::DICOM_TAG_PATIENT_ID, "hello", false);
-            tags.SetValue(Orthanc::DICOM_TAG_PATIENT_NAME, "HELLO^WORLD", false);
-            tags.SetValue(Orthanc::DICOM_TAG_PATIENT_ORIENTATION, "", false);
-            tags.SetValue(Orthanc::DICOM_TAG_PATIENT_SEX, "M", false);
-            tags.SetValue(Orthanc::DICOM_TAG_REFERRING_PHYSICIAN_NAME, "HOUSE^MD", false);
-            tags.SetValue(Orthanc::DICOM_TAG_SERIES_NUMBER, "1", false);
-            tags.SetValue(Orthanc::DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.1", false);
-            tags.SetValue(Orthanc::DICOM_TAG_STUDY_ID, "STUDY", false);
-            tags.SetValue(Orthanc::DICOM_TAG_VIEW_POSITION, "", false);
+          widget.SetScene(scene);
+        };break;
+
+        case 'e':
+        {
+          Orthanc::DicomMap tags;
 
-            if (context_ != NULL)
-            {
-              widget.GetScene().ExportDicom(context_->GetOrthancApiClient(),
-                                            tags, std::string(), 0.1, 0.1, widget.IsInverted(),
-                                            widget.GetInterpolation(), EXPORT_USING_PAM);
-            }
-            
-            break;
+          // Minimal set of tags to generate a valid CR image
+          tags.SetValue(Orthanc::DICOM_TAG_ACCESSION_NUMBER, "NOPE", false);
+          tags.SetValue(Orthanc::DICOM_TAG_BODY_PART_EXAMINED, "PELVIS", false);
+          tags.SetValue(Orthanc::DICOM_TAG_INSTANCE_NUMBER, "1", false);
+          //tags.SetValue(Orthanc::DICOM_TAG_LATERALITY, "", false);
+          tags.SetValue(Orthanc::DICOM_TAG_MANUFACTURER, "OSIMIS", false);
+          tags.SetValue(Orthanc::DICOM_TAG_MODALITY, "CR", false);
+          tags.SetValue(Orthanc::DICOM_TAG_PATIENT_BIRTH_DATE, "20000101", false);
+          tags.SetValue(Orthanc::DICOM_TAG_PATIENT_ID, "hello", false);
+          tags.SetValue(Orthanc::DICOM_TAG_PATIENT_NAME, "HELLO^WORLD", false);
+          tags.SetValue(Orthanc::DICOM_TAG_PATIENT_ORIENTATION, "", false);
+          tags.SetValue(Orthanc::DICOM_TAG_PATIENT_SEX, "M", false);
+          tags.SetValue(Orthanc::DICOM_TAG_REFERRING_PHYSICIAN_NAME, "HOUSE^MD", false);
+          tags.SetValue(Orthanc::DICOM_TAG_SERIES_NUMBER, "1", false);
+          tags.SetValue(Orthanc::DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.1", false);
+          tags.SetValue(Orthanc::DICOM_TAG_STUDY_ID, "STUDY", false);
+          tags.SetValue(Orthanc::DICOM_TAG_VIEW_POSITION, "", false);
+
+          if (context_ != NULL)
+          {
+            widget.GetScene().ExportDicom(context_->GetOrthancApiClient(),
+                                          tags, std::string(), 0.1, 0.1, widget.IsInverted(),
+                                          widget.GetInterpolation(), EXPORT_USING_PAM);
           }
 
-          case 'i':
-            widget.SwitchInvert();
-            break;
-        
-          case 'm':
-            tool_ = Tool_Move;
+          break;
+        }
+
+        case 'i':
+          widget.SwitchInvert();
+          break;
+
+        case 'm':
+          tool_ = Tool_Move;
+          break;
+
+        case 'n':
+        {
+          switch (widget.GetInterpolation())
+          {
+          case ImageInterpolation_Nearest:
+            LOG(INFO) << "Switching to bilinear interpolation";
+            widget.SetInterpolation(ImageInterpolation_Bilinear);
             break;
 
-          case 'n':
-          {
-            switch (widget.GetInterpolation())
-            {
-              case ImageInterpolation_Nearest:
-                LOG(INFO) << "Switching to bilinear interpolation";
-                widget.SetInterpolation(ImageInterpolation_Bilinear);
-                break;
-              
-              case ImageInterpolation_Bilinear:
-                LOG(INFO) << "Switching to nearest neighbor interpolation";
-                widget.SetInterpolation(ImageInterpolation_Nearest);
-                break;
-
-              default:
-                throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-            }
-          
-            break;
-          }
-        
-          case 'r':
-            tool_ = Tool_Rotate;
+          case ImageInterpolation_Bilinear:
+            LOG(INFO) << "Switching to nearest neighbor interpolation";
+            widget.SetInterpolation(ImageInterpolation_Nearest);
             break;
 
-          case 's':
-            tool_ = Tool_Resize;
-            break;
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+          }
+          
+          break;
+        }
 
-          case 'w':
-            tool_ = Tool_Windowing;
-            break;
+        case 'r':
+          tool_ = Tool_Rotate;
+          break;
+
+        case 's':
+          tool_ = Tool_Resize;
+          break;
+
+        case 'w':
+          tool_ = Tool_Windowing;
+          break;
 
-          case 'y':
-            if (modifiers & KeyboardModifiers_Control)
-            {
-              undoRedoStack_.Redo();
-              widget.NotifyContentChanged();
-            }
-            break;
+        case 'y':
+          if (modifiers & KeyboardModifiers_Control)
+          {
+            undoRedoStack_.Redo();
+            widget.NotifyContentChanged();
+          }
+          break;
 
-          case 'z':
-            if (modifiers & KeyboardModifiers_Control)
-            {
-              undoRedoStack_.Undo();
-              widget.NotifyContentChanged();
-            }
-            break;
-        
-          default:
-            break;
+        case 'z':
+          if (modifiers & KeyboardModifiers_Control)
+          {
+            undoRedoStack_.Undo();
+            widget.NotifyContentChanged();
+          }
+          break;
+
+        default:
+          break;
         }
       }
     };
 
-  
-  
+
+
     class SingleFrameEditorApplication :
-      public SampleSingleCanvasApplicationBase,
-      public IObserver
+        public SampleSingleCanvasApplicationBase,
+        public IObserver
     {
     private:
-      boost::shared_ptr<RadiographyScene>  scene_;
-      RadiographyEditorInteractor      interactor_;
+      boost::shared_ptr<RadiographyScene>   scene_;
+      RadiographyEditorInteractor           interactor_;
+      Orthanc::FontRegistry                 fontRegistry_;
 
     public:
       SingleFrameEditorApplication(MessageBroker& broker) :
@@ -399,11 +424,11 @@
       {
         boost::program_options::options_description generic("Sample options");
         generic.add_options()
-          ("instance", boost::program_options::value<std::string>(),
-           "Orthanc ID of the instance")
-          ("frame", boost::program_options::value<unsigned int>()->default_value(0),
-           "Number of the frame, for multi-frame DICOM instances")
-          ;
+            ("instance", boost::program_options::value<std::string>(),
+             "Orthanc ID of the instance")
+            ("frame", boost::program_options::value<unsigned int>()->default_value(0),
+             "Number of the frame, for multi-frame DICOM instances")
+            ;
 
         options.add(generic);
       }
@@ -440,12 +465,11 @@
         std::string instance = parameters["instance"].as<std::string>();
         int frame = parameters["frame"].as<unsigned int>();
 
-        Orthanc::FontRegistry fonts;
-        fonts.AddFromResource(Orthanc::EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16);
+        fontRegistry_.AddFromResource(Orthanc::EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16);
         
         scene_.reset(new RadiographyScene(GetBroker()));
         //scene_->LoadDicomFrame(instance, frame, false); //.SetPan(200, 0);
-        scene_->LoadDicomFrame(context->GetOrthancApiClient(), "61f3143e-96f34791-ad6bbb8d-62559e75-45943e1b", 0, false);
+        scene_->LoadDicomFrame(context->GetOrthancApiClient(), "61f3143e-96f34791-ad6bbb8d-62559e75-45943e1b", 0, false, NULL);
 
 #if !defined(ORTHANC_ENABLE_WASM) || ORTHANC_ENABLE_WASM != 1
         Orthanc::HttpClient::ConfigureSsl(true, "/etc/ssl/certs/ca-certificates.crt");
@@ -454,12 +478,12 @@
         //scene_->LoadDicomWebFrame(context->GetWebService());
         
         {
-          RadiographyLayer& layer = scene_->LoadText(fonts.GetFont(0), "Hello\nworld");
+          RadiographyLayer& layer = scene_->LoadText(fontRegistry_.GetFont(0), "Hello\nworld", NULL);
           layer.SetResizeable(true);
         }
         
         {
-          RadiographyLayer& layer = scene_->LoadTestBlock(100, 50);
+          RadiographyLayer& layer = scene_->LoadTestBlock(100, 50, NULL);
           layer.SetResizeable(true);
           layer.SetPan(0, 200);
         }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographyAlphaLayer.cpp	Thu Nov 29 15:11:19 2018 +0100
@@ -0,0 +1,132 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "RadiographyAlphaLayer.h"
+
+#include "RadiographyScene.h"
+
+#include <Core/Images/Image.h>
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+
+  void RadiographyAlphaLayer::SetAlpha(Orthanc::ImageAccessor* image)
+  {
+    std::auto_ptr<Orthanc::ImageAccessor> raii(image);
+
+    if (image == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+
+    if (image->GetFormat() != Orthanc::PixelFormat_Grayscale8)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+    }
+
+    SetSize(image->GetWidth(), image->GetHeight());
+    alpha_ = raii;
+  }
+
+  void RadiographyAlphaLayer::Render(Orthanc::ImageAccessor& buffer,
+                                     const AffineTransform2D& viewTransform,
+                                     ImageInterpolation interpolation) const
+  {
+    if (alpha_.get() == NULL)
+    {
+      return;
+    }
+
+    if (buffer.GetFormat() != Orthanc::PixelFormat_Float32)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+    }
+
+    unsigned int cropX, cropY, cropWidth, cropHeight;
+    GetCrop(cropX, cropY, cropWidth, cropHeight);
+
+    const AffineTransform2D t = AffineTransform2D::Combine(
+          viewTransform, GetTransform(),
+          AffineTransform2D::CreateOffset(cropX, cropY));
+
+    Orthanc::ImageAccessor cropped;
+    alpha_->GetRegion(cropped, cropX, cropY, cropWidth, cropHeight);
+
+    Orthanc::Image tmp(Orthanc::PixelFormat_Grayscale8, buffer.GetWidth(), buffer.GetHeight(), false);
+
+    t.Apply(tmp, cropped, interpolation, true /* clear */);
+
+    // Blit
+    const unsigned int width = buffer.GetWidth();
+    const unsigned int height = buffer.GetHeight();
+
+    float value = foreground_;
+
+    if (useWindowing_)
+    {
+      float center, width;
+      if (scene_.GetWindowing(center, width))
+      {
+        value = center + width / 2.0f;  // TODO: shouldn't it be center alone ?
+      }
+    }
+
+    for (unsigned int y = 0; y < height; y++)
+    {
+      float *q = reinterpret_cast<float*>(buffer.GetRow(y));
+      const uint8_t *p = reinterpret_cast<uint8_t*>(tmp.GetRow(y));
+
+      for (unsigned int x = 0; x < width; x++, p++, q++)
+      {
+        float a = static_cast<float>(*p) / 255.0f;
+
+        *q = (a * value + (1.0f - a) * (*q));
+      }
+    }
+  }
+
+  bool RadiographyAlphaLayer::GetRange(float& minValue,
+                                       float& maxValue) const
+  {
+    if (useWindowing_)
+    {
+      return false;
+    }
+    else
+    {
+      minValue = 0;
+      maxValue = 0;
+
+      if (foreground_ < 0)
+      {
+        minValue = foreground_;
+      }
+
+      if (foreground_ > 0)
+      {
+        maxValue = foreground_;
+      }
+
+      return true;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographyAlphaLayer.h	Thu Nov 29 15:11:19 2018 +0100
@@ -0,0 +1,89 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "RadiographyLayer.h"
+
+namespace OrthancStone
+{
+  class RadiographyScene;
+
+  // creates a transparent layer whose alpha channel is provided as a UINT8 image to SetAlpha.
+  // The color of the "mask" is either defined by a ForegroundValue or by the center value of the
+  // windowing from the scene.
+  class RadiographyAlphaLayer : public RadiographyLayer
+  {
+  private:
+    const RadiographyScene&                scene_;
+    std::auto_ptr<Orthanc::ImageAccessor>  alpha_;      // Grayscale8
+    bool                                   useWindowing_;
+    float                                  foreground_;
+
+  public:
+    RadiographyAlphaLayer(const RadiographyScene& scene) :
+      scene_(scene),
+      useWindowing_(true),
+      foreground_(0)
+    {
+    }
+
+
+    void SetForegroundValue(float foreground)
+    {
+      useWindowing_ = false;
+      foreground_ = foreground;
+    }
+
+    float GetForegroundValue() const
+    {
+      return foreground_;
+    }
+
+    bool IsUsingWindowing() const
+    {
+      return useWindowing_;
+    }
+
+    void SetAlpha(Orthanc::ImageAccessor* image);
+
+    virtual bool GetDefaultWindowing(float& center,
+                                     float& width) const
+    {
+      return false;
+    }
+
+
+    virtual void Render(Orthanc::ImageAccessor& buffer,
+                        const AffineTransform2D& viewTransform,
+                        ImageInterpolation interpolation) const;
+
+    virtual bool GetRange(float& minValue,
+                          float& maxValue) const;
+
+    const Orthanc::ImageAccessor& GetAlpha() const
+    {
+      return *(alpha_.get());
+    }
+
+
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographyDicomLayer.cpp	Thu Nov 29 15:11:19 2018 +0100
@@ -0,0 +1,159 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "RadiographyDicomLayer.h"
+
+#include "RadiographyScene.h"
+#include "../Toolbox/DicomFrameConverter.h"
+
+#include <Core/OrthancException.h>
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Plugins/Samples/Common/DicomDatasetReader.h>
+
+static OrthancPlugins::DicomTag  ConvertTag(const Orthanc::DicomTag& tag)
+{
+  return OrthancPlugins::DicomTag(tag.GetGroup(), tag.GetElement());
+}
+
+namespace OrthancStone
+{
+
+  void RadiographyDicomLayer::ApplyConverter()
+  {
+    if (source_.get() != NULL &&
+        converter_.get() != NULL)
+    {
+      converted_.reset(converter_->ConvertFrame(*source_));
+    }
+  }
+
+
+  void RadiographyDicomLayer::SetDicomTags(const OrthancPlugins::FullOrthancDataset& dataset)
+  {
+    converter_.reset(new DicomFrameConverter);
+    converter_->ReadParameters(dataset);
+    ApplyConverter();
+
+    std::string tmp;
+    Vector pixelSpacing;
+
+    if (dataset.GetStringValue(tmp, ConvertTag(Orthanc::DICOM_TAG_PIXEL_SPACING)) &&
+        LinearAlgebra::ParseVector(pixelSpacing, tmp) &&
+        pixelSpacing.size() == 2)
+    {
+      SetPixelSpacing(pixelSpacing[0], pixelSpacing[1]);
+    }
+
+    //SetPan(-0.5 * GetPixelSpacingX(), -0.5 * GetPixelSpacingY());
+
+    OrthancPlugins::DicomDatasetReader reader(dataset);
+
+    unsigned int width, height;
+    if (!reader.GetUnsignedIntegerValue(width, ConvertTag(Orthanc::DICOM_TAG_COLUMNS)) ||
+        !reader.GetUnsignedIntegerValue(height, ConvertTag(Orthanc::DICOM_TAG_ROWS)))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+    else
+    {
+      SetSize(width, height);
+    }
+  }
+
+  void RadiographyDicomLayer::SetSourceImage(Orthanc::ImageAccessor* image)   // Takes ownership
+  {
+    std::auto_ptr<Orthanc::ImageAccessor> raii(image);
+
+    if (image == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+
+    SetSize(image->GetWidth(), image->GetHeight());
+
+    source_ = raii;
+    ApplyConverter();
+  }
+
+  void RadiographyDicomLayer::Render(Orthanc::ImageAccessor& buffer,
+                                     const AffineTransform2D& viewTransform,
+                                     ImageInterpolation interpolation) const
+  {
+    if (converted_.get() != NULL)
+    {
+      if (converted_->GetFormat() != Orthanc::PixelFormat_Float32)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      unsigned int cropX, cropY, cropWidth, cropHeight;
+      GetCrop(cropX, cropY, cropWidth, cropHeight);
+
+      AffineTransform2D t = AffineTransform2D::Combine(
+            viewTransform, GetTransform(),
+            AffineTransform2D::CreateOffset(cropX, cropY));
+
+      Orthanc::ImageAccessor cropped;
+      converted_->GetRegion(cropped, cropX, cropY, cropWidth, cropHeight);
+
+      t.Apply(buffer, cropped, interpolation, false);
+    }
+  }
+
+
+  bool RadiographyDicomLayer::GetDefaultWindowing(float& center,
+                                                  float& width) const
+  {
+    if (converter_.get() != NULL &&
+        converter_->HasDefaultWindow())
+    {
+      center = static_cast<float>(converter_->GetDefaultWindowCenter());
+      width = static_cast<float>(converter_->GetDefaultWindowWidth());
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  bool RadiographyDicomLayer::GetRange(float& minValue,
+                                       float& maxValue) const
+  {
+    if (converted_.get() != NULL)
+    {
+      if (converted_->GetFormat() != Orthanc::PixelFormat_Float32)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      Orthanc::ImageProcessing::GetMinMaxFloatValue(minValue, maxValue, *converted_);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographyDicomLayer.h	Thu Nov 29 15:11:19 2018 +0100
@@ -0,0 +1,74 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "RadiographyLayer.h"
+#include <Plugins/Samples/Common/FullOrthancDataset.h>
+
+namespace OrthancStone
+{
+  class RadiographyScene;
+  class DicomFrameConverter;
+
+  class RadiographyDicomLayer : public RadiographyLayer
+  {
+  private:
+    std::auto_ptr<Orthanc::ImageAccessor>  source_;  // Content of PixelData
+    std::auto_ptr<DicomFrameConverter>     converter_;
+    std::auto_ptr<Orthanc::ImageAccessor>  converted_;  // Float32
+    std::string                            instanceId_;
+    unsigned int                           frame_;
+
+    void ApplyConverter();
+
+  public:
+    void SetInstance(const std::string& instanceId, unsigned int frame)
+    {
+      instanceId_ = instanceId;
+      frame_ = frame;
+    }
+
+    std::string GetInstanceId() const
+    {
+      return instanceId_;
+    }
+
+    unsigned int GetFrame() const
+    {
+      return frame_;
+    }
+
+    void SetDicomTags(const OrthancPlugins::FullOrthancDataset& dataset);
+
+    void SetSourceImage(Orthanc::ImageAccessor* image);   // Takes ownership
+
+    virtual void Render(Orthanc::ImageAccessor& buffer,
+                        const AffineTransform2D& viewTransform,
+                        ImageInterpolation interpolation) const;
+
+    virtual bool GetDefaultWindowing(float& center,
+                                     float& width) const;
+
+    virtual bool GetRange(float& minValue,
+                          float& maxValue) const;
+  };
+}
--- a/Framework/Radiography/RadiographyLayer.cpp	Wed Nov 28 10:46:32 2018 +0100
+++ b/Framework/Radiography/RadiographyLayer.cpp	Thu Nov 29 15:11:19 2018 +0100
@@ -32,18 +32,41 @@
   }
 
 
+  RadiographyLayer::Geometry::Geometry() :
+    hasCrop_(false),
+    panX_(0),
+    panY_(0),
+    angle_(0),
+    resizeable_(false),
+    pixelSpacingX_(1),
+    pixelSpacingY_(1)
+  {
+
+  }
+
+  void RadiographyLayer::Geometry::GetCrop(unsigned int &x, unsigned int &y, unsigned int &width, unsigned int &height) const
+  {
+    if (!hasCrop_)
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);  // you should probably use Radiography::GetCrop() or at least call HasCrop() before
+
+    x = cropX_;
+    y = cropY_;
+    width = cropWidth_;
+    height = cropHeight_;
+  }
+
   void RadiographyLayer::UpdateTransform()
   {
-    transform_ = AffineTransform2D::CreateScaling(pixelSpacingX_, pixelSpacingY_);
+    transform_ = AffineTransform2D::CreateScaling(geometry_.GetPixelSpacingX(), geometry_.GetPixelSpacingY());
 
     double centerX, centerY;
     GetCenter(centerX, centerY);
 
     transform_ = AffineTransform2D::Combine(
-      AffineTransform2D::CreateOffset(panX_ + centerX, panY_ + centerY),
-      AffineTransform2D::CreateRotation(angle_),
-      AffineTransform2D::CreateOffset(-centerX, -centerY),
-      transform_);
+          AffineTransform2D::CreateOffset(geometry_.GetPanX() + centerX, geometry_.GetPanY() + centerY),
+          AffineTransform2D::CreateRotation(geometry_.GetAngle()),
+          AffineTransform2D::CreateOffset(-centerX, -centerY),
+          transform_);
 
     transformInverse_ = AffineTransform2D::Invert(transform_);
   }
@@ -73,28 +96,28 @@
 
     switch (corner)
     {
-      case Corner_TopLeft:
-        x = dx;
-        y = dy;
-        break;
+    case Corner_TopLeft:
+      x = dx;
+      y = dy;
+      break;
 
-      case Corner_TopRight:
-        x = dx + dwidth;
-        y = dy;
-        break;
+    case Corner_TopRight:
+      x = dx + dwidth;
+      y = dy;
+      break;
 
-      case Corner_BottomLeft:
-        x = dx;
-        y = dy + dheight;
-        break;
+    case Corner_BottomLeft:
+      x = dx;
+      y = dy + dheight;
+      break;
 
-      case Corner_BottomRight:
-        x = dx + dwidth;
-        y = dy + dheight;
-        break;
+    case Corner_BottomRight:
+      x = dx + dwidth;
+      y = dy + dheight;
+      break;
 
-      default:
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    default:
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
 
     transform_.Apply(x, y);
@@ -105,7 +128,7 @@
                                   double y) const
   {
     transformInverse_.Apply(x, y);
-        
+
     unsigned int cropX, cropY, cropWidth, cropHeight;
     GetCrop(cropX, cropY, cropWidth, cropHeight);
 
@@ -127,7 +150,7 @@
 
     cairo_t* cr = context.GetObject();
     cairo_set_line_width(cr, 2.0 / zoom);
-        
+
     double x, y;
     x = dx;
     y = dy;
@@ -162,18 +185,16 @@
     index_(0),
     hasSize_(false),
     width_(0),
-    height_(0),
-    hasCrop_(false),
-    pixelSpacingX_(1),
-    pixelSpacingY_(1),
-    panX_(0),
-    panY_(0),
-    angle_(0),
-    resizeable_(false)
+    height_(0)
   {
     UpdateTransform();
   }
 
+  void RadiographyLayer::ResetCrop()
+  {
+    geometry_.ResetCrop();
+    UpdateTransform();
+  }
 
   void RadiographyLayer::SetCrop(unsigned int x,
                                  unsigned int y,
@@ -184,34 +205,36 @@
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
     }
-        
+
     if (x + width > width_ ||
         y + height > height_)
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
-        
-    hasCrop_ = true;
-    cropX_ = x;
-    cropY_ = y;
-    cropWidth_ = width;
-    cropHeight_ = height;
 
+    geometry_.SetCrop(x, y, width, height);
     UpdateTransform();
   }
 
-      
+  void RadiographyLayer::SetGeometry(const Geometry& geometry)
+  {
+    geometry_ = geometry;
+
+    if (hasSize_)
+    {
+      UpdateTransform();
+    }
+  }
+
+
   void RadiographyLayer::GetCrop(unsigned int& x,
                                  unsigned int& y,
                                  unsigned int& width,
                                  unsigned int& height) const
   {
-    if (hasCrop_)
+    if (GetGeometry().HasCrop())
     {
-      x = cropX_;
-      y = cropY_;
-      width = cropWidth_;
-      height = cropHeight_;
+      GetGeometry().GetCrop(x, y, width, height);
     }
     else 
     {
@@ -222,10 +245,10 @@
     }
   }
 
-      
+  
   void RadiographyLayer::SetAngle(double angle)
   {
-    angle_ = angle;
+    geometry_.SetAngle(angle);
     UpdateTransform();
   }
 
@@ -239,7 +262,7 @@
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize);
     }
-        
+
     hasSize_ = true;
     width_ = width;
     height_ = height;
@@ -251,7 +274,7 @@
   Extent2D RadiographyLayer::GetExtent() const
   {
     Extent2D extent;
-       
+
     unsigned int x, y, width, height;
     GetCrop(x, y, width, height);
 
@@ -264,7 +287,7 @@
     AddToExtent(extent, dx + dwidth, dy);
     AddToExtent(extent, dx, dy + dheight);
     AddToExtent(extent, dx + dwidth, dy + dheight);
-        
+
     return extent;
   }
 
@@ -282,7 +305,7 @@
     else
     {
       transformInverse_.Apply(sceneX, sceneY);
-        
+
       int x = static_cast<int>(std::floor(sceneX));
       int y = static_cast<int>(std::floor(sceneY));
 
@@ -320,8 +343,7 @@
   void RadiographyLayer::SetPan(double x,
                                 double y)
   {
-    panX_ = x;
-    panY_ = y;
+    geometry_.SetPan(x, y);
     UpdateTransform();
   }
 
@@ -329,8 +351,7 @@
   void RadiographyLayer::SetPixelSpacing(double x,
                                          double y)
   {
-    pixelSpacingX_ = x;
-    pixelSpacingY_ = y;
+    geometry_.SetPixelSpacing(x, y);
     UpdateTransform();
   }
 
@@ -352,8 +373,8 @@
     GetCrop(cropX, cropY, cropWidth, cropHeight);
     GetCornerInternal(x, y, corner, cropX, cropY, cropWidth, cropHeight);
   }
-      
-      
+
+
   bool RadiographyLayer::LookupCorner(Corner& corner /* out */,
                                       double x,
                                       double y,
@@ -366,26 +387,26 @@
       Corner_BottomLeft,
       Corner_BottomRight
     };
-        
+
     unsigned int cropX, cropY, cropWidth, cropHeight;
     GetCrop(cropX, cropY, cropWidth, cropHeight);
 
     double threshold = Square(viewportDistance / zoom);
-        
+
     for (size_t i = 0; i < 4; i++)
     {
       double cx, cy;
       GetCornerInternal(cx, cy, CORNERS[i], cropX, cropY, cropWidth, cropHeight);
 
       double d = Square(cx - x) + Square(cy - y);
-        
+
       if (d <= threshold)
       {
         corner = CORNERS[i];
         return true;
       }
     }
-        
+
     return false;
   }
 }
--- a/Framework/Radiography/RadiographyLayer.h	Wed Nov 28 10:46:32 2018 +0100
+++ b/Framework/Radiography/RadiographyLayer.h	Thu Nov 29 15:11:19 2018 +0100
@@ -31,24 +31,114 @@
   {
     friend class RadiographyScene;
       
+  public:
+    class Geometry
+    {
+      bool               hasCrop_;
+      unsigned int       cropX_;
+      unsigned int       cropY_;
+      unsigned int       cropWidth_;
+      unsigned int       cropHeight_;
+      double             panX_;
+      double             panY_;
+      double             angle_;
+      bool               resizeable_;
+      double             pixelSpacingX_;
+      double             pixelSpacingY_;
+
+    public:
+      Geometry();
+
+      void ResetCrop()
+      {
+        hasCrop_ = false;
+      }
+
+      void SetCrop(unsigned int x,
+                   unsigned int y,
+                   unsigned int width,
+                   unsigned int height)
+      {
+        cropX_ = x;
+        cropY_ = y;
+        cropWidth_ = width;
+        cropHeight_ = height;
+      }
+
+      bool HasCrop() const
+      {
+        return hasCrop_;
+      }
+
+      void GetCrop(unsigned int& x,
+                   unsigned int& y,
+                   unsigned int& width,
+                   unsigned int& height) const;
+
+      void SetAngle(double angle)
+      {
+        angle_ = angle;
+      }
+
+      double GetAngle() const
+      {
+        return angle_;
+      }
+
+      void SetPan(double x,
+                  double y)
+      {
+        panX_ = x;
+        panY_ = y;
+      }
+
+      double GetPanX() const
+      {
+        return panX_;
+      }
+
+      double GetPanY() const
+      {
+        return panY_;
+      }
+
+      bool IsResizeable() const
+      {
+        return resizeable_;
+      }
+
+      void SetResizeable(bool resizeable)
+      {
+        resizeable_ = resizeable;
+      }
+
+      void SetPixelSpacing(double x,
+                           double y)
+      {
+        pixelSpacingX_ = x;
+        pixelSpacingY_ = y;
+      }
+
+      double GetPixelSpacingX() const
+      {
+        return pixelSpacingX_;
+      }
+
+      double GetPixelSpacingY() const
+      {
+        return pixelSpacingY_;
+      }
+
+    };
+
   private:
     size_t             index_;
     bool               hasSize_;
     unsigned int       width_;
     unsigned int       height_;
-    bool               hasCrop_;
-    unsigned int       cropX_;
-    unsigned int       cropY_;
-    unsigned int       cropWidth_;
-    unsigned int       cropHeight_;
     AffineTransform2D  transform_;
     AffineTransform2D  transformInverse_;
-    double             pixelSpacingX_;
-    double             pixelSpacingY_;
-    double             panX_;
-    double             panY_;
-    double             angle_;
-    bool               resizeable_;
+    Geometry           geometry_;
 
   protected:
     const AffineTransform2D& GetTransform() const
@@ -94,11 +184,15 @@
       return index_;
     }
 
-    void ResetCrop()
+    const Geometry& GetGeometry() const
     {
-      hasCrop_ = false;
+      return geometry_;
     }
 
+    void SetGeometry(const Geometry& geometry);
+
+    void ResetCrop();
+
     void SetCrop(unsigned int x,
                  unsigned int y,
                  unsigned int width,
@@ -111,14 +205,22 @@
 
     void SetAngle(double angle);
 
-    double GetAngle() const
+    void SetPan(double x,
+                double y);
+
+    void SetResizeable(bool resizeable)
     {
-      return angle_;
+      geometry_.SetResizeable(resizeable);
     }
 
     void SetSize(unsigned int width,
                  unsigned int height);
 
+    bool HasSize() const
+    {
+      return hasSize_;
+    }
+
     unsigned int GetWidth() const
     {
       return width_;
@@ -136,32 +238,9 @@
                   double sceneX,
                   double sceneY) const;
 
-    void SetPan(double x,
-                double y);
-
     void SetPixelSpacing(double x,
                          double y);
 
-    double GetPixelSpacingX() const
-    {
-      return pixelSpacingX_;
-    }   
-
-    double GetPixelSpacingY() const
-    {
-      return pixelSpacingY_;
-    }   
-
-    double GetPanX() const
-    {
-      return panX_;
-    }
-
-    double GetPanY() const
-    {
-      return panY_;
-    }
-
     void GetCenter(double& centerX,
                    double& centerY) const;
 
@@ -175,16 +254,6 @@
                       double zoom,
                       double viewportDistance) const;
 
-    bool IsResizeable() const
-    {
-      return resizeable_;
-    }
-
-    void SetResizeable(bool resizeable)
-    {
-      resizeable_ = resizeable;
-    }
-
     virtual bool GetDefaultWindowing(float& center,
                                      float& width) const = 0;
 
--- a/Framework/Radiography/RadiographyLayerCropTracker.cpp	Wed Nov 28 10:46:32 2018 +0100
+++ b/Framework/Radiography/RadiographyLayerCropTracker.cpp	Thu Nov 29 15:11:19 2018 +0100
@@ -58,7 +58,7 @@
       sourceCropWidth_(tracker.cropWidth_),
       sourceCropHeight_(tracker.cropHeight_)
     {
-      tracker.accessor_.GetLayer().GetCrop(targetCropX_, targetCropY_,
+      tracker.accessor_.GetLayer().GetGeometry().GetCrop(targetCropX_, targetCropY_,
                                            targetCropWidth_, targetCropHeight_);
     }
   };
@@ -77,7 +77,7 @@
   {
     if (accessor_.IsValid())
     {
-      accessor_.GetLayer().GetCrop(cropX_, cropY_, cropWidth_, cropHeight_);          
+      accessor_.GetLayer().GetGeometry().GetCrop(cropX_, cropY_, cropWidth_, cropHeight_);
     }
   }
 
--- a/Framework/Radiography/RadiographyLayerMoveTracker.cpp	Wed Nov 28 10:46:32 2018 +0100
+++ b/Framework/Radiography/RadiographyLayerMoveTracker.cpp	Thu Nov 29 15:11:19 2018 +0100
@@ -51,8 +51,8 @@
       RadiographySceneCommand(tracker.accessor_),
       sourceX_(tracker.panX_),
       sourceY_(tracker.panY_),
-      targetX_(tracker.accessor_.GetLayer().GetPanX()),
-      targetY_(tracker.accessor_.GetLayer().GetPanY())
+      targetX_(tracker.accessor_.GetLayer().GetGeometry().GetPanX()),
+      targetY_(tracker.accessor_.GetLayer().GetGeometry().GetPanY())
     {
     }
   };
@@ -72,8 +72,8 @@
   {
     if (accessor_.IsValid())
     {
-      panX_ = accessor_.GetLayer().GetPanX();
-      panY_ = accessor_.GetLayer().GetPanY();
+      panX_ = accessor_.GetLayer().GetGeometry().GetPanX();
+      panY_ = accessor_.GetLayer().GetGeometry().GetPanY();
     }
   }
 
--- a/Framework/Radiography/RadiographyLayerResizeTracker.cpp	Wed Nov 28 10:46:32 2018 +0100
+++ b/Framework/Radiography/RadiographyLayerResizeTracker.cpp	Thu Nov 29 15:11:19 2018 +0100
@@ -73,10 +73,10 @@
       sourceSpacingY_(tracker.originalSpacingY_),
       sourcePanX_(tracker.originalPanX_),
       sourcePanY_(tracker.originalPanY_),
-      targetSpacingX_(tracker.accessor_.GetLayer().GetPixelSpacingX()),
-      targetSpacingY_(tracker.accessor_.GetLayer().GetPixelSpacingY()),
-      targetPanX_(tracker.accessor_.GetLayer().GetPanX()),
-      targetPanY_(tracker.accessor_.GetLayer().GetPanY())
+      targetSpacingX_(tracker.accessor_.GetLayer().GetGeometry().GetPixelSpacingX()),
+      targetSpacingY_(tracker.accessor_.GetLayer().GetGeometry().GetPixelSpacingY()),
+      targetPanX_(tracker.accessor_.GetLayer().GetGeometry().GetPanX()),
+      targetPanY_(tracker.accessor_.GetLayer().GetGeometry().GetPanY())
     {
     }
   };
@@ -94,12 +94,12 @@
     roundScaling_(roundScaling)
   {
     if (accessor_.IsValid() &&
-        accessor_.GetLayer().IsResizeable())
+        accessor_.GetLayer().GetGeometry().IsResizeable())
     {
-      originalSpacingX_ = accessor_.GetLayer().GetPixelSpacingX();
-      originalSpacingY_ = accessor_.GetLayer().GetPixelSpacingY();
-      originalPanX_ = accessor_.GetLayer().GetPanX();
-      originalPanY_ = accessor_.GetLayer().GetPanY();
+      originalSpacingX_ = accessor_.GetLayer().GetGeometry().GetPixelSpacingX();
+      originalSpacingY_ = accessor_.GetLayer().GetGeometry().GetPixelSpacingY();
+      originalPanX_ = accessor_.GetLayer().GetGeometry().GetPanX();
+      originalPanY_ = accessor_.GetLayer().GetGeometry().GetPanY();
 
       switch (corner)
       {
@@ -149,7 +149,7 @@
   void RadiographyLayerResizeTracker::MouseUp()
   {
     if (accessor_.IsValid() &&
-        accessor_.GetLayer().IsResizeable())
+        accessor_.GetLayer().GetGeometry().IsResizeable())
     {
       undoRedoStack_.Add(new UndoRedoCommand(*this));
     }
@@ -164,7 +164,7 @@
     static const double ROUND_SCALING = 0.1;
         
     if (accessor_.IsValid() &&
-        accessor_.GetLayer().IsResizeable())
+        accessor_.GetLayer().GetGeometry().IsResizeable())
     {
       double scaling = ComputeDistance(oppositeX_, oppositeY_, sceneX, sceneY) * baseScaling_;
 
@@ -180,8 +180,8 @@
       // Keep the opposite corner at a fixed location
       double ox, oy;
       layer.GetCorner(ox, oy, oppositeCorner_);
-      layer.SetPan(layer.GetPanX() + oppositeX_ - ox,
-                   layer.GetPanY() + oppositeY_ - oy);
+      layer.SetPan(layer.GetGeometry().GetPanX() + oppositeX_ - ox,
+                   layer.GetGeometry().GetPanY() + oppositeY_ - oy);
     }
   }
 }
--- a/Framework/Radiography/RadiographyLayerRotateTracker.cpp	Wed Nov 28 10:46:32 2018 +0100
+++ b/Framework/Radiography/RadiographyLayerRotateTracker.cpp	Thu Nov 29 15:11:19 2018 +0100
@@ -58,7 +58,7 @@
     UndoRedoCommand(const RadiographyLayerRotateTracker& tracker) :
       RadiographySceneCommand(tracker.accessor_),
       sourceAngle_(tracker.originalAngle_),
-      targetAngle_(tracker.accessor_.GetLayer().GetAngle())
+      targetAngle_(tracker.accessor_.GetLayer().GetGeometry().GetAngle())
     {
     }
   };
@@ -100,7 +100,7 @@
     if (accessor_.IsValid())
     {
       accessor_.GetLayer().GetCenter(centerX_, centerY_);
-      originalAngle_ = accessor_.GetLayer().GetAngle();
+      originalAngle_ = accessor_.GetLayer().GetGeometry().GetAngle();
 
       double sceneX, sceneY;
       view.MapDisplayToScene(sceneX, sceneY, x, y);
--- a/Framework/Radiography/RadiographyScene.cpp	Wed Nov 28 10:46:32 2018 +0100
+++ b/Framework/Radiography/RadiographyScene.cpp	Thu Nov 29 15:11:19 2018 +0100
@@ -21,6 +21,9 @@
 
 #include "RadiographyScene.h"
 
+#include "RadiographyAlphaLayer.h"
+#include "RadiographyDicomLayer.h"
+#include "RadiographyTextLayer.h"
 #include "../Toolbox/DicomFrameConverter.h"
 
 #include <Core/Images/Image.h>
@@ -121,288 +124,6 @@
     }
   }
 
-
-
-  class RadiographyScene::AlphaLayer : public RadiographyLayer
-  {
-  private:
-    const RadiographyScene&                scene_;
-    std::auto_ptr<Orthanc::ImageAccessor>  alpha_;      // Grayscale8
-    bool                                   useWindowing_;
-    float                                  foreground_;
-
-  public:
-    AlphaLayer(const RadiographyScene& scene) :
-      scene_(scene),
-      useWindowing_(true),
-      foreground_(0)
-    {
-    }
-
-
-    void SetForegroundValue(float foreground)
-    {
-      useWindowing_ = false;
-      foreground_ = foreground;
-    }
-
-
-    void SetAlpha(Orthanc::ImageAccessor* image)
-    {
-      std::auto_ptr<Orthanc::ImageAccessor> raii(image);
-
-      if (image == NULL)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-      }
-
-      if (image->GetFormat() != Orthanc::PixelFormat_Grayscale8)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
-      }
-
-      SetSize(image->GetWidth(), image->GetHeight());
-      alpha_ = raii;
-    }
-
-
-    void LoadText(const Orthanc::Font& font,
-                  const std::string& utf8)
-    {
-      SetAlpha(font.RenderAlpha(utf8));
-    }
-
-
-    virtual bool GetDefaultWindowing(float& center,
-                                     float& width) const
-    {
-      return false;
-    }
-
-
-    virtual void Render(Orthanc::ImageAccessor& buffer,
-                        const AffineTransform2D& viewTransform,
-                        ImageInterpolation interpolation) const
-    {
-      if (alpha_.get() == NULL)
-      {
-        return;
-      }
-
-      if (buffer.GetFormat() != Orthanc::PixelFormat_Float32)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
-      }
-
-      unsigned int cropX, cropY, cropWidth, cropHeight;
-      GetCrop(cropX, cropY, cropWidth, cropHeight);
-
-      const AffineTransform2D t = AffineTransform2D::Combine(
-            viewTransform, GetTransform(),
-            AffineTransform2D::CreateOffset(cropX, cropY));
-
-      Orthanc::ImageAccessor cropped;
-      alpha_->GetRegion(cropped, cropX, cropY, cropWidth, cropHeight);
-
-      Orthanc::Image tmp(Orthanc::PixelFormat_Grayscale8, buffer.GetWidth(), buffer.GetHeight(), false);
-      
-      t.Apply(tmp, cropped, interpolation, true /* clear */);
-
-      // Blit
-      const unsigned int width = buffer.GetWidth();
-      const unsigned int height = buffer.GetHeight();
-
-      float value = foreground_;
-
-      if (useWindowing_)
-      {
-        float center, width;
-        if (scene_.GetWindowing(center, width))
-        {
-          value = center + width / 2.0f;
-        }
-      }
-
-      for (unsigned int y = 0; y < height; y++)
-      {
-        float *q = reinterpret_cast<float*>(buffer.GetRow(y));
-        const uint8_t *p = reinterpret_cast<uint8_t*>(tmp.GetRow(y));
-
-        for (unsigned int x = 0; x < width; x++, p++, q++)
-        {
-          float a = static_cast<float>(*p) / 255.0f;
-
-          *q = (a * value + (1.0f - a) * (*q));
-        }
-      }
-    }
-
-
-    virtual bool GetRange(float& minValue,
-                          float& maxValue) const
-    {
-      if (useWindowing_)
-      {
-        return false;
-      }
-      else
-      {
-        minValue = 0;
-        maxValue = 0;
-
-        if (foreground_ < 0)
-        {
-          minValue = foreground_;
-        }
-
-        if (foreground_ > 0)
-        {
-          maxValue = foreground_;
-        }
-
-        return true;
-      }
-    }
-  };
-
-
-
-  class RadiographyScene::DicomLayer : public RadiographyLayer
-  {
-  private:
-    std::auto_ptr<Orthanc::ImageAccessor>  source_;  // Content of PixelData
-    std::auto_ptr<DicomFrameConverter>     converter_;
-    std::auto_ptr<Orthanc::ImageAccessor>  converted_;  // Float32
-
-    static OrthancPlugins::DicomTag  ConvertTag(const Orthanc::DicomTag& tag)
-    {
-      return OrthancPlugins::DicomTag(tag.GetGroup(), tag.GetElement());
-    }
-
-
-    void ApplyConverter()
-    {
-      if (source_.get() != NULL &&
-          converter_.get() != NULL)
-      {
-        converted_.reset(converter_->ConvertFrame(*source_));
-      }
-    }
-
-  public:
-    void SetDicomTags(const OrthancPlugins::FullOrthancDataset& dataset)
-    {
-      converter_.reset(new DicomFrameConverter);
-      converter_->ReadParameters(dataset);
-      ApplyConverter();
-
-      std::string tmp;
-      Vector pixelSpacing;
-
-      if (dataset.GetStringValue(tmp, ConvertTag(Orthanc::DICOM_TAG_PIXEL_SPACING)) &&
-          LinearAlgebra::ParseVector(pixelSpacing, tmp) &&
-          pixelSpacing.size() == 2)
-      {
-        SetPixelSpacing(pixelSpacing[0], pixelSpacing[1]);
-      }
-
-      //SetPan(-0.5 * GetPixelSpacingX(), -0.5 * GetPixelSpacingY());
-      
-      OrthancPlugins::DicomDatasetReader reader(dataset);
-
-      unsigned int width, height;
-      if (!reader.GetUnsignedIntegerValue(width, ConvertTag(Orthanc::DICOM_TAG_COLUMNS)) ||
-          !reader.GetUnsignedIntegerValue(height, ConvertTag(Orthanc::DICOM_TAG_ROWS)))
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-      }
-      else
-      {
-        SetSize(width, height);
-      }
-    }
-
-
-    void SetSourceImage(Orthanc::ImageAccessor* image)   // Takes ownership
-    {
-      std::auto_ptr<Orthanc::ImageAccessor> raii(image);
-
-      if (image == NULL)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-      }
-
-      SetSize(image->GetWidth(), image->GetHeight());
-
-      source_ = raii;
-      ApplyConverter();
-    }
-
-
-    virtual void Render(Orthanc::ImageAccessor& buffer,
-                        const AffineTransform2D& viewTransform,
-                        ImageInterpolation interpolation) const
-    {
-      if (converted_.get() != NULL)
-      {
-        if (converted_->GetFormat() != Orthanc::PixelFormat_Float32)
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-        }
-
-        unsigned int cropX, cropY, cropWidth, cropHeight;
-        GetCrop(cropX, cropY, cropWidth, cropHeight);
-
-        AffineTransform2D t = AffineTransform2D::Combine(
-              viewTransform, GetTransform(),
-              AffineTransform2D::CreateOffset(cropX, cropY));
-
-        Orthanc::ImageAccessor cropped;
-        converted_->GetRegion(cropped, cropX, cropY, cropWidth, cropHeight);
-        
-        t.Apply(buffer, cropped, interpolation, false);
-      }
-    }
-
-
-    virtual bool GetDefaultWindowing(float& center,
-                                     float& width) const
-    {
-      if (converter_.get() != NULL &&
-          converter_->HasDefaultWindow())
-      {
-        center = static_cast<float>(converter_->GetDefaultWindowCenter());
-        width = static_cast<float>(converter_->GetDefaultWindowWidth());
-        return true;
-      }
-      else
-      {
-        return false;
-      }
-    }
-
-
-    virtual bool GetRange(float& minValue,
-                          float& maxValue) const
-    {
-      if (converted_.get() != NULL)
-      {
-        if (converted_->GetFormat() != Orthanc::PixelFormat_Float32)
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-        }
-
-        Orthanc::ImageProcessing::GetMinMaxFloatValue(minValue, maxValue, *converted_);
-        return true;
-      }
-      else
-      {
-        return false;
-      }
-    }
-  };
-
-
   RadiographyLayer& RadiographyScene::RegisterLayer(RadiographyLayer* layer)
   {
     if (layer == NULL)
@@ -418,8 +139,8 @@
     raii->SetIndex(index);
     layers_[index] = raii.release();
 
-    EmitMessage(GeometryChangedMessage(*this));
-    EmitMessage(ContentChangedMessage(*this));
+    EmitMessage(GeometryChangedMessage(*this, *layer));
+    EmitMessage(ContentChangedMessage(*this, *layer));
 
     return *layer;
   }
@@ -445,6 +166,14 @@
     }
   }
 
+  void RadiographyScene::GetLayersIndexes(std::vector<size_t>& output) const
+  {
+    for (Layers::const_iterator it = layers_.begin(); it != layers_.end(); it++)
+    {
+      output.push_back(it->first);
+    }
+  }
+
   void RadiographyScene::RemoveLayer(size_t layerIndex)
   {
     LOG(INFO) << "Removing layer: " << layerIndex;
@@ -459,13 +188,14 @@
     LOG(INFO) << "Removing layer, there are now : " << countLayers_ << " layers";
   }
 
-  RadiographyLayer& RadiographyScene::GetLayer(size_t layerIndex)
+  const RadiographyLayer& RadiographyScene::GetLayer(size_t layerIndex) const
   {
     if (layerIndex > countLayers_)
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
-    return *(layers_[layerIndex]);
+
+    return *(layers_.at(layerIndex));
   }
 
   bool RadiographyScene::GetWindowing(float& center,
@@ -505,17 +235,23 @@
 
 
   RadiographyLayer& RadiographyScene::LoadText(const Orthanc::Font& font,
-                                               const std::string& utf8)
+                                               const std::string& utf8,
+                                               RadiographyLayer::Geometry* geometry)
   {
-    std::auto_ptr<AlphaLayer>  alpha(new AlphaLayer(*this));
+    std::auto_ptr<RadiographyTextLayer>  alpha(new RadiographyTextLayer(*this));
     alpha->LoadText(font, utf8);
+    if (geometry != NULL)
+    {
+      alpha->SetGeometry(*geometry);
+    }
 
     return RegisterLayer(alpha.release());
   }
 
 
   RadiographyLayer& RadiographyScene::LoadTestBlock(unsigned int width,
-                                                    unsigned int height)
+                                                    unsigned int height,
+                                                    RadiographyLayer::Geometry* geometry)
   {
     std::auto_ptr<Orthanc::Image>  block(new Orthanc::Image(Orthanc::PixelFormat_Grayscale8, width, height, false));
 
@@ -538,19 +274,34 @@
       Orthanc::ImageProcessing::Set(region, color);
     }
 
-    std::auto_ptr<AlphaLayer>  alpha(new AlphaLayer(*this));
-    alpha->SetAlpha(block.release());
+    return LoadAlphaBitmap(block.release(), geometry);
+  }
+
+  RadiographyLayer& RadiographyScene::LoadAlphaBitmap(Orthanc::ImageAccessor* bitmap, RadiographyLayer::Geometry *geometry)
+  {
+    std::auto_ptr<RadiographyAlphaLayer>  alpha(new RadiographyAlphaLayer(*this));
+    alpha->SetAlpha(bitmap);
+    if (geometry != NULL)
+    {
+      alpha->SetGeometry(*geometry);
+    }
 
     return RegisterLayer(alpha.release());
   }
 
-
   RadiographyLayer& RadiographyScene::LoadDicomFrame(OrthancApiClient& orthanc,
                                                      const std::string& instance,
                                                      unsigned int frame,
-                                                     bool httpCompression)
+                                                     bool httpCompression,
+                                                     RadiographyLayer::Geometry* geometry)
   {
-    RadiographyLayer& layer = RegisterLayer(new DicomLayer);
+    RadiographyDicomLayer& layer = dynamic_cast<RadiographyDicomLayer&>(RegisterLayer(new RadiographyDicomLayer));
+    layer.SetInstance(instance, frame);
+
+    if (geometry != NULL)
+    {
+      layer.SetGeometry(*geometry);
+    }
 
     {
       IWebService::HttpHeaders headers;
@@ -588,7 +339,7 @@
 
   RadiographyLayer& RadiographyScene::LoadDicomWebFrame(IWebService& web)
   {
-    RadiographyLayer& layer = RegisterLayer(new DicomLayer);
+    RadiographyLayer& layer = RegisterLayer(new RadiographyDicomLayer);
 
 
     return layer;
@@ -610,7 +361,7 @@
       assert(layer->second != NULL);
 
       OrthancPlugins::FullOrthancDataset dicom(message.GetAnswer(), message.GetAnswerSize());
-      dynamic_cast<DicomLayer*>(layer->second)->SetDicomTags(dicom);
+      dynamic_cast<RadiographyDicomLayer*>(layer->second)->SetDicomTags(dicom);
 
       float c, w;
       if (!hasWindowing_ &&
@@ -621,7 +372,7 @@
         windowingWidth_ = w;
       }
 
-      EmitMessage(GeometryChangedMessage(*this));
+      EmitMessage(GeometryChangedMessage(*this, *(layer->second)));
     }
   }
 
@@ -646,9 +397,9 @@
 
       std::auto_ptr<Orthanc::PamReader> reader(new Orthanc::PamReader);
       reader->ReadFromMemory(content);
-      dynamic_cast<DicomLayer*>(layer->second)->SetSourceImage(reader.release());
+      dynamic_cast<RadiographyDicomLayer*>(layer->second)->SetSourceImage(reader.release());
 
-      EmitMessage(ContentChangedMessage(*this));
+      EmitMessage(ContentChangedMessage(*this, *(layer->second)));
     }
   }
 
@@ -672,7 +423,7 @@
                                 const AffineTransform2D& viewTransform,
                                 ImageInterpolation interpolation) const
   {
-    Orthanc::ImageProcessing::Set(buffer, 0);
+    Orthanc::ImageProcessing::Set(buffer, 0); // TODO: get background color (depending on inverted state)
 
     // Render layers in the background-to-foreground order
     for (size_t index = 0; index < countLayers_; index++)
@@ -770,7 +521,7 @@
   {
     Json::Value createDicomRequestContent;
 
-    Export(createDicomRequestContent, dicom, pixelSpacingX, pixelSpacingY, invert, interpolation, usePam);
+    ExportToCreateDicomRequest(createDicomRequestContent, dicom, pixelSpacingX, pixelSpacingY, invert, interpolation, usePam);
 
     if (!parentOrthancId.empty())
     {
@@ -786,7 +537,7 @@
 
   // Export using PAM is faster than using PNG, but requires Orthanc
   // core >= 1.4.3
-  void RadiographyScene::Export(Json::Value& createDicomRequestContent,
+  void RadiographyScene::ExportToCreateDicomRequest(Json::Value& createDicomRequestContent,
                                 const Orthanc::DicomMap& dicom,
                                 double pixelSpacingX,
                                 double pixelSpacingY,
--- a/Framework/Radiography/RadiographyScene.h	Wed Nov 28 10:46:32 2018 +0100
+++ b/Framework/Radiography/RadiographyScene.h	Thu Nov 29 15:11:19 2018 +0100
@@ -32,8 +32,46 @@
       public IObservable
   {
   public:
-    typedef OriginMessage<MessageType_Widget_GeometryChanged, RadiographyScene> GeometryChangedMessage;
-    typedef OriginMessage<MessageType_Widget_ContentChanged, RadiographyScene> ContentChangedMessage;
+    class GeometryChangedMessage :
+      public OriginMessage<MessageType_Scene_GeometryChanged, RadiographyScene>
+    {
+    private:
+      RadiographyLayer&        layer_;
+
+    public:
+      GeometryChangedMessage(const RadiographyScene& origin,
+                             RadiographyLayer& layer) :
+        OriginMessage(origin),
+        layer_(layer)
+      {
+      }
+
+      RadiographyLayer& GetLayer() const
+      {
+        return layer_;
+      }
+    };
+
+    class ContentChangedMessage :
+      public OriginMessage<MessageType_Scene_ContentChanged, RadiographyScene>
+    {
+    private:
+      RadiographyLayer&        layer_;
+
+    public:
+      ContentChangedMessage(const RadiographyScene& origin,
+                             RadiographyLayer& layer) :
+        OriginMessage(origin),
+        layer_(layer)
+      {
+      }
+
+      RadiographyLayer& GetLayer() const
+      {
+        return layer_;
+      }
+    };
+
 
     class LayerAccessor : public boost::noncopyable
     {
@@ -69,9 +107,6 @@
 
 
   private:
-    class AlphaLayer;
-    class DicomLayer;
-
     typedef std::map<size_t, RadiographyLayer*>  Layers;
 
     size_t  countLayers_;
@@ -105,26 +140,29 @@
                       float width);
 
     RadiographyLayer& LoadText(const Orthanc::Font& font,
-                               const std::string& utf8);
+                               const std::string& utf8,
+                               RadiographyLayer::Geometry* geometry);
     
     RadiographyLayer& LoadTestBlock(unsigned int width,
-                                    unsigned int height);
-    
+                                    unsigned int height,
+                                    RadiographyLayer::Geometry* geometry);
+
+    RadiographyLayer& LoadAlphaBitmap(Orthanc::ImageAccessor* bitmap,  // takes ownership
+                                      RadiographyLayer::Geometry* geometry);
+
     RadiographyLayer& LoadDicomFrame(OrthancApiClient& orthanc,
                                      const std::string& instance,
                                      unsigned int frame,
-                                     bool httpCompression);
+                                     bool httpCompression,
+                                     RadiographyLayer::Geometry* geometry); // pass NULL if you want default geometry
 
     RadiographyLayer& LoadDicomWebFrame(IWebService& web);
 
     void RemoveLayer(size_t layerIndex);
 
-    size_t GetLayerCount()
-    {
-      return countLayers_;
-    }
+    const RadiographyLayer& GetLayer(size_t layerIndex) const;
 
-    RadiographyLayer& GetLayer(size_t layerIndex);
+    void GetLayersIndexes(std::vector<size_t>& output) const;
 
     Extent2D GetSceneExtent() const;
 
@@ -155,7 +193,7 @@
                      bool usePam);
 
     // temporary version used by VSOL because we need to send the same request at another url
-    void Export(Json::Value& createDicomRequestContent,
+    void ExportToCreateDicomRequest(Json::Value& createDicomRequestContent,
                 const Orthanc::DicomMap& dicom,
                 double pixelSpacingX,
                 double pixelSpacingY,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographySceneReader.cpp	Thu Nov 29 15:11:19 2018 +0100
@@ -0,0 +1,112 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "RadiographySceneReader.h"
+
+#include <Core/Images/FontRegistry.h>
+#include <Core/Images/PngReader.h>
+#include <Core/OrthancException.h>
+#include <Core/Toolbox.h>
+
+namespace OrthancStone
+{
+  void RadiographySceneReader::Read(const Json::Value& input)
+  {
+    unsigned int version = input["version"].asUInt();
+
+    if (version != 1)
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+
+    for(size_t layerIndex = 0; layerIndex < input["layers"].size(); layerIndex++)
+    {
+      const Json::Value& jsonLayer = input["layers"][(int)layerIndex];
+      RadiographyLayer::Geometry geometry;
+
+      if (jsonLayer["type"].asString() == "dicom")
+      {
+        ReadLayerGeometry(geometry, jsonLayer);
+        scene_.LoadDicomFrame(orthancApiClient_, jsonLayer["instanceId"].asString(), jsonLayer["frame"].asUInt(), true, &geometry);
+      }
+      else if (jsonLayer["type"].asString() == "text")
+      {
+        if (fontRegistry_ == NULL || fontRegistry_->GetSize() == 0)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); // you must provide a FontRegistry if you need to re-create text layers.
+        }
+
+        ReadLayerGeometry(geometry, jsonLayer);
+        const Orthanc::Font* font = fontRegistry_->FindFont(jsonLayer["fontName"].asString());
+        if (font == NULL) // if not found, take the first font in the registry
+        {
+          font = &(fontRegistry_->GetFont(0));
+        }
+        scene_.LoadText(*font, jsonLayer["text"].asString(), &geometry);
+      }
+      else if (jsonLayer["type"].asString() == "alpha")
+      {
+        ReadLayerGeometry(geometry, jsonLayer);
+
+        const std::string& pngContentBase64 = jsonLayer["content"].asString();
+        std::string pngContent;
+        std::string mimeType;
+        Orthanc::Toolbox::DecodeDataUriScheme(mimeType, pngContent, pngContentBase64);
+
+        std::auto_ptr<Orthanc::ImageAccessor>  image;
+        if (mimeType == "image/png")
+        {
+          image.reset(new Orthanc::PngReader());
+          dynamic_cast<Orthanc::PngReader*>(image.get())->ReadFromMemory(pngContent);
+        }
+        else
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+
+        RadiographyAlphaLayer& layer = dynamic_cast<RadiographyAlphaLayer&>(scene_.LoadAlphaBitmap(image.release(), &geometry));
+
+        if (!jsonLayer["isUsingWindowing"].asBool())
+        {
+          layer.SetForegroundValue((float)(jsonLayer["foreground"].asDouble()));
+        }
+      }
+      else
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+  }
+
+  void RadiographySceneReader::ReadLayerGeometry(RadiographyLayer::Geometry& geometry, const Json::Value& jsonLayer)
+  {
+    {// crop
+      unsigned int x, y, width, height;
+      if (jsonLayer["crop"]["hasCrop"].asBool())
+      {
+        x = jsonLayer["crop"]["x"].asUInt();
+        y = jsonLayer["crop"]["y"].asUInt();
+        width = jsonLayer["crop"]["width"].asUInt();
+        height = jsonLayer["crop"]["height"].asUInt();
+        geometry.SetCrop(x, y, width, height);
+      }
+    }
+
+    geometry.SetAngle(jsonLayer["angle"].asDouble());
+    geometry.SetResizeable(jsonLayer["isResizable"].asBool());
+    geometry.SetPan(jsonLayer["pan"]["x"].asDouble(), jsonLayer["pan"]["y"].asDouble());
+    geometry.SetPixelSpacing(jsonLayer["pixelSpacing"]["x"].asDouble(), jsonLayer["pixelSpacing"]["y"].asDouble());
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographySceneReader.h	Thu Nov 29 15:11:19 2018 +0100
@@ -0,0 +1,59 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "RadiographyScene.h"
+#include "RadiographyAlphaLayer.h"
+#include "RadiographyDicomLayer.h"
+#include "RadiographyTextLayer.h"
+#include <json/value.h>
+#include <Core/Images/FontRegistry.h>
+
+namespace OrthancStone
+{
+  class OrthancApiClient;
+
+  class RadiographySceneReader : public boost::noncopyable
+  {
+    RadiographyScene&             scene_;
+    OrthancApiClient&             orthancApiClient_;
+    const Orthanc::FontRegistry*  fontRegistry_;
+
+  public:
+    RadiographySceneReader(RadiographyScene& scene, OrthancApiClient& orthancApiClient) :
+      scene_(scene),
+      orthancApiClient_(orthancApiClient),
+      fontRegistry_(NULL)
+    {
+    }
+
+    void Read(const Json::Value& output);
+
+    void SetFontRegistry(const Orthanc::FontRegistry& fontRegistry)
+    {
+      fontRegistry_ = &fontRegistry;
+    }
+
+  private:
+    void ReadLayerGeometry(RadiographyLayer::Geometry& geometry, const Json::Value& output);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographySceneWriter.cpp	Thu Nov 29 15:11:19 2018 +0100
@@ -0,0 +1,136 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "RadiographySceneWriter.h"
+
+#include <Core/OrthancException.h>
+#include <Core/Images/PngWriter.h>
+#include <Core/Toolbox.h>
+
+namespace OrthancStone
+{
+  void RadiographySceneWriter::Write(Json::Value& output, const RadiographyScene& scene)
+  {
+    output["version"] = 1;
+    output["layers"] = Json::arrayValue;
+
+    std::vector<size_t> layersIndexes;
+    scene.GetLayersIndexes(layersIndexes);
+
+    for (std::vector<size_t>::iterator itLayerIndex = layersIndexes.begin(); itLayerIndex < layersIndexes.end(); itLayerIndex++)
+    {
+      Json::Value layer;
+      WriteLayer(layer, scene.GetLayer(*itLayerIndex));
+      output["layers"].append(layer);
+    }
+  }
+
+  void RadiographySceneWriter::WriteLayer(Json::Value& output, const RadiographyDicomLayer& layer)
+  {
+    output["type"] = "dicom";
+    output["instanceId"] = layer.GetInstanceId();
+    output["frame"] = layer.GetFrame();
+  }
+
+  void RadiographySceneWriter::WriteLayer(Json::Value& output, const RadiographyTextLayer& layer)
+  {
+    output["type"] = "text";
+    output["text"] = layer.GetText();
+    output["fontName"] = layer.GetFontName();
+  }
+
+  void RadiographySceneWriter::WriteLayer(Json::Value& output, const RadiographyAlphaLayer& layer)
+  {
+    output["type"] = "alpha";
+
+    //output["bitmap"] =
+    const Orthanc::ImageAccessor& alpha = layer.GetAlpha();
+
+    Orthanc::PngWriter pngWriter;
+    std::string pngContent;
+    std::string pngContentBase64;
+    pngWriter.WriteToMemory(pngContent, alpha);
+
+    Orthanc::Toolbox::EncodeDataUriScheme(pngContentBase64, "image/png", pngContent);
+    output["content"] = pngContentBase64;
+    output["foreground"] = layer.GetForegroundValue();
+    output["isUsingWindowing"] = layer.IsUsingWindowing();
+  }
+
+  void RadiographySceneWriter::WriteLayer(Json::Value& output, const RadiographyLayer& layer)
+  {
+    const RadiographyLayer::Geometry& geometry = layer.GetGeometry();
+
+    {// crop
+      Json::Value crop;
+      if (geometry.HasCrop())
+      {
+        unsigned int x, y, width, height;
+        geometry.GetCrop(x, y, width, height);
+        crop["hasCrop"] = true;
+        crop["x"] = x;
+        crop["y"] = y;
+        crop["width"] = width;
+        crop["height"] = height;
+      }
+      else
+      {
+        crop["hasCrop"] = false;
+      }
+
+      output["crop"] = crop;
+    }
+
+    output["angle"] = geometry.GetAngle();
+    output["isResizable"] = geometry.IsResizeable();
+
+    {// pan
+      Json::Value pan;
+      pan["x"] = geometry.GetPanX();
+      pan["y"] = geometry.GetPanY();
+      output["pan"] = pan;
+    }
+
+    {// pixelSpacing
+      Json::Value pan;
+      pan["x"] = geometry.GetPixelSpacingX();
+      pan["y"] = geometry.GetPixelSpacingY();
+      output["pixelSpacing"] = pan;
+    }
+
+    if (dynamic_cast<const RadiographyTextLayer*>(&layer) != NULL)
+    {
+      WriteLayer(output, dynamic_cast<const RadiographyTextLayer&>(layer));
+    }
+    else if (dynamic_cast<const RadiographyDicomLayer*>(&layer) != NULL)
+    {
+      WriteLayer(output, dynamic_cast<const RadiographyDicomLayer&>(layer));
+    }
+    else if (dynamic_cast<const RadiographyAlphaLayer*>(&layer) != NULL)
+    {
+      WriteLayer(output, dynamic_cast<const RadiographyAlphaLayer&>(layer));
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographySceneWriter.h	Thu Nov 29 15:11:19 2018 +0100
@@ -0,0 +1,50 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "RadiographyScene.h"
+#include "RadiographyAlphaLayer.h"
+#include "RadiographyDicomLayer.h"
+#include "RadiographyTextLayer.h"
+#include <json/value.h>
+
+namespace OrthancStone
+{
+  class RadiographyScene;
+
+  class RadiographySceneWriter : public boost::noncopyable
+  {
+
+  public:
+    RadiographySceneWriter()
+    {
+    }
+
+    void Write(Json::Value& output, const RadiographyScene& scene);
+
+  private:
+    void WriteLayer(Json::Value& output, const RadiographyLayer& layer);
+    void WriteLayer(Json::Value& output, const RadiographyDicomLayer& layer);
+    void WriteLayer(Json::Value& output, const RadiographyTextLayer& layer);
+    void WriteLayer(Json::Value& output, const RadiographyAlphaLayer& layer);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographyTextLayer.cpp	Thu Nov 29 15:11:19 2018 +0100
@@ -0,0 +1,36 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "RadiographyTextLayer.h"
+
+#include "RadiographyScene.h"
+
+namespace OrthancStone
+{
+  void RadiographyTextLayer::LoadText(const Orthanc::Font& font,
+                                      const std::string& utf8)
+  {
+    text_ = utf8;
+    fontName_ = font.GetName();
+
+    SetAlpha(font.RenderAlpha(utf8));
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographyTextLayer.h	Thu Nov 29 15:11:19 2018 +0100
@@ -0,0 +1,55 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "RadiographyAlphaLayer.h"
+
+namespace OrthancStone
+{
+  class RadiographyScene;
+
+  class RadiographyTextLayer : public RadiographyAlphaLayer
+  {
+  private:
+    std::string                text_;
+    std::string                fontName_;
+
+  public:
+    RadiographyTextLayer(const RadiographyScene& scene) :
+      RadiographyAlphaLayer(scene)
+    {
+    }
+
+    void LoadText(const Orthanc::Font& font,
+                  const std::string& utf8);
+
+    const std::string& GetText() const
+    {
+      return text_;
+    }
+
+    const std::string& GetFontName() const
+    {
+      return fontName_;
+    }
+  };
+}
--- a/Framework/StoneEnumerations.h	Wed Nov 28 10:46:32 2018 +0100
+++ b/Framework/StoneEnumerations.h	Thu Nov 29 15:11:19 2018 +0100
@@ -157,6 +157,9 @@
     MessageType_OrthancApi_GenericHttpError_Ready,
     MessageType_OrthancApi_GenericEmptyResponse_Ready,
 
+    MessageType_Scene_GeometryChanged,
+    MessageType_Scene_ContentChanged,
+
     MessageType_ViewportChanged,
 
     // used in unit tests only
--- a/Framework/Toolbox/OrthancApiClient.cpp	Wed Nov 28 10:46:32 2018 +0100
+++ b/Framework/Toolbox/OrthancApiClient.cpp	Thu Nov 29 15:11:19 2018 +0100
@@ -170,7 +170,7 @@
   class OrthancApiClient::CachedHttpRequestSuccessMessage
   {
   protected:
-    const std::string&             uri_;
+    std::string                    uri_;
     void*                          answer_;
     size_t                         answerSize_;
     IWebService::HttpHeaders       answerHeaders_;
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Wed Nov 28 10:46:32 2018 +0100
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Thu Nov 29 15:11:19 2018 +0100
@@ -246,6 +246,8 @@
   ${ORTHANC_STONE_ROOT}/Framework/Layers/LineMeasureTracker.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Layers/RenderStyle.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Layers/SliceOutlineRenderer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyAlphaLayer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyDicomLayer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerCropTracker.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerMoveTracker.cpp
@@ -253,6 +255,9 @@
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerRotateTracker.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyScene.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographySceneCommand.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographySceneReader.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographySceneWriter.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyTextLayer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyWidget.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyWindowingTracker.cpp
   ${ORTHANC_STONE_ROOT}/Framework/SmartLoader.cpp