changeset 0:4a7a53257c7d

initial commit
author Sebastien Jodogne <s.jodogne@gmail.com>
date Sat, 22 Oct 2016 21:48:33 +0200
parents
children dc730d11b101
files AUTHORS Applications/ApplicationToolbox.cpp Applications/ApplicationToolbox.h Applications/CMakeLists.txt Applications/DicomToTiff.cpp Applications/Dicomizer.cpp COPYING Framework/Algorithms/PyramidReader.cpp Framework/Algorithms/PyramidReader.h Framework/Algorithms/ReconstructPyramidCommand.cpp Framework/Algorithms/ReconstructPyramidCommand.h Framework/Algorithms/TranscodeTileCommand.cpp Framework/Algorithms/TranscodeTileCommand.h Framework/DicomToolbox.cpp Framework/DicomToolbox.h Framework/DicomizerParameters.cpp Framework/DicomizerParameters.h Framework/Enumerations.cpp Framework/Enumerations.h Framework/ImageToolbox.cpp Framework/ImageToolbox.h Framework/ImagedVolumeParameters.cpp Framework/ImagedVolumeParameters.h Framework/Inputs/DecodedTiledPyramid.cpp Framework/Inputs/DecodedTiledPyramid.h Framework/Inputs/DicomPyramid.cpp Framework/Inputs/DicomPyramid.h Framework/Inputs/DicomPyramidInstance.cpp Framework/Inputs/DicomPyramidInstance.h Framework/Inputs/DicomPyramidLevel.cpp Framework/Inputs/DicomPyramidLevel.h Framework/Inputs/HierarchicalTiff.cpp Framework/Inputs/HierarchicalTiff.h Framework/Inputs/ITiledPyramid.h Framework/Inputs/OpenSlideLibrary.cpp Framework/Inputs/OpenSlideLibrary.h Framework/Inputs/OpenSlidePyramid.cpp Framework/Inputs/OpenSlidePyramid.h Framework/Inputs/PyramidWithRawTiles.cpp Framework/Inputs/PyramidWithRawTiles.h Framework/Inputs/SingleLevelDecodedPyramid.cpp Framework/Inputs/SingleLevelDecodedPyramid.h Framework/Inputs/TiledJpegImage.h Framework/Inputs/TiledPngImage.h Framework/Inputs/TiledPyramidStatistics.cpp Framework/Inputs/TiledPyramidStatistics.h Framework/Jpeg2000Reader.cpp Framework/Jpeg2000Reader.h Framework/Jpeg2000Writer.cpp Framework/Jpeg2000Writer.h Framework/Messaging/CurlOrthancConnection.cpp Framework/Messaging/CurlOrthancConnection.h Framework/Messaging/FolderTarget.cpp Framework/Messaging/FolderTarget.h Framework/Messaging/IFileTarget.h Framework/Messaging/IOrthancConnection.cpp Framework/Messaging/IOrthancConnection.h Framework/Messaging/OrthancConnectionBase.cpp Framework/Messaging/OrthancConnectionBase.h Framework/Messaging/OrthancTarget.cpp Framework/Messaging/OrthancTarget.h Framework/Messaging/PluginOrthancConnection.cpp Framework/Messaging/PluginOrthancConnection.h Framework/Orthanc/README.txt Framework/Outputs/DicomPyramidWriter.cpp Framework/Outputs/DicomPyramidWriter.h Framework/Outputs/HierarchicalTiffWriter.cpp Framework/Outputs/HierarchicalTiffWriter.h Framework/Outputs/IPyramidWriter.h Framework/Outputs/InMemoryTiledImage.cpp Framework/Outputs/InMemoryTiledImage.h Framework/Outputs/MultiframeDicomWriter.cpp Framework/Outputs/MultiframeDicomWriter.h Framework/Outputs/PyramidWriterBase.cpp Framework/Outputs/PyramidWriterBase.h Framework/Outputs/TruncatedPyramidWriter.cpp Framework/Outputs/TruncatedPyramidWriter.h NEWS README Resources/BrightfieldOpticalPath.json Resources/CMake/BoostExtendedConfiguration.cmake Resources/CMake/LibTiffConfiguration.cmake Resources/CMake/OpenJpegConfiguration.cmake Resources/CMake/OpenJpegConfiguration.patch Resources/OrthancLogoDocumentation.png Resources/OrthancWSI.doxygen Resources/SampleDataset.json Resources/SyncOrthancFolder.py Resources/sRGB.icc Resources/sRGB.txt TODO ViewerPlugin/CMakeLists.txt ViewerPlugin/OrthancExplorer.js ViewerPlugin/Plugin.cpp ViewerPlugin/viewer.html ViewerPlugin/viewer.js
diffstat 96 files changed, 14299 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/AUTHORS	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,13 @@
+Orthanc for Whole-Slide Imaging
+===============================
+
+
+Authors
+-------
+
+* Sebastien Jodogne <s.jodogne@gmail.com>
+  Department of Medical Physics
+  University Hospital of Liege
+  Belgium
+
+  Overall design and lead developer.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/ApplicationToolbox.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,174 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "ApplicationToolbox.h"
+
+#include "../Framework/Inputs/OpenSlideLibrary.h"
+#include "../Framework/Orthanc/Core/HttpClient.h"
+#include "../Framework/Orthanc/Core/Logging.h"
+#include "../Framework/Orthanc/Core/MultiThreading/BagOfTasksProcessor.h"
+#include "../Framework/Orthanc/OrthancServer/FromDcmtkBridge.h"
+
+#include <boost/lexical_cast.hpp>
+#include <boost/regex.hpp>
+
+namespace OrthancWSI
+{
+  namespace ApplicationToolbox
+  {
+    void GlobalInitialize()
+    {
+      Orthanc::Logging::Initialize();
+      Orthanc::HttpClient::InitializeOpenSsl();
+      Orthanc::HttpClient::GlobalInitialize();
+      Orthanc::FromDcmtkBridge::InitializeDictionary();
+    }
+
+
+    void GlobalFinalize()
+    {
+      OrthancWSI::OpenSlideLibrary::Finalize();
+      Orthanc::HttpClient::GlobalFinalize();
+      Orthanc::HttpClient::FinalizeOpenSsl();
+    }
+
+
+    static void PrintProgress(Orthanc::BagOfTasksProcessor::Handle* handle,
+                              bool* done)
+    {
+      unsigned int previous = 0;
+
+      while (!*done)
+      {
+        unsigned int progress = static_cast<unsigned int>(100.0f * handle->GetProgress());
+        if (previous != progress)
+        {
+          LOG(WARNING) << "Progress: " << progress << "%";
+          previous = progress;
+        }
+
+        boost::this_thread::sleep(boost::posix_time::milliseconds(100));
+      }
+    }
+
+
+    void Execute(Orthanc::BagOfTasks& tasks,
+                 unsigned int threadsCount)
+    {
+      if (threadsCount > 1)
+      {
+        LOG(WARNING) << "Running " << tasks.GetSize() << " tasks";
+        LOG(WARNING) << "Using " << threadsCount << " threads for the computation";
+        Orthanc::BagOfTasksProcessor processor(threadsCount);
+        std::auto_ptr<Orthanc::BagOfTasksProcessor::Handle> handle(processor.Submit(tasks));
+
+        bool done = false;
+        boost::thread progress(PrintProgress, handle.get(), &done);
+
+        if (handle->Join())
+        {
+          done = true;
+          LOG(WARNING) << "All tasks have finished";
+        }
+        else
+        {
+          done = true;
+          LOG(ERROR) << "Error has occurred, aborting";
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+                           
+        if (progress.joinable())
+        {
+          progress.join();
+        }
+      }
+      else
+      {
+        LOG(WARNING) << "Running " << tasks.GetSize() << " tasks without multithreading";
+
+        unsigned int previous = 0;
+        unsigned int size = tasks.GetSize();
+
+        // No multithreading
+        while (!tasks.IsEmpty())
+        {
+          std::auto_ptr<Orthanc::ICommand> task(tasks.Pop());
+          if (task->Execute())
+          {
+            unsigned int progress = static_cast<unsigned int>(100.0f * 
+                                                              static_cast<float>((size - tasks.GetSize())) / 
+                                                              static_cast<float>(size));
+            if (progress != previous)
+            {
+              LOG(WARNING) << "Progress: " << progress << "%";
+              previous = progress;
+            }
+          }
+          else
+          {
+            LOG(ERROR) << "Error has occurred, aborting";
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+          }
+        }
+      }
+    }
+
+
+    void ParseColor(uint8_t& red,
+                    uint8_t& green,
+                    uint8_t& blue,
+                    const std::string& color)
+    {
+      boost::regex pattern("([0-9]*),([0-9]*),([0-9]*)");
+    
+      bool ok = false;
+      boost::cmatch what;
+
+      try
+      {
+        if (regex_match(color.c_str(), what, pattern))
+        {
+          int r = boost::lexical_cast<int>(what[1]);
+          int g = boost::lexical_cast<int>(what[2]);
+          int b = boost::lexical_cast<int>(what[3]);
+
+          if (r >= 0 && r <= 255 &&
+              g >= 0 && g <= 255 &&
+              b >= 0 && b <= 255)
+          {
+            red = static_cast<uint8_t>(r);
+            green = static_cast<uint8_t>(g);
+            blue = static_cast<uint8_t>(b);
+            ok = true;
+          }
+        }
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+      }
+    
+      if (!ok)
+      {
+        LOG(ERROR) << "Bad color specification: " << color;
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/ApplicationToolbox.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,44 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Framework/Orthanc/Core/MultiThreading/BagOfTasks.h"
+
+#include <string>
+#include <stdint.h>
+
+namespace OrthancWSI
+{
+  namespace ApplicationToolbox
+  {
+    void GlobalInitialize();
+
+    void GlobalFinalize();
+
+    void Execute(Orthanc::BagOfTasks& tasks,
+                 unsigned int threadsCount);
+
+    void ParseColor(uint8_t& red,
+                    uint8_t& green,
+                    uint8_t& blue,
+                    const std::string& color);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/CMakeLists.txt	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,232 @@
+cmake_minimum_required(VERSION 2.8)
+project(OrthancWSIApplications)
+
+
+#####################################################################
+## Parameters of the build
+#####################################################################
+
+# Generic parameters
+SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)")
+SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages")
+
+# Optional components
+SET(ENABLE_SSL OFF CACHE BOOL "Include support for SSL")
+SET(USE_DCMTK_361 OFF CACHE BOOL "Use forthcoming DCMTK version 3.6.1 in static builds (instead of 3.6.0)")
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
+  set(TMP ON)
+else()
+  set(TMP OFF)
+endif()
+
+
+# Advanced parameters to fine-tune linking against system libraries
+SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of Boost")
+SET(USE_SYSTEM_CURL ON CACHE BOOL "Use the system version of LibCurl")
+SET(USE_SYSTEM_DCMTK ON CACHE BOOL "Use the system version of DCMTK")
+SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp")
+SET(USE_SYSTEM_LIBJPEG ON CACHE BOOL "Use the system version of libjpeg")
+SET(USE_SYSTEM_LIBPNG ON CACHE BOOL "Use the system version of libpng")
+SET(USE_SYSTEM_LIBTIFF ON CACHE BOOL "Use the system version of libtiff")
+SET(USE_SYSTEM_OPENJPEG ON CACHE BOOL "Use the system version of OpenJpeg")
+SET(USE_SYSTEM_OPENSSL ON CACHE BOOL "Use the system version of OpenSSL")
+SET(USE_SYSTEM_ZLIB ON CACHE BOOL "Use the system version of ZLib")
+
+SET(DCMTK_DICTIONARY_DIR "" CACHE PATH "Directory containing the DCMTK dictionaries \"dicom.dic\" and \"private.dic\" (only when using system version of DCMTK)") 
+
+
+#####################################################################
+## Configure mandatory third-party components
+#####################################################################
+
+SET(ORTHANC_WSI_DIR ${CMAKE_CURRENT_LIST_DIR}/..)
+SET(ORTHANC_ROOT ${ORTHANC_WSI_DIR}/Framework/Orthanc)
+SET(USE_OPENJPEG_JP2 ON)
+SET(ENABLE_JPEG OFF)           # Disable DCMTK's support for JPEG, that clashes with libtiff
+SET(ENABLE_JPEG_LOSSLESS OFF)  # Disable DCMTK's support for JPEG-LS
+SET(ENABLE_DCMTK_NETWORK OFF)  # Disable DCMTK's support for DICOM networking
+SET(STANDALONE_BUILD ON)       # Embed DCMTK's dictionaries for static builds
+
+include(CheckIncludeFiles)
+include(CheckIncludeFileCXX)
+include(CheckLibraryExists)
+include(FindPythonInterp)
+include(FindPkgConfig)
+
+include(${ORTHANC_ROOT}/Resources/CMake/Compiler.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/AutoGeneratedCode.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake)
+
+# Third-party components shipped with Orthanc
+include(${ORTHANC_ROOT}/Resources/CMake/DcmtkConfiguration.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/JsonCppConfiguration.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/LibCurlConfiguration.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/LibJpegConfiguration.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/LibPngConfiguration.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/ZlibConfiguration.cmake)
+
+include(${ORTHANC_WSI_DIR}/Resources/CMake/BoostExtendedConfiguration.cmake)
+include(${ORTHANC_WSI_DIR}/Resources/CMake/OpenJpegConfiguration.cmake)
+include(${ORTHANC_WSI_DIR}/Resources/CMake/LibTiffConfiguration.cmake)
+
+add_definitions(
+  -DORTHANC_ENABLE_BASE64=1
+  -DORTHANC_ENABLE_CURL=1
+  -DORTHANC_ENABLE_DCMTK=1
+  -DORTHANC_ENABLE_LOGGING=1
+  -DORTHANC_ENABLE_MD5=0
+  -DORTHANC_JPEG_ENABLED=0     # Disable DCMTK's support for JPEG
+  -DORTHANC_PKCS11_ENABLED=0
+  -DORTHANC_PLUGINS_ENABLED=1  # To enable class Orthanc::SharedLibrary
+  -DORTHANC_PUGIXML_ENABLED=0
+  )
+
+
+#####################################################################
+## Configure optional third-party components
+#####################################################################
+
+if (ENABLE_SSL)
+  set(ENABLE_PKCS11 OFF)
+  add_definitions(-DORTHANC_SSL_ENABLED=1)
+  include(${ORTHANC_ROOT}/Resources/CMake/OpenSslConfiguration.cmake)
+else()
+  add_definitions(-DORTHANC_SSL_ENABLED=0)
+endif()
+
+
+#####################################################################
+## Create the static library containing the framework
+#####################################################################
+
+EmbedResources(
+  ${DCMTK_DICTIONARIES}
+  BRIGHTFIELD_OPTICAL_PATH  ${ORTHANC_WSI_DIR}/Resources/BrightfieldOpticalPath.json
+  SAMPLE_DATASET            ${ORTHANC_WSI_DIR}/Resources/SampleDataset.json
+  SRGB_ICC_PROFILE          ${ORTHANC_WSI_DIR}/Resources/sRGB.icc
+  )
+
+add_library(OrthancWSIFramework STATIC
+  #${ORTHANC_WSI_DIR}/Framework/Messaging/PluginOrthancConnection.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Algorithms/PyramidReader.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Algorithms/ReconstructPyramidCommand.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Algorithms/TranscodeTileCommand.cpp
+  ${ORTHANC_WSI_DIR}/Framework/DicomToolbox.cpp
+  ${ORTHANC_WSI_DIR}/Framework/DicomizerParameters.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Enumerations.cpp
+  ${ORTHANC_WSI_DIR}/Framework/ImageToolbox.cpp
+  ${ORTHANC_WSI_DIR}/Framework/ImagedVolumeParameters.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Inputs/DecodedTiledPyramid.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Inputs/DicomPyramid.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Inputs/DicomPyramidInstance.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Inputs/DicomPyramidLevel.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Inputs/HierarchicalTiff.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Inputs/OpenSlideLibrary.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Inputs/OpenSlidePyramid.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Inputs/PyramidWithRawTiles.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Inputs/SingleLevelDecodedPyramid.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Inputs/TiledPyramidStatistics.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Jpeg2000Reader.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Jpeg2000Writer.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Messaging/CurlOrthancConnection.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Messaging/FolderTarget.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Messaging/IOrthancConnection.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Messaging/OrthancConnectionBase.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Messaging/OrthancTarget.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Outputs/DicomPyramidWriter.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Outputs/HierarchicalTiffWriter.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Outputs/InMemoryTiledImage.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Outputs/MultiframeDicomWriter.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Outputs/PyramidWriterBase.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Outputs/TruncatedPyramidWriter.cpp
+
+  ${ORTHANC_ROOT}/Core/ChunkedBuffer.cpp
+  ${ORTHANC_ROOT}/Core/DicomFormat/DicomArray.cpp
+  ${ORTHANC_ROOT}/Core/DicomFormat/DicomMap.cpp
+  ${ORTHANC_ROOT}/Core/DicomFormat/DicomTag.cpp
+  ${ORTHANC_ROOT}/Core/DicomFormat/DicomValue.cpp
+  ${ORTHANC_ROOT}/Core/Enumerations.cpp
+  ${ORTHANC_ROOT}/Core/HttpClient.cpp
+  ${ORTHANC_ROOT}/Core/Images/IImageWriter.cpp
+  ${ORTHANC_ROOT}/Core/Images/Image.cpp
+  ${ORTHANC_ROOT}/Core/Images/ImageAccessor.cpp
+  ${ORTHANC_ROOT}/Core/Images/ImageBuffer.cpp
+  ${ORTHANC_ROOT}/Core/Images/ImageProcessing.cpp
+  ${ORTHANC_ROOT}/Core/Images/JpegErrorManager.cpp
+  ${ORTHANC_ROOT}/Core/Images/JpegReader.cpp
+  ${ORTHANC_ROOT}/Core/Images/JpegWriter.cpp
+  ${ORTHANC_ROOT}/Core/Images/PngReader.cpp
+  ${ORTHANC_ROOT}/Core/Images/PngWriter.cpp
+  ${ORTHANC_ROOT}/Core/Logging.cpp
+  ${ORTHANC_ROOT}/Core/MultiThreading/BagOfTasksProcessor.cpp
+  ${ORTHANC_ROOT}/Core/MultiThreading/SharedMessageQueue.cpp
+  ${ORTHANC_ROOT}/Core/Toolbox.cpp
+  ${ORTHANC_ROOT}/Core/Uuid.cpp
+  ${ORTHANC_ROOT}/Core/WebServiceParameters.cpp
+  ${ORTHANC_ROOT}/OrthancServer/FromDcmtkBridge.cpp
+  ${ORTHANC_ROOT}/OrthancServer/ServerEnumerations.cpp
+  ${ORTHANC_ROOT}/OrthancServer/ToDcmtkBridge.cpp
+  ${ORTHANC_ROOT}/Plugins/Engine/SharedLibrary.cpp
+  ${ORTHANC_ROOT}/Resources/ThirdParty/base64/base64.cpp
+
+  # Mandatory components
+  ${BOOST_SOURCES}
+  ${JSONCPP_SOURCES}
+  ${ZLIB_SOURCES}
+  ${LIBPNG_SOURCES}
+  ${LIBJPEG_SOURCES}
+  ${DCMTK_SOURCES}
+  ${LIBTIFF_SOURCES}
+  ${OPENJPEG_SOURCES}
+
+  # Optional components
+  ${OPENSSL_SOURCES}
+  ${CURL_SOURCES}
+
+  ${AUTOGENERATED_SOURCES}
+  )
+
+
+#####################################################################
+## Build the WSI DICOM-izer
+#####################################################################
+
+add_executable(OrthancWSIDicomizer
+  Dicomizer.cpp
+  ApplicationToolbox.cpp
+  )
+
+target_link_libraries(OrthancWSIDicomizer OrthancWSIFramework ${DCMTK_LIBRARIES})
+
+
+#####################################################################
+## Build the DICOM-to-TIFF conversion tool
+#####################################################################
+
+add_executable(OrthancWSIDicomToTiff
+  DicomToTiff.cpp
+  ApplicationToolbox.cpp
+  )
+
+target_link_libraries(OrthancWSIDicomToTiff OrthancWSIFramework ${DCMTK_LIBRARIES})
+
+
+#####################################################################
+## Generate the documentation if Doxygen is present
+#####################################################################
+
+find_package(Doxygen)
+if (DOXYGEN_FOUND)
+  configure_file(
+    ${ORTHANC_WSI_DIR}/Resources/OrthancWSI.doxygen
+    ${CMAKE_CURRENT_BINARY_DIR}/OrthancWSI.doxygen
+    @ONLY)
+
+  add_custom_target(doc
+    ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/OrthancWSI.doxygen
+    COMMENT "Generating documentation with Doxygen" VERBATIM
+    )
+else()
+  message("Doxygen not found. The documentation will not be built.")
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/DicomToTiff.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,321 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../Framework/DicomToolbox.h"
+#include "../Framework/ImageToolbox.h"
+#include "../Framework/Inputs/DicomPyramid.h"
+#include "../Framework/Messaging/CurlOrthancConnection.h"
+#include "../Framework/Orthanc/Core/Logging.h"
+#include "../Framework/Orthanc/Core/OrthancException.h"
+#include "../Framework/Outputs/HierarchicalTiffWriter.h"
+
+#include "ApplicationToolbox.h"
+
+#include <boost/program_options.hpp>
+
+
+static bool ParseParameters(int& exitStatus,
+                            boost::program_options::variables_map& options,
+                            int argc, 
+                            char* argv[])
+{
+  // Declare the supported parameters
+  boost::program_options::options_description generic("Generic options");
+  generic.add_options()
+    ("help", "Display this help and exit")
+    ("verbose", "Be verbose in logs")
+    ;
+
+  boost::program_options::options_description source("Options for the source DICOM image");
+  source.add_options()
+    ("orthanc", boost::program_options::value<std::string>()->default_value("http://localhost:8042/"),
+     "URL to the REST API of the target Orthanc server")
+    ("username", boost::program_options::value<std::string>(), "Username for the target Orthanc server")
+    ("password", boost::program_options::value<std::string>(), "Password for the target Orthanc server")
+    ;
+
+  boost::program_options::options_description target("Options for the target TIFF image");
+  target.add_options()
+    ("color", boost::program_options::value<std::string>(), "Color of the background for missing tiles (e.g. \"255,0,0\")")
+    ("reencode", boost::program_options::value<bool>(), 
+     "Whether to reencode each tile in JPEG (no transcoding, much slower) (Boolean)")
+    ("jpeg-quality", boost::program_options::value<int>(), "Set quality level for JPEG (0..100)")
+    ;
+
+  boost::program_options::options_description hidden;
+  hidden.add_options()
+    ("input", boost::program_options::value<std::string>(), "Orthanc identifier of the input series of interest")
+    ("output", boost::program_options::value<std::string>(), "Output TIFF file");
+  ;
+
+  boost::program_options::options_description allWithoutHidden;
+  allWithoutHidden.add(generic).add(source).add(target);
+
+  boost::program_options::options_description all = allWithoutHidden;
+  all.add(hidden);
+
+  boost::program_options::positional_options_description positional;
+  positional.add("input", 1);
+  positional.add("output", 1);
+
+  bool error = false;
+
+  try
+  {
+    boost::program_options::store(boost::program_options::command_line_parser(argc, argv).
+                                  options(all).positional(positional).run(), options);
+    boost::program_options::notify(options);    
+  }
+  catch (boost::program_options::error& e)
+  {
+    LOG(ERROR) << "Error while parsing the command-line arguments: " << e.what();
+    error = true;
+  }
+
+  if (!error &&
+      options.count("help") == 0)
+  {
+    if (options.count("input") != 1)
+    {
+      LOG(ERROR) << "No input series was specified";
+      error = true;
+    }
+
+    if (options.count("output") != 1)
+    {
+      LOG(ERROR) << "No output file was specified";
+      error = true;
+    }
+  }
+
+  if (error || options.count("help")) 
+  {
+    std::cout << std::endl
+              << "Usage: " << argv[0] << " [OPTION]... [INPUT] [OUTPUT]"
+              << std::endl
+              << "Orthanc, lightweight, RESTful DICOM server for healthcare and medical research."
+              << std::endl << std::endl
+              << "Convert a DICOM for digital pathology stored in some Orthanc server as a standard hierarchical TIFF."
+              << std::endl;
+
+    std::cout << allWithoutHidden << "\n";
+
+    if (error)
+    {
+      exitStatus = -1;
+    }
+
+    return false;
+  }
+
+  if (options.count("verbose"))
+  {
+    Orthanc::Logging::EnableInfoLevel(true);
+  }
+
+  return true;
+}
+
+
+
+static Orthanc::ImageAccessor* CreateEmptyTile(const OrthancWSI::IPyramidWriter& writer,
+                                               const boost::program_options::variables_map& options)
+{
+  std::auto_ptr<Orthanc::ImageAccessor> tile
+    (OrthancWSI::ImageToolbox::Allocate(writer.GetPixelFormat(), 
+                                        writer.GetTileWidth(), 
+                                        writer.GetTileHeight()));
+
+  uint8_t red = 255;
+  uint8_t green = 255;
+  uint8_t blue = 255;
+
+  if (options.count("color"))
+  {
+    OrthancWSI::ApplicationToolbox::ParseColor(red, green, blue, options["color"].as<std::string>());
+  }
+
+  OrthancWSI::ImageToolbox::Set(*tile, red, green, blue);
+
+  return tile.release();
+}
+
+
+
+static void RunTranscode(OrthancWSI::ITiledPyramid& source,
+                         const boost::program_options::variables_map& options)
+{
+  OrthancWSI::HierarchicalTiffWriter target(options["output"].as<std::string>(),
+                                            source.GetPixelFormat(), 
+                                            source.GetImageCompression(), 
+                                            source.GetTileWidth(), 
+                                            source.GetTileHeight());
+
+  std::auto_ptr<Orthanc::ImageAccessor> empty(CreateEmptyTile(target, options));
+
+  for (unsigned int level = 0; level < source.GetLevelCount(); level++)
+  {
+    LOG(WARNING) << "Creating level " << level << " of size " 
+                 << source.GetLevelWidth(level) << "x" << source.GetLevelHeight(level);
+    target.AddLevel(source.GetLevelWidth(level), source.GetLevelHeight(level));
+  }
+
+  for (unsigned int level = 0; level < source.GetLevelCount(); level++)
+  {
+    LOG(WARNING) << "Transcoding level " << level;
+
+    unsigned int countX = OrthancWSI::CeilingDivision(source.GetLevelWidth(level), source.GetTileWidth());
+    unsigned int countY = OrthancWSI::CeilingDivision(source.GetLevelHeight(level), source.GetTileHeight());
+
+    for (unsigned int tileY = 0; tileY < countY; tileY++)
+    {
+      for (unsigned int tileX = 0; tileX < countX; tileX++)
+      {
+        LOG(INFO) << "Dealing with tile (" << tileX << "," << tileY << ") at level " << level;
+        std::string tile;
+
+        if (source.ReadRawTile(tile, level, tileX, tileY))
+        {
+          target.WriteRawTile(tile, source.GetImageCompression(), level, tileX, tileY);
+        }
+        else
+        {
+          target.EncodeTile(*empty, level, tileX, tileY);
+        }
+      }        
+    }
+
+    target.Flush();
+  }
+}
+
+
+static void RunReencode(OrthancWSI::ITiledPyramid& source,
+                        const boost::program_options::variables_map& options)
+{
+  OrthancWSI::HierarchicalTiffWriter target(options["output"].as<std::string>(),
+                                            source.GetPixelFormat(), 
+                                            OrthancWSI::ImageCompression_Jpeg,
+                                            source.GetTileWidth(), 
+                                            source.GetTileHeight());
+
+  if (options.count("jpeg-quality"))
+  {
+    target.SetJpegQuality(options["jpeg-quality"].as<int>());
+  }
+
+  std::auto_ptr<Orthanc::ImageAccessor> empty(CreateEmptyTile(target, options));
+
+  for (unsigned int level = 0; level < source.GetLevelCount(); level++)
+  {
+    LOG(WARNING) << "Creating level " << level << " of size " 
+                 << source.GetLevelWidth(level) << "x" << source.GetLevelHeight(level);
+    target.AddLevel(source.GetLevelWidth(level), source.GetLevelHeight(level));
+  }
+
+  for (unsigned int level = 0; level < source.GetLevelCount(); level++)
+  {
+    LOG(WARNING) << "Reencoding level " << level;
+
+    unsigned int countX = OrthancWSI::CeilingDivision(source.GetLevelWidth(level), source.GetTileWidth());
+    unsigned int countY = OrthancWSI::CeilingDivision(source.GetLevelHeight(level), source.GetTileHeight());
+
+    for (unsigned int tileY = 0; tileY < countY; tileY++)
+    {
+      for (unsigned int tileX = 0; tileX < countX; tileX++)
+      {
+        LOG(INFO) << "Dealing with tile (" << tileX << "," << tileY << ") at level " << level;
+
+        std::auto_ptr<Orthanc::ImageAccessor> tile(source.DecodeTile(level, tileX, tileY));
+        if (tile.get() == NULL)
+        {
+          target.EncodeTile(*empty, level, tileX, tileY);
+        }
+        else
+        {
+          target.EncodeTile(*tile, level, tileX, tileY);
+        }
+      }        
+    }
+
+    target.Flush();
+  }
+}
+
+
+
+int main(int argc, char* argv[])
+{
+  OrthancWSI::ApplicationToolbox::GlobalInitialize();
+
+  int exitStatus = 0;
+  boost::program_options::variables_map options;
+
+  try
+  {
+    if (ParseParameters(exitStatus, options, argc, argv))
+    {
+      Orthanc::WebServiceParameters params;
+
+      if (options.count("orthanc"))
+      {
+        params.SetUrl(options["orthanc"].as<std::string>());
+      }
+
+      if (options.count("username"))
+      {
+        params.SetUsername(options["username"].as<std::string>());
+      }
+
+      if (options.count("password"))
+      {
+        params.SetPassword(options["password"].as<std::string>());
+      }
+
+      OrthancWSI::CurlOrthancConnection orthanc(params);
+      OrthancWSI::DicomPyramid source(orthanc, options["input"].as<std::string>());
+
+      if (options.count("reencode") &&
+          options["reencode"].as<bool>())
+      {
+        RunReencode(source, options);
+      }
+      else
+      {
+        RunTranscode(source, options);
+      }
+    }
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "Terminating on exception: " << e.What();
+
+    if (options.count("reencode") == 0)
+    {
+      LOG(ERROR) << "Consider using option \"--reencode\"";
+    }
+    
+    exitStatus = -1;
+  }
+
+  OrthancWSI::ApplicationToolbox::GlobalFinalize();
+
+  return exitStatus;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Dicomizer.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,877 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../Framework/Algorithms/ReconstructPyramidCommand.h"
+#include "../Framework/Algorithms/TranscodeTileCommand.h"
+#include "../Framework/DicomToolbox.h"
+#include "../Framework/DicomizerParameters.h"
+#include "../Framework/ImagedVolumeParameters.h"
+#include "../Framework/Inputs/HierarchicalTiff.h"
+#include "../Framework/Inputs/OpenSlidePyramid.h"
+#include "../Framework/Inputs/TiledJpegImage.h"
+#include "../Framework/Inputs/TiledPngImage.h"
+#include "../Framework/Inputs/TiledPyramidStatistics.h"
+#include "../Framework/Orthanc/Core/HttpClient.h"
+#include "../Framework/Orthanc/Core/Logging.h"
+#include "../Framework/Orthanc/Core/MultiThreading/BagOfTasksProcessor.h"
+#include "../Framework/Orthanc/Core/Toolbox.h"
+#include "../Framework/Orthanc/OrthancServer/FromDcmtkBridge.h"
+#include "../Framework/Outputs/DicomPyramidWriter.h"
+#include "../Framework/Outputs/TruncatedPyramidWriter.h"
+
+#include "ApplicationToolbox.h"
+
+#include <EmbeddedResources.h>
+
+#include <boost/program_options.hpp>
+
+#include <dcmtk/dcmdata/dcdeftag.h>
+#include <dcmtk/dcmdata/dcuid.h>
+#include <dcmtk/dcmdata/dcvrobow.h>
+#include <dcmtk/dcmdata/dcvrat.h>
+
+
+static void TranscodePyramid(OrthancWSI::PyramidWriterBase& target,
+                             OrthancWSI::ITiledPyramid& source,
+                             const OrthancWSI::DicomizerParameters& parameters)
+{
+  Orthanc::BagOfTasks tasks;
+
+  for (unsigned int i = 0; i < source.GetLevelCount(); i++)
+  {
+    LOG(WARNING) << "Creating level " << i << " of size " 
+                 << source.GetLevelWidth(i) << "x" << source.GetLevelHeight(i);
+    target.AddLevel(source.GetLevelWidth(i), source.GetLevelHeight(i));
+  }
+
+  LOG(WARNING) << "Transcoding the source pyramid";
+
+  OrthancWSI::TranscodeTileCommand::PrepareBagOfTasks(tasks, target, source, parameters);
+  OrthancWSI::ApplicationToolbox::Execute(tasks, parameters.GetThreadsCount());
+}
+
+
+static void ReconstructPyramid(OrthancWSI::PyramidWriterBase& target,
+                               OrthancWSI::ITiledPyramid& source,
+                               const OrthancWSI::DicomizerParameters& parameters)
+{
+  Orthanc::BagOfTasks tasks;
+
+  unsigned int levelsCount = parameters.GetPyramidLevelsCount(target, source);
+  LOG(WARNING) << "The target pyramid will have " << levelsCount << " levels";
+  assert(levelsCount >= 1);
+
+  for (unsigned int i = 0; i < levelsCount; i++)
+  {
+    unsigned int width = OrthancWSI::CeilingDivision(source.GetLevelWidth(0), 1 << i);
+    unsigned int height = OrthancWSI::CeilingDivision(source.GetLevelHeight(0), 1 << i);
+
+    LOG(WARNING) << "Creating level " << i << " of size " << width << "x" << height;
+    target.AddLevel(width, height);
+  }
+
+  unsigned int lowerLevelsCount = parameters.GetPyramidLowerLevelsCount(target, source);
+  if (lowerLevelsCount > levelsCount)
+  {
+    LOG(WARNING) << "The number of lower levels (" << lowerLevelsCount
+                 << ") exceeds the number of levels (" << levelsCount
+                 << "), cropping it";
+    lowerLevelsCount = levelsCount;
+  }
+
+  assert(lowerLevelsCount <= levelsCount);
+  if (lowerLevelsCount != levelsCount)
+  {
+    LOG(WARNING) << "Constructing the " << lowerLevelsCount << " lower levels of the pyramid";
+    OrthancWSI::TruncatedPyramidWriter truncated(target, lowerLevelsCount);
+    OrthancWSI::ReconstructPyramidCommand::PrepareBagOfTasks
+      (tasks, truncated, source, lowerLevelsCount + 1, 0, parameters);
+    OrthancWSI::ApplicationToolbox::Execute(tasks, parameters.GetThreadsCount());
+
+    assert(tasks.GetSize() == 0);
+
+    const unsigned int upperLevelsCount = levelsCount - lowerLevelsCount;
+    LOG(WARNING) << "Constructing the " << upperLevelsCount << " upper levels of the pyramid";
+    OrthancWSI::ReconstructPyramidCommand::PrepareBagOfTasks
+      (tasks, target, truncated.GetUpperLevel(), 
+       upperLevelsCount, lowerLevelsCount, parameters); 
+    OrthancWSI::ApplicationToolbox::Execute(tasks, parameters.GetThreadsCount());
+  }
+  else
+  {
+    LOG(WARNING) << "Constructing the pyramid";
+    OrthancWSI::ReconstructPyramidCommand::PrepareBagOfTasks
+      (tasks, target, source, levelsCount, 0, parameters);
+    OrthancWSI::ApplicationToolbox::Execute(tasks, parameters.GetThreadsCount());
+  }
+}
+
+
+static void Recompress(OrthancWSI::IFileTarget& output,
+                       OrthancWSI::ITiledPyramid& source,
+                       const DcmDataset& dataset,
+                       const OrthancWSI::DicomizerParameters& parameters,
+                       const OrthancWSI::ImagedVolumeParameters& volume)
+{
+  OrthancWSI::TiledPyramidStatistics stats(source);
+
+  LOG(WARNING) << "Size of source tiles: " << stats.GetTileWidth() << "x" << stats.GetTileHeight();
+  LOG(WARNING) << "Source image compression: " << OrthancWSI::EnumerationToString(stats.GetImageCompression());
+  LOG(WARNING) << "Pixel format: " << Orthanc::EnumerationToString(stats.GetPixelFormat());
+  LOG(WARNING) << "Smoothing is " << (parameters.IsSmoothEnabled() ? "enabled" : "disabled");
+
+  if (parameters.IsRepaintBackground())
+  {
+    LOG(WARNING) << "Repainting the background with color: (" 
+                 << static_cast<int>(parameters.GetBackgroundColorRed()) << ","
+                 << static_cast<int>(parameters.GetBackgroundColorGreen()) << ","
+                 << static_cast<int>(parameters.GetBackgroundColorBlue()) << ")";
+  }
+  else
+  {
+    LOG(WARNING) << "No repainting of the background";
+  }
+
+  OrthancWSI::DicomPyramidWriter target(output, dataset, 
+                                        source.GetPixelFormat(), 
+                                        parameters.GetTargetCompression(),
+                                        parameters.GetTargetTileWidth(source),
+                                        parameters.GetTargetTileHeight(source),
+                                        parameters.GetDicomMaxFileSize(),
+                                        volume);
+
+  LOG(WARNING) << "Size of target tiles: " << target.GetTileWidth() << "x" << target.GetTileHeight();
+
+  if (target.GetImageCompression() == OrthancWSI::ImageCompression_Jpeg)
+  {
+    LOG(WARNING) << "Target image compression: Jpeg with quality " << static_cast<int>(target.GetJpegQuality());
+    target.SetJpegQuality(target.GetJpegQuality());
+  }
+  else
+  {
+    LOG(WARNING) << "Target image compression: " << OrthancWSI::EnumerationToString(target.GetImageCompression());
+  }
+
+  if (stats.GetTileWidth() % target.GetTileWidth() != 0 ||
+      stats.GetTileHeight() % target.GetTileHeight() != 0)
+  {
+    LOG(ERROR) << "When resampling the tile size, it must be a integer divisor of the original tile size";
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize);
+  }
+
+  if (target.GetTileWidth() <= 16 ||
+      target.GetTileHeight() <= 16)
+  {
+    LOG(ERROR) << "Tiles are too small (16 pixels minimum): "
+               << target.GetTileWidth() << "x" << target.GetTileHeight();
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize);
+  }
+
+  if (parameters.IsReconstructPyramid())
+  {
+    ReconstructPyramid(target, stats, parameters);
+  }
+  else
+  {
+    TranscodePyramid(target, stats, parameters);
+  }
+
+  target.Flush();
+}
+
+
+
+static DcmDataset* ParseDataset(const std::string& path)
+{
+  Json::Value json;
+
+  if (path.empty())
+  {
+    json = Json::objectValue;   // Empty dataset => TODO EMBED
+  }
+  else
+  {
+    std::string content;
+    Orthanc::Toolbox::ReadFile(content, path);
+
+    Json::Reader reader;
+    if (!reader.parse(content, json, false))
+    {
+      LOG(ERROR) << "Cannot parse the JSON file in: " << path;
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+  }
+
+  std::auto_ptr<DcmDataset> dataset(Orthanc::FromDcmtkBridge::FromJson(json, true, true, Orthanc::Encoding_Latin1));
+  if (dataset.get() == NULL)
+  {
+    LOG(ERROR) << "Cannot convert to JSON file to a DICOM dataset: " << path;
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+  }
+
+  // VL Whole Slide Microscopy Image IOD
+  OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_SOPClassUID, "1.2.840.10008.5.1.4.1.1.77.1.6");
+
+  // Slide Microscopy
+  OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_Modality, "SM");  
+
+  // Patient orientation makes no sense in whole-slide images
+  OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_PatientOrientation, "");  
+
+  // Some basic coordinate information
+  OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_VolumetricProperties, "VOLUME");
+  OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_ImageOrientationSlide, "0\\-1\\0\\-1\\0\\0");
+
+  std::string date, time;
+  Orthanc::Toolbox::GetNowDicom(date, time);
+  OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_StudyDate, date);
+  OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_StudyTime, time);
+  OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_SeriesDate, date);
+  OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_SeriesTime, time);
+  OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_ContentDate, date);
+  OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_ContentTime, time);
+  OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_AcquisitionDateTime, date + time);
+
+  return dataset.release();
+}
+
+
+
+static void SetupDimension(DcmDataset& dataset,
+                           const std::string& opticalPathId,
+                           const OrthancWSI::ITiledPyramid& source,
+                           const OrthancWSI::ImagedVolumeParameters& volume)
+{
+  std::string uid;
+  DcmItem* previous = OrthancWSI::DicomToolbox::ExtractSingleSequenceItem(dataset, DCM_DimensionOrganizationSequence);
+
+  if (previous != NULL)
+  {
+    const char* tmp = NULL;
+    if (previous->findAndGetString(DCM_DimensionOrganizationUID, tmp).good() &&
+        tmp != NULL)
+    {
+      uid.assign(tmp);
+    }
+  }
+
+  if (uid.empty())
+  {
+    // Generate an unique identifier for the Dimension Organization
+    uid = Orthanc::FromDcmtkBridge::GenerateUniqueIdentifier(Orthanc::ResourceType_Instance);
+  }
+
+  dataset.remove(DCM_DimensionIndexSequence);
+
+  std::auto_ptr<DcmItem> item(new DcmItem);
+  std::auto_ptr<DcmSequenceOfItems> sequence(new DcmSequenceOfItems(DCM_DimensionOrganizationSequence));
+
+  if (!item->putAndInsertString(DCM_DimensionOrganizationUID, uid.c_str()).good() ||
+      !sequence->insert(item.release(), false, false).good() ||
+      !dataset.insert(sequence.release(), true, false).good())
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+  }
+
+  item.reset(new DcmItem);
+  sequence.reset(new DcmSequenceOfItems(DCM_DimensionIndexSequence));
+
+  std::auto_ptr<DcmAttributeTag> a1(new DcmAttributeTag(DCM_FunctionalGroupPointer));
+  std::auto_ptr<DcmAttributeTag> a2(new DcmAttributeTag(DCM_DimensionIndexPointer));
+
+  if (!item->putAndInsertString(DCM_DimensionOrganizationUID, uid.c_str()).good() ||
+      !a1->putTagVal(DCM_FrameContentSequence).good() ||
+      !a2->putTagVal(DCM_DimensionIndexValues).good() ||
+      !item->insert(a1.release(), uid.c_str()).good() ||
+      !item->insert(a2.release(), uid.c_str()).good() ||
+      !sequence->insert(item.release(), false, false).good() ||
+      !dataset.insert(sequence.release(), true, false).good())
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+  }
+
+  float spacingX = volume.GetWidth() / static_cast<float>(source.GetLevelHeight(0));  // Remember to switch X/Y!
+  float spacingY = volume.GetHeight() / static_cast<float>(source.GetLevelWidth(0));  // Remember to switch X/Y!
+  std::string spacing = (boost::lexical_cast<std::string>(spacingX) + '\\' +
+                         boost::lexical_cast<std::string>(spacingY));
+
+  item.reset(new DcmItem);
+  sequence.reset(new DcmSequenceOfItems(DCM_SharedFunctionalGroupsSequence));
+  std::auto_ptr<DcmItem> item2(new DcmItem);
+  std::auto_ptr<DcmItem> item3(new DcmItem);
+  std::auto_ptr<DcmSequenceOfItems> sequence2(new DcmSequenceOfItems(DCM_PixelMeasuresSequence));
+  std::auto_ptr<DcmSequenceOfItems> sequence3(new DcmSequenceOfItems(DCM_OpticalPathIdentificationSequence));
+
+  OrthancWSI::DicomToolbox::SetStringTag(*item2, DCM_SliceThickness, boost::lexical_cast<std::string>(volume.GetDepth()));
+  OrthancWSI::DicomToolbox::SetStringTag(*item2, DCM_PixelSpacing, spacing);
+  OrthancWSI::DicomToolbox::SetStringTag(*item3, DCM_OpticalPathIdentifier, opticalPathId);
+
+  if (!sequence2->insert(item2.release(), false, false).good() ||
+      !sequence3->insert(item3.release(), false, false).good() ||
+      !item->insert(sequence2.release(), false, false).good() ||
+      !item->insert(sequence3.release(), false, false).good() ||
+      !sequence->insert(item.release(), false, false).good() ||
+      !dataset.insert(sequence.release(), true, false).good())
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+  }
+}
+
+
+static void EnrichDataset(DcmDataset& dataset,
+                          const OrthancWSI::ITiledPyramid& source,
+                          const OrthancWSI::DicomizerParameters& parameters,
+                          const OrthancWSI::ImagedVolumeParameters& volume)
+{
+  Orthanc::Encoding encoding = Orthanc::FromDcmtkBridge::DetectEncoding(dataset, Orthanc::Encoding_Latin1);
+
+  if (source.GetImageCompression() == OrthancWSI::ImageCompression_Jpeg ||
+      parameters.GetTargetCompression() == OrthancWSI::ImageCompression_Jpeg)
+  {
+    // Takes as estimation a 1:10 compression ratio
+    OrthancWSI::DicomToolbox::SetStringTag(dataset, DCM_LossyImageCompression, "01");
+    OrthancWSI::DicomToolbox::SetStringTag(dataset, DCM_LossyImageCompressionRatio, "10");
+    OrthancWSI::DicomToolbox::SetStringTag(dataset, DCM_LossyImageCompressionMethod, "ISO_10918_1"); // JPEG Lossy Compression
+  }
+  else
+  {
+    OrthancWSI::DicomToolbox::SetStringTag(dataset, DCM_LossyImageCompression, "00");
+  }
+
+  OrthancWSI::DicomToolbox::SetStringTag(dataset, DCM_ImagedVolumeWidth, boost::lexical_cast<std::string>(volume.GetWidth()));
+  OrthancWSI::DicomToolbox::SetStringTag(dataset, DCM_ImagedVolumeHeight, boost::lexical_cast<std::string>(volume.GetHeight()));
+  OrthancWSI::DicomToolbox::SetStringTag(dataset, DCM_ImagedVolumeDepth, boost::lexical_cast<std::string>(volume.GetDepth()));
+
+  std::auto_ptr<DcmItem> origin(new DcmItem);
+  OrthancWSI::DicomToolbox::SetStringTag(*origin, DCM_XOffsetInSlideCoordinateSystem, 
+                                         boost::lexical_cast<std::string>(volume.GetOffsetX()));
+  OrthancWSI::DicomToolbox::SetStringTag(*origin, DCM_YOffsetInSlideCoordinateSystem, 
+                                         boost::lexical_cast<std::string>(volume.GetOffsetY()));
+
+  std::auto_ptr<DcmSequenceOfItems> sequenceOrigin(new DcmSequenceOfItems(DCM_TotalPixelMatrixOriginSequence));
+  if (!sequenceOrigin->insert(origin.release(), false, false).good() ||
+      !dataset.insert(sequenceOrigin.release(), false, false).good())
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+  }
+
+
+  if (parameters.GetOpticalPath() == OrthancWSI::OpticalPath_Brightfield)
+  {
+    if (dataset.tagExists(DCM_OpticalPathSequence))
+    {
+      LOG(ERROR) << "The user DICOM dataset already contains an optical path sequence, giving up";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);      
+    }
+
+    std::string brightfield;
+    Orthanc::EmbeddedResources::GetFileResource(brightfield, Orthanc::EmbeddedResources::BRIGHTFIELD_OPTICAL_PATH);
+
+    Json::Value json;
+    Json::Reader reader;
+    if (!reader.parse(brightfield, json, false))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    std::auto_ptr<DcmElement> element(Orthanc::FromDcmtkBridge::FromJson(
+                                        Orthanc::DicomTag(DCM_OpticalPathSequence.getGroup(),
+                                                          DCM_OpticalPathSequence.getElement()),
+                                        json, false, encoding));
+    if (!dataset.insert(element.release()).good())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  }
+
+
+  std::string profile;
+  if (parameters.GetIccProfilePath().empty())
+  {
+    Orthanc::EmbeddedResources::GetFileResource(profile, Orthanc::EmbeddedResources::SRGB_ICC_PROFILE);
+  }
+  else
+  {
+    Orthanc::Toolbox::ReadFile(profile, parameters.GetIccProfilePath());
+  }
+
+  
+  DcmItem* opticalPath = OrthancWSI::DicomToolbox::ExtractSingleSequenceItem(dataset, DCM_OpticalPathSequence);
+  if (opticalPath == NULL)
+  {
+    LOG(ERROR) << "No optical path specified";
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+  }
+
+  if (!opticalPath->tagExists(DCM_ICCProfile))
+  {
+    std::auto_ptr<DcmOtherByteOtherWord> icc(new DcmOtherByteOtherWord(DCM_ICCProfile));
+
+    if (!icc->putUint8Array(reinterpret_cast<const Uint8*>(profile.c_str()), profile.size()).good() ||
+        !opticalPath->insert(icc.release()).good())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  }
+
+  const char* opticalPathId = NULL;
+  if (!opticalPath->findAndGetString(DCM_OpticalPathIdentifier, opticalPathId).good() ||
+      opticalPathId == NULL)
+  {
+    LOG(ERROR) << "No identifier in the optical path";
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);    
+  }
+
+  SetupDimension(dataset, opticalPathId, source, volume);
+}
+
+
+static bool ParseParameters(int& exitStatus,
+                            OrthancWSI::DicomizerParameters& parameters,
+                            OrthancWSI::ImagedVolumeParameters& volume,
+                            int argc, 
+                            char* argv[])
+{
+  // Declare the supported parameters
+  boost::program_options::options_description generic("Generic options");
+  generic.add_options()
+    ("help", "Display this help and exit")
+    ("verbose", "Be verbose in logs")
+    ("threads", boost::program_options::value<int>()->default_value(parameters.GetThreadsCount()), 
+     "Number of processing threads to be used")
+    ("openslide", boost::program_options::value<std::string>(), 
+     "Path to the shared library of OpenSlide (not necessary if converting from standard hierarchical TIFF)")
+    ;
+
+  boost::program_options::options_description source("Options for the source image");
+  source.add_options()
+    ("dataset", boost::program_options::value<std::string>(), "Path to a JSON file containing the DICOM dataset")
+    ("sample-dataset", "Display a minimalistic sample DICOM dataset in JSON format, then exit")
+    ("reencode", boost::program_options::value<bool>(), "Whether to reencode each tile (no transcoding, much slower) (Boolean)")
+    ("repaint", boost::program_options::value<bool>(), "Whether to repaint the background of the image (Boolean)")
+    ("color", boost::program_options::value<std::string>(), "Color of the background (e.g. \"255,0,0\")")
+    ;
+
+  boost::program_options::options_description pyramid("Options to construct the pyramid");
+  pyramid.add_options()
+    ("pyramid", boost::program_options::value<bool>()->default_value(false), 
+     "Reconstruct the full pyramid (slow) (Boolean)")
+    ("smooth", boost::program_options::value<bool>()->default_value(false), 
+     "Apply smoothing when reconstructing the pyramid "
+     "(slower, but higher quality) (Boolean)")
+    ("levels", boost::program_options::value<int>(), "Number of levels in the target pyramid")
+    ;
+
+  boost::program_options::options_description target("Options for the target image");
+  target.add_options()
+    ("tile-width", boost::program_options::value<int>(), "Width of the tiles in the target image")
+    ("tile-height", boost::program_options::value<int>(), "Height of the tiles in the target image")
+    ("compression", boost::program_options::value<std::string>(), 
+     "Compression of the target image (\"none\", \"jpeg\" or \"jpeg2000\")")
+    ("jpeg-quality", boost::program_options::value<int>(), "Set quality level for JPEG (0..100)")
+    ("max-size", boost::program_options::value<int>()->default_value(10), "Maximum size per DICOM instance (in MB)")
+    ("folder", boost::program_options::value<std::string>(),
+     "Folder where to store the output DICOM instances")
+    ("folder-pattern", boost::program_options::value<std::string>()->default_value("wsi-%06d.dcm"),
+     "Pattern for the files in the output folder")
+    ("orthanc", boost::program_options::value<std::string>()->default_value("http://localhost:8042/"),
+     "URL to the REST API of the target Orthanc server")
+    ("username", boost::program_options::value<std::string>(), "Username for the target Orthanc server")
+    ("password", boost::program_options::value<std::string>(), "Password for the target Orthanc server")
+    ;
+
+  boost::program_options::options_description volumeOptions("Description of the imaged volume");
+  volumeOptions.add_options()
+    ("imaged-width", boost::program_options::value<float>()->default_value(15), "With of the specimen (in mm)")
+    ("imaged-height", boost::program_options::value<float>()->default_value(15), "Height of the specimen (in mm)")
+    ("imaged-depth", boost::program_options::value<float>()->default_value(1), "Depth of the specimen (in mm)")
+    ("offset-x", boost::program_options::value<float>()->default_value(20), 
+     "X offset the specimen, wrt. slide coordinates origin (in mm)")
+    ("offset-y", boost::program_options::value<float>()->default_value(40), 
+     "Y offset the specimen, wrt. slide coordinates origin (in mm)")
+    ;
+
+  boost::program_options::options_description advancedOptions("Advanced options");
+  advancedOptions.add_options()
+    ("optical-path", boost::program_options::value<std::string>()->default_value("brightfield"), 
+     "Optical path to be automatically added to the DICOM dataset (\"none\" or \"brightfield\")")
+    ("icc-profile", boost::program_options::value<std::string>(), 
+     "Path to the ICC profile to be included. If empty, a default sRGB profile will be added.")
+    ("safety", boost::program_options::value<bool>()->default_value(true), 
+     "Whether to do additional checks to verify the source image is supported (might slow down) (Boolean)")
+    ("lower-levels", boost::program_options::value<int>(), "Number of pyramid levels up to which multithreading "
+     "should be applied (only for performance/memory tuning)")
+    ;
+
+  boost::program_options::options_description hidden;
+  hidden.add_options()
+    ("input", boost::program_options::value<std::string>(), "Input file");
+  ;
+
+  boost::program_options::options_description allWithoutHidden;
+  allWithoutHidden.add(generic).add(source).add(pyramid).add(target).add(volumeOptions).add(advancedOptions);
+
+  boost::program_options::options_description all = allWithoutHidden;
+  all.add(hidden);
+
+  boost::program_options::positional_options_description positional;
+  positional.add("input", 1);
+
+  boost::program_options::variables_map options;
+  bool error = false;
+
+  try
+  {
+    boost::program_options::store(boost::program_options::command_line_parser(argc, argv).
+                                  options(all).positional(positional).run(), options);
+    boost::program_options::notify(options);    
+  }
+  catch (boost::program_options::error& e)
+  {
+    LOG(ERROR) << "Error while parsing the command-line arguments: " << e.what();
+    error = true;
+  }
+
+  if (!error &&
+      options.count("sample-dataset"))
+  {
+    std::string sample;
+    Orthanc::EmbeddedResources::GetFileResource(sample, Orthanc::EmbeddedResources::SAMPLE_DATASET);
+
+    std::cout << std::endl << sample << std::endl;
+
+    return false;
+  }
+
+  if (!error &&
+      options.count("help") == 0 &&
+      options.count("input") != 1)
+  {
+    LOG(ERROR) << "No input file was specified";
+    error = true;
+  }
+
+  if (error || options.count("help")) 
+  {
+    std::cout << std::endl
+              << "Usage: " << argv[0] << " [OPTION]... [INPUT]"
+              << std::endl
+              << "Orthanc, lightweight, RESTful DICOM server for healthcare and medical research."
+              << std::endl << std::endl
+              << "Create a DICOM file from a digital pathology image."
+              << std::endl;
+
+    std::cout << allWithoutHidden << "\n";
+
+    if (error)
+    {
+      exitStatus = -1;
+    }
+
+    return false;
+  }
+
+  if (options.count("verbose"))
+  {
+    Orthanc::Logging::EnableInfoLevel(true);
+  }
+
+  if (options.count("openslide"))
+  {
+    OrthancWSI::OpenSlideLibrary::Initialize(options["openslide"].as<std::string>());
+  }
+
+  if (options.count("pyramid") &&
+      options["pyramid"].as<bool>())
+  {
+    parameters.SetReconstructPyramid(true);
+  }
+
+  if (options.count("smooth") &&
+      options["smooth"].as<bool>())
+  {
+    parameters.SetSmoothEnabled(true);
+  }
+
+  if (options.count("safety") &&
+      options["safety"].as<bool>())
+  {
+    parameters.SetSafetyCheck(true);
+  }
+
+  if (options.count("reencode") &&
+      options["reencode"].as<bool>())
+  {
+    parameters.SetForceReencode(true);
+  }
+
+  if (options.count("repaint") &&
+      options["repaint"].as<bool>())
+  {
+    parameters.SetRepaintBackground(true);
+  }
+
+  if (options.count("tile-width") ||
+      options.count("tile-height"))
+  {
+    int w = 0;
+    if (options.count("tile-width"))
+    {
+      w = options["tile-width"].as<int>();
+    }
+
+    unsigned int h = 0;
+    if (options.count("tile-height"))
+    {
+      h = options["tile-height"].as<int>();
+    }
+
+    if (w < 0 || h < 0)
+    {
+      LOG(ERROR) << "Negative target tile size specified: " << w << "x" << h;
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    parameters.SetTargetTileSize(w, h);
+  }
+
+  parameters.SetInputFile(options["input"].as<std::string>());
+
+  if (options.count("color"))
+  {
+    uint8_t r, g, b;
+    OrthancWSI::ApplicationToolbox::ParseColor(r, g, b, options["color"].as<std::string>());
+    parameters.SetBackgroundColor(r, g, b);
+  }
+
+  if (options.count("compression"))
+  {
+    std::string s = options["compression"].as<std::string>();
+    if (s == "none")
+    {
+      parameters.SetTargetCompression(OrthancWSI::ImageCompression_None);
+    }
+    else if (s == "jpeg")
+    {
+      parameters.SetTargetCompression(OrthancWSI::ImageCompression_Jpeg);
+    }
+    else if (s == "jpeg2000")
+    {
+      parameters.SetTargetCompression(OrthancWSI::ImageCompression_Jpeg2000);
+    }
+    else
+    {
+      LOG(ERROR) << "Unknown image compression for the target image: " << s;
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  if (options.count("jpeg-quality"))
+  {
+    parameters.SetJpegQuality(options["jpeg-quality"].as<int>());
+  }
+
+  if (options.count("levels"))
+  {
+    parameters.SetPyramidLevelsCount(options["levels"].as<int>());
+  }
+
+  if (options.count("lower-levels"))
+  {
+    parameters.SetPyramidLowerLevelsCount(options["lower-levels"].as<int>());
+  }
+
+  if (options.count("threads"))
+  {
+    parameters.SetThreadsCount(options["threads"].as<int>());
+  }
+
+  if (options.count("max-size"))
+  {
+    parameters.SetDicomMaxFileSize(options["max-size"].as<int>() * 1024 * 1024);
+  }
+
+  if (options.count("folder"))
+  {
+    parameters.SetTargetFolder(options["folder"].as<std::string>());
+  }
+
+  if (options.count("folder-pattern"))
+  {
+    parameters.SetTargetFolderPattern(options["folder-pattern"].as<std::string>());
+  }
+
+  if (options.count("orthanc"))
+  {
+    parameters.GetOrthancParameters().SetUrl(options["orthanc"].as<std::string>());
+
+    if (options.count("username") &&
+        options.count("password"))
+    {
+      parameters.GetOrthancParameters().SetUsername(options["username"].as<std::string>());
+      parameters.GetOrthancParameters().SetPassword(options["password"].as<std::string>());
+    }
+  }
+
+  if (options.count("dataset"))
+  {
+    parameters.SetDatasetPath(options["dataset"].as<std::string>());
+  }
+
+  if (options.count("imaged-width"))
+  {
+    volume.SetWidth(options["imaged-width"].as<float>());
+  }
+
+  if (options.count("imaged-height"))
+  {
+    volume.SetHeight(options["imaged-height"].as<float>());
+  }
+
+  if (options.count("imaged-depth"))
+  {
+    volume.SetDepth(options["imaged-depth"].as<float>());
+  }
+
+  if (options.count("offset-x"))
+  {
+    volume.SetOffsetX(options["offset-x"].as<float>());
+  }
+
+  if (options.count("offset-y"))
+  {
+    volume.SetOffsetY(options["offset-y"].as<float>());
+  }
+
+  if (options.count("optical-path"))
+  {
+    std::string s = options["optical-path"].as<std::string>();
+    if (s == "none")
+    {
+      parameters.SetOpticalPath(OrthancWSI::OpticalPath_None);
+    }
+    else if (s == "brightfield")
+    {
+      parameters.SetOpticalPath(OrthancWSI::OpticalPath_Brightfield);
+    }
+    else
+    {
+      LOG(ERROR) << "Unknown optical path definition: " << s;
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  if (options.count("icc-profile"))
+  {
+    parameters.SetIccProfilePath(options["icc-profile"].as<std::string>());
+  }
+
+  return true;
+}
+
+
+OrthancWSI::ITiledPyramid* OpenInputPyramid(const std::string& path,
+                                            const OrthancWSI::DicomizerParameters& parameters)
+{
+  LOG(WARNING) << "The input image is: " << path;
+
+  OrthancWSI::ImageCompression format = OrthancWSI::DetectFormatFromFile(path);
+  LOG(WARNING) << "File format of the input image: " << EnumerationToString(format);
+
+  switch (format)
+  {
+    case OrthancWSI::ImageCompression_Png:
+      return new OrthancWSI::TiledPngImage(path, 
+                                           parameters.GetTargetTileWidth(512), 
+                                           parameters.GetTargetTileHeight(512));
+
+    case OrthancWSI::ImageCompression_Jpeg:
+      return new OrthancWSI::TiledJpegImage(path, 
+                                            parameters.GetTargetTileWidth(512), 
+                                            parameters.GetTargetTileHeight(512));
+
+    case OrthancWSI::ImageCompression_Tiff:
+    {
+      try
+      {
+        return new OrthancWSI::HierarchicalTiff(path);
+      }
+      catch (Orthanc::OrthancException&)
+      {
+        LOG(WARNING) << "This is not a standard hierarchical TIFF file";
+      }
+    }
+
+    default:
+      break;
+  }
+
+  try
+  {
+    LOG(WARNING) << "Trying to open the input pyramid with OpenSlide";
+    return new OrthancWSI::OpenSlidePyramid(path, 
+                                            parameters.GetTargetTileWidth(512), 
+                                            parameters.GetTargetTileHeight(512));
+  }
+  catch (Orthanc::OrthancException&)
+  {
+    LOG(ERROR) << "This file is not supported by OpenSlide";
+    return NULL;
+  }
+}
+
+
+int main(int argc, char* argv[])
+{
+  OrthancWSI::ApplicationToolbox::GlobalInitialize();
+
+  int exitStatus = 0;
+
+  try
+  {
+    OrthancWSI::DicomizerParameters  parameters;
+    OrthancWSI::ImagedVolumeParameters volume;
+
+    if (ParseParameters(exitStatus, parameters, volume, argc, argv))
+    {
+      std::auto_ptr<OrthancWSI::ITiledPyramid> source(OpenInputPyramid(parameters.GetInputFile(), parameters));
+      if (source.get() == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+      
+      // Create the shared DICOM tags
+      std::auto_ptr<DcmDataset> dataset(ParseDataset(parameters.GetDatasetPath()));
+      EnrichDataset(*dataset, *source, parameters, volume);
+
+      std::auto_ptr<OrthancWSI::IFileTarget> output(parameters.CreateTarget());
+      Recompress(*output, *source, *dataset, parameters, volume);
+    }
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "Terminating on exception: " << e.What();
+    exitStatus = -1;
+  }
+
+  OrthancWSI::ApplicationToolbox::GlobalFinalize();
+
+  return exitStatus;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/COPYING	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,661 @@
+                    GNU AFFERO GENERAL PUBLIC LICENSE
+                       Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+  A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate.  Many developers of free software are heartened and
+encouraged by the resulting cooperation.  However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+  The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community.  It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server.  Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+  An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals.  This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU Affero General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Remote Network Interaction; Use with the GNU General Public License.
+
+  Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software.  This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time.  Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source.  For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code.  There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<http://www.gnu.org/licenses/>.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Algorithms/PyramidReader.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,305 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "PyramidReader.h"
+
+#include "../ImageToolbox.h"
+#include "../Orthanc/Core/Logging.h"
+#include "../Orthanc/Core/OrthancException.h"
+
+#include <cassert>
+
+namespace OrthancWSI
+{
+  class PyramidReader::SourceTile : public boost::noncopyable
+  {
+  private: 
+    PyramidReader&  that_;
+    unsigned int    tileX_;
+    unsigned int    tileY_;
+    bool            hasRawTile_;
+    std::string     rawTile_;
+
+    std::auto_ptr<Orthanc::ImageAccessor>  decoded_;
+
+    bool IsRepaintNeeded() const
+    {
+      return (that_.parameters_.IsRepaintBackground() &&
+              ((tileX_ + 1) * that_.sourceTileWidth_ > that_.levelWidth_ ||
+               (tileY_ + 1) * that_.sourceTileHeight_ > that_.levelHeight_));
+    }
+
+    void RepaintBackground()
+    {
+      assert(decoded_.get() != NULL);
+      LOG(INFO) << "Repainting background of tile ("
+                << tileX_ << "," << tileY_ << ") at level " << that_.level_;
+
+      if ((tileY_ + 1) * that_.sourceTileHeight_ > that_.levelHeight_)
+      {
+        // Bottom overflow
+        assert(tileY_ * that_.sourceTileHeight_ < that_.levelHeight_);
+
+        unsigned int bottom = that_.levelHeight_ - tileY_ * that_.sourceTileHeight_;
+        Orthanc::ImageAccessor a = decoded_->GetRegion(0, bottom, 
+                                                       that_.sourceTileWidth_, 
+                                                       that_.sourceTileHeight_ - bottom);
+        ImageToolbox::Set(a, 
+                          that_.parameters_.GetBackgroundColorRed(),
+                          that_.parameters_.GetBackgroundColorGreen(),
+                          that_.parameters_.GetBackgroundColorBlue());
+
+      }
+
+      if ((tileX_ + 1) * that_.sourceTileWidth_ > that_.levelWidth_)
+      {
+        // Right overflow
+        assert(tileX_ * that_.sourceTileWidth_ < that_.levelWidth_);
+
+        unsigned int right = that_.levelWidth_ - tileX_ * that_.sourceTileWidth_;
+        Orthanc::ImageAccessor a = decoded_->GetRegion(right, 0, 
+                                                       that_.sourceTileWidth_ - right, 
+                                                       that_.sourceTileHeight_);
+        ImageToolbox::Set(a,
+                          that_.parameters_.GetBackgroundColorRed(),
+                          that_.parameters_.GetBackgroundColorGreen(),
+                          that_.parameters_.GetBackgroundColorBlue());
+      }
+    }
+
+
+  public:
+    SourceTile(PyramidReader& that,
+               unsigned int tileX,
+               unsigned int tileY) :
+      that_(that),
+      tileX_(tileX),
+      tileY_(tileY)
+    {
+      if (!that_.parameters_.IsForceReencode() &&
+          !IsRepaintNeeded() &&
+          that_.source_.ReadRawTile(rawTile_, that_.level_, tileX, tileY))
+      {
+        hasRawTile_ = true;
+      }
+      else
+      {
+        hasRawTile_ = false;
+        decoded_.reset(that_.source_.DecodeTile(that_.level_, tileX, tileY));
+        if (decoded_.get() == NULL)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+
+        RepaintBackground();
+      }
+    }
+
+    bool HasRawTile() const
+    {
+      return hasRawTile_;
+    }
+
+    const std::string& GetRawTile() const
+    {
+      if (hasRawTile_)
+      {
+        return rawTile_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+    }
+
+    const Orthanc::ImageAccessor& GetDecodedTile()
+    {
+      if (decoded_.get() == NULL)
+      {
+        if (!hasRawTile_)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+
+        decoded_.reset(ImageToolbox::DecodeTile(rawTile_, that_.source_.GetImageCompression()));
+        if (decoded_.get() == NULL)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+          
+        RepaintBackground();
+      }
+
+      return *decoded_;
+    }
+  };
+
+
+  Orthanc::ImageAccessor& PyramidReader::GetOutsideTile()
+  {
+    if (outside_.get() == NULL)
+    {
+      outside_.reset(ImageToolbox::Allocate(source_.GetPixelFormat(), targetTileWidth_, targetTileHeight_));
+      ImageToolbox::Set(*outside_,
+                        parameters_.GetBackgroundColorRed(),
+                        parameters_.GetBackgroundColorGreen(),
+                        parameters_.GetBackgroundColorBlue());
+    }
+
+    return *outside_;
+  }
+
+
+  void PyramidReader::CheckTileSize(const Orthanc::ImageAccessor& tile) const
+  {
+    if (tile.GetWidth() != sourceTileWidth_ ||
+        tile.GetHeight() != sourceTileHeight_)
+    {
+      LOG(ERROR) << "One tile in the input image has size " << tile.GetWidth() << "x" << tile.GetHeight() 
+                 << " instead of required " << source_.GetTileWidth() << "x" << source_.GetTileHeight();
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize);
+    }
+  }
+
+
+  void PyramidReader::CheckTileSize(const std::string& tile) const
+  {
+    if (parameters_.IsSafetyCheck())
+    {
+      std::auto_ptr<Orthanc::ImageAccessor> decoded(ImageToolbox::DecodeTile(tile, source_.GetImageCompression()));
+      CheckTileSize(*decoded);
+    }
+  }
+
+
+  PyramidReader::SourceTile& PyramidReader::AccessSourceTile(const Location& location)
+  {
+    Cache::iterator found = cache_.find(location);
+    if (found != cache_.end())
+    {
+      return *found->second;
+    }
+    else
+    {
+      SourceTile *tile = new SourceTile(*this, location.first, location.second);
+      cache_[location] = tile;
+      return *tile;
+    }
+  }
+
+
+  PyramidReader::Location PyramidReader::MapTargetToSourceLocation(unsigned int tileX,
+                                                                   unsigned int tileY)
+  {
+    return std::make_pair(tileX / (sourceTileWidth_ / targetTileWidth_),
+                          tileY / (sourceTileHeight_ / targetTileHeight_));
+  }
+
+
+  PyramidReader::PyramidReader(ITiledPyramid& source,
+                               unsigned int level,
+                               unsigned int targetTileWidth,
+                               unsigned int targetTileHeight,
+                               const DicomizerParameters& parameters) :
+    source_(source),
+    level_(level),
+    levelWidth_(source.GetLevelWidth(level)),
+    levelHeight_(source.GetLevelHeight(level)),
+    sourceTileWidth_(source.GetTileWidth()),
+    sourceTileHeight_(source.GetTileHeight()),
+                                                                         targetTileWidth_(targetTileWidth),
+                                                                         targetTileHeight_(targetTileHeight),
+                                                                         parameters_(parameters)
+  {
+    if (sourceTileWidth_ % targetTileWidth_ != 0 ||
+        sourceTileHeight_ % targetTileHeight_ != 0)
+    {
+      LOG(ERROR) << "When resampling the tile size, it must be a integer divisor of the original tile size";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize);
+    }
+  }
+
+
+  PyramidReader::~PyramidReader()
+  {
+    for (Cache::iterator it = cache_.begin(); it != cache_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+  }
+
+
+  const std::string* PyramidReader::GetRawTile(unsigned int tileX,
+                                               unsigned int tileY)
+  {
+    if (sourceTileWidth_ != targetTileWidth_ ||
+        sourceTileHeight_ != targetTileHeight_)
+    {
+      return NULL;
+    }
+
+    SourceTile& source = AccessSourceTile(MapTargetToSourceLocation(tileX, tileY));
+    if (source.HasRawTile())
+    {
+      CheckTileSize(source.GetRawTile());
+      return &source.GetRawTile();
+    }
+    else
+    {
+      return NULL;
+    }
+  }
+
+
+  Orthanc::ImageAccessor PyramidReader::GetDecodedTile(unsigned int tileX,
+                                                       unsigned int tileY)
+  {
+    if (tileX * targetTileWidth_ >= levelWidth_ ||
+        tileY * targetTileHeight_ >= levelHeight_)
+    {
+      // Accessing a tile out of the source image
+      return GetOutsideTile();
+    }
+
+    SourceTile& source = AccessSourceTile(MapTargetToSourceLocation(tileX, tileY));
+    const Orthanc::ImageAccessor& tile = source.GetDecodedTile();
+
+    CheckTileSize(tile);
+
+    assert(sourceTileWidth_ % targetTileWidth_ == 0 &&
+           sourceTileHeight_ % targetTileHeight_ == 0);
+
+    unsigned int xx = tileX % (sourceTileWidth_ / targetTileWidth_);
+    unsigned int yy = tileY % (sourceTileHeight_ / targetTileHeight_);
+
+    const uint8_t* bytes = 
+      reinterpret_cast<const uint8_t*>(tile.GetConstRow(yy * targetTileHeight_)) +
+      GetBytesPerPixel(tile.GetFormat()) * xx * targetTileWidth_;
+
+    Orthanc::ImageAccessor region;
+    region.AssignReadOnly(tile.GetFormat(),
+                          targetTileWidth_,
+                          targetTileHeight_,
+                          tile.GetPitch(), bytes);                                    
+
+    return region;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Algorithms/PyramidReader.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,96 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "../Inputs/ITiledPyramid.h"
+#include "../DicomizerParameters.h"
+
+#include <map>
+#include <memory>
+
+namespace OrthancWSI
+{
+  // This class is not thread-safe
+  class PyramidReader : public boost::noncopyable
+  {
+  private:
+    class SourceTile;
+
+    typedef std::pair<unsigned int, unsigned int>  Location;
+    typedef std::map<Location, SourceTile*>        Cache;
+
+    ITiledPyramid&  source_;
+    unsigned int    level_;
+    unsigned int    levelWidth_;
+    unsigned int    levelHeight_;
+    unsigned int    sourceTileWidth_;
+    unsigned int    sourceTileHeight_;
+    unsigned int    targetTileWidth_;
+    unsigned int    targetTileHeight_;
+    const DicomizerParameters& parameters_;
+
+    Cache           cache_;
+
+    std::auto_ptr<Orthanc::ImageAccessor>  outside_;
+
+
+    Orthanc::ImageAccessor& GetOutsideTile();
+
+    void CheckTileSize(const Orthanc::ImageAccessor& tile) const;
+
+    void CheckTileSize(const std::string& tile) const;
+
+    SourceTile& AccessSourceTile(const Location& location);
+
+    Location MapTargetToSourceLocation(unsigned int tileX,
+                                       unsigned int tileY);
+
+  public:
+    PyramidReader(ITiledPyramid& source,
+                  unsigned int level,
+                  unsigned int targetTileWidth,
+                  unsigned int targetTileHeight,
+                  const DicomizerParameters& parameters);
+
+    ~PyramidReader();
+
+    const DicomizerParameters& GetParameters() const
+    {
+      return parameters_;
+    }
+
+    ImageCompression GetImageCompression() const
+    {
+      return source_.GetImageCompression();
+    }
+
+    Orthanc::PixelFormat GetPixelFormat() const
+    {
+      return source_.GetPixelFormat();
+    }
+
+    const std::string* GetRawTile(unsigned int tileX,
+                                  unsigned int tileY);
+
+    Orthanc::ImageAccessor GetDecodedTile(unsigned int tileX,
+                                          unsigned int tileY);  
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Algorithms/ReconstructPyramidCommand.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,184 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "ReconstructPyramidCommand.h"
+
+#include "../ImageToolbox.h"
+#include "../Orthanc/Core/Logging.h"
+#include "../Orthanc/Core/OrthancException.h"
+#include "../Orthanc/Core/Images/Image.h"
+
+#include <cassert>
+
+namespace OrthancWSI
+{
+  Orthanc::ImageAccessor* ReconstructPyramidCommand::Explore(unsigned int level,
+                                                             unsigned int offsetX,
+                                                             unsigned int offsetY)
+  {
+    unsigned int zoom = 1 << level;
+    assert(x_ % zoom == 0 && y_ % zoom == 0);
+    unsigned int x = x_ / zoom + offsetX;
+    unsigned int y = y_ / zoom + offsetY;
+
+    if (x >= target_.GetCountTilesX(level + shiftTargetLevel_) ||
+        y >= target_.GetCountTilesY(level + shiftTargetLevel_))
+    {
+      return NULL;
+    }
+
+    std::auto_ptr<Orthanc::ImageAccessor> result;
+
+    if (level == 0)
+    {
+      result.reset(new Orthanc::ImageAccessor(source_.GetDecodedTile(x, y)));
+
+      const std::string* rawTile = source_.GetRawTile(x, y);
+
+      if (rawTile != NULL)
+      {
+        // Simple transcoding
+        target_.WriteRawTile(*rawTile, source_.GetImageCompression(), level + shiftTargetLevel_, x, y);
+      }
+      else
+      {
+        // Re-encoding the file
+        target_.EncodeTile(*result, level + shiftTargetLevel_, x, y);
+      }
+    }
+    else
+    {
+      std::auto_ptr<Orthanc::ImageAccessor> mosaic(ImageToolbox::Allocate(source_.GetPixelFormat(), 
+                                                                          2 * target_.GetTileWidth(), 
+                                                                          2 * target_.GetTileHeight()));
+      ImageToolbox::Set(*mosaic, 
+                        source_.GetParameters().GetBackgroundColorRed(),
+                        source_.GetParameters().GetBackgroundColorGreen(),
+                        source_.GetParameters().GetBackgroundColorBlue());
+
+      {
+        std::auto_ptr<Orthanc::ImageAccessor> subTile(Explore(level - 1, 2 * offsetX, 2 * offsetY));
+        if (subTile.get() != NULL)
+        {
+          ImageToolbox::Embed(*mosaic, *subTile, 0, 0);
+        }
+      }
+
+      {
+        std::auto_ptr<Orthanc::ImageAccessor> subTile(Explore(level - 1, 2 * offsetX + 1, 2 * offsetY));
+        if (subTile.get() != NULL)
+        {
+          ImageToolbox::Embed(*mosaic, *subTile, target_.GetTileWidth(), 0);
+        }
+      }
+
+      {
+        std::auto_ptr<Orthanc::ImageAccessor> subTile(Explore(level - 1, 2 * offsetX, 2 * offsetY + 1));
+        if (subTile.get() != NULL)
+        {
+          ImageToolbox::Embed(*mosaic, *subTile, 0, target_.GetTileHeight());
+        }
+      }
+
+      {
+        std::auto_ptr<Orthanc::ImageAccessor> subTile(Explore(level - 1, 2 * offsetX + 1, 2 * offsetY + 1));
+        if (subTile.get() != NULL)
+        {
+          ImageToolbox::Embed(*mosaic, *subTile, target_.GetTileWidth(), target_.GetTileHeight());
+        }
+      }
+
+      result.reset(ImageToolbox::Halve(*mosaic, source_.GetParameters().IsSmoothEnabled()));
+
+      target_.EncodeTile(*result, level + shiftTargetLevel_, x, y);
+    }
+
+    return result.release();
+  }
+
+  
+  ReconstructPyramidCommand::ReconstructPyramidCommand(IPyramidWriter& target,
+                                                       ITiledPyramid& source,
+                                                       unsigned int upToLevel,
+                                                       unsigned int x,
+                                                       unsigned int y,
+                                                       const DicomizerParameters& parameters) :
+    target_(target),
+    source_(source, 0, target.GetTileWidth(), target.GetTileHeight(), parameters),
+    upToLevel_(upToLevel),
+    x_(x),
+    y_(y),
+    shiftTargetLevel_(0)
+  {
+    unsigned int zoom = 1 << upToLevel;
+    if (x % zoom != 0 ||
+        y % zoom != 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    if (target.GetPixelFormat() != source.GetPixelFormat())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+    }
+  }
+
+
+  bool ReconstructPyramidCommand::Execute()
+  {
+    std::auto_ptr<Orthanc::ImageAccessor> root(Explore(upToLevel_, 0, 0));
+    return true;
+  }
+
+
+  void ReconstructPyramidCommand::PrepareBagOfTasks(Orthanc::BagOfTasks& tasks,
+                                                    IPyramidWriter& target,
+                                                    ITiledPyramid& source,
+                                                    unsigned int countLevels,
+                                                    unsigned int shiftTargetLevel,    
+                                                    const DicomizerParameters& parameters)
+  {
+    if (countLevels == 0)
+    {
+      return;
+    }
+
+    if (shiftTargetLevel + countLevels > target.GetLevelCount())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    const unsigned int targetCountTilesX = target.GetCountTilesX(shiftTargetLevel);
+    const unsigned int targetCountTilesY = target.GetCountTilesY(shiftTargetLevel);
+    const unsigned int step = 1 << (countLevels - 1);
+
+    for (unsigned int y = 0; y < targetCountTilesY; y += step)
+    {
+      for (unsigned int x = 0; x < targetCountTilesX; x += step)
+      {
+        std::auto_ptr<ReconstructPyramidCommand> command;
+        command.reset(new ReconstructPyramidCommand
+                      (target, source, countLevels - 1, x, y, parameters));
+        command->SetShiftTargetLevel(shiftTargetLevel);
+        tasks.Push(command.release());
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Algorithms/ReconstructPyramidCommand.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,72 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "PyramidReader.h"
+#include "../Outputs/IPyramidWriter.h"
+#include "../Orthanc/Core/MultiThreading/BagOfTasks.h"
+
+
+namespace OrthancWSI
+{
+  class ReconstructPyramidCommand : public Orthanc::ICommand
+  {
+  private:
+    IPyramidWriter& target_;
+    PyramidReader  source_;
+
+    unsigned int upToLevel_;
+    unsigned int x_;
+    unsigned int y_;
+    unsigned int shiftTargetLevel_;
+
+    Orthanc::ImageAccessor* Explore(unsigned int level,
+                                    unsigned int offsetX,
+                                    unsigned int offsetY);
+
+  public:
+    ReconstructPyramidCommand(IPyramidWriter& target,
+                              ITiledPyramid& source,
+                              unsigned int upToLevel,
+                              unsigned int x,
+                              unsigned int y,
+                              const DicomizerParameters& parameters);
+
+    void SetShiftTargetLevel(unsigned int shift)
+    {
+      shiftTargetLevel_ = shift;
+    }
+
+    unsigned int GetShiftTargetLevel() const
+    {
+      return shiftTargetLevel_;
+    }
+
+    virtual bool Execute();
+
+    static void PrepareBagOfTasks(Orthanc::BagOfTasks& tasks,
+                                  IPyramidWriter& target,
+                                  ITiledPyramid& source,
+                                  unsigned int countLevels,
+                                  unsigned int shiftTargetLevel,    
+                                  const DicomizerParameters& parameters);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Algorithms/TranscodeTileCommand.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,120 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "TranscodeTileCommand.h"
+
+#include "../Orthanc/Core/OrthancException.h"
+#include "../Orthanc/Core/Logging.h"
+
+#include <cassert>
+
+namespace OrthancWSI
+{
+  TranscodeTileCommand::TranscodeTileCommand(IPyramidWriter& target,
+                                             ITiledPyramid& source,
+                                             unsigned int level,
+                                             unsigned int x,
+                                             unsigned int y,
+                                             unsigned int countTilesX,
+                                             unsigned int countTilesY,
+                                             const DicomizerParameters& parameters) :
+  target_(target),
+    source_(source, level, target.GetTileWidth(), target.GetTileHeight(), parameters),
+    level_(level),
+    x_(x),
+    y_(y),
+    countTilesX_(countTilesX),
+    countTilesY_(countTilesY)
+  {
+    assert(x_ + countTilesX_ <= target_.GetCountTilesX(level_) &&
+           y_ + countTilesY_ <= target_.GetCountTilesY(level_));
+
+    if (target.GetPixelFormat() != source.GetPixelFormat())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+    }
+  }
+
+
+  bool TranscodeTileCommand::Execute()
+  {
+    for (unsigned int x = x_; x < x_ + countTilesX_; x++)
+    {
+      for (unsigned int y = y_; y < y_ + countTilesY_; y++)
+      {
+        LOG(INFO) << "Adding tile (" << x << "," << y << ") at level " << level_;
+        const std::string* rawTile = source_.GetRawTile(x, y);
+
+        if (rawTile != NULL)
+        {
+          // Simple transcoding
+          target_.WriteRawTile(*rawTile, source_.GetImageCompression(), level_, x, y);
+        }
+        else
+        {
+          Orthanc::ImageAccessor tile = source_.GetDecodedTile(x, y);
+
+          // Re-encoding the file
+          target_.EncodeTile(tile, level_, x, y);
+        }
+      }
+    }
+
+    return true;
+  }
+
+
+  void TranscodeTileCommand::PrepareBagOfTasks(Orthanc::BagOfTasks& tasks,
+                                               IPyramidWriter& target,
+                                               ITiledPyramid& source,
+                                               const DicomizerParameters& parameters)
+  {
+    const unsigned int stepX = source.GetTileWidth() / target.GetTileWidth();
+    const unsigned int stepY = source.GetTileHeight() / target.GetTileHeight();
+    assert(stepX >= 1 && stepY >= 1);
+
+    for (unsigned int level = 0; level < source.GetLevelCount(); level++)
+    {
+      const unsigned int targetCountTilesX = target.GetCountTilesX(level);
+      const unsigned int targetCountTilesY = target.GetCountTilesY(level);
+
+      for (unsigned int y = 0; y < targetCountTilesY; y += stepY)
+      {
+        unsigned int cy = stepY;
+        if (y + cy > targetCountTilesY)
+        {
+          cy = targetCountTilesY - y;
+        }
+
+        for (unsigned int x = 0; x < targetCountTilesX; x += stepX)
+        {
+          unsigned int cx = stepX;
+          if (x + cx > targetCountTilesX)
+          {
+            cx = targetCountTilesX - x;
+          }
+
+          tasks.Push(new TranscodeTileCommand
+                     (target, source, level, x, y, cx, cy, parameters));
+        }
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Algorithms/TranscodeTileCommand.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,59 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "PyramidReader.h"
+#include "../Outputs/IPyramidWriter.h"
+
+#include "../Orthanc/Core/MultiThreading/BagOfTasks.h"
+
+namespace OrthancWSI
+{
+  class TranscodeTileCommand : public Orthanc::ICommand
+  {
+  private:
+    IPyramidWriter& target_;
+    PyramidReader  source_;
+
+    unsigned int  level_;
+    unsigned int x_;
+    unsigned int y_;
+    unsigned int countTilesX_;
+    unsigned int countTilesY_;
+
+  public:
+    TranscodeTileCommand(IPyramidWriter& target,
+                         ITiledPyramid& source,
+                         unsigned int level,
+                         unsigned int x,
+                         unsigned int y,
+                         unsigned int countTilesX,
+                         unsigned int countTilesY,
+                         const DicomizerParameters& parameters);
+
+    virtual bool Execute();
+
+    static void PrepareBagOfTasks(Orthanc::BagOfTasks& tasks,
+                                  IPyramidWriter& target,
+                                  ITiledPyramid& source,
+                                  const DicomizerParameters& parameters);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/DicomToolbox.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,250 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "DicomToolbox.h"
+
+#include "Orthanc/Core/Logging.h"
+#include "Orthanc/Core/OrthancException.h"
+#include "Orthanc/Core/Toolbox.h"
+
+#if ORTHANC_ENABLE_DCMTK == 1
+#  include <dcmtk/dcmdata/dcelem.h>
+#  include <dcmtk/dcmdata/dcsequen.h>
+#endif
+
+#include <boost/lexical_cast.hpp>
+
+namespace OrthancWSI
+{
+  namespace DicomToolbox
+  {
+#if ORTHANC_ENABLE_DCMTK == 1
+    void SetStringTag(DcmItem& dataset,
+                      const DcmTagKey& key,
+                      const std::string& value)
+    {
+      if (!dataset.tagExists(key) &&
+          !dataset.putAndInsertString(key, value.c_str()).good())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+    }
+
+    void SetUint16Tag(DcmItem& dataset,
+                      const DcmTagKey& key,
+                      uint16_t value)
+    {
+      if (!dataset.tagExists(key) &&
+          !dataset.putAndInsertUint16(key, value).good())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+    }
+
+
+    void SetUint32Tag(DcmItem& dataset,
+                      const DcmTagKey& key,
+                      uint32_t value)
+    {
+      if (!dataset.tagExists(key) &&
+          !dataset.putAndInsertUint32(key, value).good())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+    }
+
+
+    DcmItem* ExtractSingleSequenceItem(DcmItem& dataset,
+                                       const DcmTagKey& key)
+    {
+      DcmElement* element = NULL;
+      if (!const_cast<DcmItem&>(dataset).findAndGetElement(key, element).good() ||
+          element == NULL)
+      {
+        return NULL;
+      }
+
+      if (element->getVR() != EVR_SQ)
+      {
+        DcmTag tag(key);
+        LOG(ERROR) << "The following element in the DICOM dataset is not a sequence as expected: " << tag.getTagName();
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+
+      DcmSequenceOfItems& sequence = dynamic_cast<DcmSequenceOfItems&>(*element);
+      if (sequence.card() != 1)
+      {
+        LOG(ERROR) << "Bad number of elements in the sequence (it must contain exactly 1 element)";
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+
+      return sequence.getItem(0);
+    }
+
+
+    uint16_t GetUint16Tag(DcmItem& dataset,
+                          const DcmTagKey& key)
+    {
+      uint16_t value;
+      if (dataset.findAndGetUint16(key, value).good())
+      {
+        return value;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentTag);
+      }
+    }
+
+
+    uint32_t GetUint32Tag(DcmItem& dataset,
+                          const DcmTagKey& key)
+    {
+      Uint32 value;
+      if (dataset.findAndGetUint32(key, value).good())
+      {
+        return value;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentTag);
+      }
+    }
+
+
+    int32_t GetInt32Tag(DcmItem& dataset,
+                        const DcmTagKey& key)
+    {
+      Sint32 value;
+      if (dataset.findAndGetSint32(key, value).good())
+      {
+        return value;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentTag);
+      }
+    }
+
+
+    std::string GetStringTag(DcmItem& dataset,
+                             const DcmTagKey& key)
+    {
+      const char* value = NULL;
+      if (dataset.findAndGetString(key, value).good() &&
+          value != NULL)
+      {
+        return Orthanc::Toolbox::StripSpaces(value);
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentTag);
+      }
+    }
+#endif
+
+
+    bool GetStringTag(std::string& result,
+                      const Json::Value& simplifiedTags,
+                      const std::string& tagName,
+                      const std::string& defaultValue)
+    {
+      if (simplifiedTags.type() != Json::objectValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+
+      if (!simplifiedTags.isMember(tagName))
+      {
+        result = defaultValue;
+        return false;
+      }
+      else if (simplifiedTags[tagName].type() != Json::stringValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        result = simplifiedTags[tagName].asString();
+        return true;
+      }
+    }
+
+
+    std::string GetMandatoryStringTag(const Json::Value& simplifiedTags,
+                                      const std::string& tagName)
+    {
+      std::string s;
+      if (GetStringTag(s, simplifiedTags, tagName, ""))
+      {
+        return s;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentTag);        
+      }
+    }
+
+
+    const Json::Value& GetSequenceTag(const Json::Value& simplifiedTags,
+                                      const std::string& tagName)
+    {
+      if (simplifiedTags.type() != Json::objectValue ||
+          !simplifiedTags.isMember(tagName) ||
+          simplifiedTags[tagName].type() != Json::arrayValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+
+      return simplifiedTags[tagName];
+    }
+
+
+    int GetIntegerTag(const Json::Value& simplifiedTags,
+                      const std::string& tagName)
+    {
+      try
+      {
+        std::string s = Orthanc::Toolbox::StripSpaces(GetMandatoryStringTag(simplifiedTags, tagName));
+        return boost::lexical_cast<int>(s);
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);        
+      }
+    }
+
+
+    unsigned int GetUnsignedIntegerTag(const Json::Value& simplifiedTags,
+                                       const std::string& tagName)
+    {
+      int value = GetIntegerTag(simplifiedTags, tagName);
+
+      if (value >= 0)
+      {
+        return static_cast<unsigned int>(value);
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/DicomToolbox.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,83 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if ORTHANC_ENABLE_DCMTK == 1
+#  include <dcmtk/dcmdata/dcdeftag.h>
+#  include <dcmtk/dcmdata/dcitem.h>
+#  include <dcmtk/dcmdata/dctagkey.h>
+#endif
+
+#include <string>
+#include <stdint.h>
+#include <json/value.h>
+
+namespace OrthancWSI
+{
+  namespace DicomToolbox
+  {
+#if ORTHANC_ENABLE_DCMTK == 1
+    void SetStringTag(DcmItem& dataset,
+                      const DcmTagKey& key,
+                      const std::string& value);
+
+    void SetUint16Tag(DcmItem& dataset,
+                      const DcmTagKey& key,
+                      uint16_t value);
+
+    void SetUint32Tag(DcmItem& dataset,
+                      const DcmTagKey& key,
+                      uint32_t value);
+
+    DcmItem* ExtractSingleSequenceItem(DcmItem& dataset,
+                                       const DcmTagKey& key);
+
+    uint16_t GetUint16Tag(DcmItem& dataset,
+                          const DcmTagKey& key);
+
+    uint32_t GetUint32Tag(DcmItem& dataset,
+                          const DcmTagKey& key);
+
+    int32_t GetInt32Tag(DcmItem& dataset,
+                        const DcmTagKey& key);
+
+    std::string GetStringTag(DcmItem& dataset,
+                             const DcmTagKey& key);
+#endif
+
+    bool GetStringTag(std::string& result,
+                      const Json::Value& simplifiedTags,
+                      const std::string& tagName,
+                      const std::string& defaultValue);
+
+    std::string GetMandatoryStringTag(const Json::Value& simplifiedTags,
+                                      const std::string& tagName);
+
+    const Json::Value& GetSequenceTag(const Json::Value& simplifiedTags,
+                                      const std::string& tagName);
+
+    int GetIntegerTag(const Json::Value& simplifiedTags,
+                      const std::string& tagName);
+
+    unsigned int GetUnsignedIntegerTag(const Json::Value& simplifiedTags,
+                                       const std::string& tagName);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/DicomizerParameters.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,274 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "DicomizerParameters.h"
+
+#include "Messaging/FolderTarget.h"
+#include "Messaging/OrthancTarget.h"
+
+#include "Orthanc/Core/OrthancException.h"
+
+#include <boost/thread.hpp>
+#include <boost/lexical_cast.hpp>
+
+namespace OrthancWSI
+{
+  static unsigned int ChooseNumberOfThreads()
+  {
+    unsigned int nthreads = boost::thread::hardware_concurrency();
+
+    if (nthreads % 2 == 0)
+    {
+      nthreads = nthreads / 2;
+    }
+    else
+    {
+      nthreads = nthreads / 2 + 1;
+    }
+
+    if (nthreads == 0)
+    {
+      nthreads = 1;
+    }
+
+    return nthreads;
+  }
+
+
+  DicomizerParameters::DicomizerParameters()
+  {
+    safetyCheck_ = false;
+    repaintBackground_ = false;
+    backgroundColor_[0] = 255;
+    backgroundColor_[1] = 255;
+    backgroundColor_[2] = 255;
+    targetCompression_ = ImageCompression_Jpeg;
+    hasTargetTileSize_ = false;
+    threadsCount_ = ChooseNumberOfThreads();
+    maxDicomFileSize_ = 10 * 1024 * 1024;   // 10MB
+    reconstructPyramid_ = false;
+    pyramidLevelsCount_ = 0;
+    pyramidLowerLevelsCount_ = 0;
+    smooth_ = false;
+    jpegQuality_ = 90;
+    forceReencode_ = false;
+    opticalPath_ = OpticalPath_Brightfield;
+  }
+
+
+  void DicomizerParameters::SetBackgroundColor(uint8_t red,
+                                               uint8_t green,
+                                               uint8_t blue)
+  {
+    repaintBackground_ = true;
+    backgroundColor_[0] = red;
+    backgroundColor_[1] = green;
+    backgroundColor_[2] = blue;
+  }
+
+
+  void DicomizerParameters::SetTargetTileSize(unsigned int width,
+                                              unsigned int height)
+  {
+    hasTargetTileSize_ = true;
+    targetTileWidth_ = width;
+    targetTileHeight_ = height;
+  }
+
+
+  unsigned int DicomizerParameters::GetTargetTileWidth(unsigned int defaultWidth) const
+  {
+    if (hasTargetTileSize_ &&
+        targetTileWidth_ != 0)
+    {
+      return targetTileWidth_;
+    }
+    else
+    {
+      return defaultWidth;
+    }
+  }
+
+
+  unsigned int DicomizerParameters::GetTargetTileHeight(unsigned int defaultHeight) const
+  {
+    if (hasTargetTileSize_ &&
+        targetTileHeight_ != 0)
+    {
+      return targetTileHeight_;
+    }
+    else
+    {
+      return defaultHeight;
+    }
+  }
+
+
+  void DicomizerParameters::SetThreadsCount(unsigned int threads)
+  {
+    if (threads == 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    threadsCount_ = threads;
+  }
+
+
+  void DicomizerParameters::SetDicomMaxFileSize(unsigned int size)
+  {
+    if (size <= 1024)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    maxDicomFileSize_ = size;
+  }
+
+
+  void DicomizerParameters::SetPyramidLevelsCount(unsigned int count)
+  {
+    if (count <= 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    pyramidLevelsCount_ = count;
+  }
+
+
+  unsigned int DicomizerParameters::GetPyramidLevelsCount(const IPyramidWriter& target,
+                                                          const ITiledPyramid& source) const
+  {
+    if (!reconstructPyramid_)
+    {
+      // Only makes sense if reconstructing the pyramid
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    if (pyramidLevelsCount_ != 0)
+    {
+      return pyramidLevelsCount_;
+    }
+
+    // By default, the number of levels for the pyramid is taken so that
+    // the upper level is reduced either to 1 column of tiles, or to 1
+    // row of tiles.
+    unsigned int totalWidth = source.GetLevelWidth(0);
+    unsigned int totalHeight = source.GetLevelHeight(0);
+
+    unsigned int countLevels = 1;
+    for (;;)
+    {
+      unsigned int zoom = 1 << (countLevels - 1);
+
+      if (CeilingDivision(totalWidth, zoom) <= target.GetTileWidth() ||
+          CeilingDivision(totalHeight, zoom) <= target.GetTileHeight())
+      {
+        break;
+      }
+
+      countLevels += 1;
+    }
+
+    return countLevels;
+  }
+
+
+  void DicomizerParameters::SetPyramidLowerLevelsCount(unsigned int count)
+  {
+    if (count <= 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    pyramidLowerLevelsCount_ = count;
+  }
+
+
+  unsigned int DicomizerParameters::GetPyramidLowerLevelsCount(const IPyramidWriter& target,
+                                                               const ITiledPyramid& source) const
+  {
+    if (!reconstructPyramid_)
+    {
+      // Only makes sense if reconstructing the pyramid
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    if (pyramidLowerLevelsCount_ != 0)
+    {
+      return pyramidLowerLevelsCount_;
+    }
+
+    unsigned int fullNumberOfTiles = (
+      CeilingDivision(source.GetLevelWidth(0), source.GetTileWidth()) * 
+      CeilingDivision(source.GetLevelHeight(0), source.GetTileHeight()));
+
+    // By default, the number of lower levels in the pyramid is chosen
+    // as a compromise between the number of tasks (there should not be
+    // too few tasks, otherwise multithreading would not be efficient)
+    // and memory consumption (maximum 64MB of RAM due to the decoding
+    // of the tiles of the source image per thread: cf. PyramidReader).
+    unsigned int result = 1;
+    for (;;)
+    {
+      unsigned int zoom = 1 << (result - 1);
+      unsigned int numberOfTiles = CeilingDivision(fullNumberOfTiles, zoom * zoom);
+
+      if (result + 1 > target.GetLevelCount() ||
+          numberOfTiles < 4 * GetThreadsCount() ||
+          zoom * target.GetTileWidth() > 4096 ||
+          zoom * target.GetTileHeight() > 4096)
+      {
+        break;
+      }
+
+      result += 1;
+    }
+
+    return result - 1;
+  }
+
+
+  void DicomizerParameters::SetJpegQuality(int quality)
+  {
+    if (quality <= 0 ||
+        quality > 100)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    jpegQuality_ = quality;
+  }
+
+
+  IFileTarget* DicomizerParameters::CreateTarget() const
+  {
+    if (folder_.empty() ||
+        folderPattern_.empty())
+    {
+      return new OrthancTarget(orthanc_);
+    }
+    else
+    {
+      return new FolderTarget(folder_ + "/" + folderPattern_);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/DicomizerParameters.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,257 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "Inputs/ITiledPyramid.h"
+#include "Outputs/IPyramidWriter.h"
+#include "Messaging/IFileTarget.h"
+#include "DicomToolbox.h"
+#include "Orthanc/Core/WebServiceParameters.h"
+
+#include <stdint.h>
+
+namespace OrthancWSI
+{
+  class DicomizerParameters
+  {
+  private:
+    bool              safetyCheck_;
+    bool              repaintBackground_;
+    uint8_t           backgroundColor_[3];
+    ImageCompression  targetCompression_;
+    bool              hasTargetTileSize_;
+    unsigned int      targetTileWidth_;
+    unsigned int      targetTileHeight_;
+    unsigned int      threadsCount_;
+    unsigned int      maxDicomFileSize_;
+    bool              reconstructPyramid_;
+    unsigned int      pyramidLevelsCount_;  // "0" means use default choice
+    unsigned int      pyramidLowerLevelsCount_;  // "0" means use default choice
+    bool              smooth_;
+    std::string       inputFile_;
+    uint8_t           jpegQuality_;
+    bool              forceReencode_;
+    std::string       folder_;
+    std::string       folderPattern_;
+    std::string       dataset_;
+    OpticalPath       opticalPath_;
+    std::string       iccProfile_;
+
+    Orthanc::WebServiceParameters  orthanc_;
+
+  public:
+    DicomizerParameters();
+
+    void SetSafetyCheck(bool safety)
+    {
+      safetyCheck_ = safety;
+    }
+
+    bool IsSafetyCheck() const
+    {
+      return safetyCheck_;
+    }
+
+    bool IsRepaintBackground() const
+    {
+      return repaintBackground_;
+    }
+
+    void SetRepaintBackground(bool repaint)
+    {
+      repaintBackground_ = repaint;
+    }
+
+    void SetBackgroundColor(uint8_t red,
+                            uint8_t green,
+                            uint8_t blue);
+
+    uint8_t GetBackgroundColorRed() const
+    {
+      return backgroundColor_[0];
+    }
+
+    uint8_t GetBackgroundColorGreen() const
+    {
+      return backgroundColor_[1];
+    }
+
+    uint8_t GetBackgroundColorBlue() const
+    {
+      return backgroundColor_[2];
+    }
+
+    void SetTargetCompression(ImageCompression compression)
+    {
+      targetCompression_ = compression;
+    }
+
+    ImageCompression GetTargetCompression() const
+    {
+      return targetCompression_;
+    }
+
+    void SetTargetTileSize(unsigned int width,
+                           unsigned int height);
+
+    unsigned int GetTargetTileWidth(unsigned int defaultWidth) const;
+
+    unsigned int GetTargetTileWidth(const ITiledPyramid& source) const
+    {
+      return GetTargetTileWidth(source.GetTileWidth());
+    }
+
+    unsigned int GetTargetTileHeight(unsigned int defaultHeight) const;
+
+    unsigned int GetTargetTileHeight(const ITiledPyramid& source) const
+    {
+      return GetTargetTileHeight(source.GetTileHeight());
+    }
+
+    void SetThreadsCount(unsigned int threads);
+
+    unsigned int GetThreadsCount() const
+    {
+      return threadsCount_;
+    }
+
+    void SetDicomMaxFileSize(unsigned int size);
+
+    unsigned int GetDicomMaxFileSize() const
+    {
+      return maxDicomFileSize_;
+    }
+
+    bool IsReconstructPyramid() const
+    {
+      return reconstructPyramid_;
+    }
+
+    void SetReconstructPyramid(bool reconstruct)
+    {
+      reconstructPyramid_ = reconstruct;
+    }
+    
+    void SetPyramidLevelsCount(unsigned int count);
+
+    unsigned int GetPyramidLevelsCount(const IPyramidWriter& target,
+                                       const ITiledPyramid& source) const;
+
+    void SetPyramidLowerLevelsCount(unsigned int count);
+
+    unsigned int GetPyramidLowerLevelsCount(const IPyramidWriter& target,
+                                            const ITiledPyramid& source) const;
+
+    void SetSmoothEnabled(bool smooth)
+    {
+      smooth_ = smooth;
+    }
+
+    bool IsSmoothEnabled() const
+    {
+      return smooth_;
+    }
+
+    void SetInputFile(const std::string& path)
+    {
+      inputFile_ = path;
+    }
+
+    const std::string& GetInputFile() const
+    {
+      return inputFile_;
+    }
+
+    void SetJpegQuality(int quality);
+
+    uint8_t GetJpegQuality() const
+    {
+      return jpegQuality_;
+    }
+
+    void SetForceReencode(bool force)
+    {
+      forceReencode_ = force;
+    }
+
+    bool IsForceReencode() const
+    {
+      return forceReencode_;
+    }
+
+    void SetTargetFolder(const std::string& folder)
+    {
+      folder_ = folder;
+    }
+
+    const std::string& GetTargetFolderPattern() const
+    {
+      return folderPattern_;
+    }
+
+    void SetTargetFolderPattern(const std::string& pattern)
+    {
+      folderPattern_ = pattern;
+    }
+
+    Orthanc::WebServiceParameters& GetOrthancParameters()
+    {
+      return orthanc_;
+    }
+
+    const Orthanc::WebServiceParameters& GetOrthancParameters() const
+    {
+      return orthanc_;
+    }
+
+    IFileTarget* CreateTarget() const;
+
+    void SetDatasetPath(const std::string& path)
+    {
+      dataset_ = path;
+    }
+
+    const std::string& GetDatasetPath() const
+    {
+      return dataset_;
+    }
+
+    void SetOpticalPath(OpticalPath opticalPath)
+    {
+      opticalPath_ = opticalPath;
+    }
+
+    OpticalPath GetOpticalPath() const
+    {
+      return opticalPath_;
+    }
+
+    void SetIccProfilePath(const std::string& path)
+    {
+      iccProfile_ = path;
+    }
+
+    const std::string& GetIccProfilePath() const
+    {
+      return iccProfile_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Enumerations.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,184 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "Enumerations.h"
+
+#include "Jpeg2000Reader.h"
+#include "Orthanc/Core/OrthancException.h"
+#include "Orthanc/Core/Toolbox.h"
+
+#include <string.h>
+#include <boost/algorithm/string/predicate.hpp>
+
+#define HEADER(s) (const void*) (s), sizeof(s)-1
+
+namespace OrthancWSI
+{
+  const char* EnumerationToString(ImageCompression compression)
+  {
+    switch (compression)
+    {
+      case ImageCompression_Unknown:
+        return "Unknown";
+
+      case ImageCompression_None:
+        return "Raw image";
+
+      case ImageCompression_Png:
+        return "PNG";
+
+      case ImageCompression_Jpeg:
+        return "JPEG";
+
+      case ImageCompression_Jpeg2000:
+        return "JPEG2000";
+
+      case ImageCompression_Tiff:
+        return "TIFF";
+
+      case ImageCompression_Dicom:
+        return "DICOM";
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  static bool MatchHeader(const void* actual,
+                          size_t actualSize,
+                          const void* expected,
+                          size_t expectedSize)
+  {
+    if (actualSize < expectedSize)
+    {
+      return false;
+    }
+    else
+    {
+      return memcmp(actual, expected, expectedSize) == 0;
+    }
+  }
+
+
+  ImageCompression DetectFormatFromFile(const std::string& path)
+  {
+    std::string header;
+    Orthanc::Toolbox::ReadHeader(header, path, 256);
+
+    ImageCompression tmp = DetectFormatFromMemory(header.c_str(), header.size());
+    if (tmp != ImageCompression_Unknown)
+    {
+      return tmp;
+    }
+
+    // Cannot detect the format using the header, fallback to the use
+    // of the filename extension
+    
+    std::string lower;
+    Orthanc::Toolbox::ToLowerCase(lower, path);
+
+    if (boost::algorithm::ends_with(lower, ".jpeg") ||
+        boost::algorithm::ends_with(lower, ".jpg"))
+    {
+      return ImageCompression_Jpeg;
+    }
+
+    if (boost::algorithm::ends_with(lower, ".png"))
+    {
+      return ImageCompression_Png;
+    }
+
+    if (boost::algorithm::ends_with(lower, ".tiff") ||
+        boost::algorithm::ends_with(lower, ".tif"))
+    {
+      return ImageCompression_Tiff;
+    }
+
+    if (boost::algorithm::ends_with(lower, ".jp2") ||
+        boost::algorithm::ends_with(lower, ".j2k"))
+    {
+      return ImageCompression_Jpeg2000;
+    }
+
+    if (boost::algorithm::ends_with(lower, ".dcm"))
+    {
+      return ImageCompression_Dicom;
+    }
+
+    return ImageCompression_Unknown;
+  }
+
+
+  ImageCompression DetectFormatFromMemory(const void* buffer,
+                                          size_t size) 
+  {
+    if (MatchHeader(buffer, size, HEADER("\377\330\377")))
+    {
+      return ImageCompression_Jpeg;
+    }
+
+    if (MatchHeader(buffer, size, HEADER("\xff\x4f\xff\x51")) ||
+        MatchHeader(buffer, size, HEADER("\x00\x00\x00\x0c\x6a\x50\x20\x20\x0d\x0a\x87\x0a")))
+    {
+      return ImageCompression_Jpeg2000;
+    }
+
+    if (MatchHeader(buffer, size, HEADER("\211PNG\r\n\032\n")))
+    {
+      return ImageCompression_Png;
+    }
+
+    if (MatchHeader(buffer, size, HEADER("\115\115\000\052")) ||
+        MatchHeader(buffer, size, HEADER("\111\111\052\000")) ||
+        MatchHeader(buffer, size, HEADER("\115\115\000\053\000\010\000\000")) ||
+        MatchHeader(buffer, size, HEADER("\111\111\053\000\010\000\000\000")))
+    {
+      return ImageCompression_Tiff;
+    }
+
+    if (size >= 128 + 4 &&
+        MatchHeader(reinterpret_cast<const uint8_t*>(buffer) + 128, size - 128, HEADER("DICM")))
+    {
+      bool ok = true;
+      for (size_t i = 0; ok && i < 128; i++)
+      {
+        if (reinterpret_cast<const uint8_t*>(buffer)[i] != 0)
+        {
+          ok = false;
+        }
+      }
+
+      if (ok)
+      {
+        return ImageCompression_Dicom;
+      }
+    }        
+
+    Jpeg2000Format jpeg2000 = Jpeg2000Reader::DetectFormatFromMemory(buffer, size);
+    if (jpeg2000 == Jpeg2000Format_JP2 ||
+        jpeg2000 == Jpeg2000Format_J2K)
+    {
+      return ImageCompression_Jpeg2000;
+    }
+    
+    return ImageCompression_Unknown;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Enumerations.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,66 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "Orthanc/Core/Enumerations.h"
+
+#include <stdint.h>
+#include <string>
+
+namespace OrthancWSI
+{
+  enum ImageCompression
+  {
+    ImageCompression_Unknown,
+    ImageCompression_None,
+    ImageCompression_Dicom,
+    ImageCompression_Png,
+    ImageCompression_Jpeg,
+    ImageCompression_Jpeg2000,
+    ImageCompression_Tiff
+  };
+
+  enum OpticalPath
+  {
+    OpticalPath_None,
+    OpticalPath_Brightfield
+  };
+
+  const char* EnumerationToString(ImageCompression compression);
+
+  ImageCompression DetectFormatFromFile(const std::string& path);
+
+  ImageCompression DetectFormatFromMemory(const void* buffer,
+                                          size_t size);
+
+  inline unsigned int CeilingDivision(unsigned int a,
+                                      unsigned int b)
+  {
+    if (a % b == 0)
+    {
+      return a / b;
+    }
+    else
+    {
+      return a / b + 1;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/ImageToolbox.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,367 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "ImageToolbox.h"
+
+#include "Jpeg2000Reader.h"
+#include "Jpeg2000Writer.h"
+
+#include "Orthanc/Core/OrthancException.h"
+#include "Orthanc/Core/Images/ImageProcessing.h"
+#include "Orthanc/Core/Images/PngReader.h"
+#include "Orthanc/Core/Images/PngWriter.h"
+#include "Orthanc/Core/Images/JpegReader.h"
+#include "Orthanc/Core/Images/JpegWriter.h"
+#include "Orthanc/Core/Logging.h"
+
+#include <string.h>
+#include <memory>
+
+
+namespace OrthancWSI
+{
+  namespace ImageToolbox
+  {
+    Orthanc::ImageAccessor* Allocate(Orthanc::PixelFormat format,
+                                     unsigned int width,
+                                     unsigned int height)
+    {
+      return new Orthanc::Image(format, width, height, false);
+    }
+
+
+    void Embed(Orthanc::ImageAccessor& target,
+               const Orthanc::ImageAccessor& source,
+               unsigned int x,
+               unsigned int y)
+    {
+      if (target.GetFormat() != source.GetFormat())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+      }
+
+      if (x >= target.GetWidth() ||
+          y >= target.GetHeight())
+      {
+        return;
+      }
+
+      unsigned int h = std::min(source.GetHeight(), target.GetHeight() - y);
+      unsigned int w = std::min(source.GetWidth(), target.GetWidth() - x);
+
+      Orthanc::ImageAccessor targetRegion = target.GetRegion(x, y, w, h);
+      Orthanc::ImageAccessor sourceRegion = source.GetRegion(0, 0, w, h);
+      Orthanc::ImageProcessing::Copy(targetRegion, sourceRegion);
+    }
+
+
+
+    void Set(Orthanc::ImageAccessor& image,
+             uint8_t r,
+             uint8_t g,
+             uint8_t b)
+    {
+      if (image.GetWidth() == 0 ||
+          image.GetHeight() == 0)
+      {
+        return;
+      }
+
+      uint8_t grayscale = (2126 * static_cast<uint16_t>(r) + 
+                           7152 * static_cast<uint16_t>(g) +
+                           0722 * static_cast<uint16_t>(b)) / 10000;
+
+      switch (image.GetFormat())
+      {
+        case Orthanc::PixelFormat_Grayscale8:
+        {
+          for (unsigned int y = 0; y < image.GetHeight(); y++)
+          {
+            memset(image.GetRow(y), grayscale, image.GetWidth());
+          }
+
+          break;
+        }
+
+        case Orthanc::PixelFormat_RGB24:
+        {
+          for (unsigned int y = 0; y < image.GetHeight(); y++)
+          {
+            uint8_t* p = reinterpret_cast<uint8_t*>(image.GetRow(y));
+            for (unsigned int x = 0; x < image.GetWidth(); x++, p += 3)
+            {
+              p[0] = r;
+              p[1] = g;
+              p[2] = b;
+            }
+          }
+
+          break;
+        }
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+    }
+
+
+    Orthanc::ImageAccessor* DecodeTile(const std::string& source,
+                                       ImageCompression compression)
+    {
+      switch (compression)
+      {
+        case ImageCompression_Png:
+        {
+          std::auto_ptr<Orthanc::PngReader> reader(new Orthanc::PngReader);
+          reader->ReadFromMemory(source);
+          return reader.release();
+        }
+
+        case ImageCompression_Jpeg:
+        {
+          std::auto_ptr<Orthanc::JpegReader> reader(new Orthanc::JpegReader);
+          reader->ReadFromMemory(source);
+          return reader.release();
+        }
+
+        case ImageCompression_Jpeg2000:
+        {
+          std::auto_ptr<Jpeg2000Reader> reader(new Jpeg2000Reader);
+          reader->ReadFromMemory(source);
+          return reader.release();
+        }
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+
+    void EncodeTile(std::string& target,
+                    const Orthanc::ImageAccessor& source,
+                    ImageCompression compression,
+                    uint8_t quality)
+    {
+      if (compression == ImageCompression_None)
+      {
+        unsigned int pitch = GetBytesPerPixel(source.GetFormat()) * source.GetWidth();
+        target.resize(pitch * source.GetHeight());
+
+        for (unsigned int i = 0; i < source.GetHeight(); i++)
+        {
+          memcpy(&target[i * pitch], source.GetConstRow(i), pitch);
+        }
+      }
+      else
+      {
+        std::auto_ptr<Orthanc::IImageWriter> writer;
+
+        switch (compression)
+        {
+          case ImageCompression_Png:
+            writer.reset(new Orthanc::PngWriter);
+            break;
+
+          case ImageCompression_Jpeg:
+            writer.reset(new Orthanc::JpegWriter);
+            dynamic_cast<Orthanc::JpegWriter&>(*writer).SetQuality(quality);
+            break;
+
+          case ImageCompression_Jpeg2000:
+            writer.reset(new Jpeg2000Writer);
+            break;
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+
+        writer->WriteToMemory(target, source);
+      }
+    }
+
+
+    void ChangeTileCompression(std::string& target,
+                               const std::string& source,
+                               ImageCompression sourceCompression,
+                               ImageCompression targetCompression,
+                               uint8_t quality)
+    {
+      if (sourceCompression == targetCompression)
+      {
+        target = source;
+      }
+      else
+      {
+        std::auto_ptr<Orthanc::ImageAccessor> decoded(DecodeTile(source, sourceCompression));
+        EncodeTile(target, *decoded, targetCompression, quality);
+      }
+    }
+
+
+    static uint8_t GetPixelValue(const Orthanc::ImageAccessor& source,
+                                 unsigned int x,
+                                 unsigned int y,
+                                 unsigned int channel,
+                                 int offsetX,
+                                 int offsetY)
+    {
+      assert(channel < source.GetBytesPerPixel());
+      assert(source.GetFormat() == Orthanc::PixelFormat_Grayscale8 ||
+             source.GetFormat() == Orthanc::PixelFormat_RGB24 ||
+             source.GetFormat() == Orthanc::PixelFormat_RGBA32);  // 16bpp is unsupported
+
+      if (static_cast<int>(x) + offsetX < 0)
+      {
+        x = 0;
+      }
+      else
+      {
+        x += offsetX;
+        if (x >= source.GetWidth())
+        {
+          x = source.GetWidth() - 1;
+        }
+      }
+
+      if (static_cast<int>(y) + offsetY < 0)
+      {
+        y = 0;
+      }
+      else
+      {
+        y += offsetY;
+        if (y >= source.GetHeight())
+        {
+          y = source.GetHeight() - 1;
+        }
+      }
+
+      return *(reinterpret_cast<const uint8_t*>(source.GetConstBuffer()) +
+               y * source.GetPitch() + x * source.GetBytesPerPixel() + channel);
+    }
+
+
+    static uint8_t SmoothPixelValue(const Orthanc::ImageAccessor& source,
+                                    unsigned int x,
+                                    unsigned int y,
+                                    unsigned int channel)
+    {
+      static const uint32_t kernel[5] = { 1, 4, 6, 4, 1 };
+      static const uint32_t normalization = 2 * (1 + 4 + 6 + 4 + 1);
+
+      uint32_t accumulator = 0;
+
+      // Horizontal smoothing
+      for (int offset = -2; offset <= 2; offset++)
+      {
+        accumulator += kernel[offset + 2] * GetPixelValue(source, x, y, channel, offset, 0);
+      }
+
+      // Vertical smoothing
+      for (int offset = -2; offset <= 2; offset++)
+      {
+        accumulator += kernel[offset + 2] * GetPixelValue(source, x, y, channel, 0, offset);
+      }
+
+      return static_cast<uint8_t>(accumulator / normalization);
+    }
+
+
+    Orthanc::ImageAccessor* Halve(const Orthanc::ImageAccessor& source,
+                                  bool smooth)
+    {
+      if (source.GetWidth() % 2 == 1 ||
+          source.GetHeight() % 2 == 1)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize);
+      }
+
+      if (source.GetFormat() != Orthanc::PixelFormat_Grayscale8 &&
+          source.GetFormat() != Orthanc::PixelFormat_RGB24 &&
+          source.GetFormat() != Orthanc::PixelFormat_RGBA32)  // 16bpp is not supported (*)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+
+      unsigned int channelsCount = source.GetBytesPerPixel();  // OK tx (*)
+
+      std::auto_ptr<Orthanc::ImageAccessor> result(Allocate(source.GetFormat(), 
+                                                            source.GetWidth() / 2, 
+                                                            source.GetHeight() / 2));
+
+      for (unsigned int y = 0; y < source.GetHeight() / 2; y++)
+      {
+        uint8_t* q = reinterpret_cast<uint8_t*>(result->GetRow(y));
+
+        for (unsigned int x = 0; x < source.GetWidth() / 2; x++, q += 3)
+        {
+          for (unsigned int c = 0; c < channelsCount; c++)
+          {
+            if (smooth)
+            {
+              q[c] = SmoothPixelValue(source, 2 * x, 2 * y, c);
+            }
+            else
+            {
+              q[c] = GetPixelValue(source, 2 * x, 2 * y, c, 0, 0);
+            }
+          }
+        }
+      }
+
+      return result.release();
+    }
+
+
+    Orthanc::ImageAccessor* Clone(const Orthanc::ImageAccessor& accessor)
+    {
+      std::auto_ptr<Orthanc::ImageAccessor> result(Allocate(accessor.GetFormat(),
+                                                            accessor.GetWidth(),
+                                                            accessor.GetHeight()));
+      Embed(*result, accessor, 0, 0);
+
+      return result.release();
+    }
+
+
+    Orthanc::ImageAccessor* Render(ITiledPyramid& pyramid,
+                                   unsigned int level)
+    {
+      std::auto_ptr<Orthanc::ImageAccessor> result(Allocate(pyramid.GetPixelFormat(), 
+                                                            pyramid.GetLevelWidth(level),
+                                                            pyramid.GetLevelHeight(level)));
+
+      LOG(INFO) << "Rendering a tiled image of size " << result->GetWidth() << "x" << result->GetHeight();
+
+      for (unsigned int y = 0; y < result->GetHeight(); y += pyramid.GetTileHeight())
+      {
+        for (unsigned int x = 0; x < result->GetWidth(); x += pyramid.GetTileWidth())
+        {
+          std::auto_ptr<Orthanc::ImageAccessor> tile(pyramid.DecodeTile(level,
+                                                                        x / pyramid.GetTileWidth(),
+                                                                        y / pyramid.GetTileHeight()));
+          Embed(*result, *tile, x, y);
+        }
+      }
+
+      return result.release();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/ImageToolbox.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,75 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "Orthanc/Core/Images/ImageAccessor.h"
+#include "Enumerations.h"
+#include "Inputs/ITiledPyramid.h"
+
+#include <stdint.h>
+
+namespace OrthancWSI
+{
+  namespace ImageToolbox
+  {
+    Orthanc::ImageAccessor* Allocate(Orthanc::PixelFormat format,
+                                     unsigned int width,
+                                     unsigned int height);
+
+    void Embed(Orthanc::ImageAccessor& target,
+               const Orthanc::ImageAccessor& source,
+               unsigned int x,
+               unsigned int y);
+
+    inline void Copy(Orthanc::ImageAccessor& target,
+                     const Orthanc::ImageAccessor& source)
+    {
+      Embed(target, source, 0, 0);
+    }
+
+    void Set(Orthanc::ImageAccessor& image,
+             uint8_t r,
+             uint8_t g,
+             uint8_t b);
+
+    Orthanc::ImageAccessor* DecodeTile(const std::string& source,
+                                       ImageCompression compression);
+    
+    void EncodeTile(std::string& target,
+                    const Orthanc::ImageAccessor& source,
+                    ImageCompression compression,
+                    uint8_t quality);  // Only for JPEG compression
+
+    void ChangeTileCompression(std::string& target,
+                               const std::string& source,
+                               ImageCompression sourceCompression,
+                               ImageCompression targetCompression,
+                               uint8_t quality);  // Only for JPEG compression
+
+    Orthanc::ImageAccessor* Halve(const Orthanc::ImageAccessor& source,
+                                  bool smooth);
+
+    Orthanc::ImageAccessor* Clone(const Orthanc::ImageAccessor& accessor);
+
+    Orthanc::ImageAccessor* Render(ITiledPyramid& pyramid,
+                                   unsigned int level);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/ImagedVolumeParameters.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,88 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "ImagedVolumeParameters.h"
+
+#include "Orthanc/Core/OrthancException.h"
+
+namespace OrthancWSI
+{
+  ImagedVolumeParameters::ImagedVolumeParameters()
+  {
+    // Typical parameters of a specimen millimeters
+    width_ = 15;
+    height_ = 15;
+    depth_ = 1;
+    offsetX_ = 20;
+    offsetY_ = 40;
+  }
+
+
+  void ImagedVolumeParameters::SetWidth(float width)
+  {
+    if (width <= 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    width_ = width;
+  }
+    
+
+  void ImagedVolumeParameters::SetHeight(float height)
+  {
+    if (height <= 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    height_ = height;
+  }
+
+    
+  void ImagedVolumeParameters::SetDepth(float depth)
+  {
+    if (depth <= 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    depth_ = depth;
+  }
+    
+
+  void ImagedVolumeParameters::GetLocation(float& physicalX,
+                                           float& physicalY,
+                                           unsigned int imageX,
+                                           unsigned int imageY,
+                                           unsigned int totalWidth,
+                                           unsigned int totalHeight) const
+  {
+    if (imageX >= totalWidth ||
+        imageY >= totalHeight)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    // WARNING: The physical X/Y axes are switched wrt. the image X/Y
+    physicalX = offsetX_ - GetHeight() * static_cast<float>(imageX) / static_cast<float>(totalWidth);
+    physicalY = offsetY_ - GetWidth() * static_cast<float>(imageY) / static_cast<float>(totalHeight);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/ImagedVolumeParameters.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,85 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+namespace OrthancWSI
+{
+  class ImagedVolumeParameters
+  {
+  private:
+    float  width_;
+    float  height_;
+    float  depth_;
+    float  offsetX_;
+    float  offsetY_;
+
+  public:
+    ImagedVolumeParameters();
+
+    float GetWidth() const
+    {
+      return width_;
+    }
+    
+    float GetHeight() const
+    {
+      return height_;
+    }
+    
+    float GetDepth() const
+    {
+      return depth_;
+    }
+
+    float GetOffsetX() const
+    {
+      return offsetX_;
+    }
+
+    float GetOffsetY() const
+    {
+      return offsetY_;
+    }
+
+    void SetWidth(float width);
+    
+    void SetHeight(float height);
+    
+    void SetDepth(float depth);
+    
+    void SetOffsetX(float offset)
+    {
+      offsetX_ = offset;
+    }
+    
+    void SetOffsetY(float offset)
+    {
+      offsetY_ = offset;
+    }
+
+    void GetLocation(float& physicalX,
+                     float& physicalY,
+                     unsigned int imageX,
+                     unsigned int imageY,
+                     unsigned int totalWidth,
+                     unsigned int totalHeight) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/DecodedTiledPyramid.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,117 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "DecodedTiledPyramid.h"
+
+#include "../ImageToolbox.h"
+
+#include <memory>
+#include <cassert>
+
+namespace OrthancWSI
+{
+  DecodedTiledPyramid::DecodedTiledPyramid()
+  {
+    SetBackgroundColor(255, 255, 255);
+  }
+
+
+  void DecodedTiledPyramid::SetBackgroundColor(uint8_t red,
+                                               uint8_t green,
+                                               uint8_t blue)
+  {
+    backgroundColor_[0] = red;
+    backgroundColor_[1] = green;
+    backgroundColor_[2] = blue;
+  }
+
+
+  void DecodedTiledPyramid::GetBackgroundColor(uint8_t& red,
+                                               uint8_t& green,
+                                               uint8_t& blue) const
+  {
+    red = backgroundColor_[0];
+    green = backgroundColor_[1];
+    blue = backgroundColor_[2];
+  }
+
+
+  Orthanc::ImageAccessor* DecodedTiledPyramid::DecodeTile(unsigned int level,
+                                                          unsigned int tileX,
+                                                          unsigned int tileY)
+  {
+    unsigned int x = tileX * GetTileWidth();
+    unsigned int y = tileY * GetTileHeight();
+
+    std::auto_ptr<Orthanc::ImageAccessor> tile
+      (ImageToolbox::Allocate(GetPixelFormat(), GetTileWidth(), GetTileHeight()));
+
+    if (x >= GetLevelWidth(level) ||
+        y >= GetLevelHeight(level))   // (*)
+    {
+      ImageToolbox::Set(*tile, backgroundColor_[0], backgroundColor_[1], backgroundColor_[2]);
+      return tile.release();
+    }
+
+    bool fit = true;
+    unsigned int regionWidth;
+    if (x + GetTileWidth() <= GetLevelWidth(level))
+    {
+      regionWidth = GetTileWidth();
+    }
+    else
+    {
+      assert(GetLevelWidth(level) >= x);   // This results from (*)
+      regionWidth = GetLevelWidth(level) - x;
+      fit = false;
+    }
+    
+    unsigned int regionHeight;
+    if (y + GetTileHeight() <= GetLevelHeight(level))
+    {
+      regionHeight = GetTileHeight();
+    }
+    else
+    {
+      assert(GetLevelHeight(level) >= y);   // This results from (*)
+      regionHeight = GetLevelHeight(level) - y;
+      fit = false;
+    }
+
+    if (fit)
+    {
+      // The tile entirely lies inside the image
+      ReadRegion(*tile, level, x, y);
+    }
+    else
+    {
+      // The tile exceeds the size of image, decode it to a temporary buffer
+      std::auto_ptr<Orthanc::ImageAccessor> cropped
+        (ImageToolbox::Allocate(GetPixelFormat(), regionWidth, regionHeight));
+      ReadRegion(*cropped, level, x, y);
+
+      // Create a white tile, and fill it with the cropped content
+      ImageToolbox::Set(*tile, backgroundColor_[0], backgroundColor_[1], backgroundColor_[2]);
+      ImageToolbox::Embed(*tile, *cropped, 0, 0);
+    }
+
+    return tile.release();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/DecodedTiledPyramid.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,75 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "ITiledPyramid.h"
+
+
+namespace OrthancWSI
+{
+  /**
+   * This class acts as a wrapper for "cropped" tiled images, where
+   * the tiles at the right or at the bottom might not have the same
+   * dimensions of the other slides.
+   **/
+  class DecodedTiledPyramid : public ITiledPyramid
+  {
+  private:
+    uint8_t  backgroundColor_[3];
+
+  protected:
+    // Subclasses can assume that the requested region is fully inside
+    // the image, and that target has the proper size to store the
+    // region. Pay attention to implement mutual exclusion in subclasses.
+    virtual void ReadRegion(Orthanc::ImageAccessor& target,
+                            unsigned int level,
+                            unsigned int x,
+                            unsigned int y) = 0;
+
+  public:
+    DecodedTiledPyramid();
+
+    void SetBackgroundColor(uint8_t red,
+                            uint8_t green,
+                            uint8_t blue);
+
+    void GetBackgroundColor(uint8_t& red,
+                            uint8_t& green,
+                            uint8_t& blue) const;
+
+    virtual Orthanc::ImageAccessor* DecodeTile(unsigned int level,
+                                               unsigned int tileX,
+                                               unsigned int tileY);
+
+    virtual ImageCompression GetImageCompression() const
+    {
+      return ImageCompression_None;
+    }
+
+    virtual bool ReadRawTile(std::string& tile,
+                             unsigned int level,
+                             unsigned int tileX,
+                             unsigned int tileY)
+    {
+      return false;   // No access to the raw tiles
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/DicomPyramid.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,237 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "DicomPyramid.h"
+
+#include "../DicomToolbox.h"
+#include "../Orthanc/Core/Logging.h"
+#include "../Orthanc/Core/OrthancException.h"
+
+#include <algorithm>
+#include <cassert>
+
+namespace OrthancWSI
+{
+  struct DicomPyramid::Comparator
+  {
+    bool operator() (DicomPyramidInstance* const& a,
+                     DicomPyramidInstance* const& b) const
+    {
+      return a->GetTotalWidth() > b->GetTotalWidth();
+    }
+  };
+
+
+  void DicomPyramid::Clear()
+  {
+    for (size_t i = 0; i < levels_.size(); i++)
+    {
+      if (levels_[i] != NULL)
+      {
+        delete levels_[i];
+      }
+    }
+
+    for (size_t i = 0; i < instances_.size(); i++)
+    {
+      if (instances_[i] != NULL)
+      {
+        delete instances_[i];
+      }
+    }
+  }
+
+
+  void DicomPyramid::RegisterInstances(const std::string& seriesId)
+  {
+    Json::Value series;
+    IOrthancConnection::RestApiGet(series, orthanc_, "/series/" + seriesId);
+
+    if (series.type() != Json::objectValue)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+    }
+
+    const Json::Value& instances = DicomToolbox::GetSequenceTag(series, "Instances");
+    instances_.reserve(instances.size());
+
+    for (Json::Value::ArrayIndex i = 0; i < instances.size(); i++)
+    {
+      if (instances[i].type() != Json::stringValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+      }
+
+      std::string instance = instances[i].asString();
+
+      try
+      {
+        instances_.push_back(new DicomPyramidInstance(orthanc_, instance));
+      }
+      catch (Orthanc::OrthancException&)
+      {
+        LOG(ERROR) << "Skipping a DICOM instance that is not part of a whole-slide image: " << instance;
+      }
+    }
+  }
+
+
+  void DicomPyramid::Check(const std::string& seriesId) const
+  {
+    if (instances_.empty())
+    {
+      LOG(ERROR) << "This series does not contain a whole-slide image: " << seriesId;
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+    }
+
+    const DicomPyramidInstance& a = *instances_[0];
+
+    for (size_t i = 1; i < instances_.size(); i++)
+    {
+      const DicomPyramidInstance& b = *instances_[i];
+
+      if (a.GetImageCompression() != b.GetImageCompression() ||
+          a.GetPixelFormat() != b.GetPixelFormat() ||
+          a.GetTileWidth() != b.GetTileWidth() ||
+          a.GetTileHeight() != b.GetTileHeight() ||
+          a.GetTotalWidth() < b.GetTotalWidth() ||
+          a.GetTotalHeight() < b.GetTotalHeight())            
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+      }
+
+      if (a.GetTotalWidth() == b.GetTotalWidth() &&
+          a.GetTotalHeight() != b.GetTotalHeight())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+      }
+    }
+  }
+
+
+  void DicomPyramid::CheckLevel(size_t level) const
+  {
+    if (level >= levels_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  DicomPyramid::DicomPyramid(IOrthancConnection& orthanc,
+                             const std::string& seriesId) :
+    orthanc_(orthanc),
+    seriesId_(seriesId)
+  {
+    RegisterInstances(seriesId);
+
+    // Sort the instances of the pyramid by decreasing total widths
+    std::sort(instances_.begin(), instances_.end(), Comparator());
+
+    try
+    {
+      Check(seriesId);
+    }
+    catch (Orthanc::OrthancException&)
+    {
+      Clear();
+      throw;
+    }
+
+    for (size_t i = 0; i < instances_.size(); i++)
+    {
+      if (i == 0 ||
+          instances_[i - 1]->GetTotalWidth() != instances_[i]->GetTotalWidth())
+      {
+        levels_.push_back(new DicomPyramidLevel(*instances_[i]));
+      }
+      else
+      {
+        assert(levels_.back() != NULL);
+        levels_.back()->AddInstance(*instances_[i]);
+      }
+    }
+  }
+
+
+  unsigned int DicomPyramid::GetLevelWidth(unsigned int level) const
+  {
+    CheckLevel(level);
+    return levels_[level]->GetTotalWidth();
+  }
+
+
+  unsigned int DicomPyramid::GetLevelHeight(unsigned int level) const
+  {
+    CheckLevel(level);
+    return levels_[level]->GetTotalHeight();
+  }
+
+
+  unsigned int DicomPyramid::GetTileWidth() const
+  {
+    assert(!levels_.empty() && levels_[0] != NULL);
+    return levels_[0]->GetTileWidth();
+  }
+
+
+  unsigned int DicomPyramid::GetTileHeight() const
+  {
+    assert(!levels_.empty() && levels_[0] != NULL);
+    return levels_[0]->GetTileHeight();
+  }
+
+
+  bool DicomPyramid::ReadRawTile(std::string& tile,
+                                 unsigned int level,
+                                 unsigned int tileX,
+                                 unsigned int tileY)
+  {
+    CheckLevel(level);
+      
+    ImageCompression compression;
+    Orthanc::PixelFormat format;
+      
+    if (levels_[level]->DownloadRawTile(compression, format, tile, orthanc_, tileX, tileY))
+    {
+      assert(compression == GetImageCompression() &&
+             format == GetPixelFormat());
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  ImageCompression DicomPyramid::GetImageCompression() const
+  {
+    assert(!instances_.empty() && instances_[0] != NULL);
+    return instances_[0]->GetImageCompression();
+  }
+
+
+  Orthanc::PixelFormat DicomPyramid::GetPixelFormat() const
+  {
+    assert(!instances_.empty() && instances_[0] != NULL);
+    return instances_[0]->GetPixelFormat();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/DicomPyramid.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,83 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "PyramidWithRawTiles.h"
+#include "DicomPyramidInstance.h"
+#include "DicomPyramidLevel.h"
+
+namespace OrthancWSI
+{
+  class DicomPyramid : public PyramidWithRawTiles
+  {
+  private:
+    struct Comparator;
+
+    IOrthancConnection&                 orthanc_;
+    std::string                         seriesId_;
+    std::vector<DicomPyramidInstance*>  instances_;
+    std::vector<DicomPyramidLevel*>     levels_;
+
+    void Clear();
+
+    void RegisterInstances(const std::string& seriesId);
+
+    void Check(const std::string& seriesId) const;
+
+    void CheckLevel(size_t level) const;
+
+  public:
+    DicomPyramid(IOrthancConnection& orthanc,
+                 const std::string& seriesId);
+
+    virtual ~DicomPyramid()
+    {
+      Clear();
+    }
+
+    const std::string& GetSeriesId() const
+    {
+      return seriesId_;
+    }
+
+    virtual unsigned int GetLevelCount() const
+    {
+      return levels_.size();
+    }
+
+    virtual unsigned int GetLevelWidth(unsigned int level) const;
+
+    virtual unsigned int GetLevelHeight(unsigned int level) const;
+
+    virtual unsigned int GetTileWidth() const;
+
+    virtual unsigned int GetTileHeight() const;
+
+    virtual bool ReadRawTile(std::string& tile,
+                             unsigned int level,
+                             unsigned int tileX,
+                             unsigned int tileY);
+
+    virtual ImageCompression GetImageCompression() const;
+
+    virtual Orthanc::PixelFormat GetPixelFormat() const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/DicomPyramidInstance.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,170 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "DicomPyramidInstance.h"
+
+#include "../Orthanc/Core/Logging.h"
+#include "../Orthanc/Core/OrthancException.h"
+#include "../Orthanc/Core/Toolbox.h"
+#include "../DicomToolbox.h"
+
+#include <cassert>
+
+namespace OrthancWSI
+{
+  static ImageCompression DetectImageCompression(const Json::Value& header)
+  {
+    std::string s = Orthanc::Toolbox::StripSpaces
+      (DicomToolbox::GetMandatoryStringTag(header, "TransferSyntaxUID"));
+
+    if (s == "1.2.840.10008.1.2" ||
+        s == "1.2.840.10008.1.2.1")
+    {
+      return ImageCompression_None;
+    }
+    else if (s == "1.2.840.10008.1.2.4.50")
+    {
+      return ImageCompression_Jpeg;
+    }
+    else if (s == "1.2.840.10008.1.2.4.90" ||
+             s == "1.2.840.10008.1.2.4.91")
+    {
+      return ImageCompression_Jpeg2000;
+    }
+    else
+    {
+      LOG(ERROR) << "Unsupported transfer syntax: " << s;
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+  }
+
+
+  static Orthanc::PixelFormat DetectPixelFormat(const Json::Value& dicom)
+  {
+    std::string photometric = Orthanc::Toolbox::StripSpaces
+      (DicomToolbox::GetMandatoryStringTag(dicom, "PhotometricInterpretation"));
+
+    if (photometric == "PALETTE")
+    {
+      LOG(ERROR) << "Unsupported photometric interpretation: " << photometric;
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+
+    unsigned int bitsStored = DicomToolbox::GetUnsignedIntegerTag(dicom, "BitsStored");
+    unsigned int samplesPerPixel = DicomToolbox::GetUnsignedIntegerTag(dicom, "SamplesPerPixel");
+    bool isSigned = (DicomToolbox::GetUnsignedIntegerTag(dicom, "PixelRepresentation") != 0);
+
+    if (bitsStored == 8 &&
+        samplesPerPixel == 1 &&
+        !isSigned)
+    {
+      return Orthanc::PixelFormat_Grayscale8;
+    }
+    else if (bitsStored == 8 &&
+             samplesPerPixel == 3 &&
+             !isSigned)
+    {
+      return Orthanc::PixelFormat_RGB24;
+    }
+    else
+    {
+      LOG(ERROR) << "Unsupported pixel format";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+  }
+
+  
+  DicomPyramidInstance::DicomPyramidInstance(IOrthancConnection&  orthanc,
+                                             const std::string& instanceId) :
+    instanceId_(instanceId)
+  {
+    Json::Value dicom, header;
+    IOrthancConnection::RestApiGet(dicom, orthanc, "/instances/" + instanceId + "/tags?simplify");
+    IOrthancConnection::RestApiGet(header, orthanc, "/instances/" + instanceId + "/header?simplify");
+
+    if (DicomToolbox::GetMandatoryStringTag(dicom, "SOPClassUID") != "1.2.840.10008.5.1.4.1.1.77.1.6" ||
+        DicomToolbox::GetMandatoryStringTag(dicom, "Modality") != "SM")
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    compression_ = DetectImageCompression(header);
+    format_ = DetectPixelFormat(dicom);
+    tileWidth_ = DicomToolbox::GetUnsignedIntegerTag(dicom, "Columns");
+    tileHeight_ = DicomToolbox::GetUnsignedIntegerTag(dicom, "Rows");
+    totalWidth_ = DicomToolbox::GetUnsignedIntegerTag(dicom, "TotalPixelMatrixColumns");
+    totalHeight_ = DicomToolbox::GetUnsignedIntegerTag(dicom, "TotalPixelMatrixRows");
+
+    const Json::Value& frames = DicomToolbox::GetSequenceTag(dicom, "PerFrameFunctionalGroupsSequence");
+
+    if (frames.size() != DicomToolbox::GetUnsignedIntegerTag(dicom, "NumberOfFrames"))
+    {
+      LOG(ERROR) << "Mismatch between the number of frames in instance: " << instanceId;
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+
+    frames_.resize(frames.size());
+
+    for (Json::Value::ArrayIndex i = 0; i < frames.size(); i++)
+    {
+      const Json::Value& frame = DicomToolbox::GetSequenceTag(frames[i], "PlanePositionSlideSequence");
+      if (frame.size() != 1)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+
+      // "-1", because coordinates are shifted by 1 in DICOM
+      int xx = DicomToolbox::GetIntegerTag(frame[0], "ColumnPositionInTotalImagePixelMatrix") - 1;
+      int yy = DicomToolbox::GetIntegerTag(frame[0], "RowPositionInTotalImagePixelMatrix") - 1;
+
+      unsigned int x = static_cast<unsigned int>(xx);
+      unsigned int y = static_cast<unsigned int>(yy);
+
+      if (xx < 0 || 
+          yy < 0 ||
+          x % tileWidth_ != 0 ||
+          y % tileHeight_ != 0 ||
+          x >= totalWidth_ ||
+          y >= totalHeight_)
+      {
+        LOG(ERROR) << "Frame " << i << " with unexpected tile location (" 
+                   << x << "," << y << ") in instance: " << instanceId;
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+
+      frames_[i].first = x / tileWidth_;
+      frames_[i].second = y / tileHeight_;
+    }
+  }
+
+
+  unsigned int DicomPyramidInstance::GetFrameLocationX(size_t frame) const
+  {
+    assert(frame < frames_.size());
+    return frames_[frame].first;
+  }
+
+
+  unsigned int DicomPyramidInstance::GetFrameLocationY(size_t frame) const
+  {
+    assert(frame < frames_.size());
+    return frames_[frame].second;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/DicomPyramidInstance.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,93 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "../Enumerations.h"
+#include "../Messaging/IOrthancConnection.h"
+
+#include <boost/noncopyable.hpp>
+#include <vector>
+
+namespace OrthancWSI
+{
+  class DicomPyramidInstance : public boost::noncopyable
+  {
+  private:
+    typedef std::pair<unsigned int, unsigned int>  FrameLocation;
+
+    std::string                 instanceId_;
+    ImageCompression            compression_;
+    Orthanc::PixelFormat        format_;
+    unsigned int                tileWidth_;
+    unsigned int                tileHeight_;
+    unsigned int                totalWidth_;
+    unsigned int                totalHeight_;
+    std::vector<FrameLocation>  frames_;
+
+  public:
+    DicomPyramidInstance(IOrthancConnection&  orthanc,
+                         const std::string& instanceId);
+
+    const std::string& GetInstanceId() const
+    {
+      return instanceId_;
+    }
+
+    ImageCompression GetImageCompression() const
+    {
+      return compression_;
+    }
+
+    Orthanc::PixelFormat GetPixelFormat() const
+    {
+      return format_;
+    }
+
+    unsigned int GetTotalWidth() const
+    {
+      return totalWidth_;
+    }
+
+    unsigned int GetTotalHeight() const
+    {
+      return totalHeight_;
+    }
+
+    unsigned int GetTileWidth() const
+    {
+      return tileWidth_;
+    }
+
+    unsigned int GetTileHeight() const
+    {
+      return tileHeight_;
+    }
+
+    size_t GetFrameCount() const
+    {
+      return frames_.size();
+    }
+
+    unsigned int GetFrameLocationX(size_t frame) const;
+
+    unsigned int GetFrameLocationY(size_t frame) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/DicomPyramidLevel.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,128 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "DicomPyramidLevel.h"
+
+#include "../Orthanc/Core/Logging.h"
+#include "../Orthanc/Core/OrthancException.h"
+
+#include <boost/lexical_cast.hpp>
+
+namespace OrthancWSI
+{
+  void DicomPyramidLevel::RegisterFrame(const DicomPyramidInstance& instance,
+                                        unsigned int frame)
+  {
+    TileLocation location(instance.GetFrameLocationX(frame), 
+                          instance.GetFrameLocationY(frame));
+
+    if (tiles_.find(location) != tiles_.end())
+    {
+      LOG(ERROR) << "Tile with location (" << location.first << "," 
+                 << location.second << ") is indexed twice in level of size "
+                 << totalWidth_ << "x" << totalHeight_;
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+
+    tiles_[location] = std::make_pair(&instance, frame);
+  }
+
+
+  bool DicomPyramidLevel::LookupTile(TileContent& tile,
+                                     unsigned int tileX,
+                                     unsigned int tileY) const
+  {
+    Tiles::const_iterator found = tiles_.find(std::make_pair(tileX, tileY));
+    if (found == tiles_.end())
+    {
+      return false;
+    }
+    else
+    {
+      tile = found->second;
+      return true;
+    }
+  }
+
+
+  DicomPyramidLevel::DicomPyramidLevel(const DicomPyramidInstance& instance) :
+    totalWidth_(instance.GetTotalWidth()),
+    totalHeight_(instance.GetTotalHeight()),
+    tileWidth_(instance.GetTileWidth()),
+    tileHeight_(instance.GetTileHeight())
+  {
+    if (totalWidth_ == 0 ||
+        totalHeight_ == 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+      
+    AddInstance(instance);
+  }
+
+
+  void DicomPyramidLevel::AddInstance(const DicomPyramidInstance& instance)
+  {
+    if (instance.GetTotalWidth() != totalWidth_ ||
+        instance.GetTotalHeight() != totalHeight_ ||
+        instance.GetTileWidth() != tileWidth_ ||
+        instance.GetTileHeight() != tileHeight_)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize);
+    }
+
+    instances_.push_back(&instance);
+
+    for (size_t frame = 0; frame < instance.GetFrameCount(); frame++)
+    {
+      RegisterFrame(instance, frame);
+    }
+  }
+
+
+  bool DicomPyramidLevel::DownloadRawTile(ImageCompression& compression /* out */,
+                                          Orthanc::PixelFormat& format /* out */,
+                                          std::string& raw /* out */,
+                                          IOrthancConnection& orthanc,
+                                          unsigned int tileX,
+                                          unsigned int tileY) const
+  {
+    TileContent tile;
+    if (LookupTile(tile, tileX, tileY))
+    {
+      assert(tile.first != NULL);
+      const DicomPyramidInstance& instance = *tile.first;
+
+      std::string uri = ("/instances/" + instance.GetInstanceId() + 
+                         "/frames/" + boost::lexical_cast<std::string>(tile.second) + "/raw");
+
+      orthanc.RestApiGet(raw, uri);
+
+      compression = instance.GetImageCompression();
+      format = instance.GetPixelFormat();
+
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/DicomPyramidLevel.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,83 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "DicomPyramidInstance.h"
+
+#include <list>
+
+namespace OrthancWSI
+{
+  class DicomPyramidLevel : public boost::noncopyable
+  {
+  private:
+    typedef std::pair<unsigned int, unsigned int>                 TileLocation;
+    typedef std::pair<const DicomPyramidInstance*, unsigned int>  TileContent;
+    typedef std::map<TileLocation, TileContent>                   Tiles;
+    typedef std::list<const DicomPyramidInstance*>                Instances;
+
+    unsigned int   totalWidth_;
+    unsigned int   totalHeight_;
+    unsigned int   tileWidth_;
+    unsigned int   tileHeight_;
+    Instances      instances_;
+    Tiles          tiles_;
+
+    void RegisterFrame(const DicomPyramidInstance& instance,
+                       unsigned int frame);
+
+    bool LookupTile(TileContent& tile,
+                    unsigned int tileX,
+                    unsigned int tileY) const;
+
+  public:
+    DicomPyramidLevel(const DicomPyramidInstance& instance);
+
+    void AddInstance(const DicomPyramidInstance& instance);
+
+    unsigned int GetTotalWidth() const
+    {
+      return totalWidth_;
+    }
+
+    unsigned int GetTotalHeight() const
+    {
+      return totalHeight_;
+    }
+
+    unsigned int GetTileWidth() const
+    {
+      return tileWidth_;
+    }
+
+    unsigned int GetTileHeight() const
+    {
+      return tileHeight_;
+    }
+
+    bool DownloadRawTile(ImageCompression& compression /* out */,
+                         Orthanc::PixelFormat& format /* out */,
+                         std::string& raw /* out */,
+                         IOrthancConnection& orthanc,
+                         unsigned int tileX,
+                         unsigned int tileY) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/HierarchicalTiff.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,307 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "HierarchicalTiff.h"
+
+#include "../Orthanc/Core/Logging.h"
+#include "../Orthanc/Core/OrthancException.h"
+
+#include <iostream>
+#include <algorithm>
+#include <cassert>
+#include <string.h>
+
+namespace OrthancWSI
+{
+  HierarchicalTiff::Level::Level(TIFF* tiff,
+                                 tdir_t    directory,
+                                 unsigned int  width,
+                                 unsigned int  height) :
+    directory_(directory),
+    width_(width),
+    height_(height)
+  {
+    // Read the JPEG headers shared at that level, if any
+    uint8_t *tables = NULL;
+    uint32_t size;
+    if (TIFFGetField(tiff, TIFFTAG_JPEGTABLES, &size, &tables) &&
+        size > 0 && 
+        tables != NULL)
+    {
+      // Look for the EOI (end-of-image) tag == FF D9
+      // https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format
+
+      bool found = false;
+
+      for (size_t i = 0; i + 1 < size; i++)
+      {
+        if (tables[i] == 0xff &&
+            tables[i + 1] == 0xd9)
+        {
+          headers_.assign(reinterpret_cast<const char*>(tables), i);
+          found = true;
+        }
+      }
+
+      if (!found)
+      {
+        headers_.assign(reinterpret_cast<const char*>(tables), size);
+      }
+    }
+  }
+
+  struct HierarchicalTiff::Comparator
+  {
+    bool operator() (const HierarchicalTiff::Level& a,
+                     const HierarchicalTiff::Level& b) const
+    {
+      return a.width_ > b.width_;
+    }
+  };
+
+
+  void HierarchicalTiff::Finalize()
+  {
+    if (tiff_)
+    {
+      TIFFClose(tiff_);
+      tiff_ = NULL;
+    }
+  }
+
+
+  void HierarchicalTiff::CheckLevel(unsigned int level) const
+  {
+    if (level >= levels_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool HierarchicalTiff::GetCurrentCompression(ImageCompression& compression)
+  {
+    uint16_t c;
+    if (!TIFFGetField(tiff_, TIFFTAG_COMPRESSION, &c))
+    {
+      return false;
+    }
+
+    switch (c)
+    {
+      case COMPRESSION_NONE:
+        compression = ImageCompression_None;
+        return true;
+
+      case COMPRESSION_JPEG:
+        compression = ImageCompression_Jpeg;
+        return true;
+
+      default:
+        return false;
+    }
+  }
+
+
+  bool HierarchicalTiff::GetCurrentPixelFormat(Orthanc::PixelFormat& pixelFormat,
+                                               ImageCompression compression)
+  {
+    // http://www.awaresystems.be/imaging/tiff/tifftags/baseline.html
+
+    uint16_t channels, photometric, bpp, planar;
+    if (!TIFFGetField(tiff_, TIFFTAG_SAMPLESPERPIXEL, &channels) ||
+        channels == 0 ||
+        !TIFFGetField(tiff_, TIFFTAG_PHOTOMETRIC, &photometric) ||
+        !TIFFGetField(tiff_, TIFFTAG_BITSPERSAMPLE, &bpp) ||
+        !TIFFGetField(tiff_, TIFFTAG_PLANARCONFIG, &planar))
+    {
+      return false;
+    }
+
+    if (compression == ImageCompression_Jpeg &&
+        channels == 3 &&     // This is a color image
+        bpp == 8 &&
+        photometric == PHOTOMETRIC_YCBCR &&
+        planar == PLANARCONFIG_CONTIG)  // This is interleaved RGB
+    {
+      pixelFormat = Orthanc::PixelFormat_RGB24;
+    }
+    else
+    {
+      return false;
+    }
+
+    return true;
+  }
+
+
+  bool  HierarchicalTiff::Initialize()
+  {
+    bool first = true;
+    tdir_t pos = 0;
+
+    do
+    {
+      uint32_t w, h, tw, th;
+      ImageCompression compression;
+      Orthanc::PixelFormat pixelFormat;
+
+      if (TIFFSetDirectory(tiff_, pos) &&
+          TIFFGetField(tiff_, TIFFTAG_IMAGEWIDTH, &w) &&
+          TIFFGetField(tiff_, TIFFTAG_IMAGELENGTH, &h) &&
+          TIFFGetField(tiff_, TIFFTAG_TILEWIDTH, &tw) &&
+          TIFFGetField(tiff_, TIFFTAG_TILELENGTH, &th) &&
+          w > 0 &&
+          h > 0 &&
+          tw > 0 &&
+          th > 0 &&
+          GetCurrentCompression(compression) &&
+          GetCurrentPixelFormat(pixelFormat, compression))
+      {
+        if (first)
+        {
+          tileWidth_ = tw;
+          tileHeight_ = th;
+          compression_ = compression;
+          pixelFormat_ = pixelFormat;
+          first = false;
+        }
+        else if (tw != tileWidth_ ||
+                 th != tileHeight_ ||
+                 compression_ != compression ||
+                 pixelFormat_ != pixelFormat)
+        {
+          LOG(ERROR) << "The tile size or compression of the TIFF file varies along levels, this is not supported";
+          return false;
+        }
+
+        levels_.push_back(Level(tiff_, pos, w, h));
+      }
+
+      pos++;
+    }
+    while (TIFFReadDirectory(tiff_));
+
+    if (levels_.size() == 0)
+    {
+      LOG(ERROR) << "This is not a tiled TIFF image";
+      return false;
+    }
+
+    std::sort(levels_.begin(), levels_.end(), Comparator());
+    return true;
+  }
+
+
+  HierarchicalTiff::HierarchicalTiff(const std::string& path) :
+    tileWidth_(0),
+    tileHeight_(0)
+  {
+    tiff_ = TIFFOpen(path.c_str(), "r");
+    if (tiff_ == NULL)
+    {
+      LOG(ERROR) << "libtiff cannot open: " << path;
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentFile);
+    }
+
+    if (!Initialize())
+    {
+      Finalize();
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  unsigned int HierarchicalTiff::GetLevelWidth(unsigned int level) const
+  {
+    CheckLevel(level);
+    return levels_[level].width_;
+  }
+
+
+  unsigned int HierarchicalTiff::GetLevelHeight(unsigned int level) const
+  {
+    CheckLevel(level);
+    return levels_[level].height_;
+  }
+
+
+  bool HierarchicalTiff::ReadRawTile(std::string& tile,
+                                     unsigned int level,
+                                     unsigned int tileX,
+                                     unsigned int tileY)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    CheckLevel(level);
+
+    // Make the TIFF context point to the level of interest
+    if (!TIFFSetDirectory(tiff_, levels_[level].directory_))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_CorruptedFile);
+    }
+
+    // Get the index of the tile
+    ttile_t index = TIFFComputeTile(tiff_, tileX * tileWidth_, tileY * tileHeight_, 0 /*z*/, 0 /*sample*/);
+
+    // Read the raw tile
+    toff_t *sizes;
+    if (!TIFFGetField(tiff_, TIFFTAG_TILEBYTECOUNTS, &sizes))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_CorruptedFile);
+    }
+
+    std::string raw;
+    raw.resize(sizes[index]);
+
+    tsize_t read = TIFFReadRawTile(tiff_, index, &raw[0], raw.size());
+    if (read != static_cast<tsize_t>(sizes[index]))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_CorruptedFile);
+    }
+
+    const std::string& headers = levels_[level].headers_;
+
+    // Possibly prepend the raw tile with the shared JPEG headers
+    if (headers.empty() ||
+        compression_ != ImageCompression_Jpeg)
+    {
+      tile.swap(raw);   // Same as "tile.assign(raw)", but optimizes memory
+    }
+    else
+    {
+      assert(compression_ == ImageCompression_Jpeg);
+
+      // Check that the raw JPEG tile starts with the SOI (start-of-image) tag == FF D8
+      if (raw.size() < 2 ||
+          static_cast<uint8_t>(raw[0]) != 0xff ||
+          static_cast<uint8_t>(raw[1]) != 0xd8)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_CorruptedFile);
+      }
+
+      tile.resize(headers.size() + raw.size() - 2);
+      memcpy(&tile[0], &headers[0], headers.size());
+      memcpy(&tile[0] + headers.size(), &raw[2], raw.size() - 2);
+    }
+
+    return true;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/HierarchicalTiff.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,111 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "PyramidWithRawTiles.h"
+
+#include <tiff.h>
+#include <tiffio.h>
+#include <vector>
+#include <boost/thread.hpp>
+
+namespace OrthancWSI
+{
+  class HierarchicalTiff : public PyramidWithRawTiles
+  {
+  private:
+    struct Level
+    {
+      tdir_t    directory_;
+      unsigned int  width_;
+      unsigned int  height_;
+      std::string  headers_;
+
+      Level(TIFF* tiff,
+            tdir_t    directory,
+            unsigned int  width,
+            unsigned int  height);
+    };
+
+    struct Comparator;
+
+    boost::mutex          mutex_;
+    TIFF*                 tiff_;
+    Orthanc::PixelFormat  pixelFormat_;
+    ImageCompression      compression_;
+    unsigned int          tileWidth_;
+    unsigned int          tileHeight_;
+    std::vector<Level>    levels_;
+
+    void Finalize();
+
+    void CheckLevel(unsigned int level) const;
+
+    bool GetCurrentCompression(ImageCompression& compression);
+
+    bool GetCurrentPixelFormat(Orthanc::PixelFormat& pixelFormat,
+                               ImageCompression compression);
+
+    bool Initialize();
+
+  public:
+    HierarchicalTiff(const std::string& path);
+
+    virtual ~HierarchicalTiff()
+    {
+      Finalize();
+    }
+
+    virtual unsigned int GetLevelCount() const
+    {
+      return levels_.size();
+    }
+
+    virtual unsigned int GetLevelWidth(unsigned int level) const;
+
+    virtual unsigned int GetLevelHeight(unsigned int level) const;
+
+    virtual unsigned int GetTileWidth() const
+    {
+      return tileWidth_;
+    }
+
+    virtual unsigned int GetTileHeight() const
+    {
+      return tileHeight_;
+    }
+
+    virtual bool ReadRawTile(std::string& tile,
+                             unsigned int level,
+                             unsigned int tileX,
+                             unsigned int tileY);
+
+    virtual ImageCompression GetImageCompression() const
+    {
+      return compression_;
+    }
+
+    virtual Orthanc::PixelFormat GetPixelFormat() const
+    {
+      return pixelFormat_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/ITiledPyramid.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,68 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "../Enumerations.h"
+
+#include "../Orthanc/Core/Images/ImageAccessor.h"
+
+#include <boost/noncopyable.hpp>
+#include <string>
+
+
+namespace OrthancWSI
+{
+  /**
+   * Class that represents a whole-slide image. It is assumed to be
+   * thread-safe, which is the case of libtiff and openslide.
+   **/
+  class ITiledPyramid : public boost::noncopyable
+  {
+  public:
+    virtual ~ITiledPyramid()
+    {
+    }
+
+    virtual unsigned int GetLevelCount() const = 0;
+
+    virtual unsigned int GetLevelWidth(unsigned int level) const = 0;
+
+    virtual unsigned int GetLevelHeight(unsigned int level) const = 0;
+
+    virtual unsigned int GetTileWidth() const = 0;
+
+    virtual unsigned int GetTileHeight() const = 0;
+
+    virtual bool ReadRawTile(std::string& tile,
+                             unsigned int level,
+                             unsigned int tileX,
+                             unsigned int tileY) = 0;
+
+    virtual Orthanc::ImageAccessor* DecodeTile(unsigned int level,
+                                               unsigned int tileX,
+                                               unsigned int tileY) = 0;
+
+    // Only makes sense for images with raw access to tiles
+    virtual ImageCompression GetImageCompression() const = 0;
+
+    virtual Orthanc::PixelFormat GetPixelFormat() const = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/OpenSlideLibrary.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,233 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "OpenSlideLibrary.h"
+
+#include "../Orthanc/Core/Logging.h"
+#include "../Orthanc/Core/Images/Image.h"
+
+namespace OrthancWSI
+{
+  static std::auto_ptr<OpenSlideLibrary>  globalLibrary_;
+
+
+  OpenSlideLibrary::OpenSlideLibrary(const std::string& path) :
+    library_(path)
+  {
+    close_ = (FunctionClose) library_.GetFunction("openslide_close");
+    getLevelCount_ = (FunctionGetLevelCount) library_.GetFunction("openslide_get_level_count");
+    getLevelDimensions_ = (FunctionGetLevelDimensions) library_.GetFunction("openslide_get_level_dimensions");
+    getLevelDownsample_ = (FunctionGetLevelDownsample) library_.GetFunction("openslide_get_level_downsample");
+    open_ = (FunctionOpen) library_.GetFunction("openslide_open");
+    readRegion_ = (FunctionReadRegion) library_.GetFunction("openslide_read_region");
+  }
+
+
+  OpenSlideLibrary::Image::Level::Level() : 
+    width_(0),
+    height_(0), 
+    downsample_(1)
+  {
+  }
+
+
+  OpenSlideLibrary::Image::Level::Level(int64_t width,
+                                        int64_t height,
+                                        double downsample) :
+    width_(static_cast<unsigned int>(width)),
+    height_(static_cast<unsigned int>(height)),
+    downsample_(downsample)
+  {
+    if (width < 0 ||
+        height < 0 ||
+        downsample <= 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+
+    if (static_cast<int64_t>(width_) != width ||
+        static_cast<int64_t>(height_) != height)
+    {
+      LOG(ERROR) << "The whole-slide image is too large";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  }
+
+
+  void OpenSlideLibrary::Image::Initialize(const std::string& path)
+  {
+    handle_ = that_.open_(path.c_str());
+    if (handle_ == NULL)
+    {
+      LOG(ERROR) << "Cannot open an image with OpenSlide: " << path;
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+
+    try
+    {
+      LOG(INFO) << "Opening an image with OpenSlide: " << path;
+
+      int32_t tmp = that_.getLevelCount_(handle_);
+      if (tmp <= 0)
+      {
+        LOG(ERROR) << "Image with no pyramid level";
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+
+      levels_.resize(tmp);
+
+      for (int32_t level = 0; level < tmp; level++)
+      {
+        int64_t width, height;
+        that_.getLevelDimensions_(handle_, level, &width, &height);
+
+        double downsample = that_.getLevelDownsample_(handle_, level);
+
+        levels_[level] = Level(width, height, downsample);
+      }
+
+      for (size_t i = 1; i < levels_.size(); i++)
+      {
+        if (levels_[i].width_ >= levels_[i - 1].width_ ||
+            levels_[i].height_ >= levels_[i - 1].height_)
+        {
+          // This is not a pyramid with levels of decreasing sizes
+          // (level "0" must be the finest level)
+          LOG(ERROR) << "The pyramid does not have levels of strictly decreasing sizes";
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+      }
+    }
+    catch (Orthanc::OrthancException&)
+    {
+      Close();
+      throw;
+    }
+  }
+
+
+  OpenSlideLibrary::Image::Image(OpenSlideLibrary& that,
+                                 const std::string& path) :
+    that_(that),
+    handle_(NULL)
+  {
+    Initialize(path);
+  }
+
+
+  OpenSlideLibrary::Image::Image(const std::string& path) :
+    that_(OpenSlideLibrary::GetInstance()),
+    handle_(NULL)
+  {
+    Initialize(path);
+  }
+
+
+  void OpenSlideLibrary::Image::Close()
+  {
+    if (handle_ != NULL)
+    {
+      that_.close_(handle_);
+      handle_ = NULL;
+    }
+  }
+
+
+  void OpenSlideLibrary::Image::CheckLevel(unsigned int level) const
+  {
+    if (level >= levels_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  double OpenSlideLibrary::Image::GetLevelDownsample(unsigned int level) const
+  {
+    CheckLevel(level);
+    return levels_[level].downsample_;
+  }
+
+
+  unsigned int OpenSlideLibrary::Image::GetLevelWidth(unsigned int level) const
+  {
+    CheckLevel(level);
+    return levels_[level].width_;
+  }
+
+
+  unsigned int OpenSlideLibrary::Image::GetLevelHeight(unsigned int level) const
+  {
+    CheckLevel(level);
+    return levels_[level].height_;
+  }
+
+
+  Orthanc::ImageAccessor* OpenSlideLibrary::Image::ReadRegion(unsigned int level,
+                                                              uint64_t x,
+                                                              uint64_t y,
+                                                              unsigned int width,
+                                                              unsigned int height)
+  {
+    CheckLevel(level);
+
+    // Create a new image, with minimal pitch so as to be compatible with OpenSlide API
+    std::auto_ptr<Orthanc::ImageAccessor> region(new Orthanc::Image(Orthanc::PixelFormat_RGBA32, width, height, true));
+
+    if (region->GetWidth() != 0 &&
+        region->GetHeight() != 0)
+    {
+      double zoom = levels_[level].downsample_;
+      x = static_cast<uint64_t>(zoom * static_cast<double>(x));
+      y = static_cast<uint64_t>(zoom * static_cast<double>(y));
+          
+      that_.readRegion_(handle_, reinterpret_cast<uint32_t*>(region->GetBuffer()),
+                        x, y, level, region->GetWidth(), region->GetHeight());
+    }
+
+    return region.release();
+  }
+
+
+  OpenSlideLibrary& OpenSlideLibrary::GetInstance()
+  {
+    if (globalLibrary_.get() == NULL)
+    {
+      LOG(ERROR) << "OpenSlide has not been initialized";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *globalLibrary_;
+    }
+  }
+
+
+  void OpenSlideLibrary::Initialize(const std::string& path)
+  {
+    globalLibrary_.reset(new OpenSlideLibrary(path));
+  }
+
+
+  void OpenSlideLibrary::Finalize()
+  {
+    globalLibrary_.reset(NULL);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/OpenSlideLibrary.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,112 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "../Orthanc/Plugins/Engine/SharedLibrary.h"
+#include "../Orthanc/Core/Images/ImageAccessor.h"
+
+#include <vector>
+
+namespace OrthancWSI
+{
+  class OpenSlideLibrary : public boost::noncopyable
+  {
+  private:
+    typedef void*   (*FunctionClose) (void*);
+    typedef int32_t (*FunctionGetLevelCount) (void*);
+    typedef void    (*FunctionGetLevelDimensions) (void*, int32_t, int64_t*, int64_t*);
+    typedef double  (*FunctionGetLevelDownsample) (void*, int32_t);
+    typedef void*   (*FunctionOpen) (const char*);
+    typedef void    (*FunctionReadRegion) (void*, uint32_t*, int64_t, int64_t, int32_t, int64_t, int64_t);
+
+    Orthanc::SharedLibrary      library_;
+    FunctionClose               close_;
+    FunctionGetLevelCount       getLevelCount_;
+    FunctionGetLevelDimensions  getLevelDimensions_;
+    FunctionGetLevelDownsample  getLevelDownsample_;
+    FunctionOpen                open_;
+    FunctionReadRegion          readRegion_;
+
+  public:
+    OpenSlideLibrary(const std::string& path);
+
+    static OpenSlideLibrary& GetInstance();
+
+    static void Initialize(const std::string& path);
+
+    static void Finalize();
+
+    class Image : public boost::noncopyable
+    {
+    private:
+      struct Level
+      {
+        unsigned int  width_;
+        unsigned int  height_;
+        double        downsample_;
+
+        Level();
+
+        Level(int64_t width,
+              int64_t height,
+              double downsample);
+      };
+
+      OpenSlideLibrary&   that_;
+      void*               handle_;
+      std::vector<Level>  levels_;
+
+      void Initialize(const std::string& path);
+
+      void Close();
+
+      void CheckLevel(unsigned int level) const;
+
+    public:
+      Image(OpenSlideLibrary& that,
+            const std::string& path);
+
+      Image(const std::string& path);
+
+      ~Image()
+      {
+        Close();
+      }
+
+      unsigned int GetLevelCount() const
+      {
+        return levels_.size();
+      }
+
+      double GetLevelDownsample(unsigned int level) const;
+
+      unsigned int GetLevelWidth(unsigned int level) const;
+
+      unsigned int GetLevelHeight(unsigned int level) const;
+
+      Orthanc::ImageAccessor* ReadRegion(unsigned int level,
+                                         uint64_t x,
+                                         uint64_t y,
+                                         unsigned int width,
+                                         unsigned int height);
+    };
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/OpenSlidePyramid.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,47 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "OpenSlidePyramid.h"
+
+#include "../Orthanc/Core/Images/ImageProcessing.h"
+#include "../Orthanc/Core/OrthancException.h"
+#include "../Orthanc/Core/Logging.h"
+
+namespace OrthancWSI
+{
+  void OpenSlidePyramid::ReadRegion(Orthanc::ImageAccessor& target,
+                                    unsigned int level,
+                                    unsigned int x,
+                                    unsigned int y)
+  {
+    std::auto_ptr<Orthanc::ImageAccessor> source(image_.ReadRegion(level, x, y, target.GetWidth(), target.GetHeight()));
+    Orthanc::ImageProcessing::Convert(target, *source);
+  }
+
+
+  OpenSlidePyramid::OpenSlidePyramid(const std::string& path,
+                                     unsigned int tileWidth,
+                                     unsigned int tileHeight) :
+    image_(path),
+    tileWidth_(tileWidth),
+    tileHeight_(tileHeight)
+  {
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/OpenSlidePyramid.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,76 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "DecodedTiledPyramid.h"
+#include "OpenSlideLibrary.h"
+
+namespace OrthancWSI
+{
+  class OpenSlidePyramid : public DecodedTiledPyramid
+  {
+  private:
+    OpenSlideLibrary::Image  image_;
+    unsigned int             tileWidth_;
+    unsigned int             tileHeight_;
+
+  protected:
+    virtual void ReadRegion(Orthanc::ImageAccessor& target,
+                            unsigned int level,
+                            unsigned int x,
+                            unsigned int y);
+
+  public:
+    OpenSlidePyramid(const std::string& path,
+                     unsigned int tileWidth,
+                     unsigned int tileHeight);
+
+    virtual unsigned int GetTileWidth() const
+    {
+      return tileWidth_;
+    }
+
+    virtual unsigned int GetTileHeight() const
+    {
+      return tileHeight_;
+    }
+
+    virtual unsigned int GetLevelCount() const
+    {
+      return image_.GetLevelCount();
+    }
+
+    virtual unsigned int GetLevelWidth(unsigned int level) const
+    {
+      return image_.GetLevelWidth(level);
+    }
+
+    virtual unsigned int GetLevelHeight(unsigned int level) const
+    {
+      return image_.GetLevelHeight(level);
+    }
+
+    virtual Orthanc::PixelFormat GetPixelFormat() const
+    {
+      return Orthanc::PixelFormat_RGB24;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/PyramidWithRawTiles.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,74 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "PyramidWithRawTiles.h"
+
+#include "../Orthanc/Core/Images/PngReader.h"
+#include "../Orthanc/Core/Images/JpegReader.h"
+#include "../Orthanc/Core/OrthancException.h"
+#include "../Jpeg2000Reader.h"
+
+namespace OrthancWSI
+{
+  Orthanc::ImageAccessor* PyramidWithRawTiles::DecodeTile(unsigned int level,
+                                                          unsigned int tileX,
+                                                          unsigned int tileY)
+  {
+    std::string tile;
+    if (!ReadRawTile(tile, level, tileX, tileY))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    std::auto_ptr<Orthanc::ImageAccessor> result;
+
+    switch (GetImageCompression())
+    {
+      case ImageCompression_None:
+        result.reset(new Orthanc::ImageAccessor);
+        result->AssignReadOnly(GetPixelFormat(), 
+                               GetTileWidth(),
+                               GetTileHeight(), 
+                               GetBytesPerPixel(GetPixelFormat()) * GetTileWidth(),
+                               tile.c_str());
+        break;
+
+      case ImageCompression_Jpeg:
+        result.reset(new Orthanc::JpegReader);
+        dynamic_cast<Orthanc::JpegReader&>(*result).ReadFromMemory(tile);
+        break;
+
+      case ImageCompression_Png:
+        result.reset(new Orthanc::PngReader);
+        dynamic_cast<Orthanc::PngReader&>(*result).ReadFromMemory(tile);
+        break;
+
+      case ImageCompression_Jpeg2000:
+        result.reset(new Jpeg2000Reader);
+        dynamic_cast<Jpeg2000Reader&>(*result).ReadFromMemory(tile);
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+
+    return result.release();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/PyramidWithRawTiles.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,34 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "ITiledPyramid.h"
+
+namespace OrthancWSI
+{
+  class PyramidWithRawTiles : public ITiledPyramid
+  {
+  public:
+    virtual Orthanc::ImageAccessor* DecodeTile(unsigned int level,
+                                               unsigned int tileX,
+                                               unsigned int tileY);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/SingleLevelDecodedPyramid.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,58 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "SingleLevelDecodedPyramid.h"
+#include "../ImageToolbox.h"
+
+#include "../Orthanc/Core/OrthancException.h"
+
+namespace OrthancWSI
+{
+  void SingleLevelDecodedPyramid::ReadRegion(Orthanc::ImageAccessor& target,
+                                             unsigned int level,
+                                             unsigned int x,
+                                             unsigned int y)
+  {
+    Orthanc::ImageAccessor region = image_.GetRegion(x, y, target.GetWidth(), target.GetHeight());
+    ImageToolbox::Copy(target, region);
+  }
+
+
+  unsigned int SingleLevelDecodedPyramid::GetLevelWidth(unsigned int level) const
+  {
+    if (level != 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    return image_.GetWidth();
+  }
+
+
+  unsigned int SingleLevelDecodedPyramid::GetLevelHeight(unsigned int level) const
+  {
+    if (level != 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    return image_.GetHeight();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/SingleLevelDecodedPyramid.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,77 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "DecodedTiledPyramid.h"
+
+namespace OrthancWSI
+{
+  class SingleLevelDecodedPyramid : public DecodedTiledPyramid
+  {
+  private:
+    Orthanc::ImageAccessor  image_;
+    unsigned int            tileWidth_;
+    unsigned int            tileHeight_;
+
+  protected:
+    void SetImage(const Orthanc::ImageAccessor& image)
+    {
+      image_ = image;
+    }
+
+    virtual void ReadRegion(Orthanc::ImageAccessor& target,
+                            unsigned int level,
+                            unsigned int x,
+                            unsigned int y);
+
+  public:
+    SingleLevelDecodedPyramid(unsigned int tileWidth,
+                              unsigned int tileHeight) :
+      tileWidth_(tileWidth),
+      tileHeight_(tileHeight)
+    {
+    }
+
+    virtual unsigned int GetTileWidth() const
+    {
+      return tileWidth_;
+    }
+
+    virtual unsigned int GetTileHeight() const
+    {
+      return tileHeight_;
+    }
+
+    virtual unsigned int GetLevelCount() const
+    {
+      return 1;
+    }
+
+    virtual unsigned int GetLevelWidth(unsigned int level) const;
+
+    virtual unsigned int GetLevelHeight(unsigned int level) const;
+
+    virtual Orthanc::PixelFormat GetPixelFormat() const
+    {
+      return image_.GetFormat();
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/TiledJpegImage.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,44 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "SingleLevelDecodedPyramid.h"
+
+#include "../Orthanc/Core/Images/JpegReader.h"
+
+namespace OrthancWSI
+{
+  class TiledJpegImage : public SingleLevelDecodedPyramid
+  {
+  private:
+    Orthanc::JpegReader  reader_;
+
+  public:
+    TiledJpegImage(const std::string& path,
+                   unsigned int tileWidth,
+                   unsigned int tileHeight) :
+      SingleLevelDecodedPyramid(tileWidth, tileHeight)
+    {
+      reader_.ReadFromFile(path);
+      SetImage(reader_);
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/TiledPngImage.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,44 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "SingleLevelDecodedPyramid.h"
+
+#include "../Orthanc/Core/Images/PngReader.h"
+
+namespace OrthancWSI
+{
+  class TiledPngImage : public SingleLevelDecodedPyramid
+  {
+  private:
+    Orthanc::PngReader  reader_;
+
+  public:
+    TiledPngImage(const std::string& path,
+                  unsigned int tileWidth,
+                  unsigned int tileHeight) :
+      SingleLevelDecodedPyramid(tileWidth, tileHeight)
+    {
+      reader_.ReadFromFile(path);
+      SetImage(reader_);
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/TiledPyramidStatistics.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,73 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "TiledPyramidStatistics.h"
+
+#include "../Orthanc/Core/Logging.h"
+
+
+namespace OrthancWSI
+{
+  TiledPyramidStatistics::TiledPyramidStatistics(ITiledPyramid& source) :
+    source_(source),
+    countRawAccesses_(0),
+    countDecodedTiles_(0)
+  {
+  }
+
+
+  TiledPyramidStatistics::~TiledPyramidStatistics()
+  {
+    LOG(WARNING) << "Closing the input image (" 
+                 << countRawAccesses_ << " raw accesses to the tiles, "
+                 << countDecodedTiles_ << " decoded tiles)";
+  }
+
+
+  bool TiledPyramidStatistics::ReadRawTile(std::string& tile,
+                                           unsigned int level,
+                                           unsigned int tileX,
+                                           unsigned int tileY)
+  {
+    if (source_.ReadRawTile(tile, level, tileX, tileY))
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      countRawAccesses_++;
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  Orthanc::ImageAccessor* TiledPyramidStatistics::DecodeTile(unsigned int level,
+                                                             unsigned int tileX,
+                                                             unsigned int tileY)
+  {
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      countDecodedTiles_++;
+    }
+
+    return source_.DecodeTile(level, tileX, tileY);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Inputs/TiledPyramidStatistics.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,86 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "ITiledPyramid.h"
+
+#include <boost/thread/mutex.hpp>
+
+namespace OrthancWSI
+{
+  class TiledPyramidStatistics : public ITiledPyramid
+  {
+  private:
+    boost::mutex   mutex_;
+    ITiledPyramid& source_;
+    unsigned int   countRawAccesses_;
+    unsigned int   countDecodedTiles_;
+
+  public:
+    TiledPyramidStatistics(ITiledPyramid& source);   // Takes ownership
+
+    virtual ~TiledPyramidStatistics();
+
+    virtual unsigned int GetLevelCount() const
+    {
+      return source_.GetLevelCount();
+    }
+
+    virtual unsigned int GetLevelWidth(unsigned int level)  const
+    {
+      return source_.GetLevelWidth(level);
+    }
+
+    virtual unsigned int GetLevelHeight(unsigned int level) const
+    {
+      return source_.GetLevelHeight(level);
+    }
+
+    virtual unsigned int GetTileWidth() const
+    {
+      return source_.GetTileWidth();
+    }
+    
+    virtual unsigned int GetTileHeight() const
+    {
+      return source_.GetTileHeight();
+    }
+
+    virtual ImageCompression GetImageCompression() const
+    {
+      return source_.GetImageCompression();
+    }
+
+    virtual Orthanc::PixelFormat GetPixelFormat() const
+    {
+      return source_.GetPixelFormat();
+    }
+
+    virtual bool ReadRawTile(std::string& tile,
+                             unsigned int level,
+                             unsigned int tileX,
+                             unsigned int tileY);
+
+    virtual Orthanc::ImageAccessor* DecodeTile(unsigned int level,
+                                               unsigned int tileX,
+                                               unsigned int tileY);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Jpeg2000Reader.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,490 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "Jpeg2000Reader.h"
+
+#include "Orthanc/Core/OrthancException.h"
+#include "Orthanc/Core/Toolbox.h"
+#include "ImageToolbox.h"
+
+#include <cassert>
+#include <string.h>
+#include <openjpeg.h>
+
+#if ORTHANC_OPENJPEG_MAJOR_VERSION == 1
+#  define OPJ_CLRSPC_GRAY CLRSPC_GRAY
+#  define OPJ_CLRSPC_SRGB CLRSPC_SRGB
+#  define OPJ_CODEC_J2K   CODEC_J2K
+#  define OPJ_CODEC_JP2   CODEC_JP2
+typedef opj_dinfo_t opj_codec_t;
+typedef opj_cio_t opj_stream_t;
+#elif ORTHANC_OPENJPEG_MAJOR_VERSION == 2
+#else
+#error Unsupported version of OpenJpeg
+#endif
+
+
+namespace OrthancWSI
+{
+  namespace
+  {
+    // Check out opj_dparameters_t::decod_format
+    enum InputFormat
+    {
+      InputFormat_J2K = 0,
+      InputFormat_JP2 = 1,
+      InputFormat_JPT = 2
+    };
+
+    enum OutputFormat
+    {
+#if ORTHANC_OPENJPEG_MAJOR_VERSION == 1
+      OutputFormat_PGX = 1
+#else
+      OutputFormat_PGX = 11
+#endif
+    };
+
+    class OpenJpegDecoder : public boost::noncopyable
+    {
+    private:
+      opj_dparameters_t  parameters_;
+      opj_codec_t* dinfo_;
+
+      void SetupParameters(InputFormat format)
+      {
+        opj_set_default_decoder_parameters(&parameters_);
+
+        parameters_.decod_format = format;
+        parameters_.cod_format = OutputFormat_PGX;
+
+#if OPENJPEG_MAJOR_VERSION == 1
+        parameters_.cp_layer = 0;
+        parameters_.cp_reduce = 0;
+#endif
+      }
+
+      void Finalize()
+      {
+        if (dinfo_ != NULL)
+        {
+#if ORTHANC_OPENJPEG_MAJOR_VERSION == 1
+          opj_destroy_decompress(dinfo_);
+#else
+          opj_destroy_codec(dinfo_);
+#endif
+          dinfo_ = NULL;
+        }
+      }
+
+    public:
+      OpenJpegDecoder(Jpeg2000Format format) : dinfo_(NULL)
+      {
+        switch (format)
+        {
+          case Jpeg2000Format_J2K:
+            SetupParameters(InputFormat_J2K);
+            dinfo_ = opj_create_decompress(OPJ_CODEC_J2K);
+            break;
+
+          case Jpeg2000Format_JP2:
+            SetupParameters(InputFormat_JP2);
+            dinfo_ = opj_create_decompress(OPJ_CODEC_JP2);
+            break;
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+
+        if (!dinfo_)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+
+#if ORTHANC_OPENJPEG_MAJOR_VERSION == 1
+        opj_setup_decoder(dinfo_, &parameters_);
+#else
+        if (!opj_setup_decoder(dinfo_, &parameters_))
+        {
+          Finalize();
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+#endif
+      }
+
+      ~OpenJpegDecoder()
+      {
+        Finalize();
+      } 
+
+      opj_codec_t* GetObject()
+      {
+        return dinfo_;
+      }
+
+      const opj_dparameters_t& GetParameters() const
+      {
+        return parameters_;
+      }
+    };
+
+
+    class OpenJpegInput : public boost::noncopyable
+    {
+    private:
+      opj_stream_t* cio_;
+
+      const uint8_t* buffer_;
+      size_t         size_;
+      size_t         position_;
+      
+#if ORTHANC_OPENJPEG_MAJOR_VERSION == 2
+      static void Free(void *userData)
+      {
+      }
+
+      static OPJ_SIZE_T Read(void *target, 
+                             OPJ_SIZE_T size, 
+                             void *userData)
+      {
+        OpenJpegInput& that = *reinterpret_cast<OpenJpegInput*>(userData);
+        assert(that.position_ >= 0 && that.position_ <= that.size_);
+        assert(size >= 0);
+
+        if (that.position_ == that.size_)
+        {
+          // End of file
+          return -1;
+        }
+        else
+        {
+          if (that.position_ + size > that.size_)
+          {
+            size = that.size_ - that.position_;
+          }
+
+          if (size > 0)
+          {
+            memcpy(target, that.buffer_ + that.position_, size);
+          }
+
+          that.position_ += size;
+          return size;
+        }
+      }
+ 
+      static OPJ_OFF_T Skip(OPJ_OFF_T skip, 
+                            void *userData)
+      {
+        assert(skip >= 0);
+        OpenJpegInput& that = *reinterpret_cast<OpenJpegInput*>(userData);
+
+        if (that.position_ == that.size_)
+        {
+          // End of file
+          return -1;
+        }
+        else if (that.position_ + skip > that.size_)
+        {
+          size_t offset = that.size_ - that.position_;
+          that.position_ = that.size_;
+          return offset;
+        }
+        else
+        {
+          that.position_ += skip;
+          return skip;
+        }
+      }
+ 
+      static OPJ_BOOL Seek(OPJ_OFF_T position, 
+                           void *userData)
+      {
+        assert(position >= 0);
+        OpenJpegInput& that = *reinterpret_cast<OpenJpegInput*>(userData);
+        
+        if (static_cast<size_t>(position) > that.size_)
+        {
+          that.position_ = that.size_;
+          return false;
+        }
+        else
+        {
+          that.position_ = position;
+          return true;
+        }
+      }
+#endif
+
+    public:
+      OpenJpegInput(OpenJpegDecoder& decoder,
+                    const void* buffer,
+                    size_t size) :
+        buffer_(reinterpret_cast<const uint8_t*>(buffer)),
+        size_(size),
+        position_(0)
+      {
+#if ORTHANC_OPENJPEG_MAJOR_VERSION == 1
+        cio_ = opj_cio_open(reinterpret_cast<opj_common_ptr>(decoder.GetObject()), 
+                            reinterpret_cast<unsigned char*>(const_cast<void*>(buffer)),
+                            size);
+#else
+        cio_ = opj_stream_create(size_, 1 /* input stream */);
+        if (!cio_)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+
+        // http://openjpeg.narkive.com/zHqG2fMe/opj-stream-set-user-data-length
+        // "I'd suggest to precise in the documentation that the skip
+        // and read callback functions should return -1 on end of
+        // stream, and the seek callback function should return false
+        // on end of stream."
+
+        opj_stream_set_user_data(cio_, this, Free);
+        opj_stream_set_user_data_length(cio_, size);
+        opj_stream_set_read_function(cio_, Read);
+        opj_stream_set_skip_function(cio_, Skip);
+        opj_stream_set_seek_function(cio_, Seek);
+#endif
+    }
+
+      ~OpenJpegInput()
+      {
+        if (cio_)
+        {
+ #if ORTHANC_OPENJPEG_MAJOR_VERSION == 1
+          opj_cio_close(cio_);
+#else
+          opj_stream_destroy(cio_);
+#endif
+          cio_ = NULL;
+        }
+      }
+
+      opj_stream_t* GetObject()
+      {
+        return cio_;
+      }
+    };
+
+
+    class OpenJpegImage
+    {
+    private:
+      opj_image_t* image_;
+
+      void CopyChannel(Orthanc::ImageAccessor& target,
+                       unsigned int channel,
+                       unsigned int targetIncrement)
+      {
+        int32_t* q = image_->comps[channel].data;
+        assert(q != NULL);
+
+        for (unsigned int y = 0; y < target.GetHeight(); y++)
+        {
+          uint8_t *p = reinterpret_cast<uint8_t*>(target.GetRow(y)) + channel;
+
+          for (unsigned int x = 0; x < target.GetWidth(); x++, p += targetIncrement)
+          {
+            *p = *q;
+            q++;
+          }
+        }        
+      }
+
+    public:
+      OpenJpegImage(OpenJpegDecoder& decoder,
+                    OpenJpegInput& input) :
+        image_(NULL)
+      {
+#if ORTHANC_OPENJPEG_MAJOR_VERSION == 1
+        image_ = opj_decode(decoder.GetObject(), input.GetObject());
+        if (image_ == NULL)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+#else
+        if (!opj_read_header(input.GetObject(), decoder.GetObject(), &image_) ||
+            image_ == NULL)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+
+        if (!opj_set_decode_area(decoder.GetObject(), image_, 
+                                 static_cast<int32_t>(decoder.GetParameters().DA_x0),
+                                 static_cast<int32_t>(decoder.GetParameters().DA_y0),
+                                 static_cast<int32_t>(decoder.GetParameters().DA_x1),
+                                 static_cast<int32_t>(decoder.GetParameters().DA_y1)) ||  // Decode the whole image
+            !opj_decode(decoder.GetObject(), input.GetObject(), image_) ||
+            !opj_end_decompress(decoder.GetObject(), input.GetObject()))
+        {
+          opj_image_destroy(image_);
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+#endif
+      }
+
+      ~OpenJpegImage()
+      {
+        if (image_ != NULL)
+        {
+          opj_image_destroy(image_);
+          image_ = NULL;
+        }
+      }
+
+      Orthanc::ImageAccessor* ProvideImage()
+      {
+        if (image_->x1 < 0 ||
+            image_->y1 < 0)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+
+        if (image_->x0 != 0 ||
+            image_->y0 != 0)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+        }
+
+        for (unsigned int c = 0; c < static_cast<unsigned int>(image_->numcomps); c++)
+        {
+          if (image_->comps[c].dx != 1 ||
+              image_->comps[c].dy != 1 ||
+              image_->comps[c].x0 != 0 ||
+              image_->comps[c].y0 != 0 ||
+              image_->comps[c].w != image_->x1 ||
+              image_->comps[c].h != image_->y1 ||
+              image_->comps[c].prec != 8 ||
+              image_->comps[c].sgnd != 0)
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+          }
+        }
+
+        unsigned int width = static_cast<unsigned int>(image_->x1);
+        unsigned int height = static_cast<unsigned int>(image_->y1);
+
+        Orthanc::PixelFormat format;
+        if (image_->numcomps == 1 && image_->color_space != OPJ_CLRSPC_GRAY)
+        {
+          format = Orthanc::PixelFormat_Grayscale8;
+        }
+        else if (image_->numcomps == 3 && image_->color_space != OPJ_CLRSPC_SRGB)
+        {
+          format = Orthanc::PixelFormat_RGB24;
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+        }
+
+        std::auto_ptr<Orthanc::ImageAccessor> image(ImageToolbox::Allocate(format, width, height));
+        
+        switch (format)
+        {
+          case Orthanc::PixelFormat_Grayscale8:
+          {
+            CopyChannel(*image, 0, 1);
+            break;
+          }
+
+          case Orthanc::PixelFormat_RGB24:
+          {
+            CopyChannel(*image, 0, 3);
+            CopyChannel(*image, 1, 3);
+            CopyChannel(*image, 2, 3);
+            break;
+          }
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+
+        return image.release();
+      }
+
+    };
+  }
+
+
+  void Jpeg2000Reader::ReadFromMemory(const void* buffer,
+                                      size_t size)
+  {
+    OpenJpegDecoder decoder(DetectFormatFromMemory(buffer, size));
+    OpenJpegInput input(decoder, buffer, size);
+    OpenJpegImage image(decoder, input);
+    
+    image_.reset(image.ProvideImage());
+    AssignReadOnly(image_->GetFormat(), 
+                   image_->GetWidth(),
+                   image_->GetHeight(), 
+                   image_->GetPitch(), 
+                   image_->GetConstBuffer());
+  }
+
+
+  void Jpeg2000Reader::ReadFromMemory(const std::string& buffer)
+  {
+    if (buffer.empty())
+    {
+      ReadFromMemory(NULL, 0);
+    }
+    else
+    {
+      ReadFromMemory(buffer.c_str(), buffer.size());
+    }
+  }
+
+  void Jpeg2000Reader::ReadFromFile(const std::string& filename)
+  {
+    // TODO Use opj_stream_create_file_stream() ?
+
+    std::string content;
+    Orthanc::Toolbox::ReadFile(content, filename);
+  }
+
+
+  Jpeg2000Format Jpeg2000Reader::DetectFormatFromMemory(const void* buffer,
+                                                        size_t size)
+  {
+    static const char JP2_RFC3745_HEADER[] = "\x00\x00\x00\x0c\x6a\x50\x20\x20\x0d\x0a\x87\x0a";
+    static const char JP2_HEADER[] = "\x0d\x0a\x87\x0a";
+    static const char J2K_HEADER[] = "\xff\x4f\xff\x51";
+
+    if (size < sizeof(JP2_RFC3745_HEADER) - 1)
+    {
+      return Jpeg2000Format_Unknown;
+    }
+     
+    if (memcmp(buffer, JP2_RFC3745_HEADER, sizeof(JP2_RFC3745_HEADER) - 1) == 0 || 
+        memcmp(buffer, JP2_HEADER, sizeof(JP2_HEADER) - 1) == 0)
+    {
+      return Jpeg2000Format_JP2;
+    }
+    else if (memcmp(buffer, J2K_HEADER, sizeof(J2K_HEADER) - 1) == 0)
+    {
+      return Jpeg2000Format_J2K;
+    }
+    
+    return Jpeg2000Format_Unknown;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Jpeg2000Reader.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,53 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "Orthanc/Core/Images/Image.h"
+#include <memory>
+
+namespace OrthancWSI
+{
+  enum Jpeg2000Format
+  {
+    Jpeg2000Format_J2K,
+    Jpeg2000Format_JP2,
+    Jpeg2000Format_Unknown
+  };
+
+  class Jpeg2000Reader : 
+    public Orthanc::ImageAccessor,
+    public boost::noncopyable
+  {
+  private:
+    std::auto_ptr<Orthanc::ImageAccessor> image_;
+
+  public:
+    void ReadFromFile(const std::string& filename);
+
+    void ReadFromMemory(const void* buffer,
+                        size_t size);
+
+    void ReadFromMemory(const std::string& buffer);
+
+    static Jpeg2000Format DetectFormatFromMemory(const void* buffer,
+                                                 size_t size);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Jpeg2000Writer.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,364 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "Jpeg2000Writer.h"
+
+#include "Orthanc/Core/ChunkedBuffer.h"
+#include "Orthanc/Core/OrthancException.h"
+
+#include <openjpeg.h>
+#include <string.h>
+#include <vector>
+
+#if ORTHANC_OPENJPEG_MAJOR_VERSION == 1
+#  define OPJ_CLRSPC_GRAY CLRSPC_GRAY
+#  define OPJ_CLRSPC_SRGB CLRSPC_SRGB
+#  define OPJ_CODEC_J2K   CODEC_J2K
+typedef opj_cinfo_t opj_codec_t;
+typedef opj_cio_t opj_stream_t;
+#elif ORTHANC_OPENJPEG_MAJOR_VERSION == 2
+#else
+#error Unsupported version of OpenJpeg
+#endif
+
+namespace OrthancWSI
+{
+  namespace
+  {
+    class OpenJpegImage : public boost::noncopyable
+    {
+    private:
+      std::vector<opj_image_cmptparm_t> components_;
+      COLOR_SPACE colorspace_;
+      opj_image_t* image_;
+
+      void SetupComponents(unsigned int width,
+                           unsigned int height,
+                           Orthanc::PixelFormat format)
+      {
+        switch (format)
+        {
+          case Orthanc::PixelFormat_Grayscale8:
+            colorspace_ = OPJ_CLRSPC_GRAY;
+            components_.resize(1);
+            break;
+
+          case Orthanc::PixelFormat_RGB24:
+            colorspace_ = OPJ_CLRSPC_SRGB;
+            components_.resize(3);
+            break;
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+
+        for (size_t i = 0; i < components_.size(); i++)
+        {
+          memset(&components_[i], 0, sizeof(opj_image_cmptparm_t));
+          components_[i].dx = 1;
+          components_[i].dy = 1;
+          components_[i].x0 = 0;
+          components_[i].y0 = 0;
+          components_[i].w = width;
+          components_[i].h = height;
+          components_[i].prec = 8;
+          components_[i].bpp = 8;
+          components_[i].sgnd = 0;
+        }
+      }
+
+      void CopyRGB24(unsigned int width,
+                     unsigned int height,
+                     unsigned int pitch,
+                     const void* buffer)
+      {
+        int32_t* r = image_->comps[0].data;
+        int32_t* g = image_->comps[1].data;
+        int32_t* b = image_->comps[2].data;
+
+        for (unsigned int y = 0; y < height; y++)
+        {
+          const uint8_t *p = reinterpret_cast<const uint8_t*>(buffer) + y * pitch;
+
+          for (unsigned int x = 0; x < width; x++)
+          {
+            *r = p[0];
+            *g = p[1];
+            *b = p[2];
+            p += 3;
+            r++;
+            g++;
+            b++;
+          }
+        }
+      }
+
+      void CopyGrayscale8(unsigned int width,
+                          unsigned int height,
+                          unsigned int pitch,
+                          const void* buffer)
+      {
+        int32_t* q = image_->comps[0].data;
+
+        for (unsigned int y = 0; y < height; y++)
+        {
+          const uint8_t *p = reinterpret_cast<const uint8_t*>(buffer) + y * pitch;
+
+          for (unsigned int x = 0; x < width; x++)
+          {
+            *q = *p;
+            p++;
+            q++;
+          }
+        }        
+      }
+
+    public:
+      OpenJpegImage(unsigned int width,
+                    unsigned int height,
+                    unsigned int pitch,
+                    Orthanc::PixelFormat format,
+                    const void* buffer) : image_(NULL)
+      {
+        SetupComponents(width, height, format);
+
+        image_ = opj_image_create(components_.size(), &components_[0], colorspace_);
+        if (!image_)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+
+        image_->x0 = 0;
+        image_->y0 = 0;
+        image_->x1 = width;
+        image_->y1 = height;
+
+        switch (format)
+        {
+          case Orthanc::PixelFormat_Grayscale8:
+            CopyGrayscale8(width, height, pitch, buffer);
+            break;
+
+          case Orthanc::PixelFormat_RGB24:
+            CopyRGB24(width, height, pitch, buffer);
+            break;
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+      }
+
+
+      ~OpenJpegImage()
+      {
+        if (image_)
+        {
+          opj_image_destroy(image_);
+          image_ = NULL;
+        }
+      }
+
+
+      opj_image_t* GetObject()
+      {
+        return image_;
+      }
+    };
+
+
+    class OpenJpegEncoder : public boost::noncopyable
+    {
+    private:
+      opj_codec_t* cinfo_;
+
+    public:
+      OpenJpegEncoder(opj_cparameters_t& parameters,
+                      OpenJpegImage& image) : cinfo_(NULL)
+      {
+        cinfo_ = opj_create_compress(OPJ_CODEC_J2K);
+        if (!cinfo_)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+
+        opj_setup_encoder(cinfo_, &parameters, image.GetObject());
+      }
+
+      ~OpenJpegEncoder()
+      {
+        if (cinfo_ != NULL)
+        {
+#if ORTHANC_OPENJPEG_MAJOR_VERSION == 1
+          opj_destroy_compress(cinfo_);
+#else
+          opj_destroy_codec(cinfo_);
+#endif
+          cinfo_ = NULL;
+        }
+      } 
+
+      opj_codec_t* GetObject()
+      {
+        return cinfo_;
+      }
+    };
+
+
+    class OpenJpegOutput : public boost::noncopyable
+    {
+    private:
+      opj_stream_t* cio_;
+
+#if ORTHANC_OPENJPEG_MAJOR_VERSION == 2
+      Orthanc::ChunkedBuffer buffer_;
+
+      static void Free(void *userData)
+      {
+      }
+
+      static OPJ_SIZE_T Write(void *buffer, 
+                              OPJ_SIZE_T size,
+                              void *userData)
+      {
+        OpenJpegOutput* that = reinterpret_cast<OpenJpegOutput*>(userData);
+        that->buffer_.AddChunk(reinterpret_cast<const char*>(buffer), size);
+        return size;
+      }
+#endif
+
+    public:
+      OpenJpegOutput(OpenJpegEncoder& encoder) : cio_(NULL)
+      {
+#if ORTHANC_OPENJPEG_MAJOR_VERSION == 1
+        cio_ = opj_cio_open(reinterpret_cast<opj_common_ptr>(encoder.GetObject()), NULL, 0);
+#else
+        cio_ = opj_stream_default_create(0 /* output stream */);
+        if (!cio_)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+
+        opj_stream_set_user_data(cio_, this, Free);
+        opj_stream_set_write_function(cio_, Write);
+#endif
+      }
+
+      ~OpenJpegOutput()
+      {
+        if (cio_)
+        {
+#if ORTHANC_OPENJPEG_MAJOR_VERSION == 1
+          opj_cio_close(cio_);
+#else
+          opj_stream_destroy(cio_);
+#endif
+          cio_ = NULL;
+        }
+      }
+
+      opj_stream_t* GetObject()
+      {
+        return cio_;
+      }
+
+      void Flatten(std::string& target)
+      {
+#if ORTHANC_OPENJPEG_MAJOR_VERSION == 1
+          target.assign(reinterpret_cast<const char*>(cio_->buffer), cio_tell(cio_));
+#else
+          buffer_.Flatten(target);
+#endif
+      }
+    };
+  }
+
+
+  static void SetupParameters(opj_cparameters_t& parameters,
+                              Orthanc::PixelFormat format,
+                              bool isLossless)
+  {
+    opj_set_default_encoder_parameters(&parameters);
+    parameters.cp_disto_alloc = 1;
+
+    if (isLossless)
+    {
+      parameters.tcp_numlayers = 1;
+      parameters.tcp_rates[0] = 0;
+    }
+    else
+    {
+      parameters.tcp_numlayers = 5;
+      parameters.tcp_rates[0] = 1920;
+      parameters.tcp_rates[1] = 480;
+      parameters.tcp_rates[2] = 120;
+      parameters.tcp_rates[3] = 30;
+      parameters.tcp_rates[4] = 10;
+      parameters.irreversible = 1;
+
+      if (format == Orthanc::PixelFormat_RGB24 ||
+          format == Orthanc::PixelFormat_RGBA32)
+      {
+        // This must be set to 1 if the number of color channels is >= 3
+        parameters.tcp_mct = 1;
+      }
+    }
+
+    parameters.cp_comment = const_cast<char*>("");
+  }
+
+
+  void Jpeg2000Writer::WriteToMemoryInternal(std::string& compressed,
+                                             unsigned int width,
+                                             unsigned int height,
+                                             unsigned int pitch,
+                                             Orthanc::PixelFormat format,
+                                             const void* buffer)
+  {
+    if (format != Orthanc::PixelFormat_Grayscale8 &&
+        format != Orthanc::PixelFormat_RGB24 &&
+        format != Orthanc::PixelFormat_RGBA32)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    opj_cparameters_t parameters;
+    SetupParameters(parameters, format, isLossless_);
+
+    OpenJpegImage image(width, height, pitch, format, buffer);
+    OpenJpegEncoder encoder(parameters, image);
+    OpenJpegOutput output(encoder);
+
+#if ORTHANC_OPENJPEG_MAJOR_VERSION == 1
+    if (!opj_encode(encoder.GetObject(), output.GetObject(), image.GetObject(), parameters.index))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+#else
+    if (!opj_start_compress(encoder.GetObject(), image.GetObject(), output.GetObject()) ||
+        !opj_encode(encoder.GetObject(), output.GetObject()) ||
+        !opj_end_compress(encoder.GetObject(), output.GetObject()))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+#endif
+
+    output.Flatten(compressed);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Jpeg2000Writer.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,55 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "Orthanc/Core/Images/IImageWriter.h"
+
+namespace OrthancWSI
+{
+  class Jpeg2000Writer : public Orthanc::IImageWriter
+  {
+  protected:
+    virtual void WriteToMemoryInternal(std::string& compressed,
+                                       unsigned int width,
+                                       unsigned int height,
+                                       unsigned int pitch,
+                                       Orthanc::PixelFormat format,
+                                       const void* buffer);
+
+  private:
+    bool  isLossless_;
+
+  public:
+    Jpeg2000Writer() : isLossless_(true)
+    {
+    }
+
+    void SetLossless(bool isLossless)
+    {
+      isLossless_ = isLossless;
+    }
+
+    bool IsLossless() const
+    {
+      return isLossless_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Messaging/CurlOrthancConnection.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,58 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "CurlOrthancConnection.h"
+
+#include "../Orthanc/Core/OrthancException.h"
+
+namespace OrthancWSI
+{
+  void CurlOrthancConnection::ApplyGet(std::string& result,
+                                       const std::string& uri)
+  {
+    Orthanc::HttpClient client(parameters_, uri);
+
+    // Don't follow 3xx HTTP (avoid redirections to "unsupported.png" in Orthanc)
+    client.SetRedirectionFollowed(false);  
+   
+    if (!client.Apply(result))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+    }
+  }
+
+  void CurlOrthancConnection::ApplyPost(std::string& result,
+                                        const std::string& uri,
+                                        const std::string& body)
+  {
+    Orthanc::HttpClient client(parameters_, uri);
+
+    client.SetMethod(Orthanc::HttpMethod_Post);
+    client.SetBody(body);
+
+    // Don't follow 3xx HTTP (avoid redirections to "unsupported.png" in Orthanc)
+    client.SetRedirectionFollowed(false);  
+   
+    if (!client.Apply(result))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Messaging/CurlOrthancConnection.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,53 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "OrthancConnectionBase.h"
+
+#include "../Orthanc/Core/HttpClient.h"
+
+namespace OrthancWSI
+{
+  class CurlOrthancConnection : public OrthancConnectionBase
+  {
+  private:
+    Orthanc::WebServiceParameters  parameters_;
+
+  protected:
+    virtual void ApplyGet(std::string& result,
+                          const std::string& uri);
+
+    virtual void ApplyPost(std::string& result,
+                           const std::string& uri,
+                           const std::string& body);
+
+  public:
+    CurlOrthancConnection(const Orthanc::WebServiceParameters& parameters) :
+      parameters_(parameters)
+    {
+    }
+
+    const Orthanc::WebServiceParameters& GetParameters() const
+    {
+      return parameters_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Messaging/FolderTarget.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,44 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "FolderTarget.h"
+
+#include "../Orthanc/Core/Toolbox.h"
+#include "../Orthanc/Core/Logging.h"
+
+#include <stdio.h>
+
+namespace OrthancWSI
+{
+  void FolderTarget::Write(const std::string& file)
+  {
+    std::string path;
+    path.resize(pattern_.size() + 16);
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      sprintf(&path[0], pattern_.c_str(), count_);
+      count_ += 1;
+    }
+
+    LOG(INFO) << "Writing file " << path;
+    Orthanc::Toolbox::WriteFile(file, path);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Messaging/FolderTarget.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,45 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "IFileTarget.h"
+
+#include <boost/thread.hpp>
+
+namespace OrthancWSI
+{
+  class FolderTarget : public IFileTarget
+  {
+  private:
+    boost::mutex  mutex_;
+    unsigned int  count_;
+    std::string   pattern_;
+
+  public:
+    FolderTarget(const std::string& pattern) : 
+      count_(0),
+      pattern_(pattern)
+    {
+    }
+
+    virtual void Write(const std::string& file);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Messaging/IFileTarget.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,37 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 <string>
+
+namespace OrthancWSI
+{
+  class IFileTarget : public boost::noncopyable
+  {
+  public:
+    virtual ~IFileTarget()
+    {
+    }
+
+    virtual void Write(const std::string& file) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Messaging/IOrthancConnection.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,62 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "IOrthancConnection.h"
+
+#include "../Orthanc/Core/Logging.h"
+#include "../Orthanc/Core/OrthancException.h"
+
+#include <json/reader.h>
+
+
+namespace OrthancWSI
+{
+  void IOrthancConnection::RestApiGet(Json::Value& result,
+                                      IOrthancConnection& orthanc,
+                                      const std::string& uri)
+  {
+    std::string content;
+    orthanc.RestApiGet(content, uri);
+
+    Json::Reader reader;
+    if (!reader.parse(content, result))
+    {
+      LOG(ERROR) << "Cannot parse a JSON file";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  void IOrthancConnection::RestApiPost(Json::Value& result,
+                                       IOrthancConnection& orthanc,
+                                       const std::string& uri,
+                                       const std::string& body)
+  {
+    std::string content;
+    orthanc.RestApiPost(content, uri, body);
+
+    Json::Reader reader;
+    if (!reader.parse(content, result))
+    {
+      LOG(ERROR) << "Cannot parse a JSON file";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Messaging/IOrthancConnection.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,52 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 <json/value.h>
+
+namespace OrthancWSI
+{
+  // Derived classes must be thread-safe
+  class IOrthancConnection : public boost::noncopyable
+  {
+  public:
+    virtual ~IOrthancConnection()
+    {
+    }
+
+    virtual void RestApiGet(std::string& result,
+                            const std::string& uri) = 0;
+
+    virtual void RestApiPost(std::string& result, 
+                             const std::string& uri,
+                             const std::string& body) = 0;
+
+    static void RestApiGet(Json::Value& result,
+                           IOrthancConnection& orthanc,
+                           const std::string& uri);
+
+    static void RestApiPost(Json::Value& result,
+                            IOrthancConnection& orthanc,
+                            const std::string& uri,
+                            const std::string& body);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Messaging/OrthancConnectionBase.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,40 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "OrthancConnectionBase.h"
+
+namespace OrthancWSI
+{
+  void OrthancConnectionBase::RestApiGet(std::string& result,
+                                         const std::string& uri)
+  {
+    boost::mutex::scoped_lock  lock_;
+    ApplyGet(result, uri);
+  }
+
+
+  void OrthancConnectionBase::RestApiPost(std::string& result,
+                                          const std::string& uri,
+                                          const std::string& body)
+  {
+    boost::mutex::scoped_lock  lock_;
+    ApplyPost(result, uri, body);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Messaging/OrthancConnectionBase.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,51 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "IOrthancConnection.h"
+
+#include <boost/thread/mutex.hpp>
+
+namespace OrthancWSI
+{
+  class OrthancConnectionBase : public IOrthancConnection
+  {
+  private:
+    boost::mutex  mutex_;
+
+  protected:
+    // Will be invoked in mutual exclusion
+    virtual void ApplyGet(std::string& result,
+                          const std::string& uri) = 0;
+
+    virtual void ApplyPost(std::string& result,
+                           const std::string& uri,
+                           const std::string& body) = 0;
+
+  public:
+    virtual void RestApiGet(std::string& result,
+                            const std::string& uri);
+
+    virtual void RestApiPost(std::string& result, 
+                             const std::string& uri,
+                             const std::string& body);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Messaging/OrthancTarget.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,57 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "OrthancTarget.h"
+
+#include "CurlOrthancConnection.h"
+#include "../DicomToolbox.h"
+#include "../Orthanc/Core/OrthancException.h"
+#include "../Orthanc/Core/Logging.h"
+
+namespace OrthancWSI
+{
+  OrthancTarget::OrthancTarget(const Orthanc::WebServiceParameters& parameters) :
+    orthanc_(new CurlOrthancConnection(parameters)),
+    first_(true)
+  {
+  }
+
+
+  void OrthancTarget::Write(const std::string& file)
+  {
+    Json::Value result;
+    IOrthancConnection::RestApiPost(result, *orthanc_, "/instances", file);
+
+    std::string instanceId = DicomToolbox::GetMandatoryStringTag(result, "ID");
+
+    if (first_)
+    {
+      Json::Value instance;
+      IOrthancConnection::RestApiGet(instance, *orthanc_, "/instances/" + instanceId);
+
+      std::string seriesId = DicomToolbox::GetMandatoryStringTag(instance, "ParentSeries");
+
+      LOG(WARNING) << "ID of the whole-slide image series in Orthanc: " << seriesId;
+      first_ = false;
+    }
+
+    LOG(INFO) << "New instance was added to Orthanc: " << instanceId;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Messaging/OrthancTarget.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,48 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "IFileTarget.h"
+#include "IOrthancConnection.h"
+#include "../Orthanc/Core/WebServiceParameters.h"
+
+#include <memory>
+
+namespace OrthancWSI
+{
+  class OrthancTarget : public IFileTarget
+  {
+  private:
+    std::auto_ptr<IOrthancConnection>  orthanc_;
+    bool                               first_;
+
+  public:
+    OrthancTarget(const Orthanc::WebServiceParameters& parameters);
+
+    OrthancTarget(IOrthancConnection* orthanc) :   // Takes ownership
+      orthanc_(orthanc),
+      first_(true)
+    {
+    }
+
+    virtual void Write(const std::string& file);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Messaging/PluginOrthancConnection.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,132 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "PluginOrthancConnection.h"
+
+#include "../Orthanc/Core/OrthancException.h"
+
+namespace OrthancWSI
+{
+  class PluginOrthancConnection::MemoryBuffer : public boost::noncopyable
+  {
+  private:
+    OrthancPluginContext*      context_;
+    OrthancPluginMemoryBuffer  buffer_;
+
+    void Clear()
+    {
+      if (buffer_.data != NULL)
+      {
+        OrthancPluginFreeMemoryBuffer(context_, &buffer_);
+        buffer_.data = NULL;
+        buffer_.size = 0;
+      }
+    }
+
+
+  public:
+    MemoryBuffer(OrthancPluginContext* context) : 
+      context_(context)
+    {
+      buffer_.data = NULL;
+      buffer_.size = 0;
+    }
+
+    ~MemoryBuffer()
+    {
+      Clear();
+    }
+
+    void RestApiGet(const std::string& uri)
+    {
+      Clear();
+
+      OrthancPluginErrorCode error = OrthancPluginRestApiGet(context_, &buffer_, uri.c_str());
+
+      if (error == OrthancPluginErrorCode_Success)
+      {
+        // OK, success
+      }
+      else if (error == OrthancPluginErrorCode_UnknownResource ||
+               error == OrthancPluginErrorCode_InexistentItem)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_Plugin);
+      }
+    }
+
+    void RestApiPost(const std::string& uri,
+                     const std::string& body)
+    {
+      Clear();
+
+      OrthancPluginErrorCode error = OrthancPluginRestApiPost(context_, &buffer_, uri.c_str(), body.c_str(), body.size());
+
+      if (error == OrthancPluginErrorCode_Success)
+      {
+        // OK, success
+      }
+      else if (error == OrthancPluginErrorCode_UnknownResource ||
+               error == OrthancPluginErrorCode_InexistentItem)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_Plugin);
+      }
+    }
+
+    void ToString(std::string& target) const
+    {
+      if (buffer_.size == 0)
+      {
+        target.clear();
+      }
+      else
+      {
+        target.assign(reinterpret_cast<const char*>(buffer_.data), buffer_.size);
+      }
+    }
+  };
+
+
+
+  void PluginOrthancConnection::ApplyGet(std::string& result,
+                                         const std::string& uri)
+  {
+    MemoryBuffer buffer(context_);
+    buffer.RestApiGet(uri);
+    buffer.ToString(result);
+  }
+
+    
+  void PluginOrthancConnection::ApplyPost(std::string& result,
+                                          const std::string& uri,
+                                          const std::string& body)
+  {
+    MemoryBuffer buffer(context_);
+    buffer.RestApiPost(uri, body);
+    buffer.ToString(result);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Messaging/PluginOrthancConnection.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,50 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "OrthancConnectionBase.h"
+
+#include <orthanc/OrthancCPlugin.h>
+
+namespace OrthancWSI
+{
+  class PluginOrthancConnection : public OrthancConnectionBase
+  {
+  private:
+    class MemoryBuffer;
+
+    OrthancPluginContext*   context_;
+
+  protected:
+    virtual void ApplyGet(std::string& result,
+                          const std::string& uri);
+
+    virtual void ApplyPost(std::string& result,
+                           const std::string& uri,
+                           const std::string& body);
+
+  public:
+    PluginOrthancConnection(OrthancPluginContext* context) :
+      context_(context)
+    {
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Orthanc/README.txt	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,3 @@
+This folder contains an excerpt of the source code of Orthanc. It is
+automatically generated using the "../../Resources/SyncOrthancFolder.py"
+script.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Outputs/DicomPyramidWriter.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,198 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "DicomPyramidWriter.h"
+
+#include "../DicomToolbox.h"
+
+#include "../Orthanc/Core/Logging.h"
+#include "../Orthanc/Core/OrthancException.h"
+#include "../Orthanc/OrthancServer/FromDcmtkBridge.h"
+
+#include <dcmtk/dcmdata/dcdeftag.h>
+#include <boost/lexical_cast.hpp>
+
+namespace OrthancWSI
+{
+  void DicomPyramidWriter::FlushInternal(MultiframeDicomWriter& writer,
+                                         bool force)
+  {
+    if (writer.GetFramesCount() > 0 &&
+        writer.GetSize() > 0 &&
+        (force || (maxSize_ != 0 && writer.GetSize() >= maxSize_)))
+    {
+      countInstances_ += 1;
+
+      std::string dicom;
+      writer.Flush(dicom, countInstances_);
+      target_.Write(dicom);
+    }
+  }
+
+
+  DcmItem* DicomPyramidWriter::CreateFunctionalGroup(unsigned int frame,
+                                                     unsigned int x, 
+                                                     unsigned int y,
+                                                     unsigned int totalWidth,
+                                                     unsigned int totalHeight,
+                                                     float physicalZ) const
+  {
+    float physicalX, physicalY;
+    volume_.GetLocation(physicalX, physicalY, x, y, totalWidth, totalHeight);
+
+    std::string tmpX = boost::lexical_cast<std::string>(physicalX);
+    std::string tmpY = boost::lexical_cast<std::string>(physicalY);
+    std::string tmpZ = boost::lexical_cast<std::string>(physicalZ);
+
+    std::auto_ptr<DcmItem> dimension(new DcmItem);
+    if (!dimension->putAndInsertUint32(DCM_DimensionIndexValues, frame).good())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    // From Supp 145: The column position of the top left pixel of the
+    // Total Pixel Matrix is 1. The row position of the top left pixel
+    // of the Total Pixel Matrix is 1.
+    std::auto_ptr<DcmItem> position(new DcmItem);
+    if (!position->putAndInsertSint32(DCM_ColumnPositionInTotalImagePixelMatrix, x + 1).good() ||
+        !position->putAndInsertSint32(DCM_RowPositionInTotalImagePixelMatrix, y + 1).good() ||
+        !position->putAndInsertString(DCM_XOffsetInSlideCoordinateSystem, tmpX.c_str()).good() ||
+        !position->putAndInsertString(DCM_YOffsetInSlideCoordinateSystem, tmpY.c_str()).good() ||
+        !position->putAndInsertString(DCM_ZOffsetInSlideCoordinateSystem, tmpZ.c_str()).good())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+
+    std::auto_ptr<DcmSequenceOfItems> sequencePosition(new DcmSequenceOfItems(DCM_PlanePositionSlideSequence));
+    if (!sequencePosition->insert(position.release(), false, false).good())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    std::auto_ptr<DcmSequenceOfItems> sequenceDimension(new DcmSequenceOfItems(DCM_FrameContentSequence));
+    if (!sequenceDimension->insert(dimension.release(), false, false).good())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    std::auto_ptr<DcmItem> item(new DcmItem);
+    if (!item->insert(sequencePosition.release(), false, false).good() ||
+        !item->insert(sequenceDimension.release(), false, false).good())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }  
+
+    return item.release();
+  }
+
+
+  void DicomPyramidWriter::WriteRawTileInternal(const std::string& tile,
+                                                const Level& level,
+                                                unsigned int x,
+                                                unsigned int y)
+  {
+    if (x >= level.countTilesX_ ||
+        y >= level.countTilesY_)
+    {
+      LOG(ERROR) << "Tile index out of range: " << x << "," << y
+                 << " (max: " << level.countTilesX_ << "," << level.countTilesY_ << ")";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    const unsigned int z = level.z_;
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      
+      if (z >= writers_.size())
+      {
+        writers_.resize(z + 1);
+      }
+
+      MultiframeDicomWriter* writer = writers_[z];
+
+      if (writer == NULL)
+      {
+        writer = new MultiframeDicomWriter
+          (dataset_, GetImageCompression(), GetPixelFormat(), level.width_, level.height_, 
+           GetTileWidth(), GetTileHeight());
+        writers_[z] = writer;
+      }
+
+      std::auto_ptr<DcmItem> functionalGroup(CreateFunctionalGroup(writer->GetFramesCount() + 1,
+                                                                   x * GetTileWidth(), 
+                                                                   y * GetTileHeight(), 
+                                                                   writer->GetTotalWidth(),
+                                                                   writer->GetTotalHeight(),
+                                                                   0.0f /* TODO Z-plane */));
+
+      writer->AddFrame(tile, functionalGroup.release());
+      FlushInternal(*writer, false);
+
+      countTiles_ ++;
+    }
+  }
+
+
+  DicomPyramidWriter::DicomPyramidWriter(IFileTarget&  target,
+                                         const DcmDataset& dataset,
+                                         Orthanc::PixelFormat pixelFormat,
+                                         ImageCompression compression,
+                                         unsigned int tileWidth,
+                                         unsigned int tileHeight,
+                                         size_t maxSize,   // If "0", no automatic flushing
+                                         const ImagedVolumeParameters&  volume) :
+    PyramidWriterBase(pixelFormat, compression, tileWidth, tileHeight),
+    target_(target),
+    dataset_(dataset),
+    maxSize_(maxSize),
+    countTiles_(0),
+    countInstances_(0),
+    volume_(volume)
+  {
+  }
+
+
+  DicomPyramidWriter::~DicomPyramidWriter()
+  {
+    LOG(WARNING) << "Closing the DICOM pyramid (" << countTiles_ << " tiles were written)";
+
+    for (size_t i = 0; i < writers_.size(); i++)
+    {
+      if (writers_[i] != NULL)
+      {
+        FlushInternal(*writers_[i], true);
+        delete writers_[i];
+      }
+    }
+  }
+
+
+  void DicomPyramidWriter::Flush()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+      
+    for (size_t i = 0; i < writers_.size(); i++)
+    {
+      FlushInternal(*writers_[i], true);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Outputs/DicomPyramidWriter.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,78 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "PyramidWriterBase.h"
+#include "MultiframeDicomWriter.h"
+#include "../Messaging/IFileTarget.h"
+#include "../ImagedVolumeParameters.h"
+
+namespace OrthancWSI
+{
+  class DicomPyramidWriter : public PyramidWriterBase
+  {
+  private:
+    std::vector<MultiframeDicomWriter*>  writers_;
+
+    boost::mutex       mutex_;   // Protects the access to "writers_"
+    IFileTarget&       target_;
+    const DcmDataset&  dataset_;
+    size_t             maxSize_;
+    size_t             countTiles_;
+    unsigned int       countInstances_;
+
+    const ImagedVolumeParameters&  volume_;
+
+    void FlushInternal(MultiframeDicomWriter& writer,
+                       bool force);
+
+    DcmItem* CreateFunctionalGroup(unsigned int frame,
+                                   unsigned int x, 
+                                   unsigned int y,
+                                   unsigned int totalWidth,
+                                   unsigned int totalHeight,
+                                   float physicalZ) const;
+
+  protected:
+    virtual void WriteRawTileInternal(const std::string& tile,
+                                      const Level& level,
+                                      unsigned int x,
+                                      unsigned int y);
+
+    virtual void AddLevelInternal(const Level& level)
+    {
+    }
+
+  public:
+    DicomPyramidWriter(IFileTarget&  target,
+                       const DcmDataset& dataset,
+                       Orthanc::PixelFormat pixelFormat,
+                       ImageCompression compression,
+                       unsigned int tileWidth,
+                       unsigned int tileHeight,
+                       size_t maxSize,   // If "0", no automatic flushing
+                       const ImagedVolumeParameters&  volume);
+
+    virtual ~DicomPyramidWriter();
+
+    virtual void Flush();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Outputs/HierarchicalTiffWriter.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,421 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "HierarchicalTiffWriter.h"
+
+#include "../Orthanc/Core/Logging.h"
+#include "../Orthanc/Core/OrthancException.h"
+#include "../Orthanc/Core/Uuid.h"
+
+namespace OrthancWSI
+{
+  class HierarchicalTiffWriter::PendingTile
+  {
+  private:
+    HierarchicalTiffWriter&          that_;
+    unsigned int                     level_;
+    unsigned int                     tileX_;
+    unsigned int                     tileY_;
+    Orthanc::Toolbox::TemporaryFile  file_;
+      
+  public:
+    PendingTile(HierarchicalTiffWriter& that,
+                unsigned int level,
+                unsigned int tileX,
+                unsigned int tileY,
+                const std::string& tile) :
+      that_(that),
+      level_(level),
+      tileX_(tileX),
+      tileY_(tileY)
+    {
+      file_.Write(tile);
+    }
+
+    unsigned int GetLevel() const
+    {
+      return level_;
+    }
+
+    unsigned int GetTileX() const
+    {
+      return tileX_;
+    }
+
+    unsigned int GetTileY() const
+    {
+      return tileY_;
+    }
+
+    void Store(TIFF* tiff)
+    {
+      std::string tile;
+      file_.Read(tile);
+      that_.StoreTile(tile, tileX_, tileY_);
+    }
+  };
+
+
+  struct HierarchicalTiffWriter::Comparator
+  {
+    inline bool operator() (PendingTile* const& a,
+                            PendingTile* const& b)
+    {
+      if (a->GetLevel() < b->GetLevel())
+      {
+        return true;
+      }
+
+      if (a->GetLevel() > b->GetLevel())
+      {
+        return false;
+      }
+
+      if (a->GetTileY() < b->GetTileY())
+      {
+        return true;
+      }
+
+      if (a->GetTileY() > b->GetTileY())
+      {
+        return false;
+      }
+
+      return a->GetTileX() < b->GetTileX();
+    }
+  };
+
+
+  static uint8_t GetUint8(const std::string& tile,
+                          size_t index)
+  {
+    if (index >= tile.size())
+    {
+      return 0;
+    }
+    else
+    {
+      return static_cast<uint8_t>(tile[index]);
+    }
+  }
+
+ 
+#if 0
+  static uint16_t GetUint16(const std::string& tile,
+                            size_t index)
+  {
+    if (index + 1 >= tile.size())
+    {
+      return 0;
+    }
+    else
+    {
+      return static_cast<uint16_t>(tile[index]) * 256 + static_cast<uint16_t>(tile[index + 1]);
+    }
+  }
+#endif
+
+
+  static void CheckJpegTile(const std::string& tile,
+                            Orthanc::PixelFormat pixelFormat)
+  {
+    // Check the sampling by accessing the "Start of Frame" header of JPEG
+
+    if (tile.size() < 3 ||
+        static_cast<uint8_t>(tile[0]) != 0xff ||
+        static_cast<uint8_t>(tile[1]) != 0xd8 ||
+        static_cast<uint8_t>(tile[2]) != 0xff)
+    {
+      LOG(WARNING) << "The source image does not contain JPEG tiles";
+      return;
+    }
+
+    // Look for the "Start of Frame" header (FF C0)
+    for (size_t i = 2; i + 1 < tile.size(); i++)
+    {
+      if (static_cast<uint8_t>(tile[i]) == 0xff &&
+          static_cast<uint8_t>(tile[i + 1]) == 0xc0)
+      {
+        uint8_t numberOfComponents = GetUint8(tile, i + 9);
+          
+        switch (pixelFormat)
+        {
+          case Orthanc::PixelFormat_Grayscale8:
+            if (numberOfComponents != 3)
+            {
+              LOG(WARNING) << "The source image does not contain a grayscale image as expected";
+            }
+            break;
+
+          case Orthanc::PixelFormat_RGB24:
+          {
+            if (numberOfComponents != 3)
+            {
+              LOG(WARNING) << "The source image does not contain a RGB24 color image as expected";
+            }
+
+            // Read the header corresponding to the first component
+            const size_t component = 0;
+            const size_t offset = i + 10 + component * 3; 
+            const uint8_t sampling = GetUint8(tile, offset + 1);
+            const int samplingH = sampling / 16;
+            const int samplingV = sampling % 16;
+
+            LOG(WARNING) << "The source image uses chroma sampling " << samplingH << ":" << samplingV;
+
+            if (samplingH != 2 ||
+                samplingV != 2)
+            {
+              LOG(WARNING) << "The source image has not a chroma sampling of 2:2, "
+                           << "you should consider using option \"--reencode\"";
+            }
+
+            break;
+          }
+
+          default:
+            break;
+        }
+      }
+    }
+  }
+
+
+ 
+  void HierarchicalTiffWriter::StoreTile(const std::string& tile,
+                                         unsigned int tileX,
+                                         unsigned int tileY)
+  {
+    // Get the index of the tile
+    ttile_t index = TIFFComputeTile(tiff_, tileX * GetTileWidth(), tileY * GetTileHeight(), 0 /*z*/, 0 /*sample*/);
+
+    if (TIFFWriteRawTile(tiff_, index, tile.size() ? const_cast<char*>(&tile[0]) : NULL, tile.size()) != 
+        static_cast<tsize_t>(tile.size()))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_CannotWriteFile);
+    }
+
+    if (isFirst_ &&
+        GetImageCompression() == ImageCompression_Jpeg)
+    {
+      CheckJpegTile(tile, GetPixelFormat());
+    }
+
+    isFirst_ = false;
+  }
+
+
+  void HierarchicalTiffWriter::ConfigureLevel(const Level& level,
+                                              bool createLevel)
+  {
+    if (createLevel && TIFFWriteDirectory(tiff_) != 1)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_CannotWriteFile);
+    }
+
+    if (TIFFFlush(tiff_) != 1)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_CannotWriteFile);
+    }
+
+    currentLevel_ = level.z_;
+    nextX_ = 0;
+    nextY_ = 0;
+
+    switch (GetImageCompression())
+    {
+      case ImageCompression_Jpeg:
+      {
+        uint16_t c = COMPRESSION_JPEG;
+
+        if (TIFFSetField(tiff_, TIFFTAG_COMPRESSION, c) != 1)
+        {
+          Close();
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_CannotWriteFile);
+        }
+
+        break;
+      }
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+
+
+    switch (GetPixelFormat())
+    {
+      case Orthanc::PixelFormat_RGB24:
+      {
+        uint16_t samplesPerPixel = 3;
+        uint16_t photometric = PHOTOMETRIC_YCBCR;
+        uint16_t planar = PLANARCONFIG_CONTIG;   // Interleaved RGB
+        uint16_t bpp = 8;
+        uint16_t subsampleHorizontal = 2;
+        uint16_t subsampleVertical = 2;
+
+        if (TIFFSetField(tiff_, TIFFTAG_SAMPLESPERPIXEL, samplesPerPixel) != 1 ||
+            TIFFSetField(tiff_, TIFFTAG_PHOTOMETRIC, photometric) != 1 ||
+            TIFFSetField(tiff_, TIFFTAG_BITSPERSAMPLE, bpp) != 1 ||
+            TIFFSetField(tiff_, TIFFTAG_PLANARCONFIG, planar) != 1 ||
+            TIFFSetField(tiff_, TIFFTAG_YCBCRSUBSAMPLING, subsampleHorizontal, subsampleVertical) != 1)
+        {
+          Close();
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_CannotWriteFile);
+        }
+
+        break;
+      }
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+
+    uint32_t w = level.width_;
+    uint32_t h = level.height_;
+    uint32_t tw = GetTileWidth();
+    uint32_t th = GetTileHeight();
+
+    if (TIFFSetField(tiff_, TIFFTAG_IMAGEWIDTH, w) != 1 ||
+        TIFFSetField(tiff_, TIFFTAG_IMAGELENGTH, h) != 1 ||
+        TIFFSetField(tiff_, TIFFTAG_TILEWIDTH, tw) != 1 ||
+        TIFFSetField(tiff_, TIFFTAG_TILELENGTH, th) != 1)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_CannotWriteFile);
+    }          
+  }
+
+
+  void HierarchicalTiffWriter::AdvanceToNextTile()
+  {
+    assert(currentLevel_ < levels_.size());
+
+    nextX_ += 1;
+    if (nextX_ >= levels_[currentLevel_].countTilesX_)
+    {
+      nextX_ = 0;
+      nextY_ += 1;
+
+      if (nextY_ >= levels_[currentLevel_].countTilesY_)
+      {
+        currentLevel_ += 1;
+
+        if (currentLevel_ < levels_.size())
+        {
+          ConfigureLevel(levels_[currentLevel_], true);
+        }
+      }
+    }
+  }
+
+
+  void HierarchicalTiffWriter::ScanPending()
+  {
+    std::sort(pending_.begin(), pending_.end(), Comparator());
+
+    while (currentLevel_ < levels_.size() &&
+           !pending_.empty() &&
+           pending_.front()->GetLevel() == currentLevel_ &&
+           pending_.front()->GetTileX() == nextX_ &&
+           pending_.front()->GetTileY() == nextY_)
+    {
+      pending_.front()->Store(tiff_);
+      delete pending_.front();
+      pending_.pop_front();
+      AdvanceToNextTile();
+    }
+  }
+          
+
+  void HierarchicalTiffWriter::WriteRawTileInternal(const std::string& tile,
+                                                    const Level& level,
+                                                    unsigned int tileX,
+                                                    unsigned int tileY)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    if (level.z_ == currentLevel_ &&
+        tileX == nextX_ &&
+        tileY == nextY_)
+    {
+      StoreTile(tile, tileX, tileY);
+      AdvanceToNextTile();
+      ScanPending();
+    }
+    else
+    {
+      pending_.push_back(new PendingTile(*this, level.z_, tileX, tileY, tile));
+    }
+  }
+
+        
+  void HierarchicalTiffWriter::AddLevelInternal(const Level& level)
+  {
+    if (level.z_ == 0)
+    {
+      // Configure the finest level on initialization
+      ConfigureLevel(level, false);
+    }
+
+    levels_.push_back(level);
+  }
+
+
+  HierarchicalTiffWriter::HierarchicalTiffWriter(const std::string& path,
+                                                 Orthanc::PixelFormat pixelFormat, 
+                                                 ImageCompression compression,
+                                                 unsigned int tileWidth,
+                                                 unsigned int tileHeight) :
+    PyramidWriterBase(pixelFormat, compression, tileWidth, tileHeight),
+    currentLevel_(0),
+    isFirst_(true)
+  {
+    tiff_ = TIFFOpen(path.c_str(), "w");
+    if (tiff_ == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_CannotWriteFile);
+    }
+  }
+
+
+  HierarchicalTiffWriter::~HierarchicalTiffWriter()
+  {
+    if (pending_.size())
+    {
+      LOG(ERROR) << "Some tiles (" << pending_.size() << ") were not written to the TIFF file";
+    }
+
+    for (size_t i = 0; i < pending_.size(); i++)
+    {
+      if (pending_[i])
+      {
+        delete pending_[i];
+      }
+    }
+
+    Close();
+  }
+
+
+  void HierarchicalTiffWriter::Flush()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    ScanPending();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Outputs/HierarchicalTiffWriter.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,86 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "PyramidWriterBase.h"
+
+#include <tiff.h>
+#include <tiffio.h>
+#include <deque>
+#include <vector>
+#include <boost/thread.hpp>
+
+namespace OrthancWSI
+{
+  class HierarchicalTiffWriter : public PyramidWriterBase
+  {
+  private:
+    class PendingTile;
+    struct Comparator;
+
+    TIFF* tiff_;
+
+    boost::mutex              mutex_;
+    std::deque<PendingTile*>  pending_;
+    std::vector<Level>        levels_;
+    unsigned int              currentLevel_;
+    unsigned int              nextX_;
+    unsigned int              nextY_;
+    bool                      isFirst_;
+ 
+    void Close()
+    {
+      TIFFClose(tiff_);
+    }
+
+    void StoreTile(const std::string& tile,
+                   unsigned int tileX,
+                   unsigned int tileY);
+
+    void ConfigureLevel(const Level& level,
+                        bool createLevel);
+
+    void AdvanceToNextTile();
+
+    void ScanPending();
+
+
+  protected:
+    virtual void WriteRawTileInternal(const std::string& tile,
+                                      const Level& level,
+                                      unsigned int tileX,
+                                      unsigned int tileY);
+        
+    virtual void AddLevelInternal(const Level& level);
+
+
+  public:
+    HierarchicalTiffWriter(const std::string& path,
+                           Orthanc::PixelFormat pixelFormat, 
+                           ImageCompression compression,
+                           unsigned int tileWidth,
+                           unsigned int tileHeight);
+
+    virtual ~HierarchicalTiffWriter();
+
+    virtual void Flush();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Outputs/IPyramidWriter.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,60 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "../Enumerations.h"
+#include "../Orthanc/Core/Images/ImageAccessor.h"
+
+#include <boost/noncopyable.hpp>
+
+namespace OrthancWSI
+{
+  class IPyramidWriter : public boost::noncopyable
+  {
+  public:
+    virtual ~IPyramidWriter()
+    {
+    }
+
+    virtual unsigned int GetLevelCount() const = 0;
+    
+    virtual Orthanc::PixelFormat GetPixelFormat() const = 0;
+
+    virtual unsigned int GetTileWidth() const = 0;
+
+    virtual unsigned int GetTileHeight() const = 0;
+
+    virtual unsigned int GetCountTilesX(unsigned int level) const = 0;
+
+    virtual unsigned int GetCountTilesY(unsigned int level) const = 0;
+
+    virtual void WriteRawTile(const std::string& tile,
+                              ImageCompression compression,
+                              unsigned int level,
+                              unsigned int tileX,
+                              unsigned int tileY) = 0;
+
+    virtual void EncodeTile(const Orthanc::ImageAccessor& tile,
+                            unsigned int level,
+                            unsigned int tileX, 
+                            unsigned int tileY) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Outputs/InMemoryTiledImage.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,171 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "InMemoryTiledImage.h"
+
+#include "../ImageToolbox.h"
+#include "../Orthanc/Core/Logging.h"
+#include "../Orthanc/Core/OrthancException.h"
+
+namespace OrthancWSI
+{
+  static void CheckLevel(unsigned int level)
+  {
+    if (level != 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  InMemoryTiledImage::InMemoryTiledImage(Orthanc::PixelFormat format,
+                                         unsigned int countTilesX,
+                                         unsigned int countTilesY,
+                                         unsigned int tileWidth,
+                                         unsigned int tileHeight) :
+    format_(format),
+    countTilesX_(countTilesX),
+    countTilesY_(countTilesY),
+    tileWidth_(tileWidth),
+    tileHeight_(tileHeight)
+  {
+  }
+
+
+  InMemoryTiledImage::~InMemoryTiledImage()
+  {
+    for (Tiles::iterator it = tiles_.begin(); it != tiles_.end(); ++it)
+    {
+      delete it->second;
+    }
+  }
+
+
+  unsigned int InMemoryTiledImage::GetCountTilesX(unsigned int level) const
+  {
+    CheckLevel(level);
+    return countTilesX_;
+  }
+
+
+  unsigned int InMemoryTiledImage::GetCountTilesY(unsigned int level) const
+  {
+    CheckLevel(level);
+    return countTilesY_;
+  }
+
+
+  unsigned int InMemoryTiledImage::GetLevelWidth(unsigned int level) const
+  {
+    CheckLevel(level);
+    return tileWidth_ * countTilesX_;
+  }
+
+
+  unsigned int InMemoryTiledImage::GetLevelHeight(unsigned int level) const
+  { 
+    CheckLevel(level);
+    return tileHeight_ * countTilesY_;
+  }
+
+
+  bool InMemoryTiledImage::ReadRawTile(std::string& tile,
+                                       unsigned int level,
+                                       unsigned int tileX,
+                                       unsigned int tileY)
+  {
+    CheckLevel(level);
+    return false;  // Unavailable
+  }
+
+
+  Orthanc::ImageAccessor* InMemoryTiledImage::DecodeTile(unsigned int level,
+                                                         unsigned int tileX,
+                                                         unsigned int tileY)
+  {
+    CheckLevel(level);
+
+    if (tileX >= countTilesX_ ||
+        tileY >= countTilesY_)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      Tiles::const_iterator it = tiles_.find(std::make_pair(tileX, tileY));
+      if (it != tiles_.end())
+      {
+        return new Orthanc::ImageAccessor(*it->second);
+      }
+      else
+      {
+        LOG(ERROR) << "The following tile has not been set: " << tileX << "," << tileY;
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+    }
+  }
+
+
+  void InMemoryTiledImage::WriteRawTile(const std::string& raw,
+                                        ImageCompression compression,
+                                        unsigned int level,
+                                        unsigned int tileX,
+                                        unsigned int tileY)
+  {
+    std::auto_ptr<Orthanc::ImageAccessor> decoded(ImageToolbox::DecodeTile(raw, compression));
+    EncodeTile(*decoded, level, tileX, tileY);
+  }
+      
+
+  void InMemoryTiledImage::EncodeTile(const Orthanc::ImageAccessor& tile,
+                                      unsigned int level,
+                                      unsigned int tileX,
+                                      unsigned int tileY)
+  {
+    CheckLevel(level);
+
+    if (tileX >= countTilesX_ ||
+        tileY >= countTilesY_)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      Tiles::iterator it = tiles_.find(std::make_pair(tileX, tileY));
+      if (it == tiles_.end())
+      {
+        tiles_[std::make_pair(tileX, tileY)] = ImageToolbox::Clone(tile);
+      }
+      else
+      {
+        if (it->second)
+        {
+          delete it->second;
+        }
+
+        it->second = ImageToolbox::Clone(tile);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Outputs/InMemoryTiledImage.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,109 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "../Inputs/ITiledPyramid.h"
+#include "../Outputs/IPyramidWriter.h"
+
+#include <map>
+#include <boost/thread.hpp>
+
+namespace OrthancWSI
+{
+  class InMemoryTiledImage : 
+    public ITiledPyramid, 
+    public IPyramidWriter
+  {
+  private:
+    typedef std::pair<unsigned int, unsigned int>        Location;
+    typedef std::map<Location, Orthanc::ImageAccessor*>  Tiles;
+
+    boost::mutex          mutex_;
+    Orthanc::PixelFormat  format_;
+    unsigned int          countTilesX_;
+    unsigned int          countTilesY_;
+    unsigned int          tileWidth_;
+    unsigned int          tileHeight_;
+    Tiles                 tiles_;
+
+  public:
+    InMemoryTiledImage(Orthanc::PixelFormat format,
+                       unsigned int countTilesX,
+                       unsigned int countTilesY,
+                       unsigned int tileWidth,
+                       unsigned int tileHeight);
+
+    virtual ~InMemoryTiledImage();
+
+    virtual unsigned int GetLevelCount() const
+    {
+      return 1;
+    }
+
+    virtual unsigned int GetCountTilesX(unsigned int level) const;
+
+    virtual unsigned int GetCountTilesY(unsigned int level) const;
+
+    virtual unsigned int GetLevelWidth(unsigned int level) const;
+
+    virtual unsigned int GetLevelHeight(unsigned int level) const;
+
+    virtual unsigned int GetTileWidth() const
+    {
+      return tileWidth_;
+    }
+
+    virtual unsigned int GetTileHeight() const
+    {
+      return tileHeight_;
+    }
+
+    virtual bool ReadRawTile(std::string& tile,
+                             unsigned int level,
+                             unsigned int tileX,
+                             unsigned int tileY);
+
+    virtual Orthanc::ImageAccessor* DecodeTile(unsigned int level,
+                                               unsigned int tileX,
+                                               unsigned int tileY);
+
+    virtual ImageCompression GetImageCompression() const
+    {
+      return ImageCompression_None;
+    }
+
+    virtual Orthanc::PixelFormat GetPixelFormat() const
+    {
+      return format_;
+    }
+
+    virtual void WriteRawTile(const std::string& raw,
+                              ImageCompression compression,
+                              unsigned int level,
+                              unsigned int tileX,
+                              unsigned int tileY);
+
+    virtual void EncodeTile(const Orthanc::ImageAccessor& tile,
+                            unsigned int level,
+                            unsigned int tileX,
+                            unsigned int tileY);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Outputs/MultiframeDicomWriter.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,299 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "MultiframeDicomWriter.h"
+
+#include "../Orthanc/Core/OrthancException.h"
+#include "../Orthanc/Core/Logging.h"
+#include "../DicomToolbox.h"
+
+#include <dcmtk/dcmdata/dcuid.h>
+#include <dcmtk/dcmdata/dcdeftag.h>
+#include <dcmtk/dcmdata/dcostrmb.h>
+#include <dcmtk/dcmdata/dcpxitem.h>
+#include <dcmtk/dcmdata/dcpixel.h>
+#include <dcmtk/dcmdata/dcvrat.h>
+
+#include <boost/lexical_cast.hpp>
+
+namespace OrthancWSI
+{
+  static void SaveDicomToMemory(std::string& target,
+                                DcmFileFormat& dicom,
+                                E_TransferSyntax transferSyntax)
+  {
+    dicom.validateMetaInfo(transferSyntax);
+    dicom.removeInvalidGroups();
+
+    E_EncodingType encodingType = /*opt_sequenceType*/ EET_ExplicitLength;
+
+    // Create a memory buffer with the proper size
+    {
+      const uint32_t estimatedSize = dicom.calcElementLength(transferSyntax, encodingType);  // (*)
+      target.resize(estimatedSize);
+    }
+
+    DcmOutputBufferStream ob(&target[0], target.size());
+
+    // Fill the memory buffer with the meta-header and the dataset
+    dicom.transferInit();
+    OFCondition c = dicom.write(ob, transferSyntax, encodingType, NULL,
+                                /*opt_groupLength*/ EGL_recalcGL,
+                                /*opt_paddingType*/ EPD_withoutPadding);
+    dicom.transferEnd();
+
+    if (!c.good())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    // The DICOM file is successfully written, truncate the target
+    // buffer if its size was overestimated by (*)
+    ob.flush();
+
+    size_t effectiveSize = static_cast<size_t>(ob.tell());
+    if (effectiveSize < target.size())
+    {
+      target.resize(effectiveSize);
+    }
+  }
+
+
+  void MultiframeDicomWriter::ResetImage()
+  {
+    perFrameFunctionalGroups_.reset(new DcmSequenceOfItems(DCM_PerFrameFunctionalGroupsSequence));
+
+    if (compression_ != ImageCompression_None)
+    {
+      compressedPixelSequence_.reset(new DcmPixelSequence(DcmTag(DCM_PixelData, EVR_OB)));
+
+      offsetTable_ = new DcmPixelItem(DCM_Item, EVR_OB);
+      compressedPixelSequence_->insert(offsetTable_);
+      offsetList_.reset(new DcmOffsetList);
+    }
+
+    writtenSize_ = 0;
+    framesCount_ = 0;
+  }
+
+
+  void MultiframeDicomWriter::InjectUncompressedPixelData(DcmFileFormat& dicom)
+  {
+    static const size_t GIGABYTE = 1024 * 1024 * 1024;
+
+    std::string pixelData;
+    uncompressedPixelData_.Flatten(pixelData);
+
+    // Prevent the creation of too large DICOM files
+    // (uncompressed DICOM files are limited to 2GB)
+    if (pixelData.size() > GIGABYTE)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotEnoughMemory);
+    }
+
+    std::auto_ptr<DcmPixelData> pixels(new DcmPixelData(DCM_PixelData));
+
+    uint8_t* target = NULL;
+    if (!pixels->createUint8Array(pixelData.size(), target).good())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    if (pixelData.size() > 0)
+    {
+      if (target == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      // TODO Avoid this memcpy()
+      memcpy(target, pixelData.c_str(), pixelData.size());
+    }
+
+    if (!dicom.getDataset()->insert(pixels.release(), false, false).good())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  }
+
+
+  MultiframeDicomWriter::MultiframeDicomWriter(const DcmDataset& dataset,
+                                               ImageCompression compression,
+                                               Orthanc::PixelFormat pixelFormat,
+                                               unsigned int width,
+                                               unsigned int height,
+                                               unsigned int tileWidth,
+                                               unsigned int tileHeight) :
+    compression_(compression),
+    width_(width),
+    height_(height)
+  {
+    switch (compression)
+    {
+      case ImageCompression_None:
+        transferSyntax_ = EXS_LittleEndianImplicit;
+        break;
+
+      case ImageCompression_Jpeg:
+        // Default transfer syntax for lossy JPEG 8bit compression
+        transferSyntax_ = EXS_JPEGProcess1TransferSyntax;
+        break;
+
+      case ImageCompression_Jpeg2000:
+        // JPEG2000 compression (lossless only)
+        transferSyntax_ = EXS_JPEG2000LosslessOnly;
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    if (!sharedTags_.copyFrom(dataset).good())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    // We only take care of grayscale or RGB images in 8bpp
+    DicomToolbox::SetUint32Tag(sharedTags_, DCM_TotalPixelMatrixColumns, width);
+    DicomToolbox::SetUint32Tag(sharedTags_, DCM_TotalPixelMatrixRows, height);
+    DicomToolbox::SetUint16Tag(sharedTags_, DCM_PlanarConfiguration, 0);  // Interleaved RGB values
+    DicomToolbox::SetUint16Tag(sharedTags_, DCM_Columns, tileWidth);
+    DicomToolbox::SetUint16Tag(sharedTags_, DCM_Rows, tileHeight);
+    DicomToolbox::SetUint16Tag(sharedTags_, DCM_BitsAllocated, 8);
+    DicomToolbox::SetUint16Tag(sharedTags_, DCM_BitsStored, 8);
+    DicomToolbox::SetUint16Tag(sharedTags_, DCM_HighBit, 7);
+    DicomToolbox::SetUint16Tag(sharedTags_, DCM_PixelRepresentation, 0);   // Unsigned values
+
+    switch (pixelFormat)
+    {
+      case Orthanc::PixelFormat_RGB24:
+        uncompressedFrameSize_ = 3 * tileWidth * tileHeight;
+        DicomToolbox::SetUint16Tag(sharedTags_, DCM_SamplesPerPixel, 3);
+
+        if (compression_ == ImageCompression_Jpeg)
+        {
+          DicomToolbox::SetStringTag(sharedTags_, DCM_PhotometricInterpretation, "YBR_FULL_422");
+        }
+        else
+        {
+          DicomToolbox::SetStringTag(sharedTags_, DCM_PhotometricInterpretation, "RGB");
+        }
+
+        break;
+
+      case Orthanc::PixelFormat_Grayscale8:
+        uncompressedFrameSize_ = tileWidth * tileHeight;
+        DicomToolbox::SetUint16Tag(sharedTags_, DCM_SamplesPerPixel, 1);
+        DicomToolbox::SetStringTag(sharedTags_, DCM_PhotometricInterpretation, "MONOCHROME2");
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    ResetImage();
+  }
+
+
+  void MultiframeDicomWriter::AddFrame(const std::string& frame,
+                                       DcmItem* functionalGroup)   // This takes the ownership
+  {
+    // Free the functional group on error
+    std::auto_ptr<DcmItem> functionalGroupRaii(functionalGroup);
+
+    if (compression_ == ImageCompression_None)
+    {
+      if (frame.size() != uncompressedFrameSize_)
+      {
+        LOG(ERROR) << "An uncompressed frame has not the proper size: " 
+                   << frame.size() << " instead of " << uncompressedFrameSize_;
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize);
+      }
+
+      uncompressedPixelData_.AddChunk(frame);
+    }
+    else
+    {
+      uint8_t* bytes = const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(frame.c_str()));
+
+      compressedPixelSequence_->storeCompressedFrame(*offsetList_, bytes, frame.size(), 
+                                                     0 /* unlimited fragment size */);
+    }
+
+    if (functionalGroup != NULL)
+    {
+      perFrameFunctionalGroups_->insert(functionalGroupRaii.release());
+    }
+    else
+    {
+      perFrameFunctionalGroups_->insert(new DcmItem);
+    }
+
+    writtenSize_ += frame.size();
+    framesCount_ += 1;
+  }
+
+
+  void MultiframeDicomWriter::Flush(std::string& target,
+                                    unsigned int instanceNumber)
+  {
+    if (instanceNumber <= 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    std::string tmp = boost::lexical_cast<std::string>(instanceNumber);
+
+    std::auto_ptr<DcmFileFormat> dicom(new DcmFileFormat);
+
+    char uid[100];
+    dcmGenerateUniqueIdentifier(uid, SITE_INSTANCE_UID_ROOT);
+
+    if (!dicom->getDataset()->copyFrom(sharedTags_).good() ||
+        !dicom->getDataset()->insert(perFrameFunctionalGroups_.release(), false, false).good() ||
+        !dicom->getDataset()->putAndInsertString(DCM_SOPInstanceUID, uid).good() ||
+        !dicom->getDataset()->putAndInsertString(DCM_InstanceNumber, tmp.c_str()).good())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    DicomToolbox::SetStringTag(*dicom->getDataset(), DCM_NumberOfFrames, boost::lexical_cast<std::string>(framesCount_));
+
+    switch (compression_)
+    {
+      case ImageCompression_None:
+        InjectUncompressedPixelData(*dicom);
+        break;
+
+      case ImageCompression_Jpeg:
+      case ImageCompression_Jpeg2000:
+        offsetTable_->createOffsetTable(*offsetList_);
+        dicom->getDataset()->insert(compressedPixelSequence_.release());
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    ResetImage();
+
+    SaveDicomToMemory(target, *dicom, transferSyntax_);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Outputs/MultiframeDicomWriter.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,93 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "../Enumerations.h"
+#include "../Orthanc/Core/ChunkedBuffer.h"
+
+#include <boost/noncopyable.hpp>
+#include <memory>
+#include <stdint.h>
+
+#include <dcmtk/dcmdata/dcpixseq.h>
+#include <dcmtk/dcmdata/dcdatset.h>
+#include <dcmtk/dcmdata/dcfilefo.h>
+
+namespace OrthancWSI
+{
+  class MultiframeDicomWriter : public boost::noncopyable
+  {
+  private:
+    ImageCompression  compression_;
+    E_TransferSyntax  transferSyntax_;
+    DcmDataset        sharedTags_;
+    size_t            writtenSize_;
+    size_t            framesCount_;
+    size_t            uncompressedFrameSize_;
+    unsigned int      width_;
+    unsigned int      height_;
+
+    Orthanc::ChunkedBuffer             uncompressedPixelData_;
+    std::auto_ptr<DcmSequenceOfItems>  perFrameFunctionalGroups_;
+    std::auto_ptr<DcmPixelSequence>    compressedPixelSequence_;
+    DcmPixelItem*                      offsetTable_;
+    std::auto_ptr<DcmOffsetList>       offsetList_;
+
+    void ResetImage();
+
+    void InjectUncompressedPixelData(DcmFileFormat& dicom);
+
+  public:
+    MultiframeDicomWriter(const DcmDataset& dataset,
+                          ImageCompression compression,
+                          Orthanc::PixelFormat pixelFormat,
+                          unsigned int width,
+                          unsigned int height,
+                          unsigned int tileWidth,
+                          unsigned int tileHeight);
+
+    void AddFrame(const std::string& frame,
+                  DcmItem* functionalGroup);   // This takes the ownership
+
+    void Flush(std::string& target,
+               unsigned int instanceNumber);
+
+    unsigned int GetFramesCount() const
+    {
+      return framesCount_;
+    }
+
+    size_t GetSize() const
+    {
+      return writtenSize_;
+    }
+
+    unsigned int GetTotalWidth() const
+    {
+      return width_;
+    }
+
+    unsigned int GetTotalHeight() const
+    {
+      return height_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Outputs/PyramidWriterBase.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,150 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "PyramidWriterBase.h"
+
+#include "../ImageToolbox.h"
+#include "../Orthanc/Core/OrthancException.h"
+#include "../Orthanc/Core/Logging.h"
+
+namespace OrthancWSI
+{
+  PyramidWriterBase::Level PyramidWriterBase::GetLevel(unsigned int level) const
+  {
+    boost::mutex::scoped_lock lock(const_cast<PyramidWriterBase&>(*this).mutex_);
+
+    if (level >= levels_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return levels_[level];
+    }
+  }
+
+
+  PyramidWriterBase::PyramidWriterBase(Orthanc::PixelFormat pixelFormat,
+                                       ImageCompression compression,
+                                       unsigned int tileWidth,
+                                       unsigned int tileHeight) :
+    pixelFormat_(pixelFormat),
+    compression_(compression),
+    tileWidth_(tileWidth),
+    tileHeight_(tileHeight),
+    jpegQuality_(90),   // Default JPEG quality
+    first_(true)
+  {
+  }
+
+
+  void PyramidWriterBase::SetJpegQuality(int quality)
+  {
+    if (quality <= 0 || quality > 100)
+    {
+      LOG(ERROR) << "The JPEG quality must be in range [1;100], but " << quality << " is provided";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    jpegQuality_ = quality;
+  }
+
+
+  void PyramidWriterBase::AddLevel(unsigned int width,
+                                   unsigned int height)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+      
+    if (!first_)
+    {
+      LOG(ERROR) << "Cannot add pyramid levels after some tile has already been written";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+
+    if (!levels_.empty())
+    {
+      const Level& previous = levels_[levels_.size() - 1];
+
+      if (width >= previous.width_ ||
+          height >= previous.height_ ||
+          width == 0 ||
+          height == 0)
+      {
+        LOG(ERROR) << "Levels must have strictly decreasing sizes";
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    Level level;
+    level.z_ = levels_.size();
+    level.width_ = width;
+    level.height_ = height;
+    level.countTilesX_ = CeilingDivision(width, tileWidth_);
+    level.countTilesY_ = CeilingDivision(height, tileHeight_);
+    levels_.push_back(level);
+
+    AddLevelInternal(level);
+  }
+
+
+  unsigned int PyramidWriterBase::GetLevelCount() const
+  {
+    boost::mutex::scoped_lock lock(const_cast<PyramidWriterBase&>(*this).mutex_);
+    return levels_.size();
+  }
+
+
+  void PyramidWriterBase::WriteRawTile(const std::string& tile,
+                                       ImageCompression compression,
+                                       unsigned int z,
+                                       unsigned int x,
+                                       unsigned int y)
+  {
+    first_ = false;
+
+    const Level level = GetLevel(z);
+
+    if (compression != compression_)
+    {
+      std::string recoded;
+      ImageToolbox::ChangeTileCompression(recoded, tile, compression, compression_, jpegQuality_);
+      WriteRawTileInternal(recoded, level, x, y);
+    }
+    else
+    {
+      WriteRawTileInternal(tile, level, x, y);
+    }
+  }
+
+
+  void PyramidWriterBase::EncodeTile(const Orthanc::ImageAccessor& tile,
+                                     unsigned int z,
+                                     unsigned int x, 
+                                     unsigned int y)
+  {
+    first_ = false;
+
+    const Level level = GetLevel(z);
+
+    std::string raw;
+    ImageToolbox::EncodeTile(raw, tile, compression_, jpegQuality_);
+    WriteRawTileInternal(raw, level, x, y);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Outputs/PyramidWriterBase.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,124 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "IPyramidWriter.h"
+
+#include <boost/thread.hpp>
+
+namespace OrthancWSI
+{
+  class PyramidWriterBase : public IPyramidWriter
+  {
+  protected:
+    struct Level
+    {
+      unsigned int  z_;
+      unsigned int  width_;
+      unsigned int  height_;
+      unsigned int  countTilesX_;
+      unsigned int  countTilesY_;
+    };
+
+    // WARNING: The following method can be called from multiple
+    // threads, locking must be implemented in derived classes.
+    virtual void WriteRawTileInternal(const std::string& tile,
+                                      const Level& level,
+                                      unsigned int tileX,
+                                      unsigned int tileY) = 0;
+
+    // This function is invoked before any call to WriteRawTileInternal()
+    virtual void AddLevelInternal(const Level& level) = 0;
+
+  private:
+    boost::mutex          mutex_;   // This mutex protects access to the levels
+    Orthanc::PixelFormat  pixelFormat_;
+    ImageCompression      compression_;
+    unsigned int          tileWidth_;
+    unsigned int          tileHeight_;
+    uint8_t               jpegQuality_;
+    std::vector<Level>    levels_;
+    bool                  first_;
+
+    Level GetLevel(unsigned int level) const;
+
+  public:
+    PyramidWriterBase(Orthanc::PixelFormat pixelFormat,
+                      ImageCompression compression,
+                      unsigned int tileWidth,
+                      unsigned int tileHeight);
+
+    uint8_t GetJpegQuality() const
+    {
+      return jpegQuality_;
+    }
+
+    void SetJpegQuality(int quality);
+
+    virtual void AddLevel(unsigned int width,
+                          unsigned int height);
+
+    virtual unsigned int GetTileWidth() const
+    {
+      return tileWidth_;
+    }
+
+    virtual unsigned int GetTileHeight() const 
+    {
+      return tileHeight_;
+    }
+
+    virtual unsigned int GetCountTilesX(unsigned int level) const
+    {
+      return GetLevel(level).countTilesX_;
+    }
+
+    virtual unsigned int GetCountTilesY(unsigned int level) const
+    {
+      return GetLevel(level).countTilesY_;
+    }
+
+    virtual unsigned int GetLevelCount() const;
+
+    ImageCompression GetImageCompression() const
+    {
+      return compression_;
+    }
+
+    virtual void WriteRawTile(const std::string& tile,
+                              ImageCompression compression,
+                              unsigned int z,
+                              unsigned int tileX,
+                              unsigned int tileY);
+
+    virtual void EncodeTile(const Orthanc::ImageAccessor& tile,
+                            unsigned int z,
+                            unsigned int tileX, 
+                            unsigned int tileY);
+
+    virtual Orthanc::PixelFormat GetPixelFormat() const
+    {
+      return pixelFormat_;
+    }
+
+    virtual void Flush() = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Outputs/TruncatedPyramidWriter.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,117 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "TruncatedPyramidWriter.h"
+
+#include "../Orthanc/Core/OrthancException.h"
+
+namespace OrthancWSI
+{
+  TruncatedPyramidWriter::TruncatedPyramidWriter(IPyramidWriter& lower,
+                                                 unsigned int upperLevelIndex) :
+    lowerLevels_(lower),
+    upperLevel_(lower.GetPixelFormat(),
+                lower.GetCountTilesX(upperLevelIndex),
+                lower.GetCountTilesY(upperLevelIndex),
+                lower.GetTileWidth(),
+                lower.GetTileHeight()),
+    upperLevelIndex_(upperLevelIndex)
+  {
+    if (upperLevelIndex > lower.GetLevelCount())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  unsigned int TruncatedPyramidWriter::GetCountTilesX(unsigned int level) const
+  {
+    if (level < upperLevelIndex_)
+    {
+      return lowerLevels_.GetCountTilesX(level);
+    }
+    else if (level == upperLevelIndex_)
+    {
+      return upperLevel_.GetCountTilesX(0);
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  unsigned int TruncatedPyramidWriter::GetCountTilesY(unsigned int level) const
+  {
+    if (level < upperLevelIndex_)
+    {
+      return lowerLevels_.GetCountTilesY(level);
+    }
+    else if (level == upperLevelIndex_)
+    {
+      return upperLevel_.GetCountTilesY(0);
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  void TruncatedPyramidWriter::WriteRawTile(const std::string& tile,
+                                            ImageCompression compression,
+                                            unsigned int level,
+                                            unsigned int x,
+                                            unsigned int y)
+  {
+    if (level < upperLevelIndex_)
+    {
+      lowerLevels_.WriteRawTile(tile, compression, level, x, y);
+    }
+    else if (level == upperLevelIndex_)
+    {
+      upperLevel_.WriteRawTile(tile, compression, 0, x, y);
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  void TruncatedPyramidWriter::EncodeTile(const Orthanc::ImageAccessor& tile,
+                                          unsigned int level,
+                                          unsigned int x, 
+                                          unsigned int y)
+  {
+    if (level < upperLevelIndex_)
+    {
+      lowerLevels_.EncodeTile(tile, level, x, y);
+    }
+    else if (level == upperLevelIndex_)
+    {
+      upperLevel_.EncodeTile(tile, 0, x, y);
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Outputs/TruncatedPyramidWriter.h	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,78 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You 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 "InMemoryTiledImage.h"
+
+namespace OrthancWSI
+{
+  class TruncatedPyramidWriter : public IPyramidWriter
+  {
+  private:
+    IPyramidWriter&     lowerLevels_;
+    InMemoryTiledImage  upperLevel_;
+    unsigned int        upperLevelIndex_;
+    
+  public:
+    TruncatedPyramidWriter(IPyramidWriter& lower,
+                           unsigned int upperLevelIndex);
+
+    virtual unsigned int GetLevelCount() const
+    {
+      return upperLevelIndex_ + 1;
+    }
+
+    virtual Orthanc::PixelFormat GetPixelFormat() const 
+    {
+      return lowerLevels_.GetPixelFormat();
+    }
+
+    virtual unsigned int GetTileWidth() const
+    {
+      return lowerLevels_.GetTileWidth();
+    }
+
+    virtual unsigned int GetTileHeight() const 
+    {
+      return lowerLevels_.GetTileHeight();
+    }
+
+    virtual unsigned int GetCountTilesX(unsigned int level) const;
+
+    virtual unsigned int GetCountTilesY(unsigned int level) const;
+
+    virtual void WriteRawTile(const std::string& tile,
+                              ImageCompression compression,
+                              unsigned int level,
+                              unsigned int x,
+                              unsigned int y);
+
+    virtual void EncodeTile(const Orthanc::ImageAccessor& tile,
+                            unsigned int level,
+                            unsigned int x, 
+                            unsigned int y);
+
+    InMemoryTiledImage& GetUpperLevel()
+    {
+      return upperLevel_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/NEWS	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,4 @@
+2016-10-22
+==========
+
+* Initial release
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,85 @@
+Orthanc for Whole-Slide Imaging
+===============================
+
+
+General Information
+-------------------
+
+This repository contains the source code of the official tools to
+introduce support of whole-slide microscopic imaging (WSI) in
+Orthanc. It is made of three separate components:
+
+(a) A command-line tool called "Dicomizer" that converts whole-slide
+    images to DICOM, according to Supplement 145. 
+
+    The input images of the Dicomizer can be either formatted as
+    standard hierarchical TIFF images, or as proprietary formats for
+    digital pathology. In the latter case, OpenSlide is used to decode
+    the input images.
+
+(b) An Orthanc plugin that extends Orthanc Explorer with a Web viewer
+    of whole-slide images.
+
+(c) A command-line tool called "DicomToTiff" that converts some
+    whole-slide image stored in an Orthanc server, into a standard
+    hierarchical TIFF file. This tool can be used to export DICOM
+    images to any post-processing framework.
+
+
+
+Dependencies
+------------
+
+The whole-slide imaging framework is notably based upon the following
+projects:
+
+* Orthanc, a lightweight Vendor Neutral Archive (DICOM server):
+  http://www.orthanc-server.com/
+
+* OpenSlide, a simple interface to read whole-slide images encoded
+  using proprietary file formats:
+  http://openslide.org/
+
+* OpenLayers, a framework to put a dynamic map in any web page:
+  https://openlayers.org/
+
+* OpenJpeg, an open-source JPEG 2000 codec:
+  http://www.openjpeg.org/
+
+
+
+Installation and usage
+----------------------
+
+Build instructions are similar to that of Orthanc:
+https://orthanc.chu.ulg.ac.be/book/faq/compiling.html
+
+The two command-line tools can be found in folder "Applications".
+They come with an extensive "--help" option.
+
+The Web viewer plugin can be found in folder "ViewerPlugin".
+
+
+
+Licensing
+---------
+
+The WSI toolbox for Orthanc is licensed under the AGPL license.
+
+We also kindly ask scientific works and clinical studies that make
+use of Orthanc to cite Orthanc in their associated publications.
+Similarly, we ask open-source and closed-source products that make
+use of Orthanc to warn us about this use. You can cite our work
+using the following BibTeX entry:
+
+@inproceedings{Jodogne:ISBI2013,
+  author = {Jodogne, S. and Bernard, C. and Devillers, M. and Lenaerts, E. and Coucke, P.},
+  title = {Orthanc -- {A} Lightweight, {REST}ful {DICOM} Server for Healthcare and Medical Research},
+  booktitle={Biomedical Imaging ({ISBI}), {IEEE} 10th International Symposium on}, 
+  year={2013}, 
+  pages={190-193}, 
+  ISSN={1945-7928},
+  month=apr,
+  url={http://ieeexplore.ieee.org/xpl/articleDetails.jsp?tp=&arnumber=6556444},
+  address={San Francisco, {CA}, {USA}}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/BrightfieldOpticalPath.json	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,20 @@
+[
+  {
+    "OpticalPathIdentifier" : "1",
+    "OpticalPathDescription" : "Brightfield",
+    "IlluminationTypeCodeSequence" : [
+      {
+        "CodeValue" : "111744",
+        "CodingSchemeDesignator" : "DCM",
+        "CodeMeaning" : "Brightfield illumination"
+      }
+    ],
+    "IlluminationColorCodeSequence" : [
+      {
+        "CodeValue" : "R-102C0",
+        "CodingSchemeDesignator" : "SRT",
+        "CodeMeaning" : "Full Spectrum"
+      }
+    ]
+  }
+]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/CMake/BoostExtendedConfiguration.cmake	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,21 @@
+SET(ORTHANC_BOOST_COMPONENTS program_options)
+
+include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake)
+
+if (BOOST_STATIC)
+  list(APPEND BOOST_SOURCES
+    ${BOOST_SOURCES_DIR}/libs/program_options/src/cmdline.cpp
+    ${BOOST_SOURCES_DIR}/libs/program_options/src/config_file.cpp
+    ${BOOST_SOURCES_DIR}/libs/program_options/src/convert.cpp
+    ${BOOST_SOURCES_DIR}/libs/program_options/src/options_description.cpp
+    ${BOOST_SOURCES_DIR}/libs/program_options/src/parsers.cpp
+    ${BOOST_SOURCES_DIR}/libs/program_options/src/positional_options.cpp
+    ${BOOST_SOURCES_DIR}/libs/program_options/src/split.cpp
+    ${BOOST_SOURCES_DIR}/libs/program_options/src/utf8_codecvt_facet.cpp
+    ${BOOST_SOURCES_DIR}/libs/program_options/src/value_semantic.cpp
+    ${BOOST_SOURCES_DIR}/libs/program_options/src/variables_map.cpp
+    #${BOOST_SOURCES_DIR}/libs/program_options/src/winmain.cpp
+    )
+  add_definitions(-DBOOST_PROGRAM_OPTIONS_NO_LIB)
+endif()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/CMake/LibTiffConfiguration.cmake	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,115 @@
+if (STATIC_BUILD OR NOT USE_SYSTEM_LIBTIFF)
+  SET(LIBTIFF_SOURCES_DIR ${CMAKE_BINARY_DIR}/tiff-4.0.6)
+  SET(LIBTIFF_URL "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/WSI/tiff-4.0.6.tar.gz")
+  SET(LIBTIFF_MD5 "d1d2e940dea0b5ad435f21f03d96dd72")
+
+  DownloadPackage(${LIBTIFF_MD5} ${LIBTIFF_URL} "${LIBTIFF_SOURCES_DIR}")
+
+  if (NOT EXISTS ${LIBTIFF_SOURCES_DIR}/libtiff/tif_config.h)
+    file(WRITE ${LIBTIFF_SOURCES_DIR}/libtiff/tif_config.h "
+#include <stdint.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <string.h>
+")
+    file(WRITE ${LIBTIFF_SOURCES_DIR}/libtiff/tiffconf.h "
+#include <stdint.h>
+#include <sys/types.h>
+")
+  endif()
+
+  set(TIFF_FILLORDER FILLORDER_MSB2LSB)
+  if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "i.*86.*" OR
+      CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "amd64.*" OR
+      CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "x86_64.*")
+    set(TIFF_FILLORDER FILLORDER_LSB2MSB)
+  endif()
+
+  add_definitions(
+    -DTIFF_INT8_T=int8_t
+    -DTIFF_INT16_T=int16_t
+    -DTIFF_INT32_T=int32_t
+    -DTIFF_INT64_T=int64_t
+    -DTIFF_UINT8_T=uint8_t
+    -DTIFF_UINT16_T=uint16_t
+    -DTIFF_UINT32_T=uint32_t
+    -DTIFF_UINT64_T=uint64_t
+    -DTIFF_SSIZE_T=ssize_t
+    -DHAVE_IEEEFP=1
+    -DHOST_FILLORDER=${TIFF_FILLORDER}
+    -DHAVE_SNPRINTF=1
+    -DJPEG_SUPPORT=1
+    -DLZW_SUPPORT=1
+
+    -DTIFF_INT64_FORMAT="%lld"
+    -DTIFF_UINT64_FORMAT="%llu"
+    -DTIFF_SSIZE_FORMAT="%d"
+    )
+
+  set(LIBTIFF_SOURCES
+    #${LIBTIFF_SOURCES_DIR}/libtiff/mkg3states.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_aux.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_close.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_codec.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_color.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_compress.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_dir.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_dirinfo.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_dirread.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_dirwrite.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_dumpmode.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_error.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_extension.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_fax3.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_fax3sm.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_flush.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_getimage.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_jbig.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_jpeg_12.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_jpeg.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_luv.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_lzma.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_lzw.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_next.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_ojpeg.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_open.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_packbits.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_pixarlog.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_predict.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_print.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_read.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_strip.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_swab.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_thunder.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_tile.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_unix.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_version.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_warning.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_write.c
+    ${LIBTIFF_SOURCES_DIR}/libtiff/tif_zip.c
+    )
+
+  include_directories(${LIBTIFF_SOURCES_DIR}/libtiff)
+
+  if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "Windows")
+    list(APPEND LIBTIFF_SOURCES
+      ${LIBTIFF_SOURCES_DIR}/libtiff/tif_win32.c
+      )
+  endif()
+
+  source_group(ThirdParty\\LibTiff REGULAR_EXPRESSION ${LIBTIFF_SOURCES_DIR}/.*)
+
+else()
+  CHECK_INCLUDE_FILE_CXX(tiff.h HAVE_LIBTIFF_H)
+  if (NOT HAVE_LIBTIFF_H)
+    message(FATAL_ERROR "Please install the libtiff-dev package")
+  endif()
+
+  CHECK_LIBRARY_EXISTS(tiff TIFFGetField "" HAVE_LIBTIFF_LIB)
+  if (NOT HAVE_LIBTIFF_LIB)
+    message(FATAL_ERROR "Please install the libtiff-dev package")
+  endif()
+
+  link_libraries(tiff)
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/CMake/OpenJpegConfiguration.cmake	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,144 @@
+if (STATIC_BUILD OR NOT USE_SYSTEM_OPENJPEG)
+  SET(OPENJPEG_SOURCES_DIR ${CMAKE_BINARY_DIR}/openjpeg-version.2.1)
+  SET(OPENJPEG_URL "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/openjpeg-2.1.tar.gz")
+  SET(OPENJPEG_MD5 "3e1c451c087f8462955426da38aa3b3d")
+
+  if (IS_DIRECTORY "${OPENJPEG_SOURCES_DIR}")
+    set(FirstRun OFF)
+  else()
+    set(FirstRun ON)
+  endif()
+
+  DownloadPackage(${OPENJPEG_MD5} ${OPENJPEG_URL} "${OPENJPEG_SOURCES_DIR}")
+
+  execute_process(
+    COMMAND ${PATCH_EXECUTABLE} -p0 -N -i ${CMAKE_CURRENT_LIST_DIR}/OpenJpegConfiguration.patch
+    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+    RESULT_VARIABLE Failure
+    )
+
+  if (Failure AND FirstRun)
+    message(FATAL_ERROR "Error while patching a file")
+  endif()
+
+  if (USE_OPENJPEG_JP2)
+    set(OPENJPEG_SOURCES
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/bio.c
+      #${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/cidx_manager.c
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/cio.c
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/dwt.c
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/event.c
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/function_list.c
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/image.c
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/invert.c
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/j2k.c
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/jp2.c
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/mct.c
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/mqc.c
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/openjpeg.c
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/opj_clock.c
+      #${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/phix_manager.c
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/pi.c
+      #${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/ppix_manager.c
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/raw.c
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/t1.c
+      #${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/t1_generate_luts.c
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/t2.c
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/tcd.c
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/tgt.c
+      #${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/thix_manager.c
+      #${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/tpix_manager.c
+      )
+
+    configure_file(
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/opj_config.h.cmake.in
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/opj_config.h
+      @ONLY
+      )
+    
+    configure_file(
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/opj_config_private.h.cmake.in
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/opj_config_private.h
+      @ONLY
+      )
+
+    include_directories(
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2
+      )
+
+    # The following definition disables explicit inlining. This is
+    # necessary to bypass the "undefined reference to
+    # `opj_t1_dec_sigpass_step_mqc'" error.
+    add_definitions(
+      #-DINLINE=
+      )
+
+  else()
+    AUX_SOURCE_DIRECTORY(${OPENJPEG_SOURCES_DIR}/src/lib/openmj2 OPENJPEG_SOURCES)
+
+    configure_file(
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/opj_config.h.cmake.in
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openmj2/opj_config.h
+      @ONLY
+      )
+    
+    configure_file(
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openjp2/opj_config_private.h.cmake.in
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openmj2/opj_config_private.h
+      @ONLY
+      )
+
+    include_directories(
+      ${OPENJPEG_SOURCES_DIR}/src/lib/openmj2
+      )
+  endif()
+
+
+  add_definitions(
+    -DOPJ_STATIC
+    -DORTHANC_OPENJPEG_MAJOR_VERSION=2
+    )
+
+  if (NOT WIN32)
+    add_definitions(
+      -DOPJ_HAVE_STDINT_H=1
+      -DOPJ_HAVE_INTTYPES_H=1
+      )
+  endif()
+
+  source_group(ThirdParty\\OpenJpeg REGULAR_EXPRESSION ${OPENJPEG_SOURCES_DIR}/.*)
+
+else()
+  CHECK_INCLUDE_FILE_CXX(openjpeg.h HAVE_OPENJPEG_H)
+  if (NOT HAVE_OPENJPEG_H)
+    message(FATAL_ERROR "Please install the openjpeg-devel package")
+  endif()
+
+  find_path(OPENJPEG_INCLUDE_DIR openjpeg.h)
+
+  CHECK_LIBRARY_EXISTS(openjpeg opj_image_create "" HAVE_OPENJPEG_LIB)
+  if (NOT HAVE_OPENJPEG_LIB)
+    message(FATAL_ERROR "Please install the openjpeg-devel package")
+  endif()
+
+  # Autodetection of the version of OpenJpeg
+  file(STRINGS
+    "${OPENJPEG_INCLUDE_DIR}/openjpeg.h" 
+    OPENJPEG_VERSION REGEX
+    "#define OPENJPEG_VERSION")
+
+  string(REGEX REPLACE
+    ".*OPENJPEG_VERSION.*\"([0-9]*)\\.([0-9]*)\\.([0-9]*)\"$"
+    "\\1" 
+    OPENJPEG_MAJOR_VERSION ${OPENJPEG_VERSION})
+
+  if (OPENJPEG_MAJOR_VERSION EQUAL 1)
+    add_definitions(-DORTHANC_OPENJPEG_MAJOR_VERSION=1)
+  elseif (OPENJPEG_MAJOR_VERSION EQUAL 2)
+    add_definitions(-DORTHANC_OPENJPEG_MAJOR_VERSION=2)
+  else()
+    message(FATAL_ERROR "Cannot parse the version of OpenJpeg")
+  endif()
+
+  link_libraries(openjpeg)
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/CMake/OpenJpegConfiguration.patch	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,44 @@
+diff -urEb openjpeg-version.2.1.orig/src/lib/openjp2/t1.c openjpeg-version.2.1/src/lib/openjp2/t1.c
+--- openjpeg-version.2.1.orig/src/lib/openjp2/t1.c	2016-07-15 10:36:18.575913348 +0200
++++ openjpeg-version.2.1/src/lib/openjp2/t1.c	2016-07-15 10:36:45.571914446 +0200
+@@ -85,13 +85,13 @@
+                 OPJ_INT32 orient,
+                 OPJ_INT32 oneplushalf,
+                 OPJ_INT32 vsc);
+-static INLINE void opj_t1_dec_sigpass_step_mqc(
++static void opj_t1_dec_sigpass_step_mqc(
+                 opj_t1_t *t1,
+                 opj_flag_t *flagsp,
+                 OPJ_INT32 *datap,
+                 OPJ_INT32 orient,
+                 OPJ_INT32 oneplushalf);
+-static INLINE void opj_t1_dec_sigpass_step_mqc_vsc(
++static void opj_t1_dec_sigpass_step_mqc_vsc(
+                 opj_t1_t *t1,
+                 opj_flag_t *flagsp,
+                 OPJ_INT32 *datap,
+@@ -179,20 +179,20 @@
+                                     OPJ_UINT32 vsc);
+ #endif
+ 
+-static INLINE void  opj_t1_dec_refpass_step_raw(
++static void  opj_t1_dec_refpass_step_raw(
+                 opj_t1_t *t1,
+                 opj_flag_t *flagsp,
+                 OPJ_INT32 *datap,
+                 OPJ_INT32 poshalf,
+                 OPJ_INT32 neghalf,
+                 OPJ_INT32 vsc);
+-static INLINE void opj_t1_dec_refpass_step_mqc(
++static void opj_t1_dec_refpass_step_mqc(
+                 opj_t1_t *t1,
+                 opj_flag_t *flagsp,
+                 OPJ_INT32 *datap,
+                 OPJ_INT32 poshalf,
+                 OPJ_INT32 neghalf);
+-static INLINE void opj_t1_dec_refpass_step_mqc_vsc(
++static void opj_t1_dec_refpass_step_mqc_vsc(
+                 opj_t1_t *t1,
+                 opj_flag_t *flagsp,
+                 OPJ_INT32 *datap,
+Only in openjpeg-version.2.1/src/lib/openjp2: t1.c~
Binary file Resources/OrthancLogoDocumentation.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/OrthancWSI.doxygen	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,1794 @@
+# Doxyfile 1.8.1.2
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project.
+#
+# All text after a hash (#) is considered a comment and will be ignored.
+# The format is:
+#       TAG = value [value, ...]
+# For lists items can also be appended using:
+#       TAG += value [value, ...]
+# Values that contain spaces should be placed between quotes (" ").
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+# This tag specifies the encoding used for all characters in the config file
+# that follow. The default is UTF-8 which is also the encoding used for all
+# text before the first occurrence of this tag. Doxygen uses libiconv (or the
+# iconv built into libc) for the transcoding. See
+# http://www.gnu.org/software/libiconv for the list of possible encodings.
+
+DOXYFILE_ENCODING      = UTF-8
+
+# The PROJECT_NAME tag is a single word (or sequence of words) that should
+# identify the project. Note that if you do not use Doxywizard you need
+# to put quotes around the project name if it contains spaces.
+
+PROJECT_NAME           = OrthancWSI
+
+# The PROJECT_NUMBER tag can be used to enter a project or revision number.
+# This could be handy for archiving the generated documentation or
+# if some version control system is used.
+
+PROJECT_NUMBER         =
+
+# Using the PROJECT_BRIEF tag one can provide an optional one line description
+# for a project that appears at the top of each page and should give viewer
+# a quick idea about the purpose of the project. Keep the description short.
+
+PROJECT_BRIEF          = "Orthanc for Whole-Slide Imaging API"
+
+# With the PROJECT_LOGO tag one can specify an logo or icon that is
+# included in the documentation. The maximum height of the logo should not
+# exceed 55 pixels and the maximum width should not exceed 200 pixels.
+# Doxygen will copy the logo to the output directory.
+
+PROJECT_LOGO           = @ORTHANC_WSI_DIR@/Resources/OrthancLogoDocumentation.png
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute)
+# base path where the generated documentation will be put.
+# If a relative path is entered, it will be relative to the location
+# where doxygen was started. If left blank the current directory will be used.
+
+OUTPUT_DIRECTORY       = OrthancWSIDocumentation
+
+# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create
+# 4096 sub-directories (in 2 levels) under the output directory of each output
+# format and will distribute the generated files over these directories.
+# Enabling this option can be useful when feeding doxygen a huge amount of
+# source files, where putting all generated files in the same directory would
+# otherwise cause performance problems for the file system.
+
+CREATE_SUBDIRS         = NO
+
+# The OUTPUT_LANGUAGE tag is used to specify the language in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all constant output in the proper language.
+# The default language is English, other supported languages are:
+# Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional,
+# Croatian, Czech, Danish, Dutch, Esperanto, Farsi, Finnish, French, German,
+# Greek, Hungarian, Italian, Japanese, Japanese-en (Japanese with English
+# messages), Korean, Korean-en, Lithuanian, Norwegian, Macedonian, Persian,
+# Polish, Portuguese, Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak,
+# Slovene, Spanish, Swedish, Ukrainian, and Vietnamese.
+
+OUTPUT_LANGUAGE        = English
+
+# If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will
+# include brief member descriptions after the members that are listed in
+# the file and class documentation (similar to JavaDoc).
+# Set to NO to disable this.
+
+BRIEF_MEMBER_DESC      = YES
+
+# If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend
+# the brief description of a member or function before the detailed description.
+# Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
+# brief descriptions will be completely suppressed.
+
+REPEAT_BRIEF           = YES
+
+# This tag implements a quasi-intelligent brief description abbreviator
+# that is used to form the text in various listings. Each string
+# in this list, if found as the leading text of the brief description, will be
+# stripped from the text and the result after processing the whole list, is
+# used as the annotated text. Otherwise, the brief description is used as-is.
+# If left blank, the following values are used ("$name" is automatically
+# replaced with the name of the entity): "The $name class" "The $name widget"
+# "The $name file" "is" "provides" "specifies" "contains"
+# "represents" "a" "an" "the"
+
+ABBREVIATE_BRIEF       =
+
+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
+# Doxygen will generate a detailed section even if there is only a brief
+# description.
+
+ALWAYS_DETAILED_SEC    = NO
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
+# inherited members of a class in the documentation of that class as if those
+# members were ordinary class members. Constructors, destructors and assignment
+# operators of the base classes will not be shown.
+
+INLINE_INHERITED_MEMB  = NO
+
+# If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full
+# path before files name in the file list and in the header files. If set
+# to NO the shortest path that makes the file name unique will be used.
+
+FULL_PATH_NAMES        = NO
+
+# If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag
+# can be used to strip a user-defined part of the path. Stripping is
+# only done if one of the specified strings matches the left-hand part of
+# the path. The tag can be used to show relative paths in the file list.
+# If left blank the directory from which doxygen is run is used as the
+# path to strip.
+
+STRIP_FROM_PATH        =
+
+# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of
+# the path mentioned in the documentation of a class, which tells
+# the reader which header file to include in order to use a class.
+# If left blank only the name of the header file containing the class
+# definition is used. Otherwise one should specify the include paths that
+# are normally passed to the compiler using the -I flag.
+
+STRIP_FROM_INC_PATH    =
+
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter
+# (but less readable) file names. This can be useful if your file system
+# doesn't support long names like on DOS, Mac, or CD-ROM.
+
+SHORT_NAMES            = NO
+
+# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen
+# will interpret the first line (until the first dot) of a JavaDoc-style
+# comment as the brief description. If set to NO, the JavaDoc
+# comments will behave just like regular Qt-style comments
+# (thus requiring an explicit @brief command for a brief description.)
+
+JAVADOC_AUTOBRIEF      = NO
+
+# If the QT_AUTOBRIEF tag is set to YES then Doxygen will
+# interpret the first line (until the first dot) of a Qt-style
+# comment as the brief description. If set to NO, the comments
+# will behave just like regular Qt-style comments (thus requiring
+# an explicit \brief command for a brief description.)
+
+QT_AUTOBRIEF           = NO
+
+# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen
+# treat a multi-line C++ special comment block (i.e. a block of //! or ///
+# comments) as a brief description. This used to be the default behaviour.
+# The new default is to treat a multi-line C++ comment block as a detailed
+# description. Set this tag to YES if you prefer the old behaviour instead.
+
+MULTILINE_CPP_IS_BRIEF = NO
+
+# If the INHERIT_DOCS tag is set to YES (the default) then an undocumented
+# member inherits the documentation from any documented member that it
+# re-implements.
+
+INHERIT_DOCS           = YES
+
+# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce
+# a new page for each member. If set to NO, the documentation of a member will
+# be part of the file/class/namespace that contains it.
+
+SEPARATE_MEMBER_PAGES  = NO
+
+# The TAB_SIZE tag can be used to set the number of spaces in a tab.
+# Doxygen uses this value to replace tabs by spaces in code fragments.
+
+TAB_SIZE               = 8
+
+# This tag can be used to specify a number of aliases that acts
+# as commands in the documentation. An alias has the form "name=value".
+# For example adding "sideeffect=\par Side Effects:\n" will allow you to
+# put the command \sideeffect (or @sideeffect) in the documentation, which
+# will result in a user-defined paragraph with heading "Side Effects:".
+# You can put \n's in the value part of an alias to insert newlines.
+
+ALIASES                =
+
+# This tag can be used to specify a number of word-keyword mappings (TCL only).
+# A mapping has the form "name=value". For example adding
+# "class=itcl::class" will allow you to use the command class in the
+# itcl::class meaning.
+
+TCL_SUBST              =
+
+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C
+# sources only. Doxygen will then generate output that is more tailored for C.
+# For instance, some of the names that are used will be different. The list
+# of all members will be omitted, etc.
+
+OPTIMIZE_OUTPUT_FOR_C  = NO
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java
+# sources only. Doxygen will then generate output that is more tailored for
+# Java. For instance, namespaces will be presented as packages, qualified
+# scopes will look different, etc.
+
+OPTIMIZE_OUTPUT_JAVA   = NO
+
+# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran
+# sources only. Doxygen will then generate output that is more tailored for
+# Fortran.
+
+OPTIMIZE_FOR_FORTRAN   = NO
+
+# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL
+# sources. Doxygen will then generate output that is tailored for
+# VHDL.
+
+OPTIMIZE_OUTPUT_VHDL   = NO
+
+# Doxygen selects the parser to use depending on the extension of the files it
+# parses. With this tag you can assign which parser to use for a given extension.
+# Doxygen has a built-in mapping, but you can override or extend it using this
+# tag. The format is ext=language, where ext is a file extension, and language
+# is one of the parsers supported by doxygen: IDL, Java, Javascript, CSharp, C,
+# C++, D, PHP, Objective-C, Python, Fortran, VHDL, C, C++. For instance to make
+# doxygen treat .inc files as Fortran files (default is PHP), and .f files as C
+# (default is Fortran), use: inc=Fortran f=C. Note that for custom extensions
+# you also need to set FILE_PATTERNS otherwise the files are not read by doxygen.
+
+EXTENSION_MAPPING      =
+
+# If MARKDOWN_SUPPORT is enabled (the default) then doxygen pre-processes all
+# comments according to the Markdown format, which allows for more readable
+# documentation. See http://daringfireball.net/projects/markdown/ for details.
+# The output of markdown processing is further processed by doxygen, so you
+# can mix doxygen, HTML, and XML commands with Markdown formatting.
+# Disable only in case of backward compatibilities issues.
+
+MARKDOWN_SUPPORT       = YES
+
+# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want
+# to include (a tag file for) the STL sources as input, then you should
+# set this tag to YES in order to let doxygen match functions declarations and
+# definitions whose arguments contain STL classes (e.g. func(std::string); v.s.
+# func(std::string) {}). This also makes the inheritance and collaboration
+# diagrams that involve STL classes more complete and accurate.
+
+BUILTIN_STL_SUPPORT    = YES
+
+# If you use Microsoft's C++/CLI language, you should set this option to YES to
+# enable parsing support.
+
+CPP_CLI_SUPPORT        = NO
+
+# Set the SIP_SUPPORT tag to YES if your project consists of sip sources only.
+# Doxygen will parse them like normal C++ but will assume all classes use public
+# instead of private inheritance when no explicit protection keyword is present.
+
+SIP_SUPPORT            = NO
+
+# For Microsoft's IDL there are propget and propput attributes to indicate getter
+# and setter methods for a property. Setting this option to YES (the default)
+# will make doxygen replace the get and set methods by a property in the
+# documentation. This will only work if the methods are indeed getting or
+# setting a simple type. If this is not the case, or you want to show the
+# methods anyway, you should set this option to NO.
+
+IDL_PROPERTY_SUPPORT   = YES
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
+# tag is set to YES, then doxygen will reuse the documentation of the first
+# member in the group (if any) for the other members of the group. By default
+# all members of a group must be documented explicitly.
+
+DISTRIBUTE_GROUP_DOC   = NO
+
+# Set the SUBGROUPING tag to YES (the default) to allow class member groups of
+# the same type (for instance a group of public functions) to be put as a
+# subgroup of that type (e.g. under the Public Functions section). Set it to
+# NO to prevent subgrouping. Alternatively, this can be done per class using
+# the \nosubgrouping command.
+
+SUBGROUPING            = YES
+
+# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and
+# unions are shown inside the group in which they are included (e.g. using
+# @ingroup) instead of on a separate page (for HTML and Man pages) or
+# section (for LaTeX and RTF).
+
+INLINE_GROUPED_CLASSES = NO
+
+# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and
+# unions with only public data fields will be shown inline in the documentation
+# of the scope in which they are defined (i.e. file, namespace, or group
+# documentation), provided this scope is documented. If set to NO (the default),
+# structs, classes, and unions are shown on a separate page (for HTML and Man
+# pages) or section (for LaTeX and RTF).
+
+INLINE_SIMPLE_STRUCTS  = NO
+
+# When TYPEDEF_HIDES_STRUCT is enabled, a typedef of a struct, union, or enum
+# is documented as struct, union, or enum with the name of the typedef. So
+# typedef struct TypeS {} TypeT, will appear in the documentation as a struct
+# with name TypeT. When disabled the typedef will appear as a member of a file,
+# namespace, or class. And the struct will be named TypeS. This can typically
+# be useful for C code in case the coding convention dictates that all compound
+# types are typedef'ed and only the typedef is referenced, never the tag name.
+
+TYPEDEF_HIDES_STRUCT   = NO
+
+# The SYMBOL_CACHE_SIZE determines the size of the internal cache use to
+# determine which symbols to keep in memory and which to flush to disk.
+# When the cache is full, less often used symbols will be written to disk.
+# For small to medium size projects (<1000 input files) the default value is
+# probably good enough. For larger projects a too small cache size can cause
+# doxygen to be busy swapping symbols to and from disk most of the time
+# causing a significant performance penalty.
+# If the system has enough physical memory increasing the cache will improve the
+# performance by keeping more symbols in memory. Note that the value works on
+# a logarithmic scale so increasing the size by one will roughly double the
+# memory usage. The cache size is given by this formula:
+# 2^(16+SYMBOL_CACHE_SIZE). The valid range is 0..9, the default is 0,
+# corresponding to a cache size of 2^16 = 65536 symbols.
+
+SYMBOL_CACHE_SIZE      = 0
+
+# Similar to the SYMBOL_CACHE_SIZE the size of the symbol lookup cache can be
+# set using LOOKUP_CACHE_SIZE. This cache is used to resolve symbols given
+# their name and scope. Since this can be an expensive process and often the
+# same symbol appear multiple times in the code, doxygen keeps a cache of
+# pre-resolved symbols. If the cache is too small doxygen will become slower.
+# If the cache is too large, memory is wasted. The cache size is given by this
+# formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range is 0..9, the default is 0,
+# corresponding to a cache size of 2^16 = 65536 symbols.
+
+LOOKUP_CACHE_SIZE      = 0
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in
+# documentation are documented, even if no documentation was available.
+# Private class members and static file members will be hidden unless
+# the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES
+
+EXTRACT_ALL            = YES
+
+# If the EXTRACT_PRIVATE tag is set to YES all private members of a class
+# will be included in the documentation.
+
+EXTRACT_PRIVATE        = NO
+
+# If the EXTRACT_PACKAGE tag is set to YES all members with package or internal scope will be included in the documentation.
+
+EXTRACT_PACKAGE        = NO
+
+# If the EXTRACT_STATIC tag is set to YES all static members of a file
+# will be included in the documentation.
+
+EXTRACT_STATIC         = NO
+
+# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs)
+# defined locally in source files will be included in the documentation.
+# If set to NO only classes defined in header files are included.
+
+EXTRACT_LOCAL_CLASSES  = YES
+
+# This flag is only useful for Objective-C code. When set to YES local
+# methods, which are defined in the implementation section but not in
+# the interface are included in the documentation.
+# If set to NO (the default) only methods in the interface are included.
+
+EXTRACT_LOCAL_METHODS  = NO
+
+# If this flag is set to YES, the members of anonymous namespaces will be
+# extracted and appear in the documentation as a namespace called
+# 'anonymous_namespace{file}', where file will be replaced with the base
+# name of the file that contains the anonymous namespace. By default
+# anonymous namespaces are hidden.
+
+EXTRACT_ANON_NSPACES   = NO
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all
+# undocumented members of documented classes, files or namespaces.
+# If set to NO (the default) these members will be included in the
+# various overviews, but no documentation section is generated.
+# This option has no effect if EXTRACT_ALL is enabled.
+
+HIDE_UNDOC_MEMBERS     = NO
+
+# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all
+# undocumented classes that are normally visible in the class hierarchy.
+# If set to NO (the default) these classes will be included in the various
+# overviews. This option has no effect if EXTRACT_ALL is enabled.
+
+HIDE_UNDOC_CLASSES     = NO
+
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all
+# friend (class|struct|union) declarations.
+# If set to NO (the default) these declarations will be included in the
+# documentation.
+
+HIDE_FRIEND_COMPOUNDS  = NO
+
+# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any
+# documentation blocks found inside the body of a function.
+# If set to NO (the default) these blocks will be appended to the
+# function's detailed documentation block.
+
+HIDE_IN_BODY_DOCS      = NO
+
+# The INTERNAL_DOCS tag determines if documentation
+# that is typed after a \internal command is included. If the tag is set
+# to NO (the default) then the documentation will be excluded.
+# Set it to YES to include the internal documentation.
+
+INTERNAL_DOCS          = NO
+
+# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate
+# file names in lower-case letters. If set to YES upper-case letters are also
+# allowed. This is useful if you have classes or files whose names only differ
+# in case and if your file system supports case sensitive file names. Windows
+# and Mac users are advised to set this option to NO.
+
+CASE_SENSE_NAMES       = YES
+
+# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen
+# will show members with their full class and namespace scopes in the
+# documentation. If set to YES the scope will be hidden.
+
+HIDE_SCOPE_NAMES       = NO
+
+# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen
+# will put a list of the files that are included by a file in the documentation
+# of that file.
+
+SHOW_INCLUDE_FILES     = YES
+
+# If the FORCE_LOCAL_INCLUDES tag is set to YES then Doxygen
+# will list include files with double quotes in the documentation
+# rather than with sharp brackets.
+
+FORCE_LOCAL_INCLUDES   = NO
+
+# If the INLINE_INFO tag is set to YES (the default) then a tag [inline]
+# is inserted in the documentation for inline members.
+
+INLINE_INFO            = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen
+# will sort the (detailed) documentation of file and class members
+# alphabetically by member name. If set to NO the members will appear in
+# declaration order.
+
+SORT_MEMBER_DOCS       = YES
+
+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the
+# brief documentation of file, namespace and class members alphabetically
+# by member name. If set to NO (the default) the members will appear in
+# declaration order.
+
+SORT_BRIEF_DOCS        = NO
+
+# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen
+# will sort the (brief and detailed) documentation of class members so that
+# constructors and destructors are listed first. If set to NO (the default)
+# the constructors will appear in the respective orders defined by
+# SORT_MEMBER_DOCS and SORT_BRIEF_DOCS.
+# This tag will be ignored for brief docs if SORT_BRIEF_DOCS is set to NO
+# and ignored for detailed docs if SORT_MEMBER_DOCS is set to NO.
+
+SORT_MEMBERS_CTORS_1ST = NO
+
+# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the
+# hierarchy of group names into alphabetical order. If set to NO (the default)
+# the group names will appear in their defined order.
+
+SORT_GROUP_NAMES       = NO
+
+# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be
+# sorted by fully-qualified names, including namespaces. If set to
+# NO (the default), the class list will be sorted only by class name,
+# not including the namespace part.
+# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
+# Note: This option applies only to the class list, not to the
+# alphabetical list.
+
+SORT_BY_SCOPE_NAME     = NO
+
+# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to
+# do proper type resolution of all parameters of a function it will reject a
+# match between the prototype and the implementation of a member function even
+# if there is only one candidate or it is obvious which candidate to choose
+# by doing a simple string match. By disabling STRICT_PROTO_MATCHING doxygen
+# will still accept a match between prototype and implementation in such cases.
+
+STRICT_PROTO_MATCHING  = NO
+
+# The GENERATE_TODOLIST tag can be used to enable (YES) or
+# disable (NO) the todo list. This list is created by putting \todo
+# commands in the documentation.
+
+GENERATE_TODOLIST      = YES
+
+# The GENERATE_TESTLIST tag can be used to enable (YES) or
+# disable (NO) the test list. This list is created by putting \test
+# commands in the documentation.
+
+GENERATE_TESTLIST      = YES
+
+# The GENERATE_BUGLIST tag can be used to enable (YES) or
+# disable (NO) the bug list. This list is created by putting \bug
+# commands in the documentation.
+
+GENERATE_BUGLIST       = YES
+
+# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or
+# disable (NO) the deprecated list. This list is created by putting
+# \deprecated commands in the documentation.
+
+GENERATE_DEPRECATEDLIST= YES
+
+# The ENABLED_SECTIONS tag can be used to enable conditional
+# documentation sections, marked by \if sectionname ... \endif.
+
+ENABLED_SECTIONS       =
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines
+# the initial value of a variable or macro consists of for it to appear in
+# the documentation. If the initializer consists of more lines than specified
+# here it will be hidden. Use a value of 0 to hide initializers completely.
+# The appearance of the initializer of individual variables and macros in the
+# documentation can be controlled using \showinitializer or \hideinitializer
+# command in the documentation regardless of this setting.
+
+MAX_INITIALIZER_LINES  = 30
+
+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated
+# at the bottom of the documentation of classes and structs. If set to YES the
+# list will mention the files that were used to generate the documentation.
+
+SHOW_USED_FILES        = YES
+
+# Set the SHOW_FILES tag to NO to disable the generation of the Files page.
+# This will remove the Files entry from the Quick Index and from the
+# Folder Tree View (if specified). The default is YES.
+
+SHOW_FILES             = YES
+
+# Set the SHOW_NAMESPACES tag to NO to disable the generation of the
+# Namespaces page.
+# This will remove the Namespaces entry from the Quick Index
+# and from the Folder Tree View (if specified). The default is YES.
+
+SHOW_NAMESPACES        = YES
+
+# The FILE_VERSION_FILTER tag can be used to specify a program or script that
+# doxygen should invoke to get the current version for each file (typically from
+# the version control system). Doxygen will invoke the program by executing (via
+# popen()) the command <command> <input-file>, where <command> is the value of
+# the FILE_VERSION_FILTER tag, and <input-file> is the name of an input file
+# provided by doxygen. Whatever the program writes to standard output
+# is used as the file version. See the manual for examples.
+
+FILE_VERSION_FILTER    =
+
+# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed
+# by doxygen. The layout file controls the global structure of the generated
+# output files in an output format independent way. To create the layout file
+# that represents doxygen's defaults, run doxygen with the -l option.
+# You can optionally specify a file name after the option, if omitted
+# DoxygenLayout.xml will be used as the name of the layout file.
+
+LAYOUT_FILE            =
+
+# The CITE_BIB_FILES tag can be used to specify one or more bib files
+# containing the references data. This must be a list of .bib files. The
+# .bib extension is automatically appended if omitted. Using this command
+# requires the bibtex tool to be installed. See also
+# http://en.wikipedia.org/wiki/BibTeX for more info. For LaTeX the style
+# of the bibliography can be controlled using LATEX_BIB_STYLE. To use this
+# feature you need bibtex and perl available in the search path.
+
+CITE_BIB_FILES         =
+
+#---------------------------------------------------------------------------
+# configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+# The QUIET tag can be used to turn on/off the messages that are generated
+# by doxygen. Possible values are YES and NO. If left blank NO is used.
+
+QUIET                  = NO
+
+# The WARNINGS tag can be used to turn on/off the warning messages that are
+# generated by doxygen. Possible values are YES and NO. If left blank
+# NO is used.
+
+WARNINGS               = YES
+
+# If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings
+# for undocumented members. If EXTRACT_ALL is set to YES then this flag will
+# automatically be disabled.
+
+WARN_IF_UNDOCUMENTED   = YES
+
+# If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for
+# potential errors in the documentation, such as not documenting some
+# parameters in a documented function, or documenting parameters that
+# don't exist or using markup commands wrongly.
+
+WARN_IF_DOC_ERROR      = YES
+
+# The WARN_NO_PARAMDOC option can be enabled to get warnings for
+# functions that are documented, but have no documentation for their parameters
+# or return value. If set to NO (the default) doxygen will only warn about
+# wrong or incomplete parameter documentation, but not about the absence of
+# documentation.
+
+WARN_NO_PARAMDOC       = NO
+
+# The WARN_FORMAT tag determines the format of the warning messages that
+# doxygen can produce. The string should contain the $file, $line, and $text
+# tags, which will be replaced by the file and line number from which the
+# warning originated and the warning text. Optionally the format may contain
+# $version, which will be replaced by the version of the file (if it could
+# be obtained via FILE_VERSION_FILTER)
+
+WARN_FORMAT            = "$file:$line: $text"
+
+# The WARN_LOGFILE tag can be used to specify a file to which warning
+# and error messages should be written. If left blank the output is written
+# to stderr.
+
+WARN_LOGFILE           =
+
+#---------------------------------------------------------------------------
+# configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# The INPUT tag can be used to specify the files and/or directories that contain
+# documented source files. You may enter file names like "myfile.cpp" or
+# directories like "/usr/src/myproject". Separate the files or directories
+# with spaces.
+
+INPUT                  = @ORTHANC_WSI_DIR@/Framework
+
+# This tag can be used to specify the character encoding of the source files
+# that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is
+# also the default input encoding. Doxygen uses libiconv (or the iconv built
+# into libc) for the transcoding. See http://www.gnu.org/software/libiconv for
+# the list of possible encodings.
+
+INPUT_ENCODING         = UTF-8
+
+# If the value of the INPUT tag contains directories, you can use the
+# FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp
+# and *.h) to filter out the source-files in the directories. If left
+# blank the following patterns are tested:
+# *.c *.cc *.cxx *.cpp *.c++ *.d *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh
+# *.hxx *.hpp *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.dox *.py
+# *.f90 *.f *.for *.vhd *.vhdl
+
+FILE_PATTERNS          = *.h
+
+# The RECURSIVE tag can be used to turn specify whether or not subdirectories
+# should be searched for input files as well. Possible values are YES and NO.
+# If left blank NO is used.
+
+RECURSIVE              = YES
+
+# The EXCLUDE tag can be used to specify files and/or directories that should be
+# excluded from the INPUT source files. This way you can easily exclude a
+# subdirectory from a directory tree whose root is specified with the INPUT tag.
+# Note that relative paths are relative to the directory from which doxygen is
+# run.
+
+EXCLUDE                = @ORTHANC_WSI_DIR@/Framework/Orthanc/Resources/ \
+                         @ORTHANC_WSI_DIR@/Framework/Orthanc/Sdk-1.0.0/
+
+# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
+# directories that are symbolic links (a Unix file system feature) are excluded
+# from the input.
+
+EXCLUDE_SYMLINKS       = NO
+
+# If the value of the INPUT tag contains directories, you can use the
+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude
+# certain files from those directories. Note that the wildcards are matched
+# against the file with absolute path, so to exclude all test directories
+# for example use the pattern */test/*
+
+EXCLUDE_PATTERNS       =
+
+# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
+# (namespaces, classes, functions, etc.) that should be excluded from the
+# output. The symbol name can be a fully qualified name, a word, or if the
+# wildcard * is used, a substring. Examples: ANamespace, AClass,
+# AClass::ANamespace, ANamespace::*Test
+
+EXCLUDE_SYMBOLS        = Orthanc::Internals \
+                         OrthancPlugins::Internals
+
+# The EXAMPLE_PATH tag can be used to specify one or more files or
+# directories that contain example code fragments that are included (see
+# the \include command).
+
+EXAMPLE_PATH           =
+
+# If the value of the EXAMPLE_PATH tag contains directories, you can use the
+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp
+# and *.h) to filter out the source-files in the directories. If left
+# blank all files are included.
+
+EXAMPLE_PATTERNS       =
+
+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
+# searched for input files to be used with the \include or \dontinclude
+# commands irrespective of the value of the RECURSIVE tag.
+# Possible values are YES and NO. If left blank NO is used.
+
+EXAMPLE_RECURSIVE      = NO
+
+# The IMAGE_PATH tag can be used to specify one or more files or
+# directories that contain image that are included in the documentation (see
+# the \image command).
+
+IMAGE_PATH             =
+
+# The INPUT_FILTER tag can be used to specify a program that doxygen should
+# invoke to filter for each input file. Doxygen will invoke the filter program
+# by executing (via popen()) the command <filter> <input-file>, where <filter>
+# is the value of the INPUT_FILTER tag, and <input-file> is the name of an
+# input file. Doxygen will then use the output that the filter program writes
+# to standard output.
+# If FILTER_PATTERNS is specified, this tag will be
+# ignored.
+
+INPUT_FILTER           =
+
+# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern
+# basis.
+# Doxygen will compare the file name with each pattern and apply the
+# filter if there is a match.
+# The filters are a list of the form:
+# pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further
+# info on how filters are used. If FILTER_PATTERNS is empty or if
+# non of the patterns match the file name, INPUT_FILTER is applied.
+
+FILTER_PATTERNS        =
+
+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
+# INPUT_FILTER) will be used to filter the input files when producing source
+# files to browse (i.e. when SOURCE_BROWSER is set to YES).
+
+FILTER_SOURCE_FILES    = NO
+
+# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file
+# pattern. A pattern will override the setting for FILTER_PATTERN (if any)
+# and it is also possible to disable source filtering for a specific pattern
+# using *.ext= (so without naming a filter). This option only has effect when
+# FILTER_SOURCE_FILES is enabled.
+
+FILTER_SOURCE_PATTERNS =
+
+#---------------------------------------------------------------------------
+# configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+# If the SOURCE_BROWSER tag is set to YES then a list of source files will
+# be generated. Documented entities will be cross-referenced with these sources.
+# Note: To get rid of all source code in the generated output, make sure also
+# VERBATIM_HEADERS is set to NO.
+
+SOURCE_BROWSER         = NO
+
+# Setting the INLINE_SOURCES tag to YES will include the body
+# of functions and classes directly in the documentation.
+
+INLINE_SOURCES         = NO
+
+# Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct
+# doxygen to hide any special comment blocks from generated source code
+# fragments. Normal C, C++ and Fortran comments will always remain visible.
+
+STRIP_CODE_COMMENTS    = YES
+
+# If the REFERENCED_BY_RELATION tag is set to YES
+# then for each documented function all documented
+# functions referencing it will be listed.
+
+REFERENCED_BY_RELATION = NO
+
+# If the REFERENCES_RELATION tag is set to YES
+# then for each documented function all documented entities
+# called/used by that function will be listed.
+
+REFERENCES_RELATION    = NO
+
+# If the REFERENCES_LINK_SOURCE tag is set to YES (the default)
+# and SOURCE_BROWSER tag is set to YES, then the hyperlinks from
+# functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will
+# link to the source code.
+# Otherwise they will link to the documentation.
+
+REFERENCES_LINK_SOURCE = YES
+
+# If the USE_HTAGS tag is set to YES then the references to source code
+# will point to the HTML generated by the htags(1) tool instead of doxygen
+# built-in source browser. The htags tool is part of GNU's global source
+# tagging system (see http://www.gnu.org/software/global/global.html). You
+# will need version 4.8.6 or higher.
+
+USE_HTAGS              = NO
+
+# If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen
+# will generate a verbatim copy of the header file for each class for
+# which an include is specified. Set to NO to disable this.
+
+VERBATIM_HEADERS       = YES
+
+#---------------------------------------------------------------------------
+# configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index
+# of all compounds will be generated. Enable this if the project
+# contains a lot of classes, structs, unions or interfaces.
+
+ALPHABETICAL_INDEX     = YES
+
+# If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then
+# the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns
+# in which this list will be split (can be a number in the range [1..20])
+
+COLS_IN_ALPHA_INDEX    = 5
+
+# In case all classes in a project start with a common prefix, all
+# classes will be put under the same header in the alphabetical index.
+# The IGNORE_PREFIX tag can be used to specify one or more prefixes that
+# should be ignored while generating the index headers.
+
+IGNORE_PREFIX          =
+
+#---------------------------------------------------------------------------
+# configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES (the default) Doxygen will
+# generate HTML output.
+
+GENERATE_HTML          = YES
+
+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `html' will be used as the default path.
+
+HTML_OUTPUT            = doc
+
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for
+# each generated HTML page (for example: .htm,.php,.asp). If it is left blank
+# doxygen will generate files with .html extension.
+
+HTML_FILE_EXTENSION    = .html
+
+# The HTML_HEADER tag can be used to specify a personal HTML header for
+# each generated HTML page. If it is left blank doxygen will generate a
+# standard header. Note that when using a custom header you are responsible
+#  for the proper inclusion of any scripts and style sheets that doxygen
+# needs, which is dependent on the configuration options used.
+# It is advised to generate a default header using "doxygen -w html
+# header.html footer.html stylesheet.css YourConfigFile" and then modify
+# that header. Note that the header is subject to change so you typically
+# have to redo this when upgrading to a newer version of doxygen or when
+# changing the value of configuration settings such as GENERATE_TREEVIEW!
+
+HTML_HEADER            =
+
+# The HTML_FOOTER tag can be used to specify a personal HTML footer for
+# each generated HTML page. If it is left blank doxygen will generate a
+# standard footer.
+
+HTML_FOOTER            =
+
+# The HTML_STYLESHEET tag can be used to specify a user-defined cascading
+# style sheet that is used by each HTML page. It can be used to
+# fine-tune the look of the HTML output. If the tag is left blank doxygen
+# will generate a default style sheet. Note that doxygen will try to copy
+# the style sheet file to the HTML output directory, so don't put your own
+# style sheet in the HTML output directory as well, or it will be erased!
+
+HTML_STYLESHEET        =
+
+# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the HTML output directory. Note
+# that these files will be copied to the base HTML output directory. Use the
+# $relpath$ marker in the HTML_HEADER and/or HTML_FOOTER files to load these
+# files. In the HTML_STYLESHEET file, use the file name only. Also note that
+# the files will be copied as-is; there are no commands or markers available.
+
+HTML_EXTRA_FILES       =
+
+# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output.
+# Doxygen will adjust the colors in the style sheet and background images
+# according to this color. Hue is specified as an angle on a colorwheel,
+# see http://en.wikipedia.org/wiki/Hue for more information.
+# For instance the value 0 represents red, 60 is yellow, 120 is green,
+# 180 is cyan, 240 is blue, 300 purple, and 360 is red again.
+# The allowed range is 0 to 359.
+
+HTML_COLORSTYLE_HUE    = 220
+
+# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of
+# the colors in the HTML output. For a value of 0 the output will use
+# grayscales only. A value of 255 will produce the most vivid colors.
+
+HTML_COLORSTYLE_SAT    = 100
+
+# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to
+# the luminance component of the colors in the HTML output. Values below
+# 100 gradually make the output lighter, whereas values above 100 make
+# the output darker. The value divided by 100 is the actual gamma applied,
+# so 80 represents a gamma of 0.8, The value 220 represents a gamma of 2.2,
+# and 100 does not change the gamma.
+
+HTML_COLORSTYLE_GAMMA  = 80
+
+# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
+# page will contain the date and time when the page was generated. Setting
+# this to NO can help when comparing the output of multiple runs.
+
+HTML_TIMESTAMP         = NO
+
+# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML
+# documentation will contain sections that can be hidden and shown after the
+# page has loaded.
+
+HTML_DYNAMIC_SECTIONS  = NO
+
+# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of
+# entries shown in the various tree structured indices initially; the user
+# can expand and collapse entries dynamically later on. Doxygen will expand
+# the tree to such a level that at most the specified number of entries are
+# visible (unless a fully collapsed tree already exceeds this amount).
+# So setting the number of entries 1 will produce a full collapsed tree by
+# default. 0 is a special value representing an infinite number of entries
+# and will result in a full expanded tree by default.
+
+HTML_INDEX_NUM_ENTRIES = 100
+
+# If the GENERATE_DOCSET tag is set to YES, additional index files
+# will be generated that can be used as input for Apple's Xcode 3
+# integrated development environment, introduced with OSX 10.5 (Leopard).
+# To create a documentation set, doxygen will generate a Makefile in the
+# HTML output directory. Running make will produce the docset in that
+# directory and running "make install" will install the docset in
+# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find
+# it at startup.
+# See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html
+# for more information.
+
+GENERATE_DOCSET        = NO
+
+# When GENERATE_DOCSET tag is set to YES, this tag determines the name of the
+# feed. A documentation feed provides an umbrella under which multiple
+# documentation sets from a single provider (such as a company or product suite)
+# can be grouped.
+
+DOCSET_FEEDNAME        = "Doxygen generated docs"
+
+# When GENERATE_DOCSET tag is set to YES, this tag specifies a string that
+# should uniquely identify the documentation set bundle. This should be a
+# reverse domain-name style string, e.g. com.mycompany.MyDocSet. Doxygen
+# will append .docset to the name.
+
+DOCSET_BUNDLE_ID       = org.doxygen.Project
+
+# When GENERATE_PUBLISHER_ID tag specifies a string that should uniquely identify
+# the documentation publisher. This should be a reverse domain-name style
+# string, e.g. com.mycompany.MyDocSet.documentation.
+
+DOCSET_PUBLISHER_ID    = org.doxygen.Publisher
+
+# The GENERATE_PUBLISHER_NAME tag identifies the documentation publisher.
+
+DOCSET_PUBLISHER_NAME  = Publisher
+
+# If the GENERATE_HTMLHELP tag is set to YES, additional index files
+# will be generated that can be used as input for tools like the
+# Microsoft HTML help workshop to generate a compiled HTML help file (.chm)
+# of the generated HTML documentation.
+
+GENERATE_HTMLHELP      = NO
+
+# If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can
+# be used to specify the file name of the resulting .chm file. You
+# can add a path in front of the file if the result should not be
+# written to the html output directory.
+
+CHM_FILE               =
+
+# If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can
+# be used to specify the location (absolute path including file name) of
+# the HTML help compiler (hhc.exe). If non-empty doxygen will try to run
+# the HTML help compiler on the generated index.hhp.
+
+HHC_LOCATION           =
+
+# If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag
+# controls if a separate .chi index file is generated (YES) or that
+# it should be included in the master .chm file (NO).
+
+GENERATE_CHI           = NO
+
+# If the GENERATE_HTMLHELP tag is set to YES, the CHM_INDEX_ENCODING
+# is used to encode HtmlHelp index (hhk), content (hhc) and project file
+# content.
+
+CHM_INDEX_ENCODING     =
+
+# If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag
+# controls whether a binary table of contents is generated (YES) or a
+# normal table of contents (NO) in the .chm file.
+
+BINARY_TOC             = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members
+# to the contents of the HTML help documentation and to the tree view.
+
+TOC_EXPAND             = NO
+
+# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
+# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated
+# that can be used as input for Qt's qhelpgenerator to generate a
+# Qt Compressed Help (.qch) of the generated HTML documentation.
+
+GENERATE_QHP           = NO
+
+# If the QHG_LOCATION tag is specified, the QCH_FILE tag can
+# be used to specify the file name of the resulting .qch file.
+# The path specified is relative to the HTML output folder.
+
+QCH_FILE               =
+
+# The QHP_NAMESPACE tag specifies the namespace to use when generating
+# Qt Help Project output. For more information please see
+# http://doc.trolltech.com/qthelpproject.html#namespace
+
+QHP_NAMESPACE          = org.doxygen.Project
+
+# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating
+# Qt Help Project output. For more information please see
+# http://doc.trolltech.com/qthelpproject.html#virtual-folders
+
+QHP_VIRTUAL_FOLDER     = doc
+
+# If QHP_CUST_FILTER_NAME is set, it specifies the name of a custom filter to
+# add. For more information please see
+# http://doc.trolltech.com/qthelpproject.html#custom-filters
+
+QHP_CUST_FILTER_NAME   =
+
+# The QHP_CUST_FILT_ATTRS tag specifies the list of the attributes of the
+# custom filter to add. For more information please see
+# <a href="http://doc.trolltech.com/qthelpproject.html#custom-filters">
+# Qt Help Project / Custom Filters</a>.
+
+QHP_CUST_FILTER_ATTRS  =
+
+# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this
+# project's
+# filter section matches.
+# <a href="http://doc.trolltech.com/qthelpproject.html#filter-attributes">
+# Qt Help Project / Filter Attributes</a>.
+
+QHP_SECT_FILTER_ATTRS  =
+
+# If the GENERATE_QHP tag is set to YES, the QHG_LOCATION tag can
+# be used to specify the location of Qt's qhelpgenerator.
+# If non-empty doxygen will try to run qhelpgenerator on the generated
+# .qhp file.
+
+QHG_LOCATION           =
+
+# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files
+#  will be generated, which together with the HTML files, form an Eclipse help
+# plugin. To install this plugin and make it available under the help contents
+# menu in Eclipse, the contents of the directory containing the HTML and XML
+# files needs to be copied into the plugins directory of eclipse. The name of
+# the directory within the plugins directory should be the same as
+# the ECLIPSE_DOC_ID value. After copying Eclipse needs to be restarted before
+# the help appears.
+
+GENERATE_ECLIPSEHELP   = NO
+
+# A unique identifier for the eclipse help plugin. When installing the plugin
+# the directory name containing the HTML and XML files should also have
+# this name.
+
+ECLIPSE_DOC_ID         = org.doxygen.Project
+
+# The DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs)
+# at top of each HTML page. The value NO (the default) enables the index and
+# the value YES disables it. Since the tabs have the same information as the
+# navigation tree you can set this option to NO if you already set
+# GENERATE_TREEVIEW to YES.
+
+DISABLE_INDEX          = NO
+
+# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
+# structure should be generated to display hierarchical information.
+# If the tag value is set to YES, a side panel will be generated
+# containing a tree-like index structure (just like the one that
+# is generated for HTML Help). For this to work a browser that supports
+# JavaScript, DHTML, CSS and frames is required (i.e. any modern browser).
+# Windows users are probably better off using the HTML help feature.
+# Since the tree basically has the same information as the tab index you
+# could consider to set DISABLE_INDEX to NO when enabling this option.
+
+GENERATE_TREEVIEW      = NO
+
+# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values
+# (range [0,1..20]) that doxygen will group on one line in the generated HTML
+# documentation. Note that a value of 0 will completely suppress the enum
+# values from appearing in the overview section.
+
+ENUM_VALUES_PER_LINE   = 1
+
+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be
+# used to set the initial width (in pixels) of the frame in which the tree
+# is shown.
+
+TREEVIEW_WIDTH         = 250
+
+# When the EXT_LINKS_IN_WINDOW option is set to YES doxygen will open
+# links to external symbols imported via tag files in a separate window.
+
+EXT_LINKS_IN_WINDOW    = NO
+
+# Use this tag to change the font size of Latex formulas included
+# as images in the HTML documentation. The default is 10. Note that
+# when you change the font size after a successful doxygen run you need
+# to manually remove any form_*.png images from the HTML output directory
+# to force them to be regenerated.
+
+FORMULA_FONTSIZE       = 10
+
+# Use the FORMULA_TRANPARENT tag to determine whether or not the images
+# generated for formulas are transparent PNGs. Transparent PNGs are
+# not supported properly for IE 6.0, but are supported on all modern browsers.
+# Note that when changing this option you need to delete any form_*.png files
+# in the HTML output before the changes have effect.
+
+FORMULA_TRANSPARENT    = YES
+
+# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax
+# (see http://www.mathjax.org) which uses client side Javascript for the
+# rendering instead of using prerendered bitmaps. Use this if you do not
+# have LaTeX installed or if you want to formulas look prettier in the HTML
+# output. When enabled you may also need to install MathJax separately and
+# configure the path to it using the MATHJAX_RELPATH option.
+
+USE_MATHJAX            = NO
+
+# When MathJax is enabled you need to specify the location relative to the
+# HTML output directory using the MATHJAX_RELPATH option. The destination
+# directory should contain the MathJax.js script. For instance, if the mathjax
+# directory is located at the same level as the HTML output directory, then
+# MATHJAX_RELPATH should be ../mathjax. The default value points to
+# the MathJax Content Delivery Network so you can quickly see the result without
+# installing MathJax.
+# However, it is strongly recommended to install a local
+# copy of MathJax from http://www.mathjax.org before deployment.
+
+MATHJAX_RELPATH        = http://www.mathjax.org/mathjax
+
+# The MATHJAX_EXTENSIONS tag can be used to specify one or MathJax extension
+# names that should be enabled during MathJax rendering.
+
+MATHJAX_EXTENSIONS     =
+
+# When the SEARCHENGINE tag is enabled doxygen will generate a search box
+# for the HTML output. The underlying search engine uses javascript
+# and DHTML and should work on any modern browser. Note that when using
+# HTML help (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets
+# (GENERATE_DOCSET) there is already a search function so this one should
+# typically be disabled. For large projects the javascript based search engine
+# can be slow, then enabling SERVER_BASED_SEARCH may provide a better solution.
+
+SEARCHENGINE           = YES
+
+# When the SERVER_BASED_SEARCH tag is enabled the search engine will be
+# implemented using a PHP enabled web server instead of at the web client
+# using Javascript. Doxygen will generate the search PHP script and index
+# file to put on the web server. The advantage of the server
+# based approach is that it scales better to large projects and allows
+# full text search. The disadvantages are that it is more difficult to setup
+# and does not have live searching capabilities.
+
+SERVER_BASED_SEARCH    = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will
+# generate Latex output.
+
+GENERATE_LATEX         = NO
+
+# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `latex' will be used as the default path.
+
+LATEX_OUTPUT           = latex
+
+# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be
+# invoked. If left blank `latex' will be used as the default command name.
+# Note that when enabling USE_PDFLATEX this option is only used for
+# generating bitmaps for formulas in the HTML output, but not in the
+# Makefile that is written to the output directory.
+
+LATEX_CMD_NAME         = latex
+
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to
+# generate index for LaTeX. If left blank `makeindex' will be used as the
+# default command name.
+
+MAKEINDEX_CMD_NAME     = makeindex
+
+# If the COMPACT_LATEX tag is set to YES Doxygen generates more compact
+# LaTeX documents. This may be useful for small projects and may help to
+# save some trees in general.
+
+COMPACT_LATEX          = NO
+
+# The PAPER_TYPE tag can be used to set the paper type that is used
+# by the printer. Possible values are: a4, letter, legal and
+# executive. If left blank a4wide will be used.
+
+PAPER_TYPE             = a4
+
+# The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX
+# packages that should be included in the LaTeX output.
+
+EXTRA_PACKAGES         =
+
+# The LATEX_HEADER tag can be used to specify a personal LaTeX header for
+# the generated latex document. The header should contain everything until
+# the first chapter. If it is left blank doxygen will generate a
+# standard header. Notice: only use this tag if you know what you are doing!
+
+LATEX_HEADER           =
+
+# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for
+# the generated latex document. The footer should contain everything after
+# the last chapter. If it is left blank doxygen will generate a
+# standard footer. Notice: only use this tag if you know what you are doing!
+
+LATEX_FOOTER           =
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated
+# is prepared for conversion to pdf (using ps2pdf). The pdf file will
+# contain links (just like the HTML output) instead of page references
+# This makes the output suitable for online browsing using a pdf viewer.
+
+PDF_HYPERLINKS         = YES
+
+# If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of
+# plain latex in the generated Makefile. Set this option to YES to get a
+# higher quality PDF documentation.
+
+USE_PDFLATEX           = YES
+
+# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode.
+# command to the generated LaTeX files. This will instruct LaTeX to keep
+# running if errors occur, instead of asking the user for help.
+# This option is also used when generating formulas in HTML.
+
+LATEX_BATCHMODE        = NO
+
+# If LATEX_HIDE_INDICES is set to YES then doxygen will not
+# include the index chapters (such as File Index, Compound Index, etc.)
+# in the output.
+
+LATEX_HIDE_INDICES     = NO
+
+# If LATEX_SOURCE_CODE is set to YES then doxygen will include
+# source code with syntax highlighting in the LaTeX output.
+# Note that which sources are shown also depends on other settings
+# such as SOURCE_BROWSER.
+
+LATEX_SOURCE_CODE      = NO
+
+# The LATEX_BIB_STYLE tag can be used to specify the style to use for the
+# bibliography, e.g. plainnat, or ieeetr. The default style is "plain". See
+# http://en.wikipedia.org/wiki/BibTeX for more info.
+
+LATEX_BIB_STYLE        = plain
+
+#---------------------------------------------------------------------------
+# configuration options related to the RTF output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output
+# The RTF output is optimized for Word 97 and may not look very pretty with
+# other RTF readers or editors.
+
+GENERATE_RTF           = NO
+
+# The RTF_OUTPUT tag is used to specify where the RTF docs will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `rtf' will be used as the default path.
+
+RTF_OUTPUT             = rtf
+
+# If the COMPACT_RTF tag is set to YES Doxygen generates more compact
+# RTF documents. This may be useful for small projects and may help to
+# save some trees in general.
+
+COMPACT_RTF            = NO
+
+# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated
+# will contain hyperlink fields. The RTF file will
+# contain links (just like the HTML output) instead of page references.
+# This makes the output suitable for online browsing using WORD or other
+# programs which support those fields.
+# Note: wordpad (write) and others do not support links.
+
+RTF_HYPERLINKS         = NO
+
+# Load style sheet definitions from file. Syntax is similar to doxygen's
+# config file, i.e. a series of assignments. You only have to provide
+# replacements, missing definitions are set to their default value.
+
+RTF_STYLESHEET_FILE    =
+
+# Set optional variables used in the generation of an rtf document.
+# Syntax is similar to doxygen's config file.
+
+RTF_EXTENSIONS_FILE    =
+
+#---------------------------------------------------------------------------
+# configuration options related to the man page output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_MAN tag is set to YES (the default) Doxygen will
+# generate man pages
+
+GENERATE_MAN           = NO
+
+# The MAN_OUTPUT tag is used to specify where the man pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `man' will be used as the default path.
+
+MAN_OUTPUT             = man
+
+# The MAN_EXTENSION tag determines the extension that is added to
+# the generated man pages (default is the subroutine's section .3)
+
+MAN_EXTENSION          = .3
+
+# If the MAN_LINKS tag is set to YES and Doxygen generates man output,
+# then it will generate one additional man file for each entity
+# documented in the real man page(s). These additional files
+# only source the real man page, but without them the man command
+# would be unable to find the correct page. The default is NO.
+
+MAN_LINKS              = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the XML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_XML tag is set to YES Doxygen will
+# generate an XML file that captures the structure of
+# the code including all documentation.
+
+GENERATE_XML           = NO
+
+# The XML_OUTPUT tag is used to specify where the XML pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `xml' will be used as the default path.
+
+XML_OUTPUT             = xml
+
+# The XML_SCHEMA tag can be used to specify an XML schema,
+# which can be used by a validating XML parser to check the
+# syntax of the XML files.
+
+XML_SCHEMA             =
+
+# The XML_DTD tag can be used to specify an XML DTD,
+# which can be used by a validating XML parser to check the
+# syntax of the XML files.
+
+XML_DTD                =
+
+# If the XML_PROGRAMLISTING tag is set to YES Doxygen will
+# dump the program listings (including syntax highlighting
+# and cross-referencing information) to the XML output. Note that
+# enabling this will significantly increase the size of the XML output.
+
+XML_PROGRAMLISTING     = YES
+
+#---------------------------------------------------------------------------
+# configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will
+# generate an AutoGen Definitions (see autogen.sf.net) file
+# that captures the structure of the code including all
+# documentation. Note that this feature is still experimental
+# and incomplete at the moment.
+
+GENERATE_AUTOGEN_DEF   = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the Perl module output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_PERLMOD tag is set to YES Doxygen will
+# generate a Perl module file that captures the structure of
+# the code including all documentation. Note that this
+# feature is still experimental and incomplete at the
+# moment.
+
+GENERATE_PERLMOD       = NO
+
+# If the PERLMOD_LATEX tag is set to YES Doxygen will generate
+# the necessary Makefile rules, Perl scripts and LaTeX code to be able
+# to generate PDF and DVI output from the Perl module output.
+
+PERLMOD_LATEX          = NO
+
+# If the PERLMOD_PRETTY tag is set to YES the Perl module output will be
+# nicely formatted so it can be parsed by a human reader.
+# This is useful
+# if you want to understand what is going on.
+# On the other hand, if this
+# tag is set to NO the size of the Perl module output will be much smaller
+# and Perl will parse it just the same.
+
+PERLMOD_PRETTY         = YES
+
+# The names of the make variables in the generated doxyrules.make file
+# are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX.
+# This is useful so different doxyrules.make files included by the same
+# Makefile don't overwrite each other's variables.
+
+PERLMOD_MAKEVAR_PREFIX =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+
+# If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will
+# evaluate all C-preprocessor directives found in the sources and include
+# files.
+
+ENABLE_PREPROCESSING   = YES
+
+# If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro
+# names in the source code. If set to NO (the default) only conditional
+# compilation will be performed. Macro expansion can be done in a controlled
+# way by setting EXPAND_ONLY_PREDEF to YES.
+
+MACRO_EXPANSION        = NO
+
+# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES
+# then the macro expansion is limited to the macros specified with the
+# PREDEFINED and EXPAND_AS_DEFINED tags.
+
+EXPAND_ONLY_PREDEF     = NO
+
+# If the SEARCH_INCLUDES tag is set to YES (the default) the includes files
+# pointed to by INCLUDE_PATH will be searched when a #include is found.
+
+SEARCH_INCLUDES        = YES
+
+# The INCLUDE_PATH tag can be used to specify one or more directories that
+# contain include files that are not input files but should be processed by
+# the preprocessor.
+
+INCLUDE_PATH           =
+
+# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard
+# patterns (like *.h and *.hpp) to filter out the header-files in the
+# directories. If left blank, the patterns specified with FILE_PATTERNS will
+# be used.
+
+INCLUDE_FILE_PATTERNS  =
+
+# The PREDEFINED tag can be used to specify one or more macro names that
+# are defined before the preprocessor is started (similar to the -D option of
+# gcc). The argument of the tag is a list of macros of the form: name
+# or name=definition (no spaces). If the definition and the = are
+# omitted =1 is assumed. To prevent a macro definition from being
+# undefined via #undef or recursively expanded use the := operator
+# instead of the = operator.
+
+PREDEFINED             = ORTHANC_ENABLE_OPENSLIDE=1
+
+# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then
+# this tag can be used to specify a list of macro names that should be expanded.
+# The macro definition that is found in the sources will be used.
+# Use the PREDEFINED tag if you want to use a different macro definition that
+# overrules the definition found in the source code.
+
+EXPAND_AS_DEFINED      =
+
+# If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then
+# doxygen's preprocessor will remove all references to function-like macros
+# that are alone on a line, have an all uppercase name, and do not end with a
+# semicolon, because these will confuse the parser if not removed.
+
+SKIP_FUNCTION_MACROS   = YES
+
+#---------------------------------------------------------------------------
+# Configuration::additions related to external references
+#---------------------------------------------------------------------------
+
+# The TAGFILES option can be used to specify one or more tagfiles. For each
+# tag file the location of the external documentation should be added. The
+# format of a tag file without this location is as follows:
+#
+# TAGFILES = file1 file2 ...
+# Adding location for the tag files is done as follows:
+#
+# TAGFILES = file1=loc1 "file2 = loc2" ...
+# where "loc1" and "loc2" can be relative or absolute paths
+# or URLs. Note that each tag file must have a unique name (where the name does
+# NOT include the path). If a tag file is not located in the directory in which
+# doxygen is run, you must also specify the path to the tagfile here.
+
+TAGFILES               =
+
+# When a file name is specified after GENERATE_TAGFILE, doxygen will create
+# a tag file that is based on the input files it reads.
+
+GENERATE_TAGFILE       =
+
+# If the ALLEXTERNALS tag is set to YES all external classes will be listed
+# in the class index. If set to NO only the inherited external classes
+# will be listed.
+
+ALLEXTERNALS           = NO
+
+# If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed
+# in the modules index. If set to NO, only the current project's groups will
+# be listed.
+
+EXTERNAL_GROUPS        = YES
+
+# The PERL_PATH should be the absolute path and name of the perl script
+# interpreter (i.e. the result of `which perl').
+
+PERL_PATH              = /usr/bin/perl
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+
+# If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will
+# generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base
+# or super classes. Setting the tag to NO turns the diagrams off. Note that
+# this option also works with HAVE_DOT disabled, but it is recommended to
+# install and use dot, since it yields more powerful graphs.
+
+CLASS_DIAGRAMS         = YES
+
+# You can define message sequence charts within doxygen comments using the \msc
+# command. Doxygen will then run the mscgen tool (see
+# http://www.mcternan.me.uk/mscgen/) to produce the chart and insert it in the
+# documentation. The MSCGEN_PATH tag allows you to specify the directory where
+# the mscgen tool resides. If left empty the tool is assumed to be found in the
+# default search path.
+
+MSCGEN_PATH            =
+
+# If set to YES, the inheritance and collaboration graphs will hide
+# inheritance and usage relations if the target is undocumented
+# or is not a class.
+
+HIDE_UNDOC_RELATIONS   = YES
+
+# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
+# available from the path. This tool is part of Graphviz, a graph visualization
+# toolkit from AT&T and Lucent Bell Labs. The other options in this section
+# have no effect if this option is set to NO (the default)
+
+HAVE_DOT               = YES
+
+# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is
+# allowed to run in parallel. When set to 0 (the default) doxygen will
+# base this on the number of processors available in the system. You can set it
+# explicitly to a value larger than 0 to get control over the balance
+# between CPU load and processing speed.
+
+DOT_NUM_THREADS        = 0
+
+# By default doxygen will use the Helvetica font for all dot files that
+# doxygen generates. When you want a differently looking font you can specify
+# the font name using DOT_FONTNAME. You need to make sure dot is able to find
+# the font, which can be done by putting it in a standard location or by setting
+# the DOTFONTPATH environment variable or by setting DOT_FONTPATH to the
+# directory containing the font.
+
+DOT_FONTNAME           = Helvetica
+
+# The DOT_FONTSIZE tag can be used to set the size of the font of dot graphs.
+# The default size is 10pt.
+
+DOT_FONTSIZE           = 10
+
+# By default doxygen will tell dot to use the Helvetica font.
+# If you specify a different font using DOT_FONTNAME you can use DOT_FONTPATH to
+# set the path where dot can find it.
+
+DOT_FONTPATH           =
+
+# If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen
+# will generate a graph for each documented class showing the direct and
+# indirect inheritance relations. Setting this tag to YES will force the
+# CLASS_DIAGRAMS tag to NO.
+
+CLASS_GRAPH            = YES
+
+# If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen
+# will generate a graph for each documented class showing the direct and
+# indirect implementation dependencies (inheritance, containment, and
+# class references variables) of the class with other documented classes.
+
+COLLABORATION_GRAPH    = NO
+
+# If the GROUP_GRAPHS and HAVE_DOT tags are set to YES then doxygen
+# will generate a graph for groups, showing the direct groups dependencies
+
+GROUP_GRAPHS           = YES
+
+# If the UML_LOOK tag is set to YES doxygen will generate inheritance and
+# collaboration diagrams in a style similar to the OMG's Unified Modeling
+# Language.
+
+UML_LOOK               = NO
+
+# If the UML_LOOK tag is enabled, the fields and methods are shown inside
+# the class node. If there are many fields or methods and many nodes the
+# graph may become too big to be useful. The UML_LIMIT_NUM_FIELDS
+# threshold limits the number of items for each type to make the size more
+# managable. Set this to 0 for no limit. Note that the threshold may be
+# exceeded by 50% before the limit is enforced.
+
+UML_LIMIT_NUM_FIELDS   = 10
+
+# If set to YES, the inheritance and collaboration graphs will show the
+# relations between templates and their instances.
+
+TEMPLATE_RELATIONS     = NO
+
+# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT
+# tags are set to YES then doxygen will generate a graph for each documented
+# file showing the direct and indirect include dependencies of the file with
+# other documented files.
+
+INCLUDE_GRAPH          = YES
+
+# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and
+# HAVE_DOT tags are set to YES then doxygen will generate a graph for each
+# documented header file showing the documented files that directly or
+# indirectly include this file.
+
+INCLUDED_BY_GRAPH      = YES
+
+# If the CALL_GRAPH and HAVE_DOT options are set to YES then
+# doxygen will generate a call dependency graph for every global function
+# or class method. Note that enabling this option will significantly increase
+# the time of a run. So in most cases it will be better to enable call graphs
+# for selected functions only using the \callgraph command.
+
+CALL_GRAPH             = NO
+
+# If the CALLER_GRAPH and HAVE_DOT tags are set to YES then
+# doxygen will generate a caller dependency graph for every global function
+# or class method. Note that enabling this option will significantly increase
+# the time of a run. So in most cases it will be better to enable caller
+# graphs for selected functions only using the \callergraph command.
+
+CALLER_GRAPH           = NO
+
+# If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen
+# will generate a graphical hierarchy of all classes instead of a textual one.
+
+GRAPHICAL_HIERARCHY    = YES
+
+# If the DIRECTORY_GRAPH and HAVE_DOT tags are set to YES
+# then doxygen will show the dependencies a directory has on other directories
+# in a graphical way. The dependency relations are determined by the #include
+# relations between the files in the directories.
+
+DIRECTORY_GRAPH        = YES
+
+# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
+# generated by dot. Possible values are svg, png, jpg, or gif.
+# If left blank png will be used. If you choose svg you need to set
+# HTML_FILE_EXTENSION to xhtml in order to make the SVG files
+# visible in IE 9+ (other browsers do not have this requirement).
+
+DOT_IMAGE_FORMAT       = png
+
+# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to
+# enable generation of interactive SVG images that allow zooming and panning.
+# Note that this requires a modern browser other than Internet Explorer.
+# Tested and working are Firefox, Chrome, Safari, and Opera. For IE 9+ you
+# need to set HTML_FILE_EXTENSION to xhtml in order to make the SVG files
+# visible. Older versions of IE do not have SVG support.
+
+INTERACTIVE_SVG        = NO
+
+# The tag DOT_PATH can be used to specify the path where the dot tool can be
+# found. If left blank, it is assumed the dot tool can be found in the path.
+
+DOT_PATH               =
+
+# The DOTFILE_DIRS tag can be used to specify one or more directories that
+# contain dot files that are included in the documentation (see the
+# \dotfile command).
+
+DOTFILE_DIRS           =
+
+# The MSCFILE_DIRS tag can be used to specify one or more directories that
+# contain msc files that are included in the documentation (see the
+# \mscfile command).
+
+MSCFILE_DIRS           =
+
+# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of
+# nodes that will be shown in the graph. If the number of nodes in a graph
+# becomes larger than this value, doxygen will truncate the graph, which is
+# visualized by representing a node as a red box. Note that doxygen if the
+# number of direct children of the root node in a graph is already larger than
+# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note
+# that the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH.
+
+DOT_GRAPH_MAX_NODES    = 50
+
+# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the
+# graphs generated by dot. A depth value of 3 means that only nodes reachable
+# from the root by following a path via at most 3 edges will be shown. Nodes
+# that lay further from the root node will be omitted. Note that setting this
+# option to 1 or 2 may greatly reduce the computation time needed for large
+# code bases. Also note that the size of a graph can be further restricted by
+# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction.
+
+MAX_DOT_GRAPH_DEPTH    = 0
+
+# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
+# background. This is disabled by default, because dot on Windows does not
+# seem to support this out of the box. Warning: Depending on the platform used,
+# enabling this option may lead to badly anti-aliased labels on the edges of
+# a graph (i.e. they become hard to read).
+
+DOT_TRANSPARENT        = NO
+
+# Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output
+# files in one run (i.e. multiple -o and -T options on the command line). This
+# makes dot run faster, but since only newer versions of dot (>1.8.10)
+# support this, this feature is disabled by default.
+
+DOT_MULTI_TARGETS      = YES
+
+# If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will
+# generate a legend page explaining the meaning of the various boxes and
+# arrows in the dot generated graphs.
+
+GENERATE_LEGEND        = YES
+
+# If the DOT_CLEANUP tag is set to YES (the default) Doxygen will
+# remove the intermediate dot files that are used to generate
+# the various graphs.
+
+DOT_CLEANUP            = YES
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/SampleDataset.json	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,43 @@
+{
+  "PatientID" : "C123456789",
+  "PatientName" : "SOME^PATIENT",
+  "PatientBirthDate" : "19700101",
+  "PatientSex" : "M",
+  "StudyID" : "NONE",
+  "SeriesNumber" : "1",
+  "ReferringPhysicianName" : "SOME^PHYSICIAN",
+  "AccessionNumber" : "123456789",
+  "Manufacturer" : "MyManufacturer",
+  "ManufacturerModelName" : "MyModel",
+  "DeviceSerialNumber" : "MySerialNumber",
+  "SoftwareVersions" : "MyVersion",
+
+  "ImageType" : "DERIVED\\PRIMARY\\VOLUME\\NONE",
+  "FocusMethod" : "AUTO",
+  "ExtendedDepthOfField" : "NO",
+
+  "AcquisitionContextSequence" : [
+  ],
+  "AcquisitionDuration" : "100",
+
+  "ContainerIdentifier" : "CI_12345",
+  "IssuerOfTheContainerIdentifierSequence" : [
+  ],
+  "ContainerTypeCodeSequence" : [
+  ],
+  "IssuerOfTheContainerIdentifierSequence" : [
+  ],
+
+  "SpecimenDescriptionSequence" : [
+    {
+      "SpecimenIdentifier" : "Specimen^Identifier",
+      "SpecimenUID" : "1.2.276.0.7230010.3.1.4.3252829876.4112.1426166133.871",
+      "IssuerOfTheSpecimenIdentifierSequence" : [
+      ],
+      "SpecimenPreparationSequence" : [
+      ]
+    }
+  ],
+  "SpecimenLabelInImage" : "NO",
+  "BurnedInAnnotation" : "NO"
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/SyncOrthancFolder.py	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,164 @@
+#!/usr/bin/python
+
+#
+# This maintenance script updates the content of the "Orthanc" folder
+# to match the latest version of the Orthanc source code.
+#
+
+import multiprocessing
+import os
+import stat
+import urllib2
+
+TARGET = os.path.join(os.path.dirname(__file__), '..', 'Framework', 'Orthanc')
+PLUGIN_SDK_VERSION = '1.0.0'
+REPOSITORY = 'http://bitbucket.org/sjodogne/orthanc/raw'
+
+FILES = [
+    'Core/ChunkedBuffer.cpp',
+    'Core/ChunkedBuffer.h',
+    'Core/DicomFormat/DicomArray.cpp',
+    'Core/DicomFormat/DicomArray.h',
+    'Core/DicomFormat/DicomElement.h',
+    'Core/DicomFormat/DicomMap.cpp',
+    'Core/DicomFormat/DicomMap.h',
+    'Core/DicomFormat/DicomTag.cpp',
+    'Core/DicomFormat/DicomTag.h',
+    'Core/DicomFormat/DicomValue.cpp',
+    'Core/DicomFormat/DicomValue.h',
+    'Core/Endianness.h',
+    'Core/EnumerationDictionary.h',
+    'Core/Enumerations.cpp',
+    'Core/Enumerations.h',
+    'Core/HttpClient.cpp',
+    'Core/HttpClient.h',
+    'Core/ICommand.h',
+    'Core/IDynamicObject.h',
+    'Core/Images/IImageWriter.cpp',
+    'Core/Images/IImageWriter.h',
+    'Core/Images/Image.cpp',
+    'Core/Images/Image.h',
+    'Core/Images/ImageAccessor.cpp',
+    'Core/Images/ImageAccessor.h',
+    'Core/Images/ImageBuffer.cpp',
+    'Core/Images/ImageBuffer.h',
+    'Core/Images/ImageProcessing.cpp',
+    'Core/Images/ImageProcessing.h',
+    'Core/Images/JpegErrorManager.cpp',
+    'Core/Images/JpegErrorManager.h',
+    'Core/Images/JpegReader.cpp',
+    'Core/Images/JpegReader.h',
+    'Core/Images/JpegWriter.cpp',
+    'Core/Images/JpegWriter.h',
+    'Core/Images/PngReader.cpp',
+    'Core/Images/PngReader.h',
+    'Core/Images/PngWriter.cpp',
+    'Core/Images/PngWriter.h',
+    'Core/Logging.cpp',
+    'Core/Logging.h',
+    'Core/MultiThreading/BagOfTasks.h',
+    'Core/MultiThreading/BagOfTasksProcessor.cpp',
+    'Core/MultiThreading/BagOfTasksProcessor.h',
+    'Core/MultiThreading/SharedMessageQueue.cpp',
+    'Core/MultiThreading/SharedMessageQueue.h',
+    'Core/MultiThreading/Semaphore.cpp',
+    'Core/MultiThreading/Semaphore.h',
+    'Core/OrthancException.h',
+    'Core/PrecompiledHeaders.h',
+    'Core/Toolbox.cpp',
+    'Core/Toolbox.h',
+    'Core/Uuid.cpp',
+    'Core/Uuid.h',
+    'Core/WebServiceParameters.cpp',
+    'Core/WebServiceParameters.h',
+    'OrthancServer/FromDcmtkBridge.cpp',
+    'OrthancServer/FromDcmtkBridge.h',
+    'OrthancServer/PrecompiledHeadersServer.h',
+    'OrthancServer/ServerEnumerations.cpp',
+    'OrthancServer/ServerEnumerations.h',
+    'OrthancServer/ToDcmtkBridge.cpp',
+    'OrthancServer/ToDcmtkBridge.h',
+    'Plugins/Engine/SharedLibrary.cpp',
+    'Plugins/Engine/SharedLibrary.h',
+    'Plugins/Samples/Common/ExportedSymbols.list',
+    'Plugins/Samples/Common/OrthancPluginCppWrapper.cpp',
+    'Plugins/Samples/Common/OrthancPluginCppWrapper.h',
+    'Plugins/Samples/Common/VersionScript.map',
+    'Resources/CMake/AutoGeneratedCode.cmake',
+    'Resources/CMake/BoostConfiguration.cmake',
+    'Resources/CMake/Compiler.cmake',
+    'Resources/CMake/DcmtkConfiguration.cmake',
+    'Resources/CMake/DownloadPackage.cmake',
+    'Resources/CMake/JsonCppConfiguration.cmake',
+    'Resources/CMake/LibCurlConfiguration.cmake',
+    'Resources/CMake/LibJpegConfiguration.cmake',
+    'Resources/CMake/LibPngConfiguration.cmake',
+    'Resources/CMake/OpenSslConfiguration.cmake',
+    'Resources/CMake/ZlibConfiguration.cmake',
+    'Resources/EmbedResources.py',
+    'Resources/MinGW-W64-Toolchain32.cmake',
+    'Resources/MinGW-W64-Toolchain64.cmake',
+    'Resources/MinGWToolchain.cmake',
+    'Resources/Patches/dcmtk-3.6.0-mingw64.patch',
+    'Resources/Patches/dcmtk-3.6.0-speed.patch',
+    'Resources/Patches/dcmtk-3.6.1-speed.patch',
+    'Resources/ThirdParty/VisualStudio/stdint.h',
+    'Resources/ThirdParty/base64/base64.cpp',
+    'Resources/ThirdParty/base64/base64.h',
+    'Resources/WindowsResources.py',
+    'Resources/WindowsResources.rc',
+]
+
+SDK = [
+    'orthanc/OrthancCPlugin.h',
+]   
+
+EXE = [
+    'Resources/EmbedResources.py',
+    'Resources/WindowsResources.py',
+]
+
+
+def Download(x):
+    branch = x[0]
+    source = x[1]
+    target = os.path.join(TARGET, x[2])
+    print target
+
+    try:
+        os.makedirs(os.path.dirname(target))
+    except:
+        pass
+
+    url = '%s/%s/%s' % (REPOSITORY, branch, source)
+
+    try:
+        with open(target, 'w') as f:
+            f.write(urllib2.urlopen(url).read())
+    except:
+        print('Cannot download file %s' % url)
+        raise
+
+
+commands = []
+
+for f in FILES:
+    commands.append([ 'default', f, f ])
+
+for f in SDK:
+    commands.append([ 
+        'Orthanc-%s' % PLUGIN_SDK_VERSION, 
+        'Plugins/Include/%s' % f,
+        'Sdk-%s/%s' % (PLUGIN_SDK_VERSION, f) 
+    ])
+
+
+pool = multiprocessing.Pool(10)  # simultaneous downloads
+pool.map(Download, commands)
+
+
+for exe in EXE:
+    path = os.path.join(TARGET, exe)
+    st = os.stat(path)
+    os.chmod(path, st.st_mode | stat.S_IEXEC)
+
Binary file Resources/sRGB.icc has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/sRGB.txt	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,69 @@
+Notes about the sRGB.icc file
+=============================
+
+The file "sRGB.icc" contains a default sRGB ICC color profile. It is
+taken from a Debian distribution, from the following path:
+/usr/share/color/icc/sRGB.icc
+
+Its MD5 hash is: 7fb30d688bf82d32a0e748daf3dba95d
+
+This profile comes from the "icc-profiles-free" package from Debian:
+https://packages.debian.org/sid/icc-profiles-free
+
+The license terms of the profile are reproduced below, and are taken
+from the source of the "icc-profiles-free" package:
+http://anonscm.debian.org/cgit/collab-maint/icc-profiles.git/tree/debian/copyright
+
+
+License
+-------
+
+Files: LCMS* compat* Gray* Cine* ITUL* sRGB.icc
+Copyright:  Kai-Uwe Behrmann <www.behrmann.name>
+            Marti Maria <www.littlecms.com>
+            Photogamut <www.photogamut.org>
+            Graeme Gill <www.argyllcms.com>
+            ColorSolutions <www.basICColor.com>
+License: Zlib
+ The zlib/libpng License
+ .
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+ .
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+ .
+    1. The origin of this software must not be misrepresented; you must not
+    claim that you wrote the original software. If you use this software
+    in a product, an acknowledgment in the product documentation would be
+    appreciated but is not required.
+ .
+    2. Altered source versions must be plainly marked as such, and must not be
+    misrepresented as being the original software.
+ .
+    3. This notice may not be removed or altered from any source
+    distribution.
+ .
+ NO WARRANTY
+ .
+   BECAUSE THE DATA IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+ FOR THE DATA, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+ OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+ PROVIDE THE DATA "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+ OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+ TO THE QUALITY AND PERFORMANCE OF THE DATA IS WITH YOU.  SHOULD THE
+ DATA PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+ REPAIR OR CORRECTION.
+ .
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+ WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+ REDISTRIBUTE THE DATA AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+ INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+ OUT OF THE USE OR INABILITY TO USE THE DATA (INCLUDING BUT NOT LIMITED
+ TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+ YOU OR THIRD PARTIES OR A FAILURE OF THE DATA TO OPERATE WITH ANY OTHER
+ PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGES.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/TODO	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,17 @@
+Orthanc for Whole-Slide Imaging
+===============================
+
+
+-------
+General
+-------
+
+* Doxygen Documentation
+* Support sparse tiling (both in encoder and decoder)
+
+
+-----------
+Performance
+-----------
+
+* Larger cache with LRU recycling to improve viewer performance
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/CMakeLists.txt	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,203 @@
+cmake_minimum_required(VERSION 2.8)
+project(OrthancWSIPlugin)
+
+set(ORTHANC_WSI_VERSION "mainline")
+
+
+#####################################################################
+## Parameters of the build
+#####################################################################
+
+# Generic parameters
+SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)")
+SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages")
+
+# Advanced parameters to fine-tune linking against system libraries
+SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of Boost")
+SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp")
+SET(USE_SYSTEM_LIBJPEG ON CACHE BOOL "Use the system version of libjpeg")
+SET(USE_SYSTEM_LIBPNG ON CACHE BOOL "Use the system version of libpng")
+SET(USE_SYSTEM_OPENJPEG ON CACHE BOOL "Use the system version of OpenJpeg")
+SET(USE_SYSTEM_ZLIB ON CACHE BOOL "Use the system version of ZLib")
+SET(USE_SYSTEM_ORTHANC_SDK ON CACHE BOOL "Use the system version of the Orthanc plugin SDK")
+
+# Parameters related to OpenLayers
+SET(USE_SYSTEM_OPENLAYERS OFF CACHE BOOL "Use the system version of OpenLayers")
+SET(OPENLAYERS_CSS "" CACHE FILEPATH "Path to the system version of OpenLayers CSS")
+SET(OPENLAYERS_JS "" CACHE FILEPATH "Path to the system version of OpenLayers JavaScript")
+
+
+#####################################################################
+## Configure mandatory third-party components
+#####################################################################
+
+SET(ORTHANC_WSI_DIR ${CMAKE_CURRENT_LIST_DIR}/..)
+SET(ORTHANC_ROOT ${ORTHANC_WSI_DIR}/Framework/Orthanc)
+
+SET(USE_OPENJPEG_JP2 ON)
+
+include(CheckIncludeFiles)
+include(CheckIncludeFileCXX)
+include(CheckLibraryExists)
+include(FindPythonInterp)
+include(FindPkgConfig)
+
+include(${ORTHANC_ROOT}/Resources/CMake/Compiler.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/AutoGeneratedCode.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake)
+
+# Third-party components shipped with Orthanc
+include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/JsonCppConfiguration.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/LibJpegConfiguration.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/LibPngConfiguration.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/ZlibConfiguration.cmake)
+
+include(${ORTHANC_WSI_DIR}/Resources/CMake/OpenJpegConfiguration.cmake)
+
+add_definitions(
+  -DORTHANC_ENABLE_BASE64=0
+  -DORTHANC_ENABLE_CURL=0
+  -DORTHANC_ENABLE_DCMTK=0
+  -DORTHANC_ENABLE_LOGGING=0
+  -DORTHANC_ENABLE_MD5=0
+  -DHAS_ORTHANC_EXCEPTION=1
+  )
+
+
+#####################################################################
+## Find the Orthanc SDK
+#####################################################################
+
+if (STATIC_BUILD OR NOT USE_SYSTEM_ORTHANC_SDK)
+  include_directories(${ORTHANC_ROOT}/Sdk-1.0.0)
+else ()
+  CHECK_INCLUDE_FILE_CXX(orthanc/OrthancCPlugin.h HAVE_ORTHANC_H)
+  if (NOT HAVE_ORTHANC_H)
+    message(FATAL_ERROR "Please install the headers of the Orthanc plugins SDK")
+  endif()
+endif()
+
+
+#####################################################################
+## Platform-specific configuration
+#####################################################################
+
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
+    ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR
+    ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
+  link_libraries(rt)
+
+elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+  SET(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -lws2_32")
+
+  execute_process(
+    COMMAND 
+    ${PYTHON_EXECUTABLE} ${ORTHANC_ROOT}/Resources/WindowsResources.py
+    ${ORTHANC_WSI_VERSION} "OrthancWSI" OrthancWSI.dll "Whole-slide imaging plugin for Orthanc"
+    ERROR_VARIABLE Failure
+    OUTPUT_FILE ${AUTOGENERATED_DIR}/Version.rc
+    )
+
+  if (Failure)
+    message(FATAL_ERROR "Error while computing the version information: ${Failure}")
+  endif()
+
+  list(APPEND AUTOGENERATED_SOURCES  ${AUTOGENERATED_DIR}/Version.rc)
+endif()
+
+if (APPLE)
+  SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework CoreFoundation")
+  SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -framework CoreFoundation")
+endif()
+
+
+#####################################################################
+## Prepare OpenLayers
+#####################################################################
+
+if (STATIC_BUILD OR NOT USE_SYSTEM_OPENLAYERS)
+  DownloadPackage(
+    "77e57aad873c2d4deea8bb1446e9b87a"
+    "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/WSI/openlayers-3.19.0-dist.zip"
+    "v3.19.0-dist")
+  
+  set(OPENLAYERS_RESOURCES
+    OPENLAYERS_CSS  ${CMAKE_CURRENT_BINARY_DIR}/v3.19.0-dist/ol.css
+    OPENLAYERS_JS   ${CMAKE_CURRENT_BINARY_DIR}/v3.19.0-dist/ol.js
+    )
+
+else()
+  set(OPENLAYERS_RESOURCES
+    OPENLAYERS_CSS  ${OPENLAYERS_CSS}
+    OPENLAYERS_JS   ${OPENLAYERS_JS}
+    )  
+endif()
+
+EmbedResources(
+  ${OPENLAYERS_RESOURCES}
+  ORTHANC_EXPLORER  ${CMAKE_SOURCE_DIR}/OrthancExplorer.js
+  VIEWER_HTML       ${CMAKE_SOURCE_DIR}/viewer.html
+  VIEWER_JS         ${CMAKE_SOURCE_DIR}/viewer.js
+  )
+
+
+#####################################################################
+## Create the plugin
+#####################################################################
+
+add_library(OrthancWSI SHARED
+  Plugin.cpp
+
+  ${ORTHANC_WSI_DIR}/Framework/DicomToolbox.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Enumerations.cpp
+  ${ORTHANC_WSI_DIR}/Framework/ImageToolbox.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Inputs/DicomPyramid.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Inputs/DicomPyramidInstance.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Inputs/DicomPyramidLevel.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Inputs/PyramidWithRawTiles.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Jpeg2000Reader.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Jpeg2000Writer.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Messaging/IOrthancConnection.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Messaging/OrthancConnectionBase.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Messaging/PluginOrthancConnection.cpp
+
+  ${ORTHANC_ROOT}/Core/ChunkedBuffer.cpp
+  ${ORTHANC_ROOT}/Core/Enumerations.cpp
+  ${ORTHANC_ROOT}/Core/Images/IImageWriter.cpp
+  ${ORTHANC_ROOT}/Core/Images/Image.cpp
+  ${ORTHANC_ROOT}/Core/Images/ImageAccessor.cpp
+  ${ORTHANC_ROOT}/Core/Images/ImageBuffer.cpp
+  ${ORTHANC_ROOT}/Core/Images/ImageProcessing.cpp
+  ${ORTHANC_ROOT}/Core/Images/JpegErrorManager.cpp
+  ${ORTHANC_ROOT}/Core/Images/JpegReader.cpp
+  ${ORTHANC_ROOT}/Core/Images/JpegWriter.cpp
+  ${ORTHANC_ROOT}/Core/Images/PngReader.cpp
+  ${ORTHANC_ROOT}/Core/Images/PngWriter.cpp
+  ${ORTHANC_ROOT}/Core/Logging.cpp
+  ${ORTHANC_ROOT}/Core/MultiThreading/Semaphore.cpp
+  ${ORTHANC_ROOT}/Core/Toolbox.cpp
+
+  ${AUTOGENERATED_SOURCES}
+
+  # Mandatory components
+  ${BOOST_SOURCES}
+  ${JSONCPP_SOURCES}
+  ${ZLIB_SOURCES}
+  ${LIBPNG_SOURCES}
+  ${LIBJPEG_SOURCES}
+  ${OPENJPEG_SOURCES}
+  )
+
+message("Setting the version of the library to ${ORTHANC_WSI_VERSION}")
+add_definitions(-DORTHANC_WSI_VERSION="${ORTHANC_WSI_VERSION}")
+
+set_target_properties(OrthancWSI PROPERTIES 
+  VERSION ${ORTHANC_WSI_VERSION} 
+  SOVERSION ${ORTHANC_WSI_VERSION})
+
+install(
+  TARGETS OrthancWSI
+  RUNTIME DESTINATION lib    # Destination for Windows
+  LIBRARY DESTINATION share/orthanc/plugins    # Destination for Linux
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/OrthancExplorer.js	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,34 @@
+$('#series').live('pagebeforeshow', function() {
+  var seriesId = $.mobile.pageData.uuid;
+
+  $('#wsi-button').remove();
+
+  // Test whether this is a whole-slide image by check the SOP Class
+  // UID of one instance of the series
+  GetResource('/series/' + seriesId, function(series) {
+    GetResource('/instances/' + series['Instances'][0] + '/tags?simplify', function(instance) {
+      console.log(instance['SOPClassUID']);
+
+      if (instance['SOPClassUID'] == '1.2.840.10008.5.1.4.1.1.77.1.6') {
+
+        // This is a whole-slide image, register the button
+        var b = $('<a>')
+          .attr('id', 'wsi-button')
+          .attr('data-role', 'button')
+          .attr('href', '#')
+          .attr('data-icon', 'search')
+          .attr('data-theme', 'e')
+          .text('Whole-Slide Imaging Viewer')
+          .button();
+
+        b.insertAfter($('#series-info'));
+        b.click(function() {
+          if ($.mobile.pageData) {
+            window.open('../wsi/app/viewer.html?series=' + seriesId);
+          }
+        });
+
+      }
+    });
+  });
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Plugin.cpp	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,390 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../Framework/Inputs/DicomPyramid.h"
+#include "../Framework/Jpeg2000Reader.h"
+#include "../Framework/Messaging/PluginOrthancConnection.h"
+#include "../Framework/Orthanc/Core/Images/PngWriter.h"
+#include "../Framework/Orthanc/Core/MultiThreading/Semaphore.h"
+#include "../Framework/Orthanc/Core/OrthancException.h"
+#include "../Framework/Orthanc/Plugins/Samples/Common/OrthancPluginCppWrapper.h"
+
+#include <EmbeddedResources.h>
+
+#include <cassert>
+
+
+
+namespace OrthancWSI
+{
+  // TODO Add LRU recycling policy
+  class DicomPyramidCache : public boost::noncopyable
+  {
+  private:
+    boost::mutex                  mutex_;
+    IOrthancConnection&           orthanc_;
+    std::auto_ptr<DicomPyramid>   pyramid_;
+
+    DicomPyramid& GetPyramid(const std::string& seriesId)
+    {
+      // Mutex is assumed to be locked
+
+      if (pyramid_.get() == NULL ||
+          pyramid_->GetSeriesId() != seriesId)
+      {
+        pyramid_.reset(new DicomPyramid(orthanc_, seriesId));
+      }
+
+      if (pyramid_.get() == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      return *pyramid_;
+    }
+
+  public:
+    DicomPyramidCache(IOrthancConnection& orthanc) :
+      orthanc_(orthanc)
+    {
+    }
+
+    void Invalidate(const std::string& seriesId)
+    {
+      boost::mutex::scoped_lock  lock(mutex_);
+
+      if (pyramid_.get() != NULL &&
+          pyramid_->GetSeriesId() == seriesId)
+      {
+        pyramid_.reset(NULL);
+      }
+    }
+
+    class Locker : public boost::noncopyable
+    {
+    private:
+      boost::mutex::scoped_lock  lock_;
+      DicomPyramid&              pyramid_;
+
+    public:
+      Locker(DicomPyramidCache& cache,
+             const std::string& seriesId) :
+        lock_(cache.mutex_),
+        pyramid_(cache.GetPyramid(seriesId))
+      {
+      }
+
+      DicomPyramid& GetPyramid() const
+      {
+        return pyramid_;
+      }
+    };
+  };
+}
+
+
+OrthancPluginContext* context_ = NULL;
+
+std::auto_ptr<OrthancWSI::PluginOrthancConnection>  orthanc_;
+std::auto_ptr<OrthancWSI::DicomPyramidCache>        cache_;
+std::auto_ptr<Orthanc::Semaphore>                   transcoderSemaphore_;
+
+
+static bool DisplayPerformanceWarning()
+{
+  (void) DisplayPerformanceWarning;   // Disable warning about unused function
+  OrthancPluginLogWarning(context_, "Performance warning in whole-slide imaging: "
+                          "Non-release build, runtime debug assertions are turned on");
+  return true;
+}
+
+
+void ServePyramid(OrthancPluginRestOutput* output,
+                  const char* url,
+                  const OrthancPluginHttpRequest* request)
+{
+  std::string seriesId(request->groups[0]);
+
+  char tmp[1024];
+  sprintf(tmp, "Accessing whole-slide pyramid of series %s", seriesId.c_str());
+  OrthancPluginLogInfo(context_, tmp);
+  
+
+  OrthancWSI::DicomPyramidCache::Locker locker(*cache_, seriesId);
+
+  unsigned int totalWidth = locker.GetPyramid().GetLevelWidth(0);
+  unsigned int totalHeight = locker.GetPyramid().GetLevelHeight(0);
+
+  Json::Value resolutions = Json::arrayValue;
+  for (unsigned int i = 0; i < locker.GetPyramid().GetLevelCount(); i++)
+  {
+    resolutions.append(static_cast<float>(totalWidth) /
+                       static_cast<float>(locker.GetPyramid().GetLevelWidth(i)));
+  }
+
+  Json::Value result;
+  result["ID"] = seriesId;
+  result["TotalWidth"] = totalWidth;
+  result["TotalHeight"] = totalHeight;
+  result["TileWidth"] = locker.GetPyramid().GetTileWidth();
+  result["TileHeight"] = locker.GetPyramid().GetTileHeight();
+  result["Resolutions"] = resolutions;
+
+  std::string s = result.toStyledString();
+  OrthancPluginAnswerBuffer(context_, output, s.c_str(), s.size(), "application/json");
+}
+
+
+void ServeTile(OrthancPluginRestOutput* output,
+               const char* url,
+               const OrthancPluginHttpRequest* request)
+{
+  std::string seriesId(request->groups[0]);
+  int level = boost::lexical_cast<int>(request->groups[1]);
+  int tileY = boost::lexical_cast<int>(request->groups[3]);
+  int tileX = boost::lexical_cast<int>(request->groups[2]);
+
+  char tmp[1024];
+  sprintf(tmp, "Accessing tile in series %s: (%d,%d) at level %d", seriesId.c_str(), tileX, tileY, level);
+  OrthancPluginLogInfo(context_, tmp);
+  
+  if (level < 0 ||
+      tileX < 0 ||
+      tileY < 0)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+  }
+
+  // Retrieve the raw tile from the WSI pyramid
+  OrthancWSI::ImageCompression compression;
+  Orthanc::PixelFormat format;
+  std::string tile;
+  unsigned int tileWidth, tileHeight;
+
+  {
+    OrthancWSI::DicomPyramidCache::Locker locker(*cache_, seriesId);
+
+    compression = locker.GetPyramid().GetImageCompression();
+    format = locker.GetPyramid().GetPixelFormat();
+    tileWidth = locker.GetPyramid().GetTileWidth();
+    tileHeight = locker.GetPyramid().GetTileHeight();
+
+    if (!locker.GetPyramid().ReadRawTile(tile, 
+                                         static_cast<unsigned int>(level),
+                                         static_cast<unsigned int>(tileX),
+                                         static_cast<unsigned int>(tileY)))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+    }
+  }
+
+  
+  // Test whether the tile is a JPEG image. In such a case, we can
+  // serve it as such, because any Web browser can handle JPEG
+
+  if (compression == OrthancWSI::ImageCompression_Jpeg)
+  {
+    OrthancPluginAnswerBuffer(context_, output, tile.c_str(), tile.size(), "image/jpeg");
+    return;   // We're done
+  }
+
+
+  // The tile does not come from a DICOM-JPEG instance, we need to
+  // decompress the raw tile
+  std::auto_ptr<Orthanc::ImageAccessor> decoded;
+
+  Orthanc::Semaphore::Locker locker(*transcoderSemaphore_);
+
+  switch (compression)
+  {
+    case OrthancWSI::ImageCompression_Jpeg2000:
+      decoded.reset(new OrthancWSI::Jpeg2000Reader);
+      dynamic_cast<OrthancWSI::Jpeg2000Reader&>(*decoded).ReadFromMemory(tile);
+      break;
+
+    case OrthancWSI::ImageCompression_None:
+    {
+      unsigned int bpp = Orthanc::GetBytesPerPixel(format);
+      if (bpp * tileWidth * tileHeight != tile.size())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+
+      decoded.reset(new Orthanc::ImageAccessor);
+      decoded->AssignReadOnly(format, tileWidth, tileHeight, bpp * tileWidth, tile.c_str());
+      break;
+    }
+
+    default:
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+  }
+
+
+  // This is a lossless frame (coming from a JPEG2000 or uncompressed
+  // DICOM instance), serve it as a PNG image so as to prevent lossy
+  // compression
+
+  std::string png;
+  Orthanc::PngWriter writer;
+  writer.WriteToMemory(png, *decoded);
+
+  OrthancPluginAnswerBuffer(context_, output, png.c_str(), png.size(), "image/png");
+}
+
+
+OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType, 
+                                        OrthancPluginResourceType resourceType, 
+                                        const char *resourceId)
+{
+  if (resourceType == OrthancPluginResourceType_Series &&
+      changeType == OrthancPluginChangeType_NewChildInstance)
+  { 
+    char tmp[1024];
+    sprintf(tmp, "New instance has been added to series %s, invalidating it", resourceId);
+    OrthancPluginLogInfo(context_, tmp);
+
+    cache_->Invalidate(resourceId);
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+
+void ServeFile(OrthancPluginRestOutput* output,
+               const char* url,
+               const OrthancPluginHttpRequest* request)
+{
+  Orthanc::EmbeddedResources::FileResourceId resource;
+
+  std::string f(request->groups[0]);
+  std::string mime;
+
+  if (f == "viewer.html")
+  {
+    resource = Orthanc::EmbeddedResources::VIEWER_HTML;
+    mime = "text/html";
+  }
+  else if (f == "viewer.js")
+  {
+    resource = Orthanc::EmbeddedResources::VIEWER_JS;
+    mime = "application/javascript";
+  }
+  else if (f == "ol.js")
+  {
+    resource = Orthanc::EmbeddedResources::OPENLAYERS_JS;
+    mime = "application/javascript";
+  }
+  else if (f == "ol.css")
+  {
+    resource = Orthanc::EmbeddedResources::OPENLAYERS_CSS;
+    mime = "text/css";
+  }
+  else
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+  }
+
+  std::string content;
+  Orthanc::EmbeddedResources::GetFileResource(content, resource);
+
+  OrthancPluginAnswerBuffer(context_, output, content.c_str(), content.size(), mime.c_str());
+}
+
+
+
+extern "C"
+{
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
+  {
+    context_ = context;
+    assert(DisplayPerformanceWarning());
+
+    /* Check the version of the Orthanc core */
+    if (OrthancPluginCheckVersion(context_) == 0)
+    {
+      char info[1024];
+      sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin",
+              context_->orthancVersion,
+              ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+      OrthancPluginLogError(context_, info);
+      return -1;
+    }
+
+    // Limit the number of PNG transcoders to the number of available
+    // hardware threads (e.g. number of CPUs or cores or
+    // hyperthreading units)
+    unsigned int threads = boost::thread::hardware_concurrency();
+    
+    if (threads <= 0)
+    {
+      threads = 1;
+    }
+    
+    transcoderSemaphore_.reset(new Orthanc::Semaphore(threads));
+
+    char info[1024];
+    sprintf(info, "The whole-slide imaging plugin will use at most %d threads to transcode the tiles", threads);
+    OrthancPluginLogWarning(context_, info);
+
+    OrthancPluginSetDescription(context, "Plugin to serve whole-slide microscopic images from Orthanc.");
+
+    orthanc_.reset(new OrthancWSI::PluginOrthancConnection(context));
+    cache_.reset(new OrthancWSI::DicomPyramidCache(*orthanc_));
+
+    OrthancPluginRegisterOnChangeCallback(context_, OnChangeCallback);
+
+    OrthancPlugins::RegisterRestCallback<ServeFile>(context, "/wsi/app/(ol.css)", true);
+    OrthancPlugins::RegisterRestCallback<ServeFile>(context, "/wsi/app/(ol.js)", true);
+    OrthancPlugins::RegisterRestCallback<ServeFile>(context, "/wsi/app/(viewer.html)", true);
+    OrthancPlugins::RegisterRestCallback<ServeFile>(context, "/wsi/app/(viewer.js)", true);
+    OrthancPlugins::RegisterRestCallback<ServePyramid>(context, "/wsi/pyramids/([0-9a-f-]+)", true);
+    OrthancPlugins::RegisterRestCallback<ServeTile>(context, "/wsi/tiles/([0-9a-f-]+)/([0-9-]+)/([0-9-]+)/([0-9-]+)", true);
+
+    // Extend the default Orthanc Explorer with custom JavaScript for WSI
+    std::string explorer;
+    Orthanc::EmbeddedResources::GetFileResource(explorer, Orthanc::EmbeddedResources::ORTHANC_EXPLORER);
+    OrthancPluginExtendOrthancExplorer(context_, explorer.c_str());
+
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+    cache_.reset(NULL);
+    orthanc_.reset(NULL);
+    transcoderSemaphore_.reset(NULL);
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return "wsi";
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return ORTHANC_WSI_VERSION;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/viewer.html	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+
+<html>
+  <head>
+    <title>Orthanc for Whole-Slide Imaging</title>
+
+    <link rel="stylesheet" href="ol.css" type="text/css">
+
+    <!-- This is the version of jQuery that is used by Orthanc Explorer -->
+    <script src="../../app/libs/jquery.min.js"></script>
+
+    <script src="ol.js"></script>
+
+    <style>
+      #map {
+      position: fixed;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+      border: 1px solid #ccc;
+      margin-bottom: 10px;
+      }
+    </style>
+
+  </head>
+  <body>
+    <div id="map" class="map"></div>
+
+    <script src="viewer.js"></script>
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/viewer.js	Sat Oct 22 21:48:33 2016 +0200
@@ -0,0 +1,125 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+// For IE compatibility
+if (!window.console) window.console = {};
+if (!window.console.log) window.console.log = function () { };
+
+
+// http://stackoverflow.com/a/21903119/881731
+function GetUrlParameter(sParam) 
+{
+  var sPageURL = decodeURIComponent(window.location.search.substring(1));
+  var sURLVariables = sPageURL.split('&');
+  var sParameterName;
+  var i;
+
+  for (i = 0; i < sURLVariables.length; i++) 
+  {
+    sParameterName = sURLVariables[i].split('=');
+
+    if (sParameterName[0] === sParam) 
+    {
+      return sParameterName[1] === undefined ? '' : sParameterName[1];
+    }
+  }
+
+  return '';
+};
+
+
+
+$(document).ready(function() {
+  var seriesId = GetUrlParameter('series');
+  if (seriesId.length == 0)
+  {
+    alert('Error - No series ID specified!');
+  }
+  else
+  {
+    $.ajax({
+      url : '../pyramids/' + seriesId,
+      error: function() {
+        alert('Error - Cannot get the pyramid structure of series: ' + seriesId);
+      },
+      success : function(series) {
+        var width = series['TotalWidth'];
+        var height = series['TotalHeight'];
+        var tileWidth = series['TileWidth'];
+        var tileHeight = series['TileHeight'];
+        var countLevels = series['Resolutions'].length;
+
+        // Maps always need a projection, but Zoomify layers are not geo-referenced, and
+        // are only measured in pixels.  So, we create a fake projection that the map
+        // can use to properly display the layer.
+        var proj = new ol.proj.Projection({
+          code: 'pixel',
+          units: 'pixels',
+          extent: [0, 0, width, height]
+        });
+
+        var extent = [0, -height, width, 0];
+        
+        // Disable the rotation of the map, and inertia while panning
+        // http://stackoverflow.com/a/25682186
+        var interactions = ol.interaction.defaults({
+          altShiftDragRotate : false, 
+          pinchRotate : false,
+          dragPan: false
+        }).extend([
+          new ol.interaction.DragPan({kinetic: false})
+        ]);
+
+        var layer = new ol.layer.Tile({
+          extent: extent,
+          source: new ol.source.TileImage({
+            projection: proj,
+            tileUrlFunction: function(tileCoord, pixelRatio, projection) {
+              return ('../tiles/' + seriesId + '/' + 
+                      (countLevels - 1 - tileCoord[0]) + '/' + tileCoord[1] + '/' + (-tileCoord[2] - 1));
+            },
+            tileGrid: new ol.tilegrid.TileGrid({
+              extent: extent,
+              resolutions: series['Resolutions'].reverse(),
+              tileSize: [tileWidth, tileHeight]
+            })
+          }),
+          wrapX: false,
+          projection: proj
+        });
+
+
+        var map = new ol.Map({
+          target: 'map',
+          layers: [ layer ],
+          view: new ol.View({
+            projection: proj,
+            center: [width / 2, -height / 2],
+            zoom: 0,
+            minResolution: 1   // Do not interpelate over pixels
+          }),
+          interactions: interactions
+        });
+
+        map.getView().fit(extent, map.getSize());
+      }
+    });
+  }
+});