changeset 408:6834c236b36d

reorganization
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 12 Nov 2018 14:52:10 +0100
parents 842a3c7cfdc0
children 99c9b3238008
files Applications/Samples/SingleFrameEditorApplication.h Framework/Radiography/RadiographyScene.cpp Framework/Radiography/RadiographyScene.h Framework/Toolbox/UndoRedoStack.cpp Framework/Toolbox/UndoRedoStack.h Resources/CMake/OrthancStoneConfiguration.cmake
diffstat 6 files changed, 1928 insertions(+), 1631 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/Samples/SingleFrameEditorApplication.h	Mon Nov 12 11:44:20 2018 +0100
+++ b/Applications/Samples/SingleFrameEditorApplication.h	Mon Nov 12 14:52:10 2018 +0100
@@ -23,9 +23,9 @@
 
 #include "SampleApplicationBase.h"
 
-#include "../../Framework/Toolbox/ImageGeometry.h"
-#include "../../Framework/Toolbox/OrthancApiClient.h"
-#include "../../Framework/Toolbox/DicomFrameConverter.h"
+#include "../../Framework/Radiography/RadiographyScene.h"
+
+#include "../../Framework/Toolbox/UndoRedoStack.h"
 
 #include <Core/Images/FontRegistry.h>
 #include <Core/Images/Image.h>
@@ -51,1478 +51,26 @@
 
 namespace OrthancStone
 {
-  class RadiologyScene :
-    public IObserver,
-    public IObservable
+  class RadiographyLayerCommand : public UndoRedoStack::ICommand
   {
-  public:
-    typedef OriginMessage<MessageType_Widget_GeometryChanged, RadiologyScene> GeometryChangedMessage;
-    typedef OriginMessage<MessageType_Widget_ContentChanged, RadiologyScene> ContentChangedMessage;
-
-
-    enum Corner
-    {
-      Corner_TopLeft,
-      Corner_TopRight,
-      Corner_BottomLeft,
-      Corner_BottomRight
-    };
-
-
-
-    class Layer : public boost::noncopyable
-    {
-      friend class RadiologyScene;
-      
-    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_;
-      Matrix        transform_;
-      Matrix        transformInverse_;
-      double        pixelSpacingX_;
-      double        pixelSpacingY_;
-      double        panX_;
-      double        panY_;
-      double        angle_;
-      bool          resizeable_;
-
-
-    protected:
-      const Matrix& GetTransform() const
-      {
-        return transform_;
-      }
-
-
-    private:
-      static void ApplyTransform(double& x /* inout */,
-                                 double& y /* inout */,
-                                 const Matrix& transform)
-      {
-        Vector p;
-        LinearAlgebra::AssignVector(p, x, y, 1);
-
-        Vector q = LinearAlgebra::Product(transform, p);
-
-        if (!LinearAlgebra::IsNear(q[2], 1.0))
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-        }
-        else
-        {
-          x = q[0];
-          y = q[1];
-        }
-      }
-      
-      
-      void UpdateTransform()
-      {
-        transform_ = CreateScalingMatrix(pixelSpacingX_, pixelSpacingY_);
-
-        double centerX, centerY;
-        GetCenter(centerX, centerY);
-
-        transform_ = LinearAlgebra::Product(
-          CreateOffsetMatrix(panX_ + centerX, panY_ + centerY),
-          CreateRotationMatrix(angle_),
-          CreateOffsetMatrix(-centerX, -centerY),
-          transform_);
-
-        LinearAlgebra::InvertMatrix(transformInverse_, transform_);
-      }
-
-
-      void AddToExtent(Extent2D& extent,
-                       double x,
-                       double y) const
-      {
-        ApplyTransform(x, y, transform_);
-        extent.AddPoint(x, y);
-      }
-
-
-      void GetCornerInternal(double& x,
-                             double& y,
-                             Corner corner,
-                             unsigned int cropX,
-                             unsigned int cropY,
-                             unsigned int cropWidth,
-                             unsigned int cropHeight) const
-      {
-        double dx = static_cast<double>(cropX);
-        double dy = static_cast<double>(cropY);
-        double dwidth = static_cast<double>(cropWidth);
-        double dheight = static_cast<double>(cropHeight);
-
-        switch (corner)
-        {
-          case Corner_TopLeft:
-            x = dx;
-            y = dy;
-            break;
-
-          case Corner_TopRight:
-            x = dx + dwidth;
-            y = dy;
-            break;
-
-          case Corner_BottomLeft:
-            x = dx;
-            y = dy + dheight;
-            break;
-
-          case Corner_BottomRight:
-            x = dx + dwidth;
-            y = dy + dheight;
-            break;
-
-          default:
-            throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-        }
-
-        ApplyTransform(x, y, transform_);
-      }
-
-
-      void SetIndex(size_t index)
-      {
-        index_ = index;
-      }
-
-      
-      bool Contains(double x,
-                    double y) const
-      {
-        ApplyTransform(x, y, transformInverse_);
-        
-        unsigned int cropX, cropY, cropWidth, cropHeight;
-        GetCrop(cropX, cropY, cropWidth, cropHeight);
-
-        return (x >= cropX && x <= cropX + cropWidth &&
-                y >= cropY && y <= cropY + cropHeight);
-      }
-
-
-      void DrawBorders(CairoContext& context,
-                       double zoom)
-      {
-        unsigned int cx, cy, width, height;
-        GetCrop(cx, cy, width, height);
-
-        double dx = static_cast<double>(cx);
-        double dy = static_cast<double>(cy);
-        double dwidth = static_cast<double>(width);
-        double dheight = static_cast<double>(height);
-
-        cairo_t* cr = context.GetObject();
-        cairo_set_line_width(cr, 2.0 / zoom);
-        
-        double x, y;
-        x = dx;
-        y = dy;
-        ApplyTransform(x, y, transform_);
-        cairo_move_to(cr, x, y);
-
-        x = dx + dwidth;
-        y = dy;
-        ApplyTransform(x, y, transform_);
-        cairo_line_to(cr, x, y);
-
-        x = dx + dwidth;
-        y = dy + dheight;
-        ApplyTransform(x, y, transform_);
-        cairo_line_to(cr, x, y);
-
-        x = dx;
-        y = dy + dheight;
-        ApplyTransform(x, y, transform_);
-        cairo_line_to(cr, x, y);
-
-        x = dx;
-        y = dy;
-        ApplyTransform(x, y, transform_);
-        cairo_line_to(cr, x, y);
-
-        cairo_stroke(cr);
-      }
-
-
-      static double Square(double x)
-      {
-        return x * x;
-      }
-
-
-    public:
-      Layer() :
-        index_(0),
-        hasSize_(false),
-        width_(0),
-        height_(0),
-        hasCrop_(false),
-        pixelSpacingX_(1),
-        pixelSpacingY_(1),
-        panX_(0),
-        panY_(0),
-        angle_(0),
-        resizeable_(false)
-      {
-        UpdateTransform();
-      }
-
-      virtual ~Layer()
-      {
-      }
-
-      size_t GetIndex() const
-      {
-        return index_;
-      }
-
-      void ResetCrop()
-      {
-        hasCrop_ = false;
-      }
-
-      void SetCrop(unsigned int x,
-                   unsigned int y,
-                   unsigned int width,
-                   unsigned int height)
-      {
-        if (!hasSize_)
-        {
-          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;
-
-        UpdateTransform();
-      }
-
-      void GetCrop(unsigned int& x,
-                   unsigned int& y,
-                   unsigned int& width,
-                   unsigned int& height) const
-      {
-        if (hasCrop_)
-        {
-          x = cropX_;
-          y = cropY_;
-          width = cropWidth_;
-          height = cropHeight_;
-        }
-        else 
-        {
-          x = 0;
-          y = 0;
-          width = width_;
-          height = height_;
-        }
-      }
-
-      void SetAngle(double angle)
-      {
-        angle_ = angle;
-        UpdateTransform();
-      }
-
-      double GetAngle() const
-      {
-        return angle_;
-      }
-
-      void SetSize(unsigned int width,
-                   unsigned int height)
-      {
-        if (hasSize_ &&
-            (width != width_ ||
-             height != height_))
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize);
-        }
-        
-        hasSize_ = true;
-        width_ = width;
-        height_ = height;
-
-        UpdateTransform();
-      }
-
-
-      unsigned int GetWidth() const
-      {
-        return width_;
-      }
-        
-
-      unsigned int GetHeight() const
-      {
-        return height_;
-      }       
-
-
-      Extent2D GetExtent() const
-      {
-        Extent2D extent;
-       
-        unsigned int x, y, width, height;
-        GetCrop(x, y, width, height);
-
-        double dx = static_cast<double>(x);
-        double dy = static_cast<double>(y);
-        double dwidth = static_cast<double>(width);
-        double dheight = static_cast<double>(height);
-
-        AddToExtent(extent, dx, dy);
-        AddToExtent(extent, dx + dwidth, dy);
-        AddToExtent(extent, dx, dy + dheight);
-        AddToExtent(extent, dx + dwidth, dy + dheight);
-        
-        return extent;
-      }
-
-
-      bool GetPixel(unsigned int& imageX,
-                    unsigned int& imageY,
-                    double sceneX,
-                    double sceneY) const
-      {
-        if (width_ == 0 ||
-            height_ == 0)
-        {
-          return false;
-        }
-        else
-        {
-          ApplyTransform(sceneX, sceneY, transformInverse_);
-        
-          int x = static_cast<int>(std::floor(sceneX));
-          int y = static_cast<int>(std::floor(sceneY));
-
-          if (x < 0)
-          {
-            imageX = 0;
-          }
-          else if (x >= static_cast<int>(width_))
-          {
-            imageX = width_;
-          }
-          else
-          {
-            imageX = static_cast<unsigned int>(x);
-          }
-
-          if (y < 0)
-          {
-            imageY = 0;
-          }
-          else if (y >= static_cast<int>(height_))
-          {
-            imageY = height_;
-          }
-          else
-          {
-            imageY = static_cast<unsigned int>(y);
-          }
-
-          return true;
-        }
-      }
-
-
-      void SetPan(double x,
-                  double y)
-      {
-        panX_ = x;
-        panY_ = y;
-        UpdateTransform();
-      }
-
-
-      void SetPixelSpacing(double x,
-                           double y)
-      {
-        pixelSpacingX_ = x;
-        pixelSpacingY_ = y;
-        UpdateTransform();
-      }
-
-      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
-      {
-        centerX = static_cast<double>(width_) / 2.0;
-        centerY = static_cast<double>(height_) / 2.0;
-        ApplyTransform(centerX, centerY, transform_);
-      }
-
-
-      void GetCorner(double& x /* out */,
-                     double& y /* out */,
-                     Corner corner) const
-      {
-        unsigned int cropX, cropY, cropWidth, cropHeight;
-        GetCrop(cropX, cropY, cropWidth, cropHeight);
-        GetCornerInternal(x, y, corner, cropX, cropY, cropWidth, cropHeight);
-      }
-      
-      
-      bool LookupCorner(Corner& corner /* out */,
-                        double x,
-                        double y,
-                        double zoom,
-                        double viewportDistance) const
-      {
-        static const Corner CORNERS[] = {
-          Corner_TopLeft,
-          Corner_TopRight,
-          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);
+  private:
+    RadiographyScene&  scene_;
+    size_t             layer_;
 
-          double d = Square(cx - x) + Square(cy - y);
-        
-          if (d <= threshold)
-          {
-            corner = CORNERS[i];
-            return true;
-          }
-        }
-        
-        return false;
-      }
-
-      bool IsResizeable() const
-      {
-        return resizeable_;
-      }
-
-      void SetResizeable(bool resizeable)
-      {
-        resizeable_ = resizeable;
-      }
-
-      virtual bool GetDefaultWindowing(float& center,
-                                       float& width) const = 0;
-
-      virtual void Render(Orthanc::ImageAccessor& buffer,
-                          const Matrix& viewTransform,
-                          ImageInterpolation interpolation) const = 0;
-
-      virtual bool GetRange(float& minValue,
-                            float& maxValue) const = 0;
-    }; 
-
-
-    class LayerAccessor : public boost::noncopyable
-    {
-    private:
-      RadiologyScene&  scene_;
-      size_t           index_;
-      Layer*           layer_;
-
-    public:
-      LayerAccessor(RadiologyScene& scene,
-                    size_t index) :
-        scene_(scene),
-        index_(index)
-      {
-        Layers::iterator layer = scene.layers_.find(index);
-        if (layer == scene.layers_.end())
-        {
-          layer_ = NULL;
-        }
-        else
-        {
-          assert(layer->second != NULL);
-          layer_ = layer->second;
-        }
-      }
-
-      LayerAccessor(RadiologyScene& scene,
-                    double x,
-                    double y) :
-        scene_(scene),
-        index_(0)  // Dummy initialization
-      {
-        if (scene.LookupLayer(index_, x, y))
-        {
-          Layers::iterator layer = scene.layers_.find(index_);
-          
-          if (layer == scene.layers_.end())
-          {
-            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-          }
-          else
-          {
-            assert(layer->second != NULL);
-            layer_ = layer->second;
-          }
-        }
-        else
-        {
-          layer_ = NULL;
-        }
-      }
-
-      void Invalidate()
-      {
-        layer_ = NULL;
-      }
-
-      bool IsValid() const
-      {
-        return layer_ != NULL;
-      }
-
-      RadiologyScene& GetScene() const
-      {
-        if (IsValid())
-        {
-          return scene_;
-        }
-        else
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-        }
-      }
-
-      size_t GetIndex() const
-      {
-        if (IsValid())
-        {
-          return index_;
-        }
-        else
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-        }
-      }
-
-      Layer& GetLayer() const
-      {
-        if (IsValid())
-        {
-          return *layer_;
-        }
-        else
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-        }
-      }    
-    };    
-
-
-  private:
-    class AlphaLayer : public Layer
-    {
-    private:
-      const RadiologyScene&                  scene_;
-      std::auto_ptr<Orthanc::ImageAccessor>  alpha_;      // Grayscale8
-      bool                                   useWindowing_;
-      float                                  foreground_;
-
-    public:
-      AlphaLayer(const RadiologyScene& 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 Matrix& 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);
-
-        Matrix m = LinearAlgebra::Product(viewTransform,
-                                          GetTransform(),
-                                          CreateOffsetMatrix(cropX, cropY));
-
-        Orthanc::ImageAccessor cropped;
-        alpha_->GetRegion(cropped, cropX, cropY, cropWidth, cropHeight);
-        
-        Orthanc::Image tmp(Orthanc::PixelFormat_Grayscale8, buffer.GetWidth(), buffer.GetHeight(), false);
-        ApplyProjectiveTransform(tmp, cropped, m, 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));
+  protected:
+    virtual void UndoInternal(RadiographyScene::Layer& layer) const = 0;
 
-          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;
-        }
-      }
-    };
-    
-    
-
-  private:
-    static Matrix CreateOffsetMatrix(double dx,
-                                     double dy)
-    {
-      Matrix m = LinearAlgebra::IdentityMatrix(3);
-      m(0, 2) = dx;
-      m(1, 2) = dy;
-      return m;
-    }
-      
-
-    static Matrix CreateScalingMatrix(double sx,
-                                      double sy)
-    {
-      Matrix m = LinearAlgebra::IdentityMatrix(3);
-      m(0, 0) = sx;
-      m(1, 1) = sy;
-      return m;
-    }
-      
-
-    static Matrix CreateRotationMatrix(double angle)
-    {
-      Matrix m;
-      const double v[] = { cos(angle), -sin(angle), 0,
-                           sin(angle), cos(angle), 0,
-                           0, 0, 1 };
-      LinearAlgebra::FillMatrix(m, 3, 3, v);
-      return m;
-    }
-      
-
-    class DicomLayer : public Layer
-    {
-    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 Matrix& 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);
-
-          Matrix m = LinearAlgebra::Product(viewTransform,
-                                            GetTransform(),
-                                            CreateOffsetMatrix(cropX, cropY));
-
-          Orthanc::ImageAccessor cropped;
-          converted_->GetRegion(cropped, cropX, cropY, cropWidth, cropHeight);
-        
-          ApplyProjectiveTransform(buffer, cropped, m, 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;
-        }
-      }
-    };
-
-
-
-
-    typedef std::map<size_t, Layer*>  Layers;
-        
-    OrthancApiClient&  orthanc_;
-    size_t             countLayers_;
-    bool               hasWindowing_;
-    float              windowingCenter_;
-    float              windowingWidth_;
-    Layers             layers_;
-
-
-    Layer& RegisterLayer(Layer* layer)
-    {
-      if (layer == NULL)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-      }
-
-      std::auto_ptr<Layer> raii(layer);
-      
-      size_t index = countLayers_++;
-      raii->SetIndex(index);
-      layers_[index] = raii.release();
-
-      EmitMessage(GeometryChangedMessage(*this));
-      EmitMessage(ContentChangedMessage(*this));
-
-      return *layer;
-    }
-    
+    virtual void RedoInternal(RadiographyScene::Layer& layer) const = 0;
 
   public:
-    RadiologyScene(MessageBroker& broker,
-                   OrthancApiClient& orthanc) :
-      IObserver(broker),
-      IObservable(broker),
-      orthanc_(orthanc),
-      countLayers_(0),
-      hasWindowing_(false),
-      windowingCenter_(0),  // Dummy initialization
-      windowingWidth_(0)    // Dummy initialization
-    {
-    }
-
-
-    virtual ~RadiologyScene()
-    {
-      for (Layers::iterator it = layers_.begin(); it != layers_.end(); it++)
-      {
-        assert(it->second != NULL);
-        delete it->second;
-      }
-    }
-
-
-    bool GetWindowing(float& center,
-                      float& width) const
-    {
-      if (hasWindowing_)
-      {
-        center = windowingCenter_;
-        width = windowingWidth_;
-        return true;
-      }
-      else
-      {
-        return false;
-      }
-    }
-
-
-    void GetWindowingWithDefault(float& center,
-                                 float& width) const
-    {
-      if (!GetWindowing(center, width))
-      {
-        center = 128;
-        width = 256;
-      }
-    }
-
-
-    void SetWindowing(float center,
-                      float width)
-
-    {
-      hasWindowing_ = true;
-      windowingCenter_ = center;
-      windowingWidth_ = width;
-    }
-
-
-    Layer& LoadText(const Orthanc::Font& font,
-                    const std::string& utf8)
-    {
-      std::auto_ptr<AlphaLayer>  alpha(new AlphaLayer(*this));
-      alpha->LoadText(font, utf8);
-
-      return RegisterLayer(alpha.release());
-    }
-
-    
-    Layer& LoadTestBlock(unsigned int width,
-                         unsigned int height)
-    {
-      std::auto_ptr<Orthanc::Image>  block(new Orthanc::Image(Orthanc::PixelFormat_Grayscale8, width, height, false));
-
-      for (unsigned int padding = 0;
-           (width > 2 * padding) && (height > 2 * padding);
-           padding++)
-      {
-        uint8_t color;
-        if (255 > 10 * padding)
-        {
-          color = 255 - 10 * padding;
-        }
-        else
-        {
-          color = 0;
-        }
-
-        Orthanc::ImageAccessor region;
-        block->GetRegion(region, padding, padding, width - 2 * padding, height - 2 * padding);
-        Orthanc::ImageProcessing::Set(region, color);
-      }
-
-      std::auto_ptr<AlphaLayer>  alpha(new AlphaLayer(*this));
-      alpha->SetAlpha(block.release());
-
-      return RegisterLayer(alpha.release());
-    }
-
-    
-    Layer& LoadDicomFrame(const std::string& instance,
-                          unsigned int frame,
-                          bool httpCompression)
-    {
-      Layer& layer = RegisterLayer(new DicomLayer);
-
-      {
-        IWebService::Headers headers;
-        std::string uri = "/instances/" + instance + "/tags";
-        orthanc_.GetBinaryAsync(uri, headers,
-                                new Callable<RadiologyScene, OrthancApiClient::BinaryResponseReadyMessage>
-                                (*this, &RadiologyScene::OnTagsReceived), NULL,
-                                new Orthanc::SingleValueObject<size_t>(layer.GetIndex()));
-      }
-
-      {
-        IWebService::Headers headers;
-        headers["Accept"] = "image/x-portable-arbitrarymap";
-
-        if (httpCompression)
-        {
-          headers["Accept-Encoding"] = "gzip";
-        }
-        
-        std::string uri = "/instances/" + instance + "/frames/" + boost::lexical_cast<std::string>(frame) + "/image-uint16";
-        orthanc_.GetBinaryAsync(uri, headers,
-                                new Callable<RadiologyScene, OrthancApiClient::BinaryResponseReadyMessage>
-                                (*this, &RadiologyScene::OnFrameReceived), NULL,
-                                new Orthanc::SingleValueObject<size_t>(layer.GetIndex()));
-      }
-
-      return layer;
-    }
-
-    
-    void OnTagsReceived(const OrthancApiClient::BinaryResponseReadyMessage& message)
-    {
-      size_t index = dynamic_cast<const Orthanc::SingleValueObject<size_t>&>(message.GetPayload()).GetValue();
-
-      LOG(INFO) << "JSON received: " << message.GetUri().c_str()
-                << " (" << message.GetAnswerSize() << " bytes) for layer " << index;
-      
-      Layers::iterator layer = layers_.find(index);
-      if (layer != layers_.end())
-      {
-        assert(layer->second != NULL);
-        
-        OrthancPlugins::FullOrthancDataset dicom(message.GetAnswer(), message.GetAnswerSize());
-        dynamic_cast<DicomLayer*>(layer->second)->SetDicomTags(dicom);
-
-        float c, w;
-        if (!hasWindowing_ &&
-            layer->second->GetDefaultWindowing(c, w))
-        {
-          hasWindowing_ = true;
-          windowingCenter_ = c;
-          windowingWidth_ = w;
-        }
-
-        EmitMessage(GeometryChangedMessage(*this));
-      }
-    }
-    
-
-    void OnFrameReceived(const OrthancApiClient::BinaryResponseReadyMessage& message)
-    {
-      size_t index = dynamic_cast<const Orthanc::SingleValueObject<size_t>&>(message.GetPayload()).GetValue();
-      
-      LOG(INFO) << "DICOM frame received: " << message.GetUri().c_str()
-                << " (" << message.GetAnswerSize() << " bytes) for layer " << index;
-      
-      Layers::iterator layer = layers_.find(index);
-      if (layer != layers_.end())
-      {
-        assert(layer->second != NULL);
-
-        std::string content;
-        if (message.GetAnswerSize() > 0)
-        {
-          content.assign(reinterpret_cast<const char*>(message.GetAnswer()), message.GetAnswerSize());
-        }
-        
-        std::auto_ptr<Orthanc::PamReader> reader(new Orthanc::PamReader);
-        reader->ReadFromMemory(content);
-        dynamic_cast<DicomLayer*>(layer->second)->SetSourceImage(reader.release());
-
-        EmitMessage(ContentChangedMessage(*this));
-      }
-    }
-
-
-    Extent2D GetSceneExtent() const
-    {
-      Extent2D extent;
-
-      for (Layers::const_iterator it = layers_.begin();
-           it != layers_.end(); ++it)
-      {
-        assert(it->second != NULL);
-        extent.Union(it->second->GetExtent());
-      }
-
-      return extent;
-    }
-    
-
-    void Render(Orthanc::ImageAccessor& buffer,
-                const Matrix& viewTransform,
-                ImageInterpolation interpolation) const
-    {
-      Orthanc::ImageProcessing::Set(buffer, 0);
-
-      // Render layers in the background-to-foreground order
-      for (size_t index = 0; index < countLayers_; index++)
-      {
-        Layers::const_iterator it = layers_.find(index);
-        if (it != layers_.end())
-        {
-          assert(it->second != NULL);
-          it->second->Render(buffer, viewTransform, interpolation);
-        }
-      }
-    }
-
-
-    bool LookupLayer(size_t& index /* out */,
-                     double x,
-                     double y) const
-    {
-      // Render layers in the foreground-to-background order
-      for (size_t i = countLayers_; i > 0; i--)
-      {
-        index = i - 1;
-        Layers::const_iterator it = layers_.find(index);
-        if (it != layers_.end())
-        {
-          assert(it->second != NULL);
-          if (it->second->Contains(x, y))
-          {
-            return true;
-          }
-        }
-      }
-
-      return false;
-    }
-
-    
-    void DrawBorder(CairoContext& context,
-                    unsigned int layer,
-                    double zoom)
-    {
-      Layers::const_iterator found = layers_.find(layer);
-        
-      if (found != layers_.end())
-      {
-        context.SetSourceColor(255, 0, 0);
-        found->second->DrawBorders(context, zoom);
-      }
-    }
-
-
-    void GetRange(float& minValue,
-                  float& maxValue) const
-    {
-      bool first = true;
-      
-      for (Layers::const_iterator it = layers_.begin();
-           it != layers_.end(); it++)
-      {
-        assert(it->second != NULL);
-
-        float a, b;
-        if (it->second->GetRange(a, b))
-        {
-          if (first)
-          {
-            minValue = a;
-            maxValue = b;
-            first = false;
-          }
-          else
-          {
-            minValue = std::min(a, minValue);
-            maxValue = std::max(b, maxValue);
-          }
-        }
-      }
-
-      if (first)
-      {
-        minValue = 0;
-        maxValue = 0;
-      }
-    }
-
-
-    // Export using PAM is faster than using PNG, but requires Orthanc
-    // core >= 1.4.3
-    void Export(const Orthanc::DicomMap& dicom,
-                double pixelSpacingX,
-                double pixelSpacingY,
-                bool invert,
-                ImageInterpolation interpolation,
-                bool usePam)
-    {
-      if (pixelSpacingX <= 0 ||
-          pixelSpacingY <= 0)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-      }
-      
-      LOG(INFO) << "Exporting DICOM";
-
-      Extent2D extent = GetSceneExtent();
-
-      int w = std::ceil(extent.GetWidth() / pixelSpacingX);
-      int h = std::ceil(extent.GetHeight() / pixelSpacingY);
-
-      if (w < 0 || h < 0)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-      }
-
-      Orthanc::Image layers(Orthanc::PixelFormat_Float32,
-                            static_cast<unsigned int>(w),
-                            static_cast<unsigned int>(h), false);
-
-      Matrix view = LinearAlgebra::Product(
-        CreateScalingMatrix(1.0 / pixelSpacingX, 1.0 / pixelSpacingY),
-        CreateOffsetMatrix(-extent.GetX1(), -extent.GetY1()));
-      
-      Render(layers, view, interpolation);
-
-      Orthanc::Image rendered(Orthanc::PixelFormat_Grayscale16,
-                              layers.GetWidth(), layers.GetHeight(), false);
-      Orthanc::ImageProcessing::Convert(rendered, layers);
-
-      std::string base64;
-
-      {
-        std::string content;
-
-        if (usePam)
-        {
-          Orthanc::PamWriter writer;
-          writer.WriteToMemory(content, rendered);
-        }
-        else
-        {
-          Orthanc::PngWriter writer;
-          writer.WriteToMemory(content, rendered);
-        }
-
-        Orthanc::Toolbox::EncodeBase64(base64, content);
-      }
-
-      std::set<Orthanc::DicomTag> tags;
-      dicom.GetTags(tags);
-
-      Json::Value json = Json::objectValue;
-      json["Tags"] = Json::objectValue;
-           
-      for (std::set<Orthanc::DicomTag>::const_iterator
-             tag = tags.begin(); tag != tags.end(); ++tag)
-      {
-        const Orthanc::DicomValue& value = dicom.GetValue(*tag);
-        if (!value.IsNull() &&
-            !value.IsBinary())
-        {
-          json["Tags"][tag->Format()] = value.GetContent();
-        }
-      }
-
-      json["Tags"][Orthanc::DICOM_TAG_PHOTOMETRIC_INTERPRETATION.Format()] =
-        (invert ? "MONOCHROME1" : "MONOCHROME2");
-
-      // WARNING: The order of PixelSpacing is Y/X. We use "%0.8f" to
-      // avoid floating-point numbers to grow over 16 characters,
-      // which would be invalid according to DICOM standard
-      // ("dciodvfy" would complain).
-      char buf[32];
-      sprintf(buf, "%0.8f\\%0.8f", pixelSpacingY, pixelSpacingX);
-      
-      json["Tags"][Orthanc::DICOM_TAG_PIXEL_SPACING.Format()] = buf;
-
-      float center, width;
-      if (GetWindowing(center, width))
-      {
-        json["Tags"][Orthanc::DICOM_TAG_WINDOW_CENTER.Format()] =
-          boost::lexical_cast<std::string>(boost::math::iround(center));
-
-        json["Tags"][Orthanc::DICOM_TAG_WINDOW_WIDTH.Format()] =
-          boost::lexical_cast<std::string>(boost::math::iround(width));
-      }
-
-      // This is Data URI scheme: https://en.wikipedia.org/wiki/Data_URI_scheme
-      json["Content"] = ("data:" +
-                         std::string(usePam ? Orthanc::MIME_PAM : Orthanc::MIME_PNG) +
-                         ";base64," + base64);
-
-      orthanc_.PostJsonAsyncExpectJson(
-        "/tools/create-dicom", json,
-        new Callable<RadiologyScene, OrthancApiClient::JsonResponseReadyMessage>
-        (*this, &RadiologyScene::OnDicomExported),
-        NULL, NULL);
-    }
-
-
-    void OnDicomExported(const OrthancApiClient::JsonResponseReadyMessage& message)
-    {
-      LOG(INFO) << "DICOM export was successful:"
-                << message.GetJson().toStyledString();
-    }
-  };
-
-
-  class UndoRedoStack : public boost::noncopyable
-  {
-  public:
-    class ICommand : public boost::noncopyable
-    {
-    public:
-      virtual ~ICommand()
-      {
-      }
-      
-      virtual void Undo() const = 0;
-      
-      virtual void Redo() const = 0;
-    };
-
-  private:
-    typedef std::list<ICommand*>  Stack;
-
-    Stack            stack_;
-    Stack::iterator  current_;
-
-    void Clear(Stack::iterator from)
-    {
-      for (Stack::iterator it = from; it != stack_.end(); ++it)
-      {
-        assert(*it != NULL);
-        delete *it;
-      }
-
-      stack_.erase(from, stack_.end());
-    }
-
-  public:
-    UndoRedoStack() :
-      current_(stack_.end())
-    {
-    }
-    
-    ~UndoRedoStack()
-    {
-      Clear(stack_.begin());
-    }
-
-    void Add(ICommand* command)
-    {
-      if (command == NULL)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-      }
-      
-      Clear(current_);
-
-      stack_.push_back(command);
-      current_ = stack_.end();
-    }
-
-    void Undo()
-    {
-      if (current_ != stack_.begin())
-      {
-        --current_;
-        
-        assert(*current_ != NULL);
-        (*current_)->Undo();
-      }
-    }
-
-    void Redo()
-    {
-      if (current_ != stack_.end())
-      {
-        assert(*current_ != NULL);
-        (*current_)->Redo();
-
-        ++current_;
-      }
-    }
-  };
-
-
-  class RadiologyLayerCommand : public UndoRedoStack::ICommand
-  {
-  private:
-    RadiologyScene&  scene_;
-    size_t           layer_;
-
-  protected:
-    virtual void UndoInternal(RadiologyScene::Layer& layer) const = 0;
-
-    virtual void RedoInternal(RadiologyScene::Layer& layer) const = 0;
-
-  public:
-    RadiologyLayerCommand(RadiologyScene& scene,
-                          size_t layer) :
+    RadiographyLayerCommand(RadiographyScene& scene,
+                            size_t layer) :
       scene_(scene),
       layer_(layer)
     {
     }
 
-    RadiologyLayerCommand(const RadiologyScene::LayerAccessor& accessor) :
+    RadiographyLayerCommand(const RadiographyScene::LayerAccessor& accessor) :
       scene_(accessor.GetScene()),
       layer_(accessor.GetIndex())
     {
@@ -1530,7 +78,7 @@
 
     virtual void Undo() const
     {
-      RadiologyScene::LayerAccessor accessor(scene_, layer_);
+      RadiographyScene::LayerAccessor accessor(scene_, layer_);
 
       if (accessor.IsValid())
       {
@@ -1540,7 +88,7 @@
 
     virtual void Redo() const
     {
-      RadiologyScene::LayerAccessor accessor(scene_, layer_);
+      RadiographyScene::LayerAccessor accessor(scene_, layer_);
 
       if (accessor.IsValid())
       {
@@ -1550,11 +98,11 @@
   };
 
 
-  class RadiologyLayerRotateTracker : public IWorldSceneMouseTracker
+  class RadiographyLayerRotateTracker : public IWorldSceneMouseTracker
   {
   private:
     UndoRedoStack&                 undoRedoStack_;
-    RadiologyScene::LayerAccessor  accessor_;
+    RadiographyScene::LayerAccessor  accessor_;
     double                         centerX_;
     double                         centerY_;
     double                         originalAngle_;
@@ -1583,7 +131,7 @@
     }
 
 
-    class UndoRedoCommand : public RadiologyLayerCommand
+    class UndoRedoCommand : public RadiographyLayerCommand
     {
     private:
       double  sourceAngle_;
@@ -1595,21 +143,21 @@
       }
       
     protected:
-      virtual void UndoInternal(RadiologyScene::Layer& layer) const
+      virtual void UndoInternal(RadiographyScene::Layer& layer) const
       {
         LOG(INFO) << "Undo - Set angle to " << ToDegrees(sourceAngle_) << " degrees";
         layer.SetAngle(sourceAngle_);
       }
 
-      virtual void RedoInternal(RadiologyScene::Layer& layer) const
+      virtual void RedoInternal(RadiographyScene::Layer& layer) const
       {
         LOG(INFO) << "Redo - Set angle to " << ToDegrees(sourceAngle_) << " degrees";
         layer.SetAngle(targetAngle_);
       }
 
     public:
-      UndoRedoCommand(const RadiologyLayerRotateTracker& tracker) :
-        RadiologyLayerCommand(tracker.accessor_),
+      UndoRedoCommand(const RadiographyLayerRotateTracker& tracker) :
+        RadiographyLayerCommand(tracker.accessor_),
         sourceAngle_(tracker.originalAngle_),
         targetAngle_(tracker.accessor_.GetLayer().GetAngle())
       {
@@ -1618,13 +166,13 @@
 
       
   public:
-    RadiologyLayerRotateTracker(UndoRedoStack& undoRedoStack,
-                                RadiologyScene& scene,
-                                const ViewportGeometry& view,
-                                size_t layer,
-                                double x,
-                                double y,
-                                bool roundAngles) :
+    RadiographyLayerRotateTracker(UndoRedoStack& undoRedoStack,
+                                  RadiographyScene& scene,
+                                  const ViewportGeometry& view,
+                                  size_t layer,
+                                  double x,
+                                  double y,
+                                  bool roundAngles) :
       undoRedoStack_(undoRedoStack),
       accessor_(scene, layer),
       roundAngles_(roundAngles)
@@ -1688,18 +236,18 @@
   };
     
     
-  class RadiologyLayerMoveTracker : public IWorldSceneMouseTracker
+  class RadiographyLayerMoveTracker : public IWorldSceneMouseTracker
   {
   private:
-    UndoRedoStack&                 undoRedoStack_;
-    RadiologyScene::LayerAccessor  accessor_;
-    double                         clickX_;
-    double                         clickY_;
-    double                         panX_;
-    double                         panY_;
-    bool                           oneAxis_;
+    UndoRedoStack&                   undoRedoStack_;
+    RadiographyScene::LayerAccessor  accessor_;
+    double                           clickX_;
+    double                           clickY_;
+    double                           panX_;
+    double                           panY_;
+    bool                             oneAxis_;
 
-    class UndoRedoCommand : public RadiologyLayerCommand
+    class UndoRedoCommand : public RadiographyLayerCommand
     {
     private:
       double  sourceX_;
@@ -1708,19 +256,19 @@
       double  targetY_;
 
     protected:
-      virtual void UndoInternal(RadiologyScene::Layer& layer) const
+      virtual void UndoInternal(RadiographyScene::Layer& layer) const
       {
         layer.SetPan(sourceX_, sourceY_);
       }
 
-      virtual void RedoInternal(RadiologyScene::Layer& layer) const
+      virtual void RedoInternal(RadiographyScene::Layer& layer) const
       {
         layer.SetPan(targetX_, targetY_);
       }
 
     public:
-      UndoRedoCommand(const RadiologyLayerMoveTracker& tracker) :
-        RadiologyLayerCommand(tracker.accessor_),
+      UndoRedoCommand(const RadiographyLayerMoveTracker& tracker) :
+        RadiographyLayerCommand(tracker.accessor_),
         sourceX_(tracker.panX_),
         sourceY_(tracker.panY_),
         targetX_(tracker.accessor_.GetLayer().GetPanX()),
@@ -1731,12 +279,12 @@
 
 
   public:
-    RadiologyLayerMoveTracker(UndoRedoStack& undoRedoStack,
-                              RadiologyScene& scene,
-                              size_t layer,
-                              double x,
-                              double y,
-                              bool oneAxis) :
+    RadiographyLayerMoveTracker(UndoRedoStack& undoRedoStack,
+                                RadiographyScene& scene,
+                                size_t layer,
+                                double x,
+                                double y,
+                                bool oneAxis) :
       undoRedoStack_(undoRedoStack),
       accessor_(scene, layer),
       clickX_(x),
@@ -1799,18 +347,18 @@
   };
 
 
-  class RadiologyLayerCropTracker : public IWorldSceneMouseTracker
+  class RadiographyLayerCropTracker : public IWorldSceneMouseTracker
   {
   private:
-    UndoRedoStack&                 undoRedoStack_;
-    RadiologyScene::LayerAccessor  accessor_;
-    RadiologyScene::Corner         corner_;
-    unsigned int                   cropX_;
-    unsigned int                   cropY_;
-    unsigned int                   cropWidth_;
-    unsigned int                   cropHeight_;
+    UndoRedoStack&                   undoRedoStack_;
+    RadiographyScene::LayerAccessor  accessor_;
+    RadiographyScene::Corner         corner_;
+    unsigned int                     cropX_;
+    unsigned int                     cropY_;
+    unsigned int                     cropWidth_;
+    unsigned int                     cropHeight_;
 
-    class UndoRedoCommand : public RadiologyLayerCommand
+    class UndoRedoCommand : public RadiographyLayerCommand
     {
     private:
       unsigned int  sourceCropX_;
@@ -1823,19 +371,19 @@
       unsigned int  targetCropHeight_;
 
     protected:
-      virtual void UndoInternal(RadiologyScene::Layer& layer) const
+      virtual void UndoInternal(RadiographyScene::Layer& layer) const
       {
         layer.SetCrop(sourceCropX_, sourceCropY_, sourceCropWidth_, sourceCropHeight_);
       }
 
-      virtual void RedoInternal(RadiologyScene::Layer& layer) const
+      virtual void RedoInternal(RadiographyScene::Layer& layer) const
       {
         layer.SetCrop(targetCropX_, targetCropY_, targetCropWidth_, targetCropHeight_);
       }
 
     public:
-      UndoRedoCommand(const RadiologyLayerCropTracker& tracker) :
-        RadiologyLayerCommand(tracker.accessor_),
+      UndoRedoCommand(const RadiographyLayerCropTracker& tracker) :
+        RadiographyLayerCommand(tracker.accessor_),
         sourceCropX_(tracker.cropX_),
         sourceCropY_(tracker.cropY_),
         sourceCropWidth_(tracker.cropWidth_),
@@ -1848,13 +396,13 @@
 
 
   public:
-    RadiologyLayerCropTracker(UndoRedoStack& undoRedoStack,
-                              RadiologyScene& scene,
-                              const ViewportGeometry& view,
-                              size_t layer,
-                              double x,
-                              double y,
-                              RadiologyScene::Corner corner) :
+    RadiographyLayerCropTracker(UndoRedoStack& undoRedoStack,
+                                RadiographyScene& scene,
+                                const ViewportGeometry& view,
+                                size_t layer,
+                                double x,
+                                double y,
+                                RadiographyScene::Corner corner) :
       undoRedoStack_(undoRedoStack),
       accessor_(scene, layer),
       corner_(corner)
@@ -1893,13 +441,13 @@
       {
         unsigned int x, y;
         
-        RadiologyScene::Layer& layer = accessor_.GetLayer();
+        RadiographyScene::Layer& layer = accessor_.GetLayer();
         if (layer.GetPixel(x, y, sceneX, sceneY))
         {
           unsigned int targetX, targetWidth;
 
-          if (corner_ == RadiologyScene::Corner_TopLeft ||
-              corner_ == RadiologyScene::Corner_BottomLeft)
+          if (corner_ == RadiographyScene::Corner_TopLeft ||
+              corner_ == RadiographyScene::Corner_BottomLeft)
           {
             targetX = std::min(x, cropX_ + cropWidth_);
             targetWidth = cropX_ + cropWidth_ - targetX;
@@ -1912,8 +460,8 @@
 
           unsigned int targetY, targetHeight;
 
-          if (corner_ == RadiologyScene::Corner_TopLeft ||
-              corner_ == RadiologyScene::Corner_TopRight)
+          if (corner_ == RadiographyScene::Corner_TopLeft ||
+              corner_ == RadiographyScene::Corner_TopRight)
           {
             targetY = std::min(y, cropY_ + cropHeight_);
             targetHeight = cropY_ + cropHeight_ - targetY;
@@ -1931,20 +479,20 @@
   };
     
     
-  class RadiologyLayerResizeTracker : public IWorldSceneMouseTracker
+  class RadiographyLayerResizeTracker : public IWorldSceneMouseTracker
   {
   private:
-    UndoRedoStack&                 undoRedoStack_;
-    RadiologyScene::LayerAccessor  accessor_;
-    bool                           roundScaling_;
-    double                         originalSpacingX_;
-    double                         originalSpacingY_;
-    double                         originalPanX_;
-    double                         originalPanY_;
-    RadiologyScene::Corner         oppositeCorner_;
-    double                         oppositeX_;
-    double                         oppositeY_;
-    double                         baseScaling_;
+    UndoRedoStack&                   undoRedoStack_;
+    RadiographyScene::LayerAccessor  accessor_;
+    bool                             roundScaling_;
+    double                           originalSpacingX_;
+    double                           originalSpacingY_;
+    double                           originalPanX_;
+    double                           originalPanY_;
+    RadiographyScene::Corner         oppositeCorner_;
+    double                           oppositeX_;
+    double                           oppositeY_;
+    double                           baseScaling_;
 
     static double ComputeDistance(double x1,
                                   double y1,
@@ -1956,7 +504,7 @@
       return sqrt(dx * dx + dy * dy);
     }
       
-    class UndoRedoCommand : public RadiologyLayerCommand
+    class UndoRedoCommand : public RadiographyLayerCommand
     {
     private:
       double   sourceSpacingX_;
@@ -1969,21 +517,21 @@
       double   targetPanY_;
 
     protected:
-      virtual void UndoInternal(RadiologyScene::Layer& layer) const
+      virtual void UndoInternal(RadiographyScene::Layer& layer) const
       {
         layer.SetPixelSpacing(sourceSpacingX_, sourceSpacingY_);
         layer.SetPan(sourcePanX_, sourcePanY_);
       }
 
-      virtual void RedoInternal(RadiologyScene::Layer& layer) const
+      virtual void RedoInternal(RadiographyScene::Layer& layer) const
       {
         layer.SetPixelSpacing(targetSpacingX_, targetSpacingY_);
         layer.SetPan(targetPanX_, targetPanY_);
       }
 
     public:
-      UndoRedoCommand(const RadiologyLayerResizeTracker& tracker) :
-        RadiologyLayerCommand(tracker.accessor_),
+      UndoRedoCommand(const RadiographyLayerResizeTracker& tracker) :
+        RadiographyLayerCommand(tracker.accessor_),
         sourceSpacingX_(tracker.originalSpacingX_),
         sourceSpacingY_(tracker.originalSpacingY_),
         sourcePanX_(tracker.originalPanX_),
@@ -1998,13 +546,13 @@
 
 
   public:
-    RadiologyLayerResizeTracker(UndoRedoStack& undoRedoStack,
-                                RadiologyScene& scene,
-                                size_t layer,
-                                double x,
-                                double y,
-                                RadiologyScene::Corner corner,
-                                bool roundScaling) :
+    RadiographyLayerResizeTracker(UndoRedoStack& undoRedoStack,
+                                  RadiographyScene& scene,
+                                  size_t layer,
+                                  double x,
+                                  double y,
+                                  RadiographyScene::Corner corner,
+                                  bool roundScaling) :
       undoRedoStack_(undoRedoStack),
       accessor_(scene, layer),
       roundScaling_(roundScaling)
@@ -2019,20 +567,20 @@
 
         switch (corner)
         {
-          case RadiologyScene::Corner_TopLeft:
-            oppositeCorner_ = RadiologyScene::Corner_BottomRight;
+          case RadiographyScene::Corner_TopLeft:
+            oppositeCorner_ = RadiographyScene::Corner_BottomRight;
             break;
 
-          case RadiologyScene::Corner_TopRight:
-            oppositeCorner_ = RadiologyScene::Corner_BottomLeft;
+          case RadiographyScene::Corner_TopRight:
+            oppositeCorner_ = RadiographyScene::Corner_BottomLeft;
             break;
 
-          case RadiologyScene::Corner_BottomLeft:
-            oppositeCorner_ = RadiologyScene::Corner_TopRight;
+          case RadiographyScene::Corner_BottomLeft:
+            oppositeCorner_ = RadiographyScene::Corner_TopRight;
             break;
 
-          case RadiologyScene::Corner_BottomRight:
-            oppositeCorner_ = RadiologyScene::Corner_TopLeft;
+          case RadiographyScene::Corner_BottomRight:
+            oppositeCorner_ = RadiographyScene::Corner_TopLeft;
             break;
 
           default:
@@ -2091,7 +639,7 @@
           scaling = boost::math::round<double>((scaling / ROUND_SCALING) * ROUND_SCALING);
         }
           
-        RadiologyScene::Layer& layer = accessor_.GetLayer();
+        RadiographyScene::Layer& layer = accessor_.GetLayer();
         layer.SetPixelSpacing(scaling * originalSpacingX_,
                               scaling * originalSpacingY_);
 
@@ -2105,7 +653,7 @@
   };
 
 
-  class RadiologyWindowingTracker : public IWorldSceneMouseTracker
+  class RadiographyWindowingTracker : public IWorldSceneMouseTracker
   {   
   public:
     enum Action
@@ -2117,17 +665,17 @@
     };
     
   private:
-    UndoRedoStack&    undoRedoStack_;
-    RadiologyScene&   scene_;
-    int               clickX_;
-    int               clickY_;
-    Action            leftAction_;
-    Action            rightAction_;
-    Action            upAction_;
-    Action            downAction_;
-    float             strength_;
-    float             sourceCenter_;
-    float             sourceWidth_;
+    UndoRedoStack&      undoRedoStack_;
+    RadiographyScene&   scene_;
+    int                 clickX_;
+    int                 clickY_;
+    Action              leftAction_;
+    Action              rightAction_;
+    Action              upAction_;
+    Action              downAction_;
+    float               strength_;
+    float               sourceCenter_;
+    float               sourceWidth_;
 
     static void ComputeAxisEffect(int& deltaCenter,
                                   int& deltaWidth,
@@ -2189,14 +737,14 @@
     class UndoRedoCommand : public UndoRedoStack::ICommand
     {
     private:
-      RadiologyScene&  scene_;
-      float            sourceCenter_;
-      float            sourceWidth_;
-      float            targetCenter_;
-      float            targetWidth_;
+      RadiographyScene&  scene_;
+      float              sourceCenter_;
+      float              sourceWidth_;
+      float              targetCenter_;
+      float              targetWidth_;
 
     public:
-      UndoRedoCommand(const RadiologyWindowingTracker& tracker) :
+      UndoRedoCommand(const RadiographyWindowingTracker& tracker) :
         scene_(tracker.scene_),
         sourceCenter_(tracker.sourceCenter_),
         sourceWidth_(tracker.sourceWidth_)
@@ -2217,14 +765,14 @@
 
 
   public:
-    RadiologyWindowingTracker(UndoRedoStack& undoRedoStack,
-                              RadiologyScene& scene,
-                              int x,
-                              int y,
-                              Action leftAction,
-                              Action rightAction,
-                              Action upAction,
-                              Action downAction) :
+    RadiographyWindowingTracker(UndoRedoStack& undoRedoStack,
+                                RadiographyScene& scene,
+                                int x,
+                                int y,
+                                Action leftAction,
+                                Action rightAction,
+                                Action upAction,
+                                Action downAction) :
       undoRedoStack_(undoRedoStack),
       scene_(scene),
       clickX_(x),
@@ -2301,12 +849,12 @@
   };
 
 
-  class RadiologyWidget :
+  class RadiographyWidget :
     public WorldSceneWidget,
     public IObserver
   {
   private:
-    RadiologyScene&                scene_;
+    RadiographyScene&              scene_;
     std::auto_ptr<Orthanc::Image>  floatBuffer_;
     std::auto_ptr<CairoSurface>    cairoBuffer_;
     bool                           invert_;
@@ -2428,9 +976,9 @@
     }
 
   public:
-    RadiologyWidget(MessageBroker& broker,
-                    RadiologyScene& scene,
-                    const std::string& name) :
+    RadiographyWidget(MessageBroker& broker,
+                      RadiographyScene& scene,
+                      const std::string& name) :
       WorldSceneWidget(name),
       IObserver(broker),
       scene_(scene),
@@ -2440,15 +988,15 @@
       selectedLayer_(0)    // Dummy initialization
     {
       scene.RegisterObserverCallback(
-        new Callable<RadiologyWidget, RadiologyScene::GeometryChangedMessage>
-        (*this, &RadiologyWidget::OnGeometryChanged));
+        new Callable<RadiographyWidget, RadiographyScene::GeometryChangedMessage>
+        (*this, &RadiographyWidget::OnGeometryChanged));
 
       scene.RegisterObserverCallback(
-        new Callable<RadiologyWidget, RadiologyScene::ContentChangedMessage>
-        (*this, &RadiologyWidget::OnContentChanged));
+        new Callable<RadiographyWidget, RadiographyScene::ContentChangedMessage>
+        (*this, &RadiographyWidget::OnContentChanged));
     }
 
-    RadiologyScene& GetScene() const
+    RadiographyScene& GetScene() const
     {
       return scene_;
     }
@@ -2477,13 +1025,13 @@
       }
     }
 
-    void OnGeometryChanged(const RadiologyScene::GeometryChangedMessage& message)
+    void OnGeometryChanged(const RadiographyScene::GeometryChangedMessage& message)
     {
       LOG(INFO) << "Geometry has changed";
       FitContent();
     }
 
-    void OnContentChanged(const RadiologyScene::ContentChangedMessage& message)
+    void OnContentChanged(const RadiographyScene::ContentChangedMessage& message)
     {
       LOG(INFO) << "Content has changed";
       NotifyContentChanged();
@@ -2527,7 +1075,7 @@
   
   namespace Samples
   {
-    class RadiologyEditorInteractor :
+    class RadiographyEditorInteractor :
       public IWorldSceneInteractor,
       public IObserver
     {
@@ -2553,7 +1101,7 @@
     
          
     public:
-      RadiologyEditorInteractor(MessageBroker& broker) :
+      RadiographyEditorInteractor(MessageBroker& broker) :
         IObserver(broker),
         tool_(Tool_Move)
       {
@@ -2569,7 +1117,7 @@
                                                           double y,
                                                           IStatusBar* statusBar)
       {
-        RadiologyWidget& widget = dynamic_cast<RadiologyWidget&>(worldWidget);
+        RadiographyWidget& widget = dynamic_cast<RadiographyWidget&>(worldWidget);
 
         if (button == MouseButton_Left)
         {
@@ -2577,12 +1125,13 @@
         
           if (tool_ == Tool_Windowing)
           {
-            return new RadiologyWindowingTracker(undoRedoStack_, widget.GetScene(),
-                                                 viewportX, viewportY,
-                                                 RadiologyWindowingTracker::Action_DecreaseWidth,
-                                                 RadiologyWindowingTracker::Action_IncreaseWidth,
-                                                 RadiologyWindowingTracker::Action_DecreaseCenter,
-                                                 RadiologyWindowingTracker::Action_IncreaseCenter);
+            return new RadiographyWindowingTracker(
+              undoRedoStack_, widget.GetScene(),
+              viewportX, viewportY,
+              RadiographyWindowingTracker::Action_DecreaseWidth,
+              RadiographyWindowingTracker::Action_IncreaseWidth,
+              RadiographyWindowingTracker::Action_DecreaseCenter,
+              RadiographyWindowingTracker::Action_IncreaseCenter);
           }
           else if (!widget.LookupSelectedLayer(selected))
           {
@@ -2598,18 +1147,20 @@
           else if (tool_ == Tool_Crop ||
                    tool_ == Tool_Resize)
           {
-            RadiologyScene::LayerAccessor accessor(widget.GetScene(), selected);
-            RadiologyScene::Corner corner;
+            RadiographyScene::LayerAccessor accessor(widget.GetScene(), selected);
+            RadiographyScene::Corner corner;
             if (accessor.GetLayer().LookupCorner(corner, x, y, view.GetZoom(), GetHandleSize()))
             {
               switch (tool_)
               {
                 case Tool_Crop:
-                  return new RadiologyLayerCropTracker(undoRedoStack_, widget.GetScene(), view, selected, x, y, corner);
+                  return new RadiographyLayerCropTracker
+                    (undoRedoStack_, widget.GetScene(), view, selected, x, y, corner);
 
                 case Tool_Resize:
-                  return new RadiologyLayerResizeTracker(undoRedoStack_, widget.GetScene(), selected, x, y, corner,
-                                                         (modifiers & KeyboardModifiers_Shift));
+                  return new RadiographyLayerResizeTracker
+                    (undoRedoStack_, widget.GetScene(), selected, x, y, corner,
+                     (modifiers & KeyboardModifiers_Shift));
 
                 default:
                   throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
@@ -2642,12 +1193,14 @@
                 switch (tool_)
                 {
                   case Tool_Move:
-                    return new RadiologyLayerMoveTracker(undoRedoStack_, widget.GetScene(), layer, x, y,
-                                                         (modifiers & KeyboardModifiers_Shift));
+                    return new RadiographyLayerMoveTracker
+                      (undoRedoStack_, widget.GetScene(), layer, x, y,
+                       (modifiers & KeyboardModifiers_Shift));
 
                   case Tool_Rotate:
-                    return new RadiologyLayerRotateTracker(undoRedoStack_, widget.GetScene(), view, layer, x, y,
-                                                           (modifiers & KeyboardModifiers_Shift));
+                    return new RadiographyLayerRotateTracker
+                      (undoRedoStack_, widget.GetScene(), view, layer, x, y,
+                       (modifiers & KeyboardModifiers_Shift));
                 
                   default:
                     break;
@@ -2681,7 +1234,7 @@
                              double y,
                              IStatusBar* statusBar)
       {
-        RadiologyWidget& widget = dynamic_cast<RadiologyWidget&>(worldWidget);
+        RadiographyWidget& widget = dynamic_cast<RadiographyWidget&>(worldWidget);
 
 #if 0
         if (statusBar != NULL)
@@ -2698,9 +1251,9 @@
             (tool_ == Tool_Crop ||
              tool_ == Tool_Resize))
         {
-          RadiologyScene::LayerAccessor accessor(widget.GetScene(), selected);
+          RadiographyScene::LayerAccessor accessor(widget.GetScene(), selected);
         
-          RadiologyScene::Corner corner;
+          RadiographyScene::Corner corner;
           if (accessor.GetLayer().LookupCorner(corner, x, y, view.GetZoom(), GetHandleSize()))
           {
             accessor.GetLayer().GetCorner(x, y, corner);
@@ -2733,7 +1286,7 @@
                               KeyboardModifiers modifiers,
                               IStatusBar* statusBar)
       {
-        RadiologyWidget& widget = dynamic_cast<RadiologyWidget&>(worldWidget);
+        RadiographyWidget& widget = dynamic_cast<RadiographyWidget&>(worldWidget);
 
         switch (keyChar)
         {
@@ -2767,8 +1320,8 @@
             tags.SetValue(Orthanc::DICOM_TAG_STUDY_ID, "STUDY", false);
             tags.SetValue(Orthanc::DICOM_TAG_VIEW_POSITION, "", false);
 
-            widget.GetScene().Export(tags, 0.1, 0.1, widget.IsInverted(),
-                                     widget.GetInterpolation(), EXPORT_USING_PAM);
+            widget.GetScene().ExportDicom(tags, 0.1, 0.1, widget.IsInverted(),
+                                          widget.GetInterpolation(), EXPORT_USING_PAM);
             break;
           }
 
@@ -2843,8 +1396,8 @@
     {
     private:
       std::auto_ptr<OrthancApiClient>  orthancApiClient_;
-      std::auto_ptr<RadiologyScene>    scene_;
-      RadiologyEditorInteractor        interactor_;
+      std::auto_ptr<RadiographyScene>  scene_;
+      RadiographyEditorInteractor      interactor_;
 
     public:
       SingleFrameEditorApplication(MessageBroker& broker) :
@@ -2907,25 +1460,25 @@
         Orthanc::FontRegistry fonts;
         fonts.AddFromResource(Orthanc::EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16);
         
-        scene_.reset(new RadiologyScene(GetBroker(), *orthancApiClient_));
+        scene_.reset(new RadiographyScene(GetBroker(), *orthancApiClient_));
         scene_->LoadDicomFrame(instance, frame, false); //.SetPan(200, 0);
         //scene_->LoadDicomFrame("61f3143e-96f34791-ad6bbb8d-62559e75-45943e1b", 0, false);
 
         {
-          RadiologyScene::Layer& layer = scene_->LoadText(fonts.GetFont(0), "Hello\nworld");
-          //dynamic_cast<RadiologyScene::Layer&>(layer).SetForegroundValue(256);
-          dynamic_cast<RadiologyScene::Layer&>(layer).SetResizeable(true);
+          RadiographyScene::Layer& layer = scene_->LoadText(fonts.GetFont(0), "Hello\nworld");
+          //dynamic_cast<RadiographyScene::Layer&>(layer).SetForegroundValue(256);
+          dynamic_cast<RadiographyScene::Layer&>(layer).SetResizeable(true);
         }
         
         {
-          RadiologyScene::Layer& layer = scene_->LoadTestBlock(100, 50);
-          //dynamic_cast<RadiologyScene::Layer&>(layer).SetForegroundValue(256);
-          dynamic_cast<RadiologyScene::Layer&>(layer).SetResizeable(true);
-          dynamic_cast<RadiologyScene::Layer&>(layer).SetPan(0, 200);
+          RadiographyScene::Layer& layer = scene_->LoadTestBlock(100, 50);
+          //dynamic_cast<RadiographyScene::Layer&>(layer).SetForegroundValue(256);
+          dynamic_cast<RadiographyScene::Layer&>(layer).SetResizeable(true);
+          dynamic_cast<RadiographyScene::Layer&>(layer).SetPan(0, 200);
         }
         
         
-        mainWidget_ = new RadiologyWidget(GetBroker(), *scene_, "main-widget");
+        mainWidget_ = new RadiographyWidget(GetBroker(), *scene_, "main-widget");
         mainWidget_->SetTransmitMouseOver(true);
         mainWidget_->SetInteractor(interactor_);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographyScene.cpp	Mon Nov 12 14:52:10 2018 +0100
@@ -0,0 +1,1262 @@
+/**
+ * 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 "RadiographyScene.h"
+
+#include "../Toolbox/ImageGeometry.h"
+#include "../Toolbox/DicomFrameConverter.h"
+
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Core/Images/PamReader.h>
+#include <Core/Images/PamWriter.h>
+#include <Core/Images/PngWriter.h>
+#include <Core/OrthancException.h>
+#include <Core/Toolbox.h>
+#include <Plugins/Samples/Common/DicomDatasetReader.h>
+#include <Plugins/Samples/Common/FullOrthancDataset.h>
+
+
+namespace OrthancStone
+{
+  static double Square(double x)
+  {
+    return x * x;
+  }
+
+
+  static Matrix CreateOffsetMatrix(double dx,
+                                   double dy)
+  {
+    Matrix m = LinearAlgebra::IdentityMatrix(3);
+    m(0, 2) = dx;
+    m(1, 2) = dy;
+    return m;
+  }
+      
+
+  static Matrix CreateScalingMatrix(double sx,
+                                    double sy)
+  {
+    Matrix m = LinearAlgebra::IdentityMatrix(3);
+    m(0, 0) = sx;
+    m(1, 1) = sy;
+    return m;
+  }
+      
+
+  static Matrix CreateRotationMatrix(double angle)
+  {
+    Matrix m;
+    const double v[] = { cos(angle), -sin(angle), 0,
+                         sin(angle), cos(angle), 0,
+                         0, 0, 1 };
+    LinearAlgebra::FillMatrix(m, 3, 3, v);
+    return m;
+  }
+
+
+  static void ApplyTransform(double& x /* inout */,
+                             double& y /* inout */,
+                             const Matrix& transform)
+  {
+    Vector p;
+    LinearAlgebra::AssignVector(p, x, y, 1);
+
+    Vector q = LinearAlgebra::Product(transform, p);
+
+    if (!LinearAlgebra::IsNear(q[2], 1.0))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+    else
+    {
+      x = q[0];
+      y = q[1];
+    }
+  }
+      
+      
+
+  void RadiographyScene::Layer::UpdateTransform()
+  {
+    transform_ = CreateScalingMatrix(pixelSpacingX_, pixelSpacingY_);
+
+    double centerX, centerY;
+    GetCenter(centerX, centerY);
+
+    transform_ = LinearAlgebra::Product(
+      CreateOffsetMatrix(panX_ + centerX, panY_ + centerY),
+      CreateRotationMatrix(angle_),
+      CreateOffsetMatrix(-centerX, -centerY),
+      transform_);
+
+    LinearAlgebra::InvertMatrix(transformInverse_, transform_);
+  }
+
+
+  void RadiographyScene::Layer::AddToExtent(Extent2D& extent,
+                                            double x,
+                                            double y) const
+  {
+    ApplyTransform(x, y, transform_);
+    extent.AddPoint(x, y);
+  }
+
+
+  void RadiographyScene::Layer::GetCornerInternal(double& x,
+                                                  double& y,
+                                                  Corner corner,
+                                                  unsigned int cropX,
+                                                  unsigned int cropY,
+                                                  unsigned int cropWidth,
+                                                  unsigned int cropHeight) const
+  {
+    double dx = static_cast<double>(cropX);
+    double dy = static_cast<double>(cropY);
+    double dwidth = static_cast<double>(cropWidth);
+    double dheight = static_cast<double>(cropHeight);
+
+    switch (corner)
+    {
+      case Corner_TopLeft:
+        x = dx;
+        y = dy;
+        break;
+
+      case Corner_TopRight:
+        x = dx + dwidth;
+        y = dy;
+        break;
+
+      case Corner_BottomLeft:
+        x = dx;
+        y = dy + dheight;
+        break;
+
+      case Corner_BottomRight:
+        x = dx + dwidth;
+        y = dy + dheight;
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    ApplyTransform(x, y, transform_);
+  }
+
+
+  bool RadiographyScene::Layer::Contains(double x,
+                                         double y) const
+  {
+    ApplyTransform(x, y, transformInverse_);
+        
+    unsigned int cropX, cropY, cropWidth, cropHeight;
+    GetCrop(cropX, cropY, cropWidth, cropHeight);
+
+    return (x >= cropX && x <= cropX + cropWidth &&
+            y >= cropY && y <= cropY + cropHeight);
+  }
+
+
+  void RadiographyScene::Layer::DrawBorders(CairoContext& context,
+                                            double zoom)
+  {
+    unsigned int cx, cy, width, height;
+    GetCrop(cx, cy, width, height);
+
+    double dx = static_cast<double>(cx);
+    double dy = static_cast<double>(cy);
+    double dwidth = static_cast<double>(width);
+    double dheight = static_cast<double>(height);
+
+    cairo_t* cr = context.GetObject();
+    cairo_set_line_width(cr, 2.0 / zoom);
+        
+    double x, y;
+    x = dx;
+    y = dy;
+    ApplyTransform(x, y, transform_);
+    cairo_move_to(cr, x, y);
+
+    x = dx + dwidth;
+    y = dy;
+    ApplyTransform(x, y, transform_);
+    cairo_line_to(cr, x, y);
+
+    x = dx + dwidth;
+    y = dy + dheight;
+    ApplyTransform(x, y, transform_);
+    cairo_line_to(cr, x, y);
+
+    x = dx;
+    y = dy + dheight;
+    ApplyTransform(x, y, transform_);
+    cairo_line_to(cr, x, y);
+
+    x = dx;
+    y = dy;
+    ApplyTransform(x, y, transform_);
+    cairo_line_to(cr, x, y);
+
+    cairo_stroke(cr);
+  }
+
+
+  RadiographyScene::Layer::Layer() :
+    index_(0),
+    hasSize_(false),
+    width_(0),
+    height_(0),
+    hasCrop_(false),
+    pixelSpacingX_(1),
+    pixelSpacingY_(1),
+    panX_(0),
+    panY_(0),
+    angle_(0),
+    resizeable_(false)
+  {
+    UpdateTransform();
+  }
+
+
+  void RadiographyScene::Layer::SetCrop(unsigned int x,
+                                        unsigned int y,
+                                        unsigned int width,
+                                        unsigned int height)
+  {
+    if (!hasSize_)
+    {
+      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;
+
+    UpdateTransform();
+  }
+
+      
+  void RadiographyScene::Layer::GetCrop(unsigned int& x,
+                                        unsigned int& y,
+                                        unsigned int& width,
+                                        unsigned int& height) const
+  {
+    if (hasCrop_)
+    {
+      x = cropX_;
+      y = cropY_;
+      width = cropWidth_;
+      height = cropHeight_;
+    }
+    else 
+    {
+      x = 0;
+      y = 0;
+      width = width_;
+      height = height_;
+    }
+  }
+
+      
+  void RadiographyScene::Layer::SetAngle(double angle)
+  {
+    angle_ = angle;
+    UpdateTransform();
+  }
+
+
+  void RadiographyScene::Layer::SetSize(unsigned int width,
+                                        unsigned int height)
+  {
+    if (hasSize_ &&
+        (width != width_ ||
+         height != height_))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize);
+    }
+        
+    hasSize_ = true;
+    width_ = width;
+    height_ = height;
+
+    UpdateTransform();
+  }
+
+
+  Extent2D RadiographyScene::Layer::GetExtent() const
+  {
+    Extent2D extent;
+       
+    unsigned int x, y, width, height;
+    GetCrop(x, y, width, height);
+
+    double dx = static_cast<double>(x);
+    double dy = static_cast<double>(y);
+    double dwidth = static_cast<double>(width);
+    double dheight = static_cast<double>(height);
+
+    AddToExtent(extent, dx, dy);
+    AddToExtent(extent, dx + dwidth, dy);
+    AddToExtent(extent, dx, dy + dheight);
+    AddToExtent(extent, dx + dwidth, dy + dheight);
+        
+    return extent;
+  }
+
+
+  bool RadiographyScene::Layer::GetPixel(unsigned int& imageX,
+                                         unsigned int& imageY,
+                                         double sceneX,
+                                         double sceneY) const
+  {
+    if (width_ == 0 ||
+        height_ == 0)
+    {
+      return false;
+    }
+    else
+    {
+      ApplyTransform(sceneX, sceneY, transformInverse_);
+        
+      int x = static_cast<int>(std::floor(sceneX));
+      int y = static_cast<int>(std::floor(sceneY));
+
+      if (x < 0)
+      {
+        imageX = 0;
+      }
+      else if (x >= static_cast<int>(width_))
+      {
+        imageX = width_;
+      }
+      else
+      {
+        imageX = static_cast<unsigned int>(x);
+      }
+
+      if (y < 0)
+      {
+        imageY = 0;
+      }
+      else if (y >= static_cast<int>(height_))
+      {
+        imageY = height_;
+      }
+      else
+      {
+        imageY = static_cast<unsigned int>(y);
+      }
+
+      return true;
+    }
+  }
+
+
+  void RadiographyScene::Layer::SetPan(double x,
+                                       double y)
+  {
+    panX_ = x;
+    panY_ = y;
+    UpdateTransform();
+  }
+
+
+  void RadiographyScene::Layer::SetPixelSpacing(double x,
+                                                double y)
+  {
+    pixelSpacingX_ = x;
+    pixelSpacingY_ = y;
+    UpdateTransform();
+  }
+
+
+  void RadiographyScene::Layer::GetCenter(double& centerX,
+                                          double& centerY) const
+  {
+    centerX = static_cast<double>(width_) / 2.0;
+    centerY = static_cast<double>(height_) / 2.0;
+    ApplyTransform(centerX, centerY, transform_);
+  }
+
+
+  void RadiographyScene::Layer::GetCorner(double& x /* out */,
+                                          double& y /* out */,
+                                          Corner corner) const
+  {
+    unsigned int cropX, cropY, cropWidth, cropHeight;
+    GetCrop(cropX, cropY, cropWidth, cropHeight);
+    GetCornerInternal(x, y, corner, cropX, cropY, cropWidth, cropHeight);
+  }
+      
+      
+  bool RadiographyScene::Layer::LookupCorner(Corner& corner /* out */,
+                                             double x,
+                                             double y,
+                                             double zoom,
+                                             double viewportDistance) const
+  {
+    static const Corner CORNERS[] = {
+      Corner_TopLeft,
+      Corner_TopRight,
+      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;
+  }
+
+      
+
+  RadiographyScene::LayerAccessor::LayerAccessor(RadiographyScene& scene,
+                                                 size_t index) :
+    scene_(scene),
+    index_(index)
+  {
+    Layers::iterator layer = scene.layers_.find(index);
+    if (layer == scene.layers_.end())
+    {
+      layer_ = NULL;
+    }
+    else
+    {
+      assert(layer->second != NULL);
+      layer_ = layer->second;
+    }
+  }
+
+      
+  RadiographyScene::LayerAccessor::LayerAccessor(RadiographyScene& scene,
+                                                 double x,
+                                                 double y) :
+    scene_(scene),
+    index_(0)  // Dummy initialization
+  {
+    if (scene.LookupLayer(index_, x, y))
+    {
+      Layers::iterator layer = scene.layers_.find(index_);
+          
+      if (layer == scene.layers_.end())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+      else
+      {
+        assert(layer->second != NULL);
+        layer_ = layer->second;
+      }
+    }
+    else
+    {
+      layer_ = NULL;
+    }
+  }
+
+
+  RadiographyScene& RadiographyScene::LayerAccessor::GetScene() const
+  {
+    if (IsValid())
+    {
+      return scene_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  size_t RadiographyScene::LayerAccessor::GetIndex() const
+  {
+    if (IsValid())
+    {
+      return index_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+  
+  RadiographyScene::Layer& RadiographyScene::LayerAccessor::GetLayer() const
+  {
+    if (IsValid())
+    {
+      return *layer_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }    
+
+
+
+  class RadiographyScene::AlphaLayer : public Layer
+  {
+  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 Matrix& 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);
+
+      Matrix m = LinearAlgebra::Product(viewTransform,
+                                        GetTransform(),
+                                        CreateOffsetMatrix(cropX, cropY));
+
+      Orthanc::ImageAccessor cropped;
+      alpha_->GetRegion(cropped, cropX, cropY, cropWidth, cropHeight);
+        
+      Orthanc::Image tmp(Orthanc::PixelFormat_Grayscale8, buffer.GetWidth(), buffer.GetHeight(), false);
+      ApplyProjectiveTransform(tmp, cropped, m, 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 Layer
+  {
+  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 Matrix& 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);
+
+        Matrix m = LinearAlgebra::Product(viewTransform,
+                                          GetTransform(),
+                                          CreateOffsetMatrix(cropX, cropY));
+
+        Orthanc::ImageAccessor cropped;
+        converted_->GetRegion(cropped, cropX, cropY, cropWidth, cropHeight);
+        
+        ApplyProjectiveTransform(buffer, cropped, m, 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;
+      }
+    }
+  };
+
+
+  RadiographyScene::Layer& RadiographyScene::RegisterLayer(RadiographyScene::Layer* layer)
+  {
+    if (layer == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+
+    std::auto_ptr<Layer> raii(layer);
+      
+    size_t index = countLayers_++;
+    raii->SetIndex(index);
+    layers_[index] = raii.release();
+
+    EmitMessage(GeometryChangedMessage(*this));
+    EmitMessage(ContentChangedMessage(*this));
+
+    return *layer;
+  }
+    
+
+  RadiographyScene::RadiographyScene(MessageBroker& broker,
+                                     OrthancApiClient& orthanc) :
+    IObserver(broker),
+    IObservable(broker),
+    orthanc_(orthanc),
+    countLayers_(0),
+    hasWindowing_(false),
+    windowingCenter_(0),  // Dummy initialization
+    windowingWidth_(0)    // Dummy initialization
+  {
+  }
+
+
+  RadiographyScene::~RadiographyScene()
+  {
+    for (Layers::iterator it = layers_.begin(); it != layers_.end(); it++)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+  }
+
+
+  bool RadiographyScene::GetWindowing(float& center,
+                                      float& width) const
+  {
+    if (hasWindowing_)
+    {
+      center = windowingCenter_;
+      width = windowingWidth_;
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  void RadiographyScene::GetWindowingWithDefault(float& center,
+                                                 float& width) const
+  {
+    if (!GetWindowing(center, width))
+    {
+      center = 128;
+      width = 256;
+    }
+  }
+
+
+  void RadiographyScene::SetWindowing(float center,
+                                      float width)
+  {
+    hasWindowing_ = true;
+    windowingCenter_ = center;
+    windowingWidth_ = width;
+  }
+
+
+  RadiographyScene::Layer& RadiographyScene::LoadText(const Orthanc::Font& font,
+                                                      const std::string& utf8)
+  {
+    std::auto_ptr<AlphaLayer>  alpha(new AlphaLayer(*this));
+    alpha->LoadText(font, utf8);
+
+    return RegisterLayer(alpha.release());
+  }
+
+    
+  RadiographyScene::Layer& RadiographyScene::LoadTestBlock(unsigned int width,
+                                                           unsigned int height)
+  {
+    std::auto_ptr<Orthanc::Image>  block(new Orthanc::Image(Orthanc::PixelFormat_Grayscale8, width, height, false));
+
+    for (unsigned int padding = 0;
+         (width > 2 * padding) && (height > 2 * padding);
+         padding++)
+    {
+      uint8_t color;
+      if (255 > 10 * padding)
+      {
+        color = 255 - 10 * padding;
+      }
+      else
+      {
+        color = 0;
+      }
+
+      Orthanc::ImageAccessor region;
+      block->GetRegion(region, padding, padding, width - 2 * padding, height - 2 * padding);
+      Orthanc::ImageProcessing::Set(region, color);
+    }
+
+    std::auto_ptr<AlphaLayer>  alpha(new AlphaLayer(*this));
+    alpha->SetAlpha(block.release());
+
+    return RegisterLayer(alpha.release());
+  }
+
+    
+  RadiographyScene::Layer& RadiographyScene::LoadDicomFrame(const std::string& instance,
+                                                            unsigned int frame,
+                                                            bool httpCompression)
+  {
+    Layer& layer = RegisterLayer(new DicomLayer);
+
+    {
+      IWebService::Headers headers;
+      std::string uri = "/instances/" + instance + "/tags";
+        
+      orthanc_.GetBinaryAsync(
+        uri, headers,
+        new Callable<RadiographyScene, OrthancApiClient::BinaryResponseReadyMessage>
+        (*this, &RadiographyScene::OnTagsReceived), NULL,
+        new Orthanc::SingleValueObject<size_t>(layer.GetIndex()));
+    }
+
+    {
+      IWebService::Headers headers;
+      headers["Accept"] = "image/x-portable-arbitrarymap";
+
+      if (httpCompression)
+      {
+        headers["Accept-Encoding"] = "gzip";
+      }
+        
+      std::string uri = ("/instances/" + instance + "/frames/" +
+                         boost::lexical_cast<std::string>(frame) + "/image-uint16");
+        
+      orthanc_.GetBinaryAsync(
+        uri, headers,
+        new Callable<RadiographyScene, OrthancApiClient::BinaryResponseReadyMessage>
+        (*this, &RadiographyScene::OnFrameReceived), NULL,
+        new Orthanc::SingleValueObject<size_t>(layer.GetIndex()));
+    }
+
+    return layer;
+  }
+
+    
+  void RadiographyScene::OnTagsReceived(const OrthancApiClient::BinaryResponseReadyMessage& message)
+  {
+    size_t index = dynamic_cast<const Orthanc::SingleValueObject<size_t>&>
+      (message.GetPayload()).GetValue();
+
+    LOG(INFO) << "JSON received: " << message.GetUri().c_str()
+              << " (" << message.GetAnswerSize() << " bytes) for layer " << index;
+      
+    Layers::iterator layer = layers_.find(index);
+    if (layer != layers_.end())
+    {
+      assert(layer->second != NULL);
+        
+      OrthancPlugins::FullOrthancDataset dicom(message.GetAnswer(), message.GetAnswerSize());
+      dynamic_cast<DicomLayer*>(layer->second)->SetDicomTags(dicom);
+
+      float c, w;
+      if (!hasWindowing_ &&
+          layer->second->GetDefaultWindowing(c, w))
+      {
+        hasWindowing_ = true;
+        windowingCenter_ = c;
+        windowingWidth_ = w;
+      }
+
+      EmitMessage(GeometryChangedMessage(*this));
+    }
+  }
+    
+
+  void RadiographyScene::OnFrameReceived(const OrthancApiClient::BinaryResponseReadyMessage& message)
+  {
+    size_t index = dynamic_cast<const Orthanc::SingleValueObject<size_t>&>(message.GetPayload()).GetValue();
+      
+    LOG(INFO) << "DICOM frame received: " << message.GetUri().c_str()
+              << " (" << message.GetAnswerSize() << " bytes) for layer " << index;
+      
+    Layers::iterator layer = layers_.find(index);
+    if (layer != layers_.end())
+    {
+      assert(layer->second != NULL);
+
+      std::string content;
+      if (message.GetAnswerSize() > 0)
+      {
+        content.assign(reinterpret_cast<const char*>(message.GetAnswer()), message.GetAnswerSize());
+      }
+        
+      std::auto_ptr<Orthanc::PamReader> reader(new Orthanc::PamReader);
+      reader->ReadFromMemory(content);
+      dynamic_cast<DicomLayer*>(layer->second)->SetSourceImage(reader.release());
+
+      EmitMessage(ContentChangedMessage(*this));
+    }
+  }
+
+
+  Extent2D RadiographyScene::GetSceneExtent() const
+  {
+    Extent2D extent;
+
+    for (Layers::const_iterator it = layers_.begin();
+         it != layers_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      extent.Union(it->second->GetExtent());
+    }
+
+    return extent;
+  }
+    
+
+  void RadiographyScene::Render(Orthanc::ImageAccessor& buffer,
+                                const Matrix& viewTransform,
+                                ImageInterpolation interpolation) const
+  {
+    Orthanc::ImageProcessing::Set(buffer, 0);
+
+    // Render layers in the background-to-foreground order
+    for (size_t index = 0; index < countLayers_; index++)
+    {
+      Layers::const_iterator it = layers_.find(index);
+      if (it != layers_.end())
+      {
+        assert(it->second != NULL);
+        it->second->Render(buffer, viewTransform, interpolation);
+      }
+    }
+  }
+
+
+  bool RadiographyScene::LookupLayer(size_t& index /* out */,
+                                     double x,
+                                     double y) const
+  {
+    // Render layers in the foreground-to-background order
+    for (size_t i = countLayers_; i > 0; i--)
+    {
+      index = i - 1;
+      Layers::const_iterator it = layers_.find(index);
+      if (it != layers_.end())
+      {
+        assert(it->second != NULL);
+        if (it->second->Contains(x, y))
+        {
+          return true;
+        }
+      }
+    }
+
+    return false;
+  }
+
+    
+  void RadiographyScene::DrawBorder(CairoContext& context,
+                                    unsigned int layer,
+                                    double zoom)
+  {
+    Layers::const_iterator found = layers_.find(layer);
+        
+    if (found != layers_.end())
+    {
+      context.SetSourceColor(255, 0, 0);
+      found->second->DrawBorders(context, zoom);
+    }
+  }
+
+
+  void RadiographyScene::GetRange(float& minValue,
+                                  float& maxValue) const
+  {
+    bool first = true;
+      
+    for (Layers::const_iterator it = layers_.begin();
+         it != layers_.end(); it++)
+    {
+      assert(it->second != NULL);
+
+      float a, b;
+      if (it->second->GetRange(a, b))
+      {
+        if (first)
+        {
+          minValue = a;
+          maxValue = b;
+          first = false;
+        }
+        else
+        {
+          minValue = std::min(a, minValue);
+          maxValue = std::max(b, maxValue);
+        }
+      }
+    }
+
+    if (first)
+    {
+      minValue = 0;
+      maxValue = 0;
+    }
+  }
+
+
+  // Export using PAM is faster than using PNG, but requires Orthanc
+  // core >= 1.4.3
+  void RadiographyScene::ExportDicom(const Orthanc::DicomMap& dicom,
+                                     double pixelSpacingX,
+                                     double pixelSpacingY,
+                                     bool invert,
+                                     ImageInterpolation interpolation,
+                                     bool usePam)
+  {
+    if (pixelSpacingX <= 0 ||
+        pixelSpacingY <= 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+      
+    LOG(INFO) << "Exporting DICOM";
+
+    Extent2D extent = GetSceneExtent();
+
+    int w = std::ceil(extent.GetWidth() / pixelSpacingX);
+    int h = std::ceil(extent.GetHeight() / pixelSpacingY);
+
+    if (w < 0 || h < 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    Orthanc::Image layers(Orthanc::PixelFormat_Float32,
+                          static_cast<unsigned int>(w),
+                          static_cast<unsigned int>(h), false);
+
+    Matrix view = LinearAlgebra::Product(
+      CreateScalingMatrix(1.0 / pixelSpacingX, 1.0 / pixelSpacingY),
+      CreateOffsetMatrix(-extent.GetX1(), -extent.GetY1()));
+      
+    Render(layers, view, interpolation);
+
+    Orthanc::Image rendered(Orthanc::PixelFormat_Grayscale16,
+                            layers.GetWidth(), layers.GetHeight(), false);
+    Orthanc::ImageProcessing::Convert(rendered, layers);
+
+    std::string base64;
+
+    {
+      std::string content;
+
+      if (usePam)
+      {
+        Orthanc::PamWriter writer;
+        writer.WriteToMemory(content, rendered);
+      }
+      else
+      {
+        Orthanc::PngWriter writer;
+        writer.WriteToMemory(content, rendered);
+      }
+
+      Orthanc::Toolbox::EncodeBase64(base64, content);
+    }
+
+    std::set<Orthanc::DicomTag> tags;
+    dicom.GetTags(tags);
+
+    Json::Value json = Json::objectValue;
+    json["Tags"] = Json::objectValue;
+           
+    for (std::set<Orthanc::DicomTag>::const_iterator
+           tag = tags.begin(); tag != tags.end(); ++tag)
+    {
+      const Orthanc::DicomValue& value = dicom.GetValue(*tag);
+      if (!value.IsNull() &&
+          !value.IsBinary())
+      {
+        json["Tags"][tag->Format()] = value.GetContent();
+      }
+    }
+
+    json["Tags"][Orthanc::DICOM_TAG_PHOTOMETRIC_INTERPRETATION.Format()] =
+      (invert ? "MONOCHROME1" : "MONOCHROME2");
+
+    // WARNING: The order of PixelSpacing is Y/X. We use "%0.8f" to
+    // avoid floating-point numbers to grow over 16 characters,
+    // which would be invalid according to DICOM standard
+    // ("dciodvfy" would complain).
+    char buf[32];
+    sprintf(buf, "%0.8f\\%0.8f", pixelSpacingY, pixelSpacingX);
+      
+    json["Tags"][Orthanc::DICOM_TAG_PIXEL_SPACING.Format()] = buf;
+
+    float center, width;
+    if (GetWindowing(center, width))
+    {
+      json["Tags"][Orthanc::DICOM_TAG_WINDOW_CENTER.Format()] =
+        boost::lexical_cast<std::string>(boost::math::iround(center));
+
+      json["Tags"][Orthanc::DICOM_TAG_WINDOW_WIDTH.Format()] =
+        boost::lexical_cast<std::string>(boost::math::iround(width));
+    }
+
+    // This is Data URI scheme: https://en.wikipedia.org/wiki/Data_URI_scheme
+    json["Content"] = ("data:" +
+                       std::string(usePam ? Orthanc::MIME_PAM : Orthanc::MIME_PNG) +
+                       ";base64," + base64);
+
+    orthanc_.PostJsonAsyncExpectJson(
+      "/tools/create-dicom", json,
+      new Callable<RadiographyScene, OrthancApiClient::JsonResponseReadyMessage>
+      (*this, &RadiographyScene::OnDicomExported),
+      NULL, NULL);
+  }
+
+
+  void RadiographyScene::OnDicomExported(const OrthancApiClient::JsonResponseReadyMessage& message)
+  {
+    LOG(INFO) << "DICOM export was successful:"
+              << message.GetJson().toStyledString();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographyScene.h	Mon Nov 12 14:52:10 2018 +0100
@@ -0,0 +1,325 @@
+/**
+ * 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 "../Toolbox/Extent2D.h"
+#include "../Toolbox/LinearAlgebra.h"
+#include "../Toolbox/OrthancApiClient.h"
+#include "../Viewport/CairoContext.h"
+
+
+namespace OrthancStone
+{
+  class RadiographyScene :
+    public IObserver,
+    public IObservable
+  {
+  public:
+    typedef OriginMessage<MessageType_Widget_GeometryChanged, RadiographyScene> GeometryChangedMessage;
+    typedef OriginMessage<MessageType_Widget_ContentChanged, RadiographyScene> ContentChangedMessage;
+
+    enum Corner
+    {
+      Corner_TopLeft,
+      Corner_TopRight,
+      Corner_BottomLeft,
+      Corner_BottomRight
+    };
+
+    class Layer : public boost::noncopyable
+    {
+      friend class RadiographyScene;
+      
+    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_;
+      Matrix        transform_;
+      Matrix        transformInverse_;
+      double        pixelSpacingX_;
+      double        pixelSpacingY_;
+      double        panX_;
+      double        panY_;
+      double        angle_;
+      bool          resizeable_;
+
+
+    protected:
+      const Matrix& GetTransform() const
+      {
+        return transform_;
+      }
+
+
+    private:
+      void UpdateTransform();
+      
+      void AddToExtent(Extent2D& extent,
+                       double x,
+                       double y) const;
+
+      void GetCornerInternal(double& x,
+                             double& y,
+                             Corner corner,
+                             unsigned int cropX,
+                             unsigned int cropY,
+                             unsigned int cropWidth,
+                             unsigned int cropHeight) const;
+
+      void SetIndex(size_t index)
+      {
+        index_ = index;
+      }
+      
+      bool Contains(double x,
+                    double y) const;
+      
+      void DrawBorders(CairoContext& context,
+                       double zoom);
+
+    public:
+      Layer();
+
+      virtual ~Layer()
+      {
+      }
+
+      size_t GetIndex() const
+      {
+        return index_;
+      }
+
+      void ResetCrop()
+      {
+        hasCrop_ = false;
+      }
+
+      void SetCrop(unsigned int x,
+                   unsigned int y,
+                   unsigned int width,
+                   unsigned int height);
+
+      void GetCrop(unsigned int& x,
+                   unsigned int& y,
+                   unsigned int& width,
+                   unsigned int& height) const;
+
+      void SetAngle(double angle);
+
+      double GetAngle() const
+      {
+        return angle_;
+      }
+
+      void SetSize(unsigned int width,
+                   unsigned int height);
+
+      unsigned int GetWidth() const
+      {
+        return width_;
+      }        
+
+      unsigned int GetHeight() const
+      {
+        return height_;
+      }       
+
+      Extent2D GetExtent() const;
+
+      bool GetPixel(unsigned int& imageX,
+                    unsigned int& imageY,
+                    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;
+
+      void GetCorner(double& x /* out */,
+                     double& y /* out */,
+                     Corner corner) const;
+      
+      bool LookupCorner(Corner& corner /* out */,
+                        double x,
+                        double y,
+                        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;
+
+      virtual void Render(Orthanc::ImageAccessor& buffer,
+                          const Matrix& viewTransform,
+                          ImageInterpolation interpolation) const = 0;
+
+      virtual bool GetRange(float& minValue,
+                            float& maxValue) const = 0;
+    }; 
+
+
+    class LayerAccessor : public boost::noncopyable
+    {
+    private:
+      RadiographyScene&  scene_;
+      size_t             index_;
+      Layer*             layer_;
+
+    public:
+      LayerAccessor(RadiographyScene& scene,
+                    size_t index);
+
+      LayerAccessor(RadiographyScene& scene,
+                    double x,
+                    double y);
+
+      void Invalidate()
+      {
+        layer_ = NULL;
+      }
+
+      bool IsValid() const
+      {
+        return layer_ != NULL;
+      }
+
+      RadiographyScene& GetScene() const;
+
+      size_t GetIndex() const;
+
+      Layer& GetLayer() const;
+    };
+
+
+  private:
+    class AlphaLayer;    
+    class DicomLayer;
+
+    typedef std::map<size_t, Layer*>  Layers;
+        
+    OrthancApiClient&  orthanc_;
+    size_t             countLayers_;
+    bool               hasWindowing_;
+    float              windowingCenter_;
+    float              windowingWidth_;
+    Layers             layers_;
+
+    Layer& RegisterLayer(Layer* layer);
+
+    void OnTagsReceived(const OrthancApiClient::BinaryResponseReadyMessage& message);
+
+    void OnFrameReceived(const OrthancApiClient::BinaryResponseReadyMessage& message);
+    
+    void OnDicomExported(const OrthancApiClient::JsonResponseReadyMessage& message);
+
+  public:
+    RadiographyScene(MessageBroker& broker,
+                     OrthancApiClient& orthanc);
+    
+    virtual ~RadiographyScene();
+
+    bool GetWindowing(float& center,
+                      float& width) const;
+
+    void GetWindowingWithDefault(float& center,
+                                 float& width) const;
+
+    void SetWindowing(float center,
+                      float width);
+
+    Layer& LoadText(const Orthanc::Font& font,
+                    const std::string& utf8);
+    
+    Layer& LoadTestBlock(unsigned int width,
+                         unsigned int height);
+    
+    Layer& LoadDicomFrame(const std::string& instance,
+                          unsigned int frame,
+                          bool httpCompression);
+
+    Extent2D GetSceneExtent() const;
+
+    void Render(Orthanc::ImageAccessor& buffer,
+                const Matrix& viewTransform,
+                ImageInterpolation interpolation) const;
+
+    bool LookupLayer(size_t& index /* out */,
+                     double x,
+                     double y) const;
+    
+    void DrawBorder(CairoContext& context,
+                    unsigned int layer,
+                    double zoom);
+
+    void GetRange(float& minValue,
+                  float& maxValue) const;
+
+    // Export using PAM is faster than using PNG, but requires Orthanc
+    // core >= 1.4.3
+    void ExportDicom(const Orthanc::DicomMap& dicom,
+                     double pixelSpacingX,
+                     double pixelSpacingY,
+                     bool invert,
+                     ImageInterpolation interpolation,
+                     bool usePam);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Toolbox/UndoRedoStack.cpp	Mon Nov 12 14:52:10 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/>.
+ **/
+
+
+#include "UndoRedoStack.h"
+
+#include <Core/OrthancException.h>
+
+#include <cassert>
+
+namespace OrthancStone
+{
+  void UndoRedoStack::Clear(UndoRedoStack::Stack::iterator from)
+  {
+    for (Stack::iterator it = from; it != stack_.end(); ++it)
+    {
+      assert(*it != NULL);
+      delete *it;
+    }
+
+    stack_.erase(from, stack_.end());
+  }
+
+
+  UndoRedoStack::UndoRedoStack() :
+    current_(stack_.end())
+  {
+  }
+
+  
+  UndoRedoStack::~UndoRedoStack()
+  {
+    Clear(stack_.begin());
+  }
+
+  
+  void UndoRedoStack::Add(ICommand* command)
+  {
+    if (command == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+      
+    Clear(current_);
+
+    stack_.push_back(command);
+    current_ = stack_.end();
+  }
+
+  
+  void UndoRedoStack::Undo()
+  {
+    if (current_ != stack_.begin())
+    {
+      --current_;
+        
+      assert(*current_ != NULL);
+      (*current_)->Undo();
+    }
+  }
+
+  void UndoRedoStack::Redo()
+  {
+    if (current_ != stack_.end())
+    {
+      assert(*current_ != NULL);
+      (*current_)->Redo();
+
+      ++current_;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Toolbox/UndoRedoStack.h	Mon Nov 12 14:52:10 2018 +0100
@@ -0,0 +1,64 @@
+/**
+ * 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 <boost/noncopyable.hpp>
+#include <list>
+
+
+namespace OrthancStone
+{
+  class UndoRedoStack : public boost::noncopyable
+  {
+  public:
+    class ICommand : public boost::noncopyable
+    {
+    public:
+      virtual ~ICommand()
+      {
+      }
+      
+      virtual void Undo() const = 0;
+      
+      virtual void Redo() const = 0;
+    };
+
+  private:
+    typedef std::list<ICommand*>  Stack;
+
+    Stack            stack_;
+    Stack::iterator  current_;
+
+    void Clear(Stack::iterator from);
+
+  public:
+    UndoRedoStack();
+    
+    ~UndoRedoStack();
+    
+    void Add(ICommand* command);
+    
+    void Undo();
+
+    void Redo();
+  };
+}
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Mon Nov 12 11:44:20 2018 +0100
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Mon Nov 12 14:52:10 2018 +0100
@@ -246,9 +246,13 @@
   ${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/RadiographyScene.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyScene.h
   ${ORTHANC_STONE_ROOT}/Framework/SmartLoader.cpp
   ${ORTHANC_STONE_ROOT}/Framework/StoneEnumerations.cpp
   ${ORTHANC_STONE_ROOT}/Framework/StoneException.h
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/UndoRedoStack.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/UndoRedoStack.h
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/CoordinateSystem3D.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/DicomFrameConverter.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/DicomStructureSet.cpp